diff --git a/CalamityCommands.hs b/CalamityCommands.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands.hs
@@ -0,0 +1,144 @@
+{- | CalamityCommands commands
+ This module exports the DSL and core types for using commands
+-}
+module CalamityCommands (
+    module CalamityCommands.Context,
+    module CalamityCommands.Dsl,
+    module CalamityCommands.Error,
+    module CalamityCommands.Handler,
+    module CalamityCommands.Utils,
+    module CalamityCommands.ParsePrefix,
+    module CalamityCommands.Help,
+
+    -- * Parameter parsers
+
+    module CalamityCommands.Parser,
+
+    -- * Commands
+    -- $commandDocs
+) where
+
+import CalamityCommands.Context
+import CalamityCommands.Dsl
+import CalamityCommands.Error
+import CalamityCommands.Handler
+import CalamityCommands.Help
+import CalamityCommands.ParsePrefix
+import CalamityCommands.Parser (Named, KleeneStarConcat, KleenePlusConcat, ParameterParser (..))
+import CalamityCommands.Utils
+
+{- $commandDocs
+
+ This module provides abstractions for writing declarative commands, that
+ support grouping, pre-invokation checks, and automatic argument parsing by
+ using a type level list of parameter types.
+
+ A DSL is provided in "CalamityCommands.Dsl" for constructing commands
+ declaratively.
+
+ You can implement 'ParameterParser' for your own types to allow them to be used in the
+ parameter list of commands.
+
+ A default help command is provided in "CalamityCommands.Help" which can be
+ added just by using 'helpCommand' inside the command declaration DSL.
+
+ Commands are parameterised over three types:
+
+   - @m@, the base monad of the command processor. This is used because all
+     commands and checks run in their base monad in the current implementation,
+     as a result, all commands will run with the monadic state remaining the
+     same as when they were created. In the future this design decision may be
+     revised to remove this constraint. For pure commands this may be
+     'Data.Functor.Identity.Identity' and for IO commands this may be 'IO'.
+
+   - @c@, the context that is provided to each command invokation. The default
+     context: 'BasicContext' stores the prefix, command, and unparsed parameters
+     to the command. The context in use is decided by the interpreter for
+     'ConstructContext'.
+
+   - @a@, the result of a command invokation, for commands performing IO actions
+     this is usually just @()@.
+
+ ==== Examples
+
+ Make a command handler, we don't actually use the context and therefore this
+ handler is generic over the context used:
+
+ @
+ h' :: 'CommandContext' 'Data.Functor.Identity.Identity' c ('Either' 'Data.Text.Lazy.Text' 'Int') => 'CommandHandler' 'Data.Functor.Identity.Identity' c ('Either' 'Data.Text.Lazy.Text' 'Int')
+ h' = 'Data.Functor.Identity.runIdentity' . 'Polysemy.runFinal' $ do
+   (h, _) <- 'buildCommands' $ do
+     'command' \@\'[Int, Int] "add" $ \ctx a b -> 'pure' $ 'Right' (a '+' b)
+     'command' \@\'[Int, Int] "mul" $ \ctx a b -> 'pure' $ 'Right' (a '*' b)
+     'helpCommand' ('pure' . 'Left')
+   pure h
+ @
+
+ To use the commands we need to provide the interpreters for
+ 'ConstructContext' and 'ParsePrefix', the default provided ones are being
+ used here: 'useBasicContext' which makes @ctx ~ 'BasicContext'@, and
+ @'useConstantPrefix' "!"@ which treats any input starting with @!@ as a
+ command.
+
+
+ The 'processCommands' function can then be used to parse and invoke commands,
+ since commands are generic over the monad they run in we use
+ @'Data.Functor.Identity.runIdentity' . 'Polysemy.runFinal' .
+ 'Polysemy.embedToFinal'@ to have the commands interpreted purely.
+
+
+ This function 'r' takes an input string such as "!add 1 2", and then looks up
+ the invoked command and runs it, returning the result.
+
+ @
+ r :: 'Data.Text.Lazy.Text' -> 'Maybe' ('Either'
+                     ('CmdInvokeFailReason' ('BasicContext' 'Data.Functor.Identity.Identity' ('Either' 'Data.Text.Lazy.Text' 'Int')))
+                     ('BasicContext' 'Data.Functor.Identity.Identity' ('Either' 'Data.Text.Lazy.Text' 'Int'), 'Either' 'Data.Text.Lazy.Text' 'Int'))
+ r = 'Data.Functor.Identity.runIdentity' . 'Polysemy.runFinal' . 'Polysemy.embedToFinal' . 'useBasicContext' . 'useConstantPrefix' "!" . 'processCommands' h'
+ @
+
+ Then to display the result of processing the command nicely, we can use a
+ something like this function, which prints the result of a command if one was
+ invoked successfully, and prints the error nicely if not.
+
+ @
+ rm :: 'Data.Text.Lazy.Text' -> IO ()
+ rm s = case r s of
+             Just (Right (_, Right r)) ->
+             print r
+
+             Just (Right (_, Left h)) ->
+             'Data.Text.Lazy.IO.putStrLn' h
+
+             Just (Left ('CommandInvokeError' _ ('ParseError' t r))) ->
+             'Data.Text.Lazy.IO.putStrLn' ("Parsing parameter " '<>' 'Data.Text.Lazy.fromStrict' t '<>' " failed with reason: " '<>' r)
+
+              _ -> pure ()
+ @
+
+ >>> rm "!add 1 1"
+ 2
+
+ >>> rm "!add blah 1"
+  Parsing parameter :Int failed with reason: 1:2:
+    |
+  1 |  blah 1
+    |  ^
+  unexpected 'b'
+  expecting '+', '-', integer, or white space
+
+ >>> rm "!help"
+ ```
+ The following commands exist:
+ - mul :Int, :Int
+ - help :[Text]
+ - add :Int, :Int
+ ```
+
+ >>> rm "!help add"
+ Help for command `add`:
+ ```
+ Usage: !add :Int, :Int
+ This command or group has no help.
+ ```
+-}
diff --git a/CalamityCommands/AliasType.hs b/CalamityCommands/AliasType.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/AliasType.hs
@@ -0,0 +1,15 @@
+-- | Named boolean for determining if something is an alias or not
+module CalamityCommands.AliasType
+  ( AliasType (..),
+  )
+where
+
+import GHC.Generics (Generic)
+import TextShow (TextShow)
+import qualified TextShow.Generic as TSG
+
+data AliasType
+  = Alias
+  | Original
+  deriving (Eq, Enum, Show, Generic)
+  deriving (TextShow) via TSG.FromGeneric AliasType
diff --git a/CalamityCommands/Check.hs b/CalamityCommands/Check.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Check.hs
@@ -0,0 +1,54 @@
+-- | Command invokation preconditions
+module CalamityCommands.Check (
+    Check (..),
+    buildCheck,
+    buildCheckPure,
+    runCheck,
+) where
+
+import CalamityCommands.Error
+import CalamityCommands.Internal.RunIntoM
+import CalamityCommands.Internal.Utils
+
+import Control.Lens hiding (Context, (<.>))
+
+import Data.Generics.Labels ()
+import Data.Maybe
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+
+import GHC.Generics
+
+import qualified Polysemy as P
+
+{- | A check for a command.
+
+ Every check for a command must return Nothing for the command to be run.
+-}
+data Check m c = MkCheck
+    { -- | The name of the check.
+      name :: S.Text
+    , -- | The callback for the check, returns Nothing if it passes, otherwise
+      -- returns the reason for it not passing.
+      callback :: c -> m (Maybe L.Text)
+    }
+    deriving (Generic)
+
+{- | Given the name of a check and a callback in the 'P.Sem' monad, build a check
+ by transforming the Polysemy action into an @m@ action.
+-}
+buildCheck :: (Monad m, P.Member (P.Final m) r) => S.Text -> (c -> P.Sem r (Maybe L.Text)) -> P.Sem r (Check m c)
+buildCheck name cb = do
+    cb' <- bindSemToM cb
+    let cb'' = fromMaybe (Just "failed internally") <.> cb'
+    pure $ MkCheck name cb''
+
+-- | Given the name of a check and a pure callback function, build a check.
+buildCheckPure :: Monad m => S.Text -> (c -> Maybe L.Text) -> Check m c
+buildCheckPure name cb = MkCheck name (pure . cb)
+
+{- | Given an invokation context @c@, run a check and transform the result into an
+ @'Either' 'CommandError' ()@.
+-}
+runCheck :: (Monad m, P.Member (P.Embed m) r) => c -> Check m c -> P.Sem r (Either CommandError ())
+runCheck ctx chk = P.embed (callback chk ctx) <&> justToEither . (CheckError (chk ^. #name) <$>)
diff --git a/CalamityCommands/Command.hs b/CalamityCommands/Command.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Command.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE NoPolyKinds #-}
+
+-- | Commands and stuff
+module CalamityCommands.Command (Command (..)) where
+
+import CalamityCommands.Check
+import CalamityCommands.Error
+import CalamityCommands.Group
+
+import Control.Lens hiding (Context, (<.>))
+
+import Data.Kind (Type)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Text as S
+import Data.Text.Lazy as L
+
+import GHC.Generics
+
+import qualified Data.List.NonEmpty as NE
+import TextShow
+import qualified TextShow.Generic as TSG
+
+-- | A command, paremeterised over it's context
+data Command (m :: Type -> Type) (c :: Type) (a :: Type) = forall p.
+  Command
+  { names :: NonEmpty S.Text
+  , parent :: Maybe (Group m c a)
+  , -- | If this command is hidden
+    hidden :: Bool
+  , -- | A list of checks that must pass for this command to be invoked
+    checks :: [Check m c]
+  , -- | A list of the parameters the command takes, only used for constructing
+    -- help messages.
+    params :: [S.Text]
+  , -- | A function producing the \'help\' for the command.
+    help :: c -> L.Text
+  , -- | A function that parses the context for the command, producing the input
+    -- @a@ for the command.
+    parser :: c -> m (Either CommandError p)
+  , -- | A function that given the context and the input (@p@) of the command,
+    -- performs the action of the command.
+    callback :: (c, p) -> m (Either L.Text a)
+  }
+
+data CommandS = CommandS
+  { names :: NonEmpty S.Text
+  , params :: [S.Text]
+  , parent :: Maybe S.Text
+  , checks :: [S.Text]
+  , hidden :: Bool
+  }
+  deriving (Generic, Show)
+  deriving (TextShow) via TSG.FromGeneric CommandS
+
+instance Show (Command m c a) where
+  showsPrec d Command{names, params, parent, checks, hidden} =
+    showsPrec d $
+      CommandS
+        names
+        params
+        (NE.head <$> parent ^? _Just . #names)
+        (checks ^.. traverse . #name)
+        hidden
+
+instance TextShow (Command m c a) where
+  showbPrec d Command{names, params, parent, checks, hidden} =
+    showbPrec d $
+      CommandS
+        names
+        params
+        (NE.head <$> parent ^? _Just . #names)
+        (checks ^.. traverse . #name)
+        hidden
diff --git a/CalamityCommands/Command.hs-boot b/CalamityCommands/Command.hs-boot
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Command.hs-boot
@@ -0,0 +1,13 @@
+-- | Commands and stuff
+module CalamityCommands.Command
+    ( Command
+    ) where
+
+import TextShow
+import Data.Kind (Type)
+
+type role Command representational representational nominal
+data Command (m :: Type -> Type) (c :: Type) (a :: Type)
+
+instance Show (Command m c a)
+instance TextShow (Command m c a)
diff --git a/CalamityCommands/CommandUtils.hs b/CalamityCommands/CommandUtils.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/CommandUtils.hs
@@ -0,0 +1,225 @@
+-- | Command utilities
+module CalamityCommands.CommandUtils (
+  TypedCommandC,
+  CommandForParsers,
+  buildCommand,
+  buildCommand',
+  buildParser,
+  buildCallback,
+  runCommand,
+  invokeCommand,
+  groupPath,
+  commandPath,
+  commandParams,
+) where
+
+import CalamityCommands.Check
+import CalamityCommands.Command
+import CalamityCommands.Context
+import CalamityCommands.Error
+import CalamityCommands.Group
+import CalamityCommands.Internal.RunIntoM
+import CalamityCommands.Internal.Utils
+import CalamityCommands.Parser
+
+import Control.Lens hiding (Context, (<.>))
+import Control.Monad
+
+import Data.Foldable
+import Data.Kind
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NE
+import Data.Maybe
+import Data.Text as S
+import Data.Text.Lazy as L
+
+import qualified Polysemy as P
+import qualified Polysemy.Error as P
+import qualified Polysemy.Fail as P
+
+groupPath :: Group m c a -> [S.Text]
+groupPath Group{names, parent} = foldMap groupPath parent <> [NE.head names]
+
+commandPath :: Command m c a -> [S.Text]
+commandPath Command{names, parent} = foldMap groupPath parent <> [NE.head names]
+
+-- | Format a command's parameters
+commandParams :: Command m c a -> L.Text
+commandParams Command{params} = L.fromStrict $ S.intercalate ", " params
+
+{- | Given the properties of a 'Command' with the @parser@ and @callback@ in the
+ 'P.Sem' monad, build a command by transforming the Polysemy actions into @m@
+ actions.
+-}
+buildCommand' ::
+  forall c m a p r.
+  (Monad m, P.Member (P.Final m) r) =>
+  -- | The name (and aliases) of the command
+  NonEmpty S.Text ->
+  -- | The parent group of the command
+  Maybe (Group m c a) ->
+  -- | If the command is hidden
+  Bool ->
+  -- | The checks for the command
+  [Check m c] ->
+  -- | The names of the command's parameters
+  [S.Text] ->
+  -- | The help generator for this command
+  (c -> L.Text) ->
+  -- | The parser for this command
+  (c -> P.Sem r (Either CommandError p)) ->
+  -- | The callback for this command
+  ((c, p) -> P.Sem (P.Fail ': r) a) ->
+  P.Sem r (Command m c a)
+buildCommand' names@(name :| _) parent hidden checks params help parser cb = do
+  cb' <- buildCallback cb
+  parser' <- buildParser name parser
+  pure $ Command names parent hidden checks params help parser' cb'
+
+{- | Given the properties of a 'Command', a callback, and a type level list of
+ the parameters, build a command by constructing a parser and wiring it up to
+ the callback.
+
+ ==== Examples
+
+ Building a command that adds two numbers.
+
+ @
+ 'buildCommand' \@\'['CalamityCommands.Parser.Named' "a" 'Int', 'CalamityCommands.Parser.Named' "b" 'Int']
+    "add" 'Nothing' [] ('const' "Add two integers") $ \\ctx a b ->
+      'pure' '$' 'Right' (a '+' b)
+ @
+-}
+buildCommand ::
+  forall ps c m a r.
+  (Monad m, P.Member (P.Final m) r, TypedCommandC ps a r, CommandContext m c a) =>
+  -- | The name (and aliases) of the command
+  NonEmpty S.Text ->
+  -- | The parent group of the command
+  Maybe (Group m c a) ->
+  -- | If the command is hidden
+  Bool ->
+  -- | The checks for the command
+  [Check m c] ->
+  -- | The help generator for this command
+  (c -> L.Text) ->
+  -- | The callback foor this command
+  (c -> CommandForParsers ps r a) ->
+  P.Sem r (Command m c a)
+buildCommand names parent hidden checks help command =
+  let (parser, cb) = buildTypedCommand @ps command
+   in buildCommand' names parent hidden checks (paramNames @ps @r) help parser cb
+
+{- | Given the name of the command the parser is for and a parser function in
+ the 'P.Sem' monad, build a parser by transforming the Polysemy action into an
+ @m@ action.
+-}
+buildParser ::
+  (Monad m, P.Member (P.Final m) r) =>
+  S.Text ->
+  (c -> P.Sem r (Either CommandError a)) ->
+  P.Sem r (c -> m (Either CommandError a))
+buildParser name cb = do
+  cb' <- bindSemToM cb
+  let cb'' ctx = fromMaybe (Left $ ParseError ("Parser for command: " <> name) "failed internally") <$> cb' ctx
+  pure cb''
+
+{- | Given a callback for a command in the 'P.Sem' monad, build a command callback by
+ transforming the Polysemy action into an @m@ action.
+-}
+buildCallback ::
+  (Monad m, P.Member (P.Final m) r) => ((c, p) -> P.Sem (P.Fail ': r) a) -> P.Sem r ((c, p) -> m (Either L.Text a))
+buildCallback cb = do
+  cb' <- bindSemToM ( \x -> P.runFail (cb x) <&> mapLeft L.pack)
+  let cb'' = fromMaybe (Left "failed internally") <.> cb'
+  pure cb''
+
+-- | Given an invokation Context @c@, run a command. This does not perform the command's checks.
+runCommand :: (Monad m, P.Member (P.Embed m) r) => c -> Command m c a -> P.Sem r (Either CommandError a)
+runCommand ctx Command{names = name :| _, parser, callback} =
+  P.embed (parser ctx) >>= \case
+    Left e -> pure $ Left e
+    Right p' -> P.embed (callback (ctx, p')) <&> mapLeft (InvokeError name)
+
+{- | Given an invokation Context @c@, first run all of the command's checks, then
+ run the command if they all pass.
+-}
+invokeCommand :: (Monad m, P.Member (P.Embed m) r) => c -> Command m c a -> P.Sem r (Either CommandError a)
+invokeCommand ctx cmd@Command{checks} = P.runError $ do
+  for_ checks (P.fromEither <=< runCheck ctx)
+  P.fromEither =<< runCommand ctx cmd
+
+type CommandSemType r a = P.Sem (P.Fail ': r) a
+
+-- | Some constraints used for making parameter typed commands work
+type TypedCommandC ps a r =
+  ( ApplyTupRes (ParserResult (ListToTup ps)) (CommandSemType r a) ~ CommandForParsers ps r a
+  , ParameterParser (ListToTup ps) r
+  , ApplyTup (ParserResult (ListToTup ps)) (CommandSemType r a)
+  , ParamNamesForParsers ps r
+  )
+
+buildTypedCommand ::
+  forall (ps :: [Type]) c m a p r.
+  (TypedCommandC ps a r, p ~ ParserResult (ListToTup ps), CommandContext m c a) =>
+  (c -> CommandForParsers ps r a) ->
+  ( c -> P.Sem r (Either CommandError p)
+  , (c, p) -> P.Sem (P.Fail ': r) a
+  )
+buildTypedCommand cmd =
+  let parser ctx = buildTypedCommandParser @ps ctx (ctxUnparsedParams @m ctx)
+      consumer (ctx, r) = applyTup (cmd ctx) r
+   in (parser, consumer)
+
+class ParamNamesForParsers (ps :: [Type]) r where
+  paramNames :: [S.Text]
+
+instance ParamNamesForParsers '[] r where
+  paramNames = []
+
+instance (ParameterParser x r, ParamNamesForParsers xs r) => ParamNamesForParsers (x : xs) r where
+  paramNames = parserName @x @r : paramNames @xs @r
+
+class ApplyTup a b where
+  type ApplyTupRes a b
+
+  applyTup :: ApplyTupRes a b -> a -> b
+
+instance ApplyTup as b => ApplyTup (a, as) b where
+  type ApplyTupRes (a, as) b = a -> ApplyTupRes as b
+
+  applyTup f (a, as) = applyTup (f a) as
+
+instance ApplyTup () b where
+  type ApplyTupRes () b = b
+
+  applyTup r () = r
+
+buildTypedCommandParser ::
+  forall (ps :: [Type]) c r.
+  ParameterParser (ListToTup ps) r =>
+  c ->
+  L.Text ->
+  P.Sem r (Either CommandError (ParserResult (ListToTup ps)))
+buildTypedCommandParser ctx t =
+  runCommandParser ctx t (parse @(ListToTup ps) @r) <&> \case
+    Right r -> Right r
+    Left (n, e) -> Left $ ParseError n e
+
+type family ListToTup (ps :: [Type]) where
+  ListToTup '[] = ()
+  ListToTup (x ': xs) = (x, ListToTup xs)
+
+{- | Transform a type level list of types implementing the 'Parser' typeclass into
+ the type a command callback matching those parameters should be.
+
+ As an example:
+
+ @
+ 'CommandForParsers' [ 'L.Text', 'Int', 'CalamityCommands.Parser.Named' "something" 'L.Text' ] r a ~
+   ('L.Text' -> 'Int' -> 'L.Text' -> 'P.Sem' r ('P.Fail' ': r) a)
+ @
+-}
+type family CommandForParsers (ps :: [Type]) r a where
+  CommandForParsers '[] r a = P.Sem (P.Fail ': r) a
+  CommandForParsers (x ': xs) r a = ParserResult x -> CommandForParsers xs r a
diff --git a/CalamityCommands/Context.hs b/CalamityCommands/Context.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Context.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Command context typeclass
+module CalamityCommands.Context (
+  CommandContext (..),
+  ConstructContext (..),
+  constructContext,
+  BasicContext (..),
+  useBasicContext,
+) where
+
+import qualified Data.Text.Lazy as L
+import GHC.Generics (Generic)
+import qualified Polysemy as P
+
+import CalamityCommands.Command
+
+class CommandContext m c a | c -> m, c -> a where
+  -- | The prefix that was used to invoke the command
+  ctxPrefix :: c -> L.Text
+
+  -- | The command that was invoked
+  ctxCommand :: c -> Command m c a
+
+  -- | The message remaining after consuming the prefix
+  ctxUnparsedParams :: c -> L.Text
+
+-- | An effect for constructing the context for a command
+data ConstructContext msg ctx m' a' m a where
+  -- | Construct a context for a command invokation, returning Just @context@ on
+  -- success, or Nothing if a context could not be constructed
+  ConstructContext ::
+    -- | The (prefix, command, remaining)
+    (L.Text, Command m' ctx a', L.Text) ->
+    -- | The message type to extract the context from
+    msg ->
+    ConstructContext msg ctx m' a' m (Maybe ctx)
+
+P.makeSem ''ConstructContext
+
+-- | A basic context that only knows the prefix used and the unparsed input
+data BasicContext m a = BasicContext
+  { bcPrefix :: L.Text
+  , bcCommand :: Command m (BasicContext m a) a
+  , bcUnparsedParams :: L.Text
+  }
+  deriving (Show, Generic)
+
+instance CommandContext m (BasicContext m a) a where
+  ctxPrefix = bcPrefix
+  ctxCommand = bcCommand
+  ctxUnparsedParams = bcUnparsedParams
+
+-- | A default interpretation for 'ConstructContext' that constructs a BasicContext
+useBasicContext :: P.Sem (ConstructContext msg (BasicContext m a') m a' ': r) a -> P.Sem r a
+useBasicContext =
+  P.interpret
+    ( \case
+        ConstructContext (pre, cmd, up) _ -> pure . Just $ BasicContext pre cmd up
+    )
diff --git a/CalamityCommands/Dsl.hs b/CalamityCommands/Dsl.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Dsl.hs
@@ -0,0 +1,395 @@
+{-# LANGUAGE RecursiveDo #-}
+
+-- | A DSL for generating commands and groups
+module CalamityCommands.Dsl (
+    -- * Commands DSL
+    -- $dslTutorial
+    command,
+    command',
+    commandA,
+    commandA',
+    hide,
+    help,
+    requires,
+    requires',
+    requiresPure,
+    group,
+    group',
+    groupA,
+    groupA',
+    DSLState,
+    raiseDSL,
+    fetchHandler,
+) where
+
+import CalamityCommands.AliasType
+import CalamityCommands.Check
+import CalamityCommands.Command hiding (help)
+import CalamityCommands.CommandUtils
+import CalamityCommands.Context
+import CalamityCommands.Error
+import CalamityCommands.Group hiding (help)
+import CalamityCommands.Handler
+import CalamityCommands.Internal.LocalWriter
+
+import qualified Data.HashMap.Lazy as LH
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Polysemy as P
+import qualified Polysemy.Fail as P
+import qualified Polysemy.Fixpoint as P
+import qualified Polysemy.Reader as P
+import qualified Polysemy.Tagged as P
+
+{- $dslTutorial
+
+ This module provides a way of constructing bot commands in a declarative way.
+
+ The main component of this is the 'command' function, which takes a
+ type-level list of command parameters, the name, and the callback and
+ produces a command. There are also the alternatives 'command'', 'commandA'
+ and 'commandA'', for when you want to handle parsing of the input yourself,
+ and/or want aliases of the command.
+
+ The functions: 'hide', 'help', 'requires', and 'group' can be used to change
+ attributes of any commands declared inside the monadic action passed to them,
+ for example:
+
+ @
+ 'hide' '$' do
+   'command' \@\'[] "test" \\ctx -> 'pure' ()
+ @
+
+ In the above block, any command declared inside 'hide' will have it's
+ \'hidden\' flag set and will not be shown by the default help command:
+ 'CalamityCommands.Help.helpCommand'
+
+ The 'CalamityCommands.Help.helpCommand' function can be used to create a
+ help command for the commands DSL action it is used in, read it's doc page
+ for more information on how it functions.
+
+ The 'CalamityCommands.Utils.buildCommands' function is used to
+ construct a 'CommandHandler' which can then be used with
+ 'CalamityCommands.Utils.processCommands' or
+ 'CalamityCommands.Utils.handleCommands' to process a command.
+-}
+
+type DSLState m c a r =
+    ( LocalWriter (LH.HashMap S.Text (Command m c a, AliasType))
+        ': LocalWriter (LH.HashMap S.Text (Group m c a, AliasType))
+            ': P.Reader (Maybe (Group m c a))
+                ': P.Tagged "hidden" (P.Reader Bool)
+                    ': P.Reader (c -> L.Text)
+                        ': P.Tagged "original-help" (P.Reader (c -> L.Text))
+                            ': P.Reader [Check m c]
+                                ': P.Reader (CommandHandler m c a)
+                                    ': P.Fixpoint
+                                        ': r
+    )
+
+raiseDSL :: P.Sem r x -> P.Sem (DSLState m c a r) x
+raiseDSL = P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise
+
+{- | Given the command name and parameter names, @parser@ and @callback@ for a
+ command in the 'P.Sem' monad, build a command by transforming the Polysemy
+ actions into @m@ actions. Then register the command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+-}
+command' ::
+    (Monad m, P.Member (P.Final m) r) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The names of the command's parameters
+    [S.Text] ->
+    -- | The parser for this command
+    (c -> P.Sem r (Either CommandError p)) ->
+    -- | The callback for this command
+    ((c, p) -> P.Sem (P.Fail ': r) a) ->
+    P.Sem (DSLState m c a r) (Command m c a)
+command' name params parser cb = commandA' name [] params parser cb
+
+{- | Given the command name, aliases, and parameter names, @parser@ and @callback@
+ for a command in the 'P.Sem' monad, build a command by transforming the
+ Polysemy actions into @m@ actions. Then register the command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+-}
+commandA' ::
+    forall p c a m r.
+    (Monad m, P.Member (P.Final m) r) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The aliases for the command
+    [S.Text] ->
+    -- | The names of the command's parameters
+    [S.Text] ->
+    -- | The parser for this command
+    (c -> P.Sem r (Either CommandError p)) ->
+    -- | The callback for this command
+    ((c, p) -> P.Sem (P.Fail ': r) a) ->
+    P.Sem (DSLState m c a r) (Command m c a)
+commandA' name aliases params parser cb = do
+    parent <- P.ask @(Maybe (Group m c a))
+    hidden <- P.tag $ P.ask @Bool
+    checks <- P.ask @[Check m c]
+    help' <- P.ask @(c -> L.Text)
+    cmd <- raiseDSL $ buildCommand' (name :| aliases) parent hidden checks params help' parser cb
+    ltell $ LH.singleton name (cmd, Original)
+    ltell $ LH.fromList [(name, (cmd, Alias)) | name <- aliases]
+    pure cmd
+
+{- | Given the name of a command and a callback, and a type level list of
+ the parameters, build and register a command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+
+ Command parameters are parsed by first invoking
+ 'CalamityCommands.Parser.parse' for the first
+ 'CalamityCommands.Parser.Parser', then running the next parser on the
+ remaining input, and so on.
+
+ ==== Examples
+
+ Building a command that adds two numbers.
+
+ @
+ 'command' \@\'['CalamityCommands.Parser.Named' "a" 'Int', 'CalamityCommands.Parser.Named' "b" 'Int']
+   "add" $ \\ctx a b -> 'pure' '$' 'Right' (a '+' b)
+ @
+-}
+command ::
+    forall ps c a m r.
+    ( Monad m
+    , P.Member (P.Final m) r
+    , TypedCommandC ps a r
+    , CommandContext m c a
+    ) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The callback for this command
+    (c -> CommandForParsers ps r a) ->
+    P.Sem (DSLState m c a r) (Command m c a)
+command name cmd = commandA @ps name [] cmd
+
+{- | Given the name and aliases of a command and a callback, and a type level list of
+ the parameters, build and register a command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+
+ ==== Examples
+
+
+ Building a command that adds two numbers.
+
+ @
+ 'commandA' \@\'['CalamityCommands.Parser.Named' "a" 'Int', 'CalamityCommands.Parser.Named' "b" 'Int']
+   "add" [] $ \\ctx a b -> 'pure' '$' 'Right' (a '+' b)
+ @
+-}
+commandA ::
+    forall ps c a m r.
+    ( Monad m
+    , P.Member (P.Final m) r
+    , TypedCommandC ps a r
+    , CommandContext m c a
+    ) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The aliases for the command
+    [S.Text] ->
+    -- | The callback for this command
+    (c -> CommandForParsers ps r a) ->
+    P.Sem (DSLState m c a r) (Command m c a)
+commandA name aliases cmd = do
+    parent <- P.ask @(Maybe (Group m c a))
+    hidden <- P.tag $ P.ask @Bool
+    checks <- P.ask @[Check m c]
+    help' <- P.ask @(c -> L.Text)
+    cmd' <- raiseDSL $ buildCommand @ps (name :| aliases) parent hidden checks help' cmd
+    ltell $ LH.singleton name (cmd', Original)
+    ltell $ LH.fromList [(name, (cmd', Alias)) | name <- aliases]
+    pure cmd'
+
+{- | Set the visibility of any groups or commands registered inside the given
+ action to hidden.
+-}
+hide ::
+    P.Member (P.Tagged "hidden" (P.Reader Bool)) r =>
+    P.Sem r x ->
+    P.Sem r x
+hide = P.tag @"hidden" . P.local @Bool (const True) . P.raise
+
+{- | Set the help for any groups or commands registered inside the given action.
+
+ ==== Examples
+
+ @
+ 'help' ('const' "Add two integers") $
+   'command' \@\'['CalamityCommands.Parser.Named' "a" 'Int', 'CalamityCommands.Parser.Named' "b" 'Int']
+     "add" $ \\ctx a b -> 'pure' '$' 'Right' (a '+' b)
+ @
+-}
+help ::
+    P.Member (P.Reader (c -> L.Text)) r =>
+    (c -> L.Text) ->
+    P.Sem r a ->
+    P.Sem r a
+help = P.local . const
+
+{- | Add to the list of checks for any commands registered inside the given
+ action.
+-}
+requires ::
+    [Check m c] ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+requires = P.local . (<>)
+
+{- | Construct a check and add it to the list of checks for any commands
+ registered inside the given action.
+
+ Refer to 'CalamityCommands.Check.Check' for more info on checks.
+-}
+requires' ::
+    (Monad m, P.Member (P.Final m) r) =>
+    -- | The name of the check
+    S.Text ->
+    -- | The callback for the check
+    (c -> P.Sem r (Maybe L.Text)) ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+requires' name cb m = do
+    check <- raiseDSL $ buildCheck name cb
+    requires [check] m
+
+{- | Construct some pure checks and add them to the list of checks for any
+ commands registered inside the given action.
+
+ Refer to 'CalamityCommands.Check.Check' for more info on checks.
+
+ ==== Examples
+
+ @
+ 'requiresPure' [("always ok", 'const' 'Nothing')] $
+   'command' \@\'['CalamityCommands.Parser.Named' "a" 'Int', 'CalamityCommands.Parser.Named' "b" 'Int']
+     "add" $ \\ctx a b -> 'pure' '$' 'Right' (a '+' b)
+ @
+-}
+requiresPure ::
+    Monad m =>
+    [(S.Text, c -> Maybe L.Text)] ->
+    -- A list of check names and check callbacks
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+requiresPure checks = requires $ map (uncurry buildCheckPure) checks
+
+{- | Construct a group and place any commands registered in the given action
+ into the new group.
+
+ This also resets the @help@ function back to it's original value, use
+ 'group'' if you don't want that (i.e. your help function is context aware).
+-}
+group ::
+    (Monad m, P.Member (P.Final m) r) =>
+    -- | The name of the group
+    S.Text ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+group name m = groupA name [] m
+
+{- | Construct a group with aliases and place any commands registered in the
+ given action into the new group.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+
+ This also resets the @help@ function back to it's original value, use
+ 'group'' if you don't want that (i.e. your help function is context aware).
+-}
+groupA ::
+    forall x c m a r.
+    (Monad m, P.Member (P.Final m) r) =>
+    -- | The name of the group
+    S.Text ->
+    -- | The aliases of the group
+    [S.Text] ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+groupA name aliases m = mdo
+    parent <- P.ask @(Maybe (Group m c a))
+    hidden <- P.tag $ P.ask @Bool
+    checks <- P.ask @[Check m c]
+    help' <- P.ask @(c -> L.Text)
+    origHelp <- fetchOrigHelp
+    let group' = Group (name :| aliases) parent hidden commands children help' checks
+    (children, (commands, res)) <-
+        llisten @(LH.HashMap S.Text (Group m c a, AliasType)) $
+            llisten @(LH.HashMap S.Text (Command m c a, AliasType)) $
+                P.local @(Maybe (Group m c a)) (const $ Just group') $
+                    P.local @(c -> L.Text) (const origHelp) m
+    ltell $ LH.singleton name (group', Original)
+    ltell $ LH.fromList [(name, (group', Alias)) | name <- aliases]
+    pure res
+
+fetchOrigHelp :: P.Member (P.Tagged "original-help" (P.Reader (c -> L.Text))) r => P.Sem r (c -> L.Text)
+fetchOrigHelp = P.tag P.ask
+
+{- | Construct a group and place any commands registered in the given action
+ into the new group.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+
+ Unlike 'help' this doesn't reset the @help@ function back to it's original
+ value.
+-}
+group' ::
+    P.Member (P.Final m) r =>
+    -- | The name of the group
+    S.Text ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+group' name m = groupA' name [] m
+
+{- | Construct a group with aliases and place any commands registered in the given action
+ into the new group.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+
+ Unlike 'help' this doesn't reset the @help@ function back to it's original
+ value.
+-}
+groupA' ::
+    forall x c m a r.
+    P.Member (P.Final m) r =>
+    -- | The name of the group
+    S.Text ->
+    -- | The aliases of the group
+    [S.Text] ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem (DSLState m c a r) x
+groupA' name aliases m = mdo
+    parent <- P.ask @(Maybe (Group m c a))
+    hidden <- P.tag $ P.ask @Bool
+    checks <- P.ask @[Check m c]
+    help' <- P.ask @(c -> L.Text)
+    let group' = Group (name :| aliases) parent hidden commands children help' checks
+    (children, (commands, res)) <-
+        llisten @(LH.HashMap S.Text (Group m c a, AliasType)) $
+            llisten @(LH.HashMap S.Text (Command m c a, AliasType)) $
+                P.local @(Maybe (Group m c a)) (const $ Just group') m
+    ltell $ LH.singleton name (group', Original)
+    ltell $ LH.fromList [(name, (group', Alias)) | name <- aliases]
+    pure res
+
+-- | Retrieve the final command handler for this block
+fetchHandler :: P.Sem (DSLState m c a r) (CommandHandler m c a)
+fetchHandler = P.ask
diff --git a/CalamityCommands/Error.hs b/CalamityCommands/Error.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Error.hs
@@ -0,0 +1,21 @@
+-- | Command errors
+module CalamityCommands.Error
+    ( CommandError(..) ) where
+
+import qualified Data.Text        as S
+import qualified Data.Text.Lazy   as L
+
+import           GHC.Generics
+
+import           TextShow
+import qualified TextShow.Generic as TSG
+
+data CommandError
+  = ParseError S.Text -- ^ The type of the parser
+               L.Text -- ^ The reason that parsing failed
+  | CheckError S.Text -- ^ The name of the check that failed
+               L.Text -- ^ The reason for the check failing
+  | InvokeError S.Text -- ^ The name of the command that failed
+                L.Text -- ^ The reason for failing
+  deriving ( Show, Generic )
+  deriving ( TextShow ) via TSG.FromGeneric CommandError
diff --git a/CalamityCommands/Group.hs b/CalamityCommands/Group.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Group.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE NoPolyKinds #-}
+
+-- | Command groups
+module CalamityCommands.Group (Group (..)) where
+
+import CalamityCommands.AliasType
+import CalamityCommands.Check
+import {-# SOURCE #-} CalamityCommands.Command
+
+import Control.Lens hiding (Context, (<.>))
+
+import qualified Data.HashMap.Lazy as LH
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+
+import GHC.Generics
+
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NE
+import TextShow
+import qualified TextShow.Generic as TSG
+
+-- | A group of commands
+data Group m c a = Group
+    { names :: NonEmpty S.Text
+    , parent :: Maybe (Group m c a)
+    , hidden :: Bool
+    , -- | Any child commands of this group
+      commands :: LH.HashMap S.Text (Command m c a, AliasType)
+    , -- | Any child groups of this group
+      children :: LH.HashMap S.Text (Group m c a, AliasType)
+    , -- | A function producing the \'help\' for the group
+      help :: c -> L.Text
+    , -- | A list of checks that must pass
+      checks :: [Check m c]
+    }
+    deriving (Generic)
+
+data GroupS m c a = GroupS
+    { names :: NonEmpty S.Text
+    , parent :: Maybe S.Text
+    , commands :: [(S.Text, (Command m c a, AliasType))]
+    , children :: [(S.Text, (Group m c a, AliasType))]
+    , hidden :: Bool
+    }
+    deriving (Generic, Show)
+    deriving (TextShow) via TSG.FromGeneric (GroupS m c a)
+
+instance Show (Group m c a) where
+    showsPrec d Group{names, parent, commands, children, hidden} =
+        showsPrec d $ GroupS names (NE.head <$> parent ^? _Just . #names) (LH.toList commands) (LH.toList children) hidden
+
+instance TextShow (Group m c a) where
+    showbPrec d Group{names, parent, commands, children, hidden} =
+      showbPrec d $ GroupS names (NE.head <$> parent ^? _Just . #names) (LH.toList commands) (LH.toList children) hidden
diff --git a/CalamityCommands/Handler.hs b/CalamityCommands/Handler.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Handler.hs
@@ -0,0 +1,35 @@
+-- | A command handler
+module CalamityCommands.Handler (CommandHandler (..)) where
+
+import CalamityCommands.AliasType
+import CalamityCommands.Command
+import CalamityCommands.Group
+
+import qualified Data.HashMap.Lazy as LH
+import qualified Data.Text as S
+
+import GHC.Generics
+
+import TextShow
+import qualified TextShow.Generic as TSG
+
+data CommandHandler m c a = CommandHandler
+    { -- | Top level groups
+      groups :: LH.HashMap S.Text (Group m c a, AliasType)
+    , -- | Top level commands
+      commands :: LH.HashMap S.Text (Command m c a, AliasType)
+    }
+    deriving (Generic)
+
+data CommandHandlerS m c a = CommandHandlerS
+    { groups :: [(S.Text, (Group m c a, AliasType))]
+    , commands :: [(S.Text, (Command m c a, AliasType))]
+    }
+    deriving (Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric (CommandHandlerS m c a)
+
+instance Show (CommandHandler m c a) where
+    showsPrec d CommandHandler{groups, commands} = showsPrec d $ CommandHandlerS (LH.toList groups) (LH.toList commands)
+
+instance TextShow (CommandHandler m c a) where
+    showbPrec d CommandHandler{groups, commands} = showbPrec d $ CommandHandlerS (LH.toList groups) (LH.toList commands)
diff --git a/CalamityCommands/Help.hs b/CalamityCommands/Help.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Help.hs
@@ -0,0 +1,249 @@
+-- | A default help command implementation
+module CalamityCommands.Help (
+  helpCommand',
+  helpCommand,
+) where
+
+import CalamityCommands.AliasType
+import CalamityCommands.Check
+import CalamityCommands.Command
+import CalamityCommands.CommandUtils
+import CalamityCommands.Context
+import CalamityCommands.Dsl
+import CalamityCommands.Group
+import CalamityCommands.Handler
+import CalamityCommands.Internal.LocalWriter
+
+import Control.Applicative
+import Control.Lens hiding (Context (..))
+
+import qualified Data.HashMap.Lazy as LH
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NE
+import Data.Maybe (mapMaybe)
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+
+import qualified Polysemy as P
+import qualified Polysemy.Fail as P
+import qualified Polysemy.Reader as P
+
+data CommandOrGroup m c a
+  = Command' (Command m c a)
+  | Group' (Group m c a) [S.Text]
+
+helpCommandHelp :: c -> L.Text
+helpCommandHelp _ = "Show help for a command or group."
+
+helpForCommand :: CommandContext m c a => c -> Command m c a -> L.Text
+helpForCommand ctx cmd@Command{names, checks, help} =
+  "```\nUsage: " <> prefix' <> path' <> " " <> params' <> "\n"
+    <> aliasesFmt
+    <> checksFmt
+    <> help ctx
+    <> "\n```"
+ where
+  prefix' = ctxPrefix ctx
+  path' = L.unwords . map L.fromStrict $ commandPath cmd
+  params' = commandParams cmd
+  aliases = map L.fromStrict $ NE.tail names
+  checks' = map (L.fromStrict . (^. #name)) checks
+  aliasesFmt =
+    if null aliases
+      then ""
+      else "Aliases: " <> L.unwords aliases <> "\n"
+  checksFmt =
+    if null checks'
+      then ""
+      else "Checks: " <> L.unwords checks' <> "\n\n"
+
+fmtCommandWithParams :: Command m c a -> L.Text
+fmtCommandWithParams cmd@Command{names} = formatWithAliases names <> " " <> commandParams cmd
+
+formatWithAliases :: NonEmpty S.Text -> L.Text
+formatWithAliases (name :| aliases) = L.fromStrict name <> aliasesFmt
+ where
+  aliasesFmt = case aliases of
+    [] -> ""
+    aliases' -> "[" <> L.intercalate "|" (map L.fromStrict aliases') <> "]"
+
+onlyOriginals :: [(a, AliasType)] -> [a]
+onlyOriginals = mapMaybe inner
+ where
+  inner (_, Alias) = Nothing
+  inner (a, Original) = Just a
+
+onlyVisibleC :: [Command m c a] -> [Command m c a]
+onlyVisibleC = mapMaybe notHiddenC
+
+onlyVisibleG :: [Group m c a] -> [Group m c a]
+onlyVisibleG = mapMaybe notHiddenG
+
+helpForGroup :: CommandContext m c a => c -> Group m c a -> L.Text
+helpForGroup ctx grp =
+  "```\nGroup: " <> path' <> "\n"
+    <> aliasesFmt
+    <> checksFmt
+    <> (grp ^. #help) ctx
+    <> "\n"
+    <> groupsMsg
+    <> commandsMsg
+    <> "\n```"
+ where
+  path' = L.fromStrict . S.unwords $ groupPath grp
+  groups = onlyVisibleG . onlyOriginals . LH.elems $ grp ^. #children
+  commands = onlyVisibleC . onlyOriginals . LH.elems $ grp ^. #commands
+  groupsFmt = map formatWithAliases (groups ^.. traverse . #names)
+  groupsMsg =
+    if null groups
+      then ""
+      else "The following child groups exist:\n" <> (L.unlines . map ("- " <>) $ groupsFmt)
+  commandsMsg =
+    if null commands
+      then ""
+      else "\nThe following child commands exist:\n" <> (L.unlines . map (("- " <>) . fmtCommandWithParams) $ commands)
+  aliases = map L.fromStrict . NE.tail $ grp ^. #names
+  checks' = map (L.fromStrict . (^. #name)) $ grp ^. #checks
+  aliasesFmt =
+    if null aliases
+      then ""
+      else "Aliases: " <> L.unwords aliases <> "\n"
+  checksFmt =
+    if null checks'
+      then ""
+      else "Checks: " <> L.unwords checks' <> "\n\n"
+
+rootHelp :: CommandHandler m c a -> L.Text
+rootHelp handler = "```\n" <> groupsMsg <> commandsMsg <> "\n```"
+ where
+  groups = onlyVisibleG . onlyOriginals . LH.elems $ handler ^. #groups
+  commands = onlyVisibleC . onlyOriginals . LH.elems $ handler ^. #commands
+  groupsFmt = map formatWithAliases (groups ^.. traverse . #names)
+  groupsMsg =
+    if null groups
+      then ""
+      else "The following groups exist:\n" <> (L.unlines . map ("- " <>) $ groupsFmt)
+  commandsMsg =
+    if null commands
+      then ""
+      else "\nThe following commands exist:\n" <> (L.unlines . map (("- " <>) . fmtCommandWithParams) $ commands)
+
+renderHelp :: CommandContext m c a => CommandHandler m c a -> c -> [S.Text] -> L.Text
+renderHelp handler ctx path =
+  case findCommandOrGroup handler path of
+    Just (Command' cmd@Command{names}) ->
+      "Help for command `" <> L.fromStrict (NE.head names) <> "`: \n" <> helpForCommand ctx cmd
+    Just (Group' grp@Group{names} remainingPath) ->
+      let failedMsg =
+            if null remainingPath
+              then ""
+              else "No command or group with the path: `" <> L.fromStrict (S.unwords remainingPath) <> "` exists for the group: `" <> L.fromStrict (NE.head names) <> "`\n"
+       in failedMsg <> "Help for group `" <> L.fromStrict (NE.head names) <> "`: \n" <> helpForGroup ctx grp
+    Nothing ->
+      let failedMsg =
+            if null path
+              then ""
+              else "No command or group with the path: `" <> L.fromStrict (S.unwords path) <> "` was found.\n"
+       in failedMsg <> rootHelp handler
+
+{- | Given a 'CommandHandler', optionally a parent 'Group', and a list of 'Check's,
+ construct a help command that will provide help for all the commands and
+ groups in the passed 'CommandHandler'.
+-}
+helpCommand' ::
+  (Monad m, P.Member (P.Final m) r, CommandContext m c a) =>
+  CommandHandler m c a ->
+  Maybe (Group m c a) ->
+  [Check m c] ->
+  (L.Text -> P.Sem (P.Fail ': r) a) ->
+  P.Sem r (Command m c a)
+helpCommand' handler parent checks render =
+  buildCommand @'[[S.Text]]
+    ("help" :| [])
+    parent
+    False
+    checks
+    helpCommandHelp
+    (\ctx path -> render $ renderHelp handler ctx path)
+
+{- | Create and register the default help command for all the commands registered
+ in the commands DSL this is used in. The @render@ parameter is used so you can
+ determine how the help should be rendered, for example it could be
+ @'putStrLn'@, or a pure function such as @'pure' . 'Left'@
+
+ The registered command will have the name \'help\', called with no parameters
+ it will render help for the top-level groups and commands, for example:
+
+ @
+ The following groups exist:
+ - reanimate
+ - prefix[prefixes]
+ - alias[aliases]
+ - remind[reminder|reminders]
+
+ The following commands exist:
+ - help :[Text]
+ @
+
+ Both commands and groups are listed in the form: @\<name\>[\<alias 0\>|\<alias 1\>]@,
+ commands also have their parameter list shown.
+
+ If a path to a group is passed, the help, aliases, and pre-invokation checks
+ will be listed, along with the subgroups and commands, For example:
+
+ @
+ Help for group remind:
+ Group: remind
+ Aliases: reminder reminders
+ Commands related to making reminders
+
+ The following child commands exist:
+ - list
+ - remove reminder_id:Text
+ - add :KleenePlusConcat Text
+ @
+
+ If a command path is passed, the usage, checks and help for the command are
+ shown, for example:
+
+ @
+ Help for command add:
+ Usage: c!prefix add prefix:Text
+ Checks: prefixLimit guildOnly
+
+ Add a new prefix
+ @
+-}
+helpCommand ::
+  forall c m a r.
+  (Monad m, P.Member (P.Final m) r, CommandContext m c a) =>
+  (L.Text -> P.Sem (P.Fail ': r) a) ->
+  P.Sem (DSLState m c a r) (Command m c a)
+helpCommand render = do
+  handler <- P.ask @(CommandHandler m c a)
+  parent <- P.ask @(Maybe (Group m c a))
+  checks <- P.ask @[Check m c]
+  cmd <- raiseDSL $ helpCommand' handler parent checks render
+  ltell $ LH.singleton "help" (cmd, Original)
+  pure cmd
+
+notHiddenC :: Command m c a -> Maybe (Command m c a)
+notHiddenC c@Command{hidden} = if hidden then Nothing else Just c
+
+notHiddenG :: Group m c a -> Maybe (Group m c a)
+notHiddenG g@Group{hidden} = if hidden then Nothing else Just g
+
+findCommandOrGroup :: CommandHandler m c a -> [S.Text] -> Maybe (CommandOrGroup m c a)
+findCommandOrGroup handler path = go (handler ^. #commands, handler ^. #groups) path
+ where
+  go ::
+    (LH.HashMap S.Text (Command m c a, AliasType), LH.HashMap S.Text (Group m c a, AliasType)) ->
+    [S.Text] ->
+    Maybe (CommandOrGroup m c a)
+  go (commands, groups) (x : xs) =
+    case LH.lookup x commands of
+      Just (notHiddenC -> Just cmd, _) -> Just (Command' cmd)
+      _ -> case LH.lookup x groups of
+        Just (notHiddenG -> Just group, _) -> go (group ^. #commands, group ^. #children) xs <|> Just (Group' group xs)
+        _ -> Nothing
+  go _ [] = Nothing
diff --git a/CalamityCommands/Internal/LocalWriter.hs b/CalamityCommands/Internal/LocalWriter.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Internal/LocalWriter.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | A Writer monad that supports local writing, reverse reader I guess?
+module CalamityCommands.Internal.LocalWriter
+    ( LocalWriter(..)
+    , ltell
+    , llisten
+    , runLocalWriter ) where
+
+import qualified Polysemy       as P
+import qualified Polysemy.State as P
+
+data LocalWriter o m a where
+  Ltell :: o -> LocalWriter o m ()
+  Llisten :: m a -> LocalWriter o m (o, a)
+
+P.makeSem ''LocalWriter
+
+runLocalWriter :: Monoid o => P.Sem (LocalWriter o ': r) a -> P.Sem r (o, a)
+runLocalWriter = P.runState mempty . P.reinterpretH
+  (\case
+     Ltell o   -> do
+       P.modify' (<> o) >>= P.pureT
+     Llisten m -> do
+       mm <- P.runT m
+       (o, fa) <- P.raise $ runLocalWriter mm
+       pure $ fmap (o, ) fa)
diff --git a/CalamityCommands/Internal/RunIntoM.hs b/CalamityCommands/Internal/RunIntoM.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Internal/RunIntoM.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Something for converting polysemy actions into monadic actions
+module CalamityCommands.Internal.RunIntoM
+    ( runSemToM
+    , bindSemToM ) where
+
+import           Data.Functor
+
+import qualified Polysemy                         as P
+import qualified Polysemy.Final                   as P
+
+runSemToM :: forall m r a. (Monad m, P.Member (P.Final m) r) => P.Sem r a -> P.Sem r (m (Maybe a))
+runSemToM m = P.withStrategicToFinal $ do
+  m' <- P.runS m
+  ins <- P.getInspectorS
+  P.liftS $ pure (P.inspect ins <$> m')
+
+bindSemToM :: forall m r p a. (Monad m, P.Member (P.Final m) r) => (p -> P.Sem r a) -> P.Sem r (p -> m (Maybe a))
+bindSemToM m = P.withStrategicToFinal $ do
+  istate <- P.getInitialStateS
+  m' <- P.bindS m
+  ins <- P.getInspectorS
+  P.liftS $ pure (\x -> P.inspect ins <$> m' (istate $> x))
diff --git a/CalamityCommands/Internal/Utils.hs b/CalamityCommands/Internal/Utils.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Internal/Utils.hs
@@ -0,0 +1,104 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+-- | Internal utilities and instances
+module CalamityCommands.Internal.Utils
+    ( -- whileMFinalIO
+    -- , untilJustFinalIO
+     whenJust
+    , whenM
+    , unlessM
+    , lastMaybe
+    , leftToMaybe
+    , rightToMaybe
+    , justToEither
+    , mapLeft
+    , (<<$>>)
+    , (<<*>>)
+    , (<.>)
+     ) where
+
+-- import           CalamityCommands.Internal.RunIntoIO
+
+import           Control.Applicative
+
+import           Data.Semigroup        ( Last(..) )
+
+-- import qualified Polysemy              as P
+
+-- -- | Like whileM, but stateful effects are not preserved to mitigate memory leaks
+-- --
+-- -- This means Polysemy.Error won't work to break the loop, etc.
+-- -- Instead, Error/Alternative will just result in the loop quitting.
+-- whileMFinalIO :: P.Member (P.Final IO) r => P.Sem r Bool -> P.Sem r ()
+-- whileMFinalIO action = do
+--   action' <- runSemToIO action
+--   P.embedFinal $ go action'
+--   where go action' = do
+--           r <- action'
+--           case r of
+--             Just True ->
+--               go action'
+--             _ ->
+--               pure ()
+
+-- -- | Like untilJust, but stateful effects are not preserved to mitigate memory leaks
+-- --
+-- -- This means Polysemy.Error won't work to break the loop, etc.
+-- -- Instead, Error/Alternative will just result in another loop.
+-- untilJustFinalIO :: P.Member (P.Final IO) r => P.Sem r (Maybe a) -> P.Sem r a
+-- untilJustFinalIO action = do
+--   action' <- runSemToIO action
+--   P.embedFinal $ go action'
+--   where go action' = do
+--           r <- action'
+--           case r of
+--             Just (Just a) ->
+--               pure a
+--             _ ->
+--               go action'
+
+whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m ()
+whenJust = flip $ maybe (pure ())
+
+whenM :: Monad m => m Bool -> m () -> m ()
+whenM p m = p >>= \case
+  True  -> m
+  False -> pure ()
+
+unlessM :: Monad m => m Bool -> m () -> m ()
+unlessM = whenM . (not <$>)
+
+lastMaybe :: Maybe a -> Maybe a -> Maybe a
+lastMaybe l r = getLast <$> fmap Last l <> fmap Last r
+
+mapLeft :: (a -> c) -> Either a b -> Either c b
+mapLeft f (Left e) = Left (f e)
+mapLeft _ (Right x) = Right x
+
+leftToMaybe :: Either e a -> Maybe e
+leftToMaybe (Left x) = Just x
+leftToMaybe _        = Nothing
+
+rightToMaybe :: Either e a -> Maybe a
+rightToMaybe (Right x) = Just x
+rightToMaybe _         = Nothing
+
+justToEither :: Maybe e -> Either e ()
+justToEither (Just x) = Left x
+justToEither _        = Right ()
+
+(<<$>>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)
+(<<$>>) = fmap . fmap
+
+infixl 4 <<$>>
+
+(<<*>>) :: (Applicative f, Applicative g) => f (g (a -> b)) -> f (g a) -> f (g b)
+(<<*>>) = liftA2 (<*>)
+
+infixl 4 <<*>>
+
+(<.>) :: Functor f => (a -> b) -> (c -> f a) -> (c -> f b)
+(<.>) f g x = f <$> g x
+
+infixl 4 <.>
+
diff --git a/CalamityCommands/ParsePrefix.hs b/CalamityCommands/ParsePrefix.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/ParsePrefix.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- | Command prefix parsing effect
+module CalamityCommands.ParsePrefix
+    ( ParsePrefix(..)
+    , parsePrefix
+    , useConstantPrefix ) where
+
+import qualified Data.Text.Lazy                       as L
+
+import qualified Polysemy                             as P
+
+-- | An effect for parsing the prefix of a command.
+data ParsePrefix msg m a where
+  -- | Parse a prefix in a message, returning a tuple of @(prefix, remaining message)@
+  ParsePrefix :: msg -> ParsePrefix msg m (Maybe (L.Text, L.Text))
+
+P.makeSem ''ParsePrefix
+
+-- | A default interpretation for 'ParsePrefix' that uses a single constant prefix.
+useConstantPrefix :: L.Text -> P.Sem (ParsePrefix L.Text ': r) a -> P.Sem r a
+useConstantPrefix pre = P.interpret (\case
+                                        ParsePrefix msg -> pure ((pre, ) <$> L.stripPrefix pre msg))
diff --git a/CalamityCommands/Parser.hs b/CalamityCommands/Parser.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Parser.hs
@@ -0,0 +1,256 @@
+-- | Something that can parse user input
+module CalamityCommands.Parser (
+  ParameterParser (..),
+  Named,
+  KleeneStarConcat,
+  KleenePlusConcat,
+  ParserEffs,
+  runCommandParser,
+  ParserState (..),
+  parseMP,
+) where
+
+import Control.Lens hiding (Context)
+import Control.Monad
+
+import Data.Char (isSpace)
+import Data.Generics.Labels ()
+import Data.Kind
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Semigroup
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+import Data.Typeable
+
+import GHC.Generics (Generic)
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+
+import qualified Polysemy as P
+import qualified Polysemy.Error as P
+import qualified Polysemy.Reader as P
+import qualified Polysemy.State as P
+
+import Numeric.Natural (Natural)
+import Text.Megaparsec hiding (parse)
+import Text.Megaparsec.Char
+import Text.Megaparsec.Char.Lexer (decimal, float, signed)
+
+data SpannedError = SpannedError L.Text !Int !Int
+  deriving (Show, Eq, Ord)
+
+showTypeOf :: forall a. Typeable a => String
+showTypeOf = show . typeRep $ Proxy @a
+
+{- | The current state of the parser, used so that the entire remaining input is
+ available.
+
+ This is used instead of just concatenating parsers to allow for more
+ flexibility, for example, this could be used to construct flag-style parsers
+ that parse a parameter from anywhere in the input message.
+-}
+data ParserState = ParserState
+  { -- | The current offset, or where the next parser should start parsing at
+    off :: Int
+  , -- | The input message ot parse
+    msg :: L.Text
+  }
+  deriving (Show, Generic)
+
+type ParserEffs c r =
+  ( P.State ParserState
+      ': P.Error (S.Text, L.Text) -- the current parser state
+        ': P.Reader c -- (failing parser name, error reason)
+          ': r -- context
+  )
+type ParserCtxE c r = P.Reader c ': r
+
+-- | Run a command parser, @ctx@ is the context, @t@ is the text input
+runCommandParser :: c -> L.Text -> P.Sem (ParserEffs c r) a -> P.Sem r (Either (S.Text, L.Text) a)
+runCommandParser ctx t = P.runReader ctx . P.runError . P.evalState (ParserState 0 t)
+
+-- | A typeclass for things that can be parsed as parameters to commands.
+--
+-- Any type that is an instance of ParamerParser can be used in the type level
+-- parameter @ps@ of 'CalamityCommands.Dsl.command',
+-- 'CalamityCommands.CommandUtils.buildCommand', etc.
+class Typeable a => ParameterParser (a :: Type) r where
+  type ParserResult a
+
+  type ParserResult a = a
+
+  parserName :: S.Text
+  default parserName :: S.Text
+  parserName = ":" <> S.pack (showTypeOf @a)
+
+  parse :: P.Sem (ParserEffs c r) (ParserResult a)
+
+{- | A named parameter, used to attach the name @s@ to a type in the command's
+ help output
+-}
+data Named (s :: Symbol) (a :: Type)
+
+instance (KnownSymbol s, ParameterParser a r) => ParameterParser (Named s a) r where
+  type ParserResult (Named s a) = ParserResult a
+
+  parserName = (S.pack . symbolVal $ Proxy @s) <> parserName @a @r
+
+  parse = mapE (_1 .~ parserName @(Named s a) @r) $ parse @a @r
+
+mapE :: P.Member (P.Error e) r => (e -> e) -> P.Sem r a -> P.Sem r a
+mapE f m = P.catch m (P.throw . f)
+
+-- | Parse a paremeter using a MegaParsec parser.
+--
+-- On failure this constructs a nice-looking megaparsec error for the failed parameter.
+parseMP :: S.Text -> ParsecT SpannedError L.Text (P.Sem (ParserCtxE c r)) a -> P.Sem (ParserEffs c r) a
+parseMP n m = do
+  s <- P.get
+  res <- P.raise . P.raise $ runParserT (skipN (s ^. #off) *> trackOffsets (space *> m)) "" (s ^. #msg)
+  case res of
+    Right (a, offset) -> do
+      P.modify (#off +~ offset)
+      pure a
+    Left s -> P.throw (n, L.pack $ errorBundlePretty s)
+
+instance ParameterParser L.Text r where
+  parse = parseMP (parserName @L.Text) item
+
+instance ParameterParser S.Text r where
+  parse = parseMP (parserName @S.Text) (L.toStrict <$> item)
+
+instance ParameterParser Integer r where
+  parse = parseMP (parserName @Integer) (signed mempty decimal)
+
+instance ParameterParser Natural r where
+  parse = parseMP (parserName @Natural) decimal
+
+instance ParameterParser Int r where
+  parse = parseMP (parserName @Int) (signed mempty decimal)
+
+instance ParameterParser Word r where
+  parse = parseMP (parserName @Word) decimal
+
+instance ParameterParser Float r where
+  parse = parseMP (parserName @Float) (signed mempty (try float <|> decimal))
+
+instance ParameterParser a r => ParameterParser (Maybe a) r where
+  type ParserResult (Maybe a) = Maybe (ParserResult a)
+
+  parse = P.catch (Just <$> parse @a) (const $ pure Nothing)
+
+instance (ParameterParser a r, ParameterParser b r) => ParameterParser (Either a b) r where
+  type ParserResult (Either a b) = Either (ParserResult a) (ParserResult b)
+
+  parse = do
+    l <- parse @(Maybe a) @r
+    case l of
+      Just l' -> pure (Left l')
+      Nothing ->
+        Right <$> parse @b @r
+
+instance ParameterParser a r => ParameterParser [a] r where
+  type ParserResult [a] = [ParserResult a]
+
+  parse = go []
+   where
+    go :: [ParserResult a] -> P.Sem (ParserEffs c r) [ParserResult a]
+    go l =
+      P.catch (Just <$> parse @a) (const $ pure Nothing) >>= \case
+        Just a -> go $ l <> [a]
+        Nothing -> pure l
+
+instance (ParameterParser a r, Typeable a) => ParameterParser (NonEmpty a) r where
+  type ParserResult (NonEmpty a) = NonEmpty (ParserResult a)
+
+  parse = do
+    a <- parse @a
+    as <- parse @[a]
+    pure $ a :| as
+
+{- | A parser that consumes zero or more of @a@ then concatenates them together.
+
+ @'KleeneStarConcat' 'L.Text'@ therefore consumes all remaining input.
+-}
+data KleeneStarConcat (a :: Type)
+
+instance (Monoid (ParserResult a), ParameterParser a r) => ParameterParser (KleeneStarConcat a) r where
+  type ParserResult (KleeneStarConcat a) = ParserResult a
+
+  parse = mconcat <$> parse @[a]
+
+instance {-# OVERLAPS #-} ParameterParser (KleeneStarConcat L.Text) r where
+  type ParserResult (KleeneStarConcat L.Text) = ParserResult L.Text
+
+  -- consume rest on text just takes everything remaining
+  parse = parseMP (parserName @(KleeneStarConcat L.Text)) manySingle
+
+instance {-# OVERLAPS #-} ParameterParser (KleeneStarConcat S.Text) r where
+  type ParserResult (KleeneStarConcat S.Text) = ParserResult S.Text
+
+  -- consume rest on text just takes everything remaining
+  parse = parseMP (parserName @(KleeneStarConcat S.Text)) (L.toStrict <$> manySingle)
+
+{- | A parser that consumes one or more of @a@ then concatenates them together.
+
+ @'KleenePlusConcat' 'L.Text'@ therefore consumes all remaining input.
+-}
+data KleenePlusConcat (a :: Type)
+
+instance (Semigroup (ParserResult a), ParameterParser a r) => ParameterParser (KleenePlusConcat a) r where
+  type ParserResult (KleenePlusConcat a) = ParserResult a
+
+  parse = sconcat <$> parse @(NonEmpty a)
+
+instance {-# OVERLAPS #-} ParameterParser (KleenePlusConcat L.Text) r where
+  type ParserResult (KleenePlusConcat L.Text) = ParserResult L.Text
+
+  -- consume rest on text just takes everything remaining
+  parse = parseMP (parserName @(KleenePlusConcat L.Text)) someSingle
+
+instance {-# OVERLAPS #-} ParameterParser (KleenePlusConcat S.Text) r where
+  type ParserResult (KleenePlusConcat S.Text) = ParserResult S.Text
+
+  -- consume rest on text just takes everything remaining
+  parse = parseMP (parserName @(KleenePlusConcat S.Text)) (L.toStrict <$> someSingle)
+
+instance (ParameterParser a r, ParameterParser b r) => ParameterParser (a, b) r where
+  type ParserResult (a, b) = (ParserResult a, ParserResult b)
+
+  parse = do
+    a <- parse @a
+    b <- parse @b
+    pure (a, b)
+
+instance ParameterParser () r where
+  parse = parseMP (parserName @()) space
+
+instance ShowErrorComponent SpannedError where
+  showErrorComponent (SpannedError t _ _) = L.unpack t
+  errorComponentLen (SpannedError _ s e) = max 1 $ e - s
+
+skipN :: (Stream s, Ord e) => Int -> ParsecT e s m ()
+skipN n = void $ takeP Nothing n
+
+trackOffsets :: MonadParsec e s m => m a -> m (a, Int)
+trackOffsets m = do
+  offs <- getOffset
+  a <- m
+  offe <- getOffset
+  pure (a, offe - offs)
+
+item :: MonadParsec e L.Text m => m L.Text
+item = try quotedString <|> someNonWS
+
+manySingle :: MonadParsec e s m => m (Tokens s)
+manySingle = takeWhileP (Just "Any character") (const True)
+
+someSingle :: MonadParsec e s m => m (Tokens s)
+someSingle = takeWhile1P (Just "any character") (const True)
+
+quotedString :: MonadParsec e L.Text m => m L.Text
+quotedString =
+  try (between (chunk "'") (chunk "'") (takeWhileP (Just "any character") (/= '\'')))
+    <|> between (chunk "\"") (chunk "\"") (takeWhileP (Just "any character") (/= '"'))
+
+someNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
+someNonWS = takeWhile1P (Just "any non-whitespace") (not . isSpace)
diff --git a/CalamityCommands/Utils.hs b/CalamityCommands/Utils.hs
new file mode 100644
--- /dev/null
+++ b/CalamityCommands/Utils.hs
@@ -0,0 +1,169 @@
+{-# LANGUAGE RecursiveDo #-}
+
+-- | Command handler utilities
+module CalamityCommands.Utils (
+  buildCommands,
+  processCommands,
+  handleCommands,
+  findCommand,
+  CmdInvokeFailReason (..),
+) where
+
+import CalamityCommands.AliasType
+import CalamityCommands.Command
+import CalamityCommands.CommandUtils
+import CalamityCommands.Context
+import CalamityCommands.Dsl
+import CalamityCommands.Error
+import CalamityCommands.Group
+import CalamityCommands.Handler
+import CalamityCommands.Internal.LocalWriter
+import CalamityCommands.ParsePrefix
+
+import Control.Lens hiding (Context)
+import Control.Monad.Fix (MonadFix)
+
+import Data.Char (isSpace)
+import qualified Data.HashMap.Lazy as LH
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+
+import GHC.Generics (Generic)
+
+import qualified Polysemy as P
+import qualified Polysemy.Error as P
+import qualified Polysemy.Fixpoint as P
+import qualified Polysemy.Reader as P
+import qualified Polysemy.Tagged as P
+
+mapLeft :: (e -> e') -> Either e a -> Either e' a
+mapLeft f (Left x) = Left $ f x
+mapLeft _ (Right x) = Right x
+
+data CmdInvokeFailReason c
+  = NoContext
+  | NotFound [L.Text]
+  | CommandInvokeError c CommandError
+  deriving (Show, Generic)
+
+{- | Manages parsing messages and handling commands for a CommandHandler.
+
+ Returns Nothing if the prefix didn't match.
+
+ Returns Right with the context and result if the command succeeded in parsing
+ and running, Left with the reason otherwise.
+-}
+processCommands ::
+  ( Monad m
+  , P.Members '[ParsePrefix msg, ConstructContext msg c m a, P.Embed m] r
+  , CommandContext m c a
+  ) =>
+  CommandHandler m c a ->
+  -- | The message that invoked the command
+  msg ->
+  P.Sem r (Maybe (Either (CmdInvokeFailReason c) (c, a)))
+processCommands handler msg =
+  parsePrefix msg >>= \case
+    Just (pre, cmd) -> Just <$> handleCommands handler msg pre cmd
+    Nothing -> pure Nothing
+
+{- | Manages finding the invoked command and parsing parameters for a
+   CommandHandler.
+
+ Returns Right with the context and result if the command succeeded in parsing
+ and running, Left with the reason otherwise.
+-}
+handleCommands ::
+  ( Monad m
+  , P.Members '[ConstructContext msg c m a, P.Embed m] r
+  , CommandContext m c a
+  ) =>
+  CommandHandler m c a ->
+  -- | The message that invoked the command
+  msg ->
+  -- | The prefix used
+  L.Text ->
+  -- | The command string, without a prefix
+  L.Text ->
+  P.Sem r (Either (CmdInvokeFailReason c) (c, a))
+handleCommands handler msg prefix cmd = P.runError $ do
+  (command, unparsedParams) <- P.fromEither . mapLeft NotFound $ findCommand handler cmd
+  ctx <- P.note NoContext =<< constructContext (prefix, command, unparsedParams) msg
+  r <- P.fromEither . mapLeft (CommandInvokeError ctx) =<< invokeCommand ctx (ctxCommand ctx)
+  pure (ctx, r)
+
+-- | Run a command DSL, returning the constructed 'CommandHandler'
+buildCommands ::
+  forall r c m a x.
+  (Monad m, MonadFix m, P.Member (P.Final m) r) =>
+  P.Sem (DSLState m c a r) x ->
+  P.Sem r (CommandHandler m c a, x)
+buildCommands m = P.fixpointToFinal $ mdo
+  (groups, (cmds, a)) <- inner handler m
+  let handler = CommandHandler groups cmds
+  pure (handler, a)
+ where
+  inner ::
+    CommandHandler m c a ->
+    P.Sem (DSLState m c a r) x ->
+    P.Sem
+      (P.Fixpoint ': r)
+      ( LH.HashMap S.Text (Group m c a, AliasType)
+      , (LH.HashMap S.Text (Command m c a, AliasType), x)
+      )
+  inner h =
+    P.runReader h
+      . P.runReader []
+      . P.runReader defaultHelp
+      . P.untag @"original-help"
+      . P.runReader defaultHelp
+      . P.runReader False
+      . P.untag @"hidden"
+      . P.runReader Nothing
+      . runLocalWriter @(LH.HashMap S.Text (Group m c a, AliasType))
+      . runLocalWriter @(LH.HashMap S.Text (Command m c a, AliasType))
+  defaultHelp = const "This command or group has no help."
+
+nextWord :: L.Text -> (L.Text, L.Text)
+nextWord = L.break isSpace . L.stripStart
+
+{- | Attempt to find what command was used.
+
+ On error: returns the path of existing groups that were found, so @"group0
+ group1 group2 notacommand"@ will error with @Left ["group0", "group1",
+ "group2"]@
+
+ On success: returns the command that was invoked, and the remaining text
+ after it.
+
+ This function isn't greedy, if you have a group and a command at the same
+ level, this will find the command first and ignore the group.
+-}
+findCommand :: forall c a m. CommandHandler m c a -> L.Text -> Either [L.Text] (Command m c a, L.Text)
+findCommand handler msg = goH $ nextWord msg
+ where
+  goH :: (L.Text, L.Text) -> Either [L.Text] (Command m c a, L.Text)
+  goH ("", _) = Left []
+  goH (x, xs) =
+    attachSoFar
+      x
+      ( ((,xs) <$> attachInitial (LH.lookup (L.toStrict x) (handler ^. #commands)))
+          <> (attachInitial (LH.lookup (L.toStrict x) (handler ^. #groups)) >>= goG (nextWord xs))
+      )
+
+  goG :: (L.Text, L.Text) -> Group m c a -> Either [L.Text] (Command m c a, L.Text)
+  goG ("", _) _ = Left []
+  goG (x, xs) g =
+    attachSoFar
+      x
+      ( ((,xs) <$> attachInitial (LH.lookup (L.toStrict x) (g ^. #commands)))
+          <> (attachInitial (LH.lookup (L.toStrict x) (g ^. #children)) >>= goG (nextWord xs))
+      )
+
+  attachInitial :: forall a b. Maybe (a, b) -> Either [L.Text] a
+  attachInitial (Just (a, _)) = Right a
+  attachInitial Nothing = Left []
+
+  attachSoFar :: forall a. L.Text -> Either [L.Text] a -> Either [L.Text] a
+  attachSoFar cmd (Left xs) = Left (cmd : xs)
+  attachSoFar _ r = r
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Changelog for Calamity Commands
+
+## 0.1.0.0
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Ben Simms
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,95 @@
+<h1 align="center">Calamity Commands</h1>
+
+<p align="center">
+  <a href="https://hackage.haskell.org/package/calamity-commands"><img src="https://img.shields.io/hackage/v/calamity-commands" alt="Hackage"></a>
+  <a href="https://gitlab.com/simmsb/calamity/pipelines"><img src="https://img.shields.io/gitlab/pipeline/simmsb/calamity" alt="Gitlab pipeline status"></a>
+  <a href="https://github.com/simmsb/calamity/blob/master/LICENSE"><img src="https://img.shields.io/github/license/simmsb/calamity" alt="License"></a>
+  <a href="https://hackage.haskell.org/package/calamity"><img src="https://img.shields.io/hackage-deps/v/calamity" alt="Hackage-Deps"></a>
+  <a href="https://discord.gg/NGCThCY"><img src="https://discord.com/api/guilds/754446998077178088/widget.png?style=shield" alt="Discord Invite"></a>
+</p>
+
+Calamity Commands is a Haskell library for constructing text-based commands, it
+uses [Polysemy](https://hackage.haskell.org/package/polysemy) as the core
+library for handling effects, allowing you to pick and choose how to handle
+certain features of the library.
+
+# Docs
+
+You can find documentation on hackage at: https://hackage.haskell.org/package/calamity-commands
+
+# Examples
+
+``` haskell
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedLabels #-}
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+
+{-# LANGUAGE TypeOperators #-}
+
+module Main where
+
+import Polysemy
+import CalamityCommands.Commands
+import CalamityCommands.Commands.Help
+import CalamityCommands.Commands.Context
+import CalamityCommands.Commands.ParsePrefix
+import Data.Functor.Identity
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as LT
+import qualified Data.Text.IO as T
+import qualified Data.Text.Lazy.IO as LT
+
+-- Make a command handler, we don't actually use the context and therefore this
+-- handler is generic over the context used
+h' :: CommandContext Identity c (Either LT.Text Int) => CommandHandler Identity c (Either LT.Text Int)
+h' = runIdentity . runFinal $ do
+  (h, _) <- buildCommands $ do
+        command @'[Int, Int] "add" $ \ctx a b -> pure $ Right (a + b)
+        command @'[Int, Int] "mul" $ \ctx a b -> pure $ Right (a * b)
+        helpCommand (pure . Left)
+  pure h
+
+-- To use the commands we need to provide the interpreters for
+-- 'ConstructContext' and 'ParsePrefix', the default provided ones are being
+-- used here: 'useBasicContext' which makes @ctx ~ 'BasicContext'@, and
+-- @'useConstantPrefix' "!"@ which treats any input starting with @!@ as a
+-- command.
+--
+--
+-- The 'processCommands' function can then be used to parse and invoke commands,
+-- since commands are generic over the monad they run in we use @'runIdentity' .
+-- 'runFinal' . 'embedToFinal'@ to have the commands interpreted purely.
+--
+--
+-- This function 'r' takes an input string such as "!add 1 2", and then looks up
+-- the invoked command and runs it, returning the result.
+r :: LT.Text -> Maybe (Either
+                       (CmdInvokeFailReason (BasicContext Identity (Either LT.Text Int)))
+                       (BasicContext Identity (Either LT.Text Int), Either LT.Text Int))
+r = runIdentity . runFinal . embedToFinal . useBasicContext . useConstantPrefix "!" . processCommands h'
+
+-- Then to display the result of processing the command nicely, we can use a
+-- something like this function, which prints the result of a command if one was
+-- invoked successfully, and prints the error nicely if not.
+rm :: LT.Text -> IO ()
+rm s = case r s of
+            Just (Right (_, Right r)) ->
+              print r
+
+            Just (Right (_, Left h)) ->
+              LT.putStrLn h
+
+            Just (Left (CommandInvokeError _ (ParseError t r))) ->
+              LT.putStrLn ("Parsing parameter " <> LT.fromStrict t <> " failed with reason: " <> r)
+
+            _ -> pure ()
+```
+
diff --git a/calamity-commands.cabal b/calamity-commands.cabal
new file mode 100644
--- /dev/null
+++ b/calamity-commands.cabal
@@ -0,0 +1,68 @@
+cabal-version: 1.18
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: b48980f203dd33dee1ccd3179f7b322756712c67a8734329a20a52230a7ec005
+
+name:           calamity-commands
+version:        0.1.0.0
+synopsis:       A library for declaring, parsing, and invoking text-input based commands
+description:    Please see the README on GitHub at <https://github.com/simmsb/calamity#readme>
+category:       Network, Web
+homepage:       https://github.com/simmsb/calamity
+bug-reports:    https://github.com/simmsb/calamity/issues
+author:         Ben Simms
+maintainer:     ben@bensimms.moe
+copyright:      2020 Ben Simms
+license:        MIT
+license-file:   LICENSE
+tested-with:    GHC == 8.10.4
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+extra-doc-files:
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/simmsb/calamity
+
+library
+  exposed-modules:
+      CalamityCommands
+      CalamityCommands.AliasType
+      CalamityCommands.Check
+      CalamityCommands.Command
+      CalamityCommands.CommandUtils
+      CalamityCommands.Context
+      CalamityCommands.Dsl
+      CalamityCommands.Error
+      CalamityCommands.Group
+      CalamityCommands.Handler
+      CalamityCommands.Help
+      CalamityCommands.Internal.LocalWriter
+      CalamityCommands.Internal.RunIntoM
+      CalamityCommands.Internal.Utils
+      CalamityCommands.ParsePrefix
+      CalamityCommands.Parser
+      CalamityCommands.Utils
+  other-modules:
+      Paths_calamity_commands
+  hs-source-dirs:
+      ./.
+  default-extensions: StrictData AllowAmbiguousTypes BlockArguments NoMonomorphismRestriction BangPatterns BinaryLiterals UndecidableInstances ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable DoAndIfThenElse EmptyDataDecls ExistentialQuantification FlexibleContexts FlexibleInstances FunctionalDependencies GADTs DerivingVia DerivingStrategies GeneralizedNewtypeDeriving StandaloneDeriving DeriveAnyClass InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns OverloadedStrings OverloadedLabels PartialTypeSignatures PatternGuards PolyKinds RankNTypes RecordWildCards ScopedTypeVariables TupleSections TypeFamilies TypeSynonymInstances ViewPatterns DuplicateRecordFields TypeOperators TypeApplications RoleAnnotations
+  ghc-options: -fplugin=Polysemy.Plugin -funbox-strict-fields -Wall -fno-warn-name-shadowing
+  build-depends:
+      base >=4.12 && <5
+    , generic-lens >=2.0 && <3
+    , lens >=4.18 && <5
+    , megaparsec >=8 && <10
+    , polysemy >=1.3 && <2
+    , polysemy-plugin >=0.2 && <0.4
+    , text >=1.2 && <2
+    , text-show >=3.8 && <4
+    , unordered-containers >=0.2 && <0.3
+  default-language: Haskell2010
