diff --git a/Calamity/Client/Client.hs b/Calamity/Client/Client.hs
--- a/Calamity/Client/Client.hs
+++ b/Calamity/Client/Client.hs
@@ -52,7 +52,6 @@
 import           Data.Proxy
 import qualified Data.Text                         as S
 import           Data.Time.Clock.POSIX
-import           Data.Typeable
 
 import qualified Di.Core                           as DC
 import qualified Df1
@@ -156,7 +155,9 @@
 -- Reacting to a custom event:
 --
 -- @
--- 'react' @(\''CustomEvt' "my-event" ('Data.Text.Text', 'Message')) $ \\(s, m) ->
+-- data MyCustomEvt = MyCustomEvt 'Data.Text.Text' 'Message'
+--
+-- 'react' @(\''CustomEvt' MyCustomEvt) $ \\(MyCustomEvt s m) ->
 --    'void' $ 'Calamity.Types.Tellable.tell' @'Data.Text.Text' m ("Somebody told me to tell you about: " '<>' s)
 -- @
 --
@@ -196,20 +197,15 @@
 
 -- | Build a Custom CalamityEvent
 --
--- You'll probably want @TypeApplications@ to specify the type of @s@.
---
--- The types of @s@ and @a@ must match up with the event handler you want to
--- receive it.
+-- The type of @a@ must match up with the event handler you want to receive it.
 --
 -- ==== Examples
 --
--- Building an event named \"my-event\":
---
 -- @
--- 'customEvt' @"my-event" ("aha" :: 'Data.Text.Text', msg)
+-- 'customEvt' (MyCustomEvent "lol")
 -- @
-customEvt :: forall s a. (Typeable s, Typeable a) => a -> CalamityEvent
-customEvt x = Custom (typeRep $ Proxy @s) (toDyn x)
+customEvt :: forall a. Typeable a => a -> CalamityEvent
+customEvt = Custom
 
 -- | Get a copy of the event stream.
 events :: BotC r => P.Sem r (OutChan CalamityEvent)
@@ -330,16 +326,15 @@
     !evt' <- P.embed $ readChan outc
     case evt' of
       Dispatch !sid !evt -> handleEvent sid evt >> pure True
-      Custom !s !d       -> handleCustomEvent s d >> pure True
+      Custom d           -> handleCustomEvent d >> pure True
       ShutDown           -> pure False
   debug "leaving client loop"
 
-handleCustomEvent :: forall r. BotC r => TypeRep -> Dynamic -> P.Sem r ()
-handleCustomEvent s d = do
-  debug $ "handling a custom event, s: " +|| s ||+ ", d: " +|| d ||+ ""
+handleCustomEvent :: forall a r. (Typeable a, BotC r) => a -> P.Sem r ()
+handleCustomEvent d = do
   eventHandlers <- P.atomicGet
 
-  let handlers = getCustomEventHandlers s (dynTypeRep d) eventHandlers
+  let handlers = getCustomEventHandlers @a eventHandlers
 
   for_ handlers (\h -> P.async . P.embed $ h d)
 
diff --git a/Calamity/Client/Types.hs b/Calamity/Client/Types.hs
--- a/Calamity/Client/Types.hs
+++ b/Calamity/Client/Types.hs
@@ -38,14 +38,12 @@
 
 import Data.Default.Class
 import Data.Dynamic
-import qualified Data.HashMap.Lazy as LH
 import Data.IORef
 import Data.Maybe
 import Data.Time
 import Data.TypeRepMap (TypeRepMap, WrapTypeable (..))
 import qualified Data.TypeRepMap as TM
 import Data.Typeable
-import Data.Void
 
 import GHC.Exts (fromList)
 import GHC.Generics
@@ -55,10 +53,12 @@
 import qualified Polysemy.AtomicState as P
 import qualified Polysemy.Reader as P
 
-import qualified Df1 as Df1
+import qualified Df1
 import qualified Di.Core as DC
 import TextShow
 import qualified TextShow.Generic as TSG
+import Data.Kind (Type)
+import Data.Void (Void)
 
 data Client = Client
   { shards :: TVar [(InChan ControlMessage, Async (Maybe ()))]
@@ -72,6 +72,7 @@
   }
   deriving (Generic)
 
+-- | Constraints required by the bot client
 type BotC r =
   ( P.Members
       '[ LogEff
@@ -167,8 +168,9 @@
   | UserUpdateEvt
   | -- | Sent when someone joins/leaves/moves voice channels
     VoiceStateUpdateEvt
-  | -- | A custom event, @s@ is the name and @a@ is the data sent to the handler
-    forall s a. CustomEvt s a
+  | -- | A custom event, @a@ is the data sent to the handler and should probably
+    -- be a newtype to disambiguate events
+    forall (a :: Type). CustomEvt a
 
 data GuildCreateStatus
   = -- | The guild was just joined
@@ -227,19 +229,24 @@
   EHType 'TypingStartEvt = (Channel, Snowflake User, UnixTimestamp)
   EHType 'UserUpdateEvt = (User, User)
   EHType 'VoiceStateUpdateEvt = (Maybe VoiceState, VoiceState)
-  EHType ( 'CustomEvt s a) = a
+  EHType ( 'CustomEvt a) = a
 
 type StoredEHType t = EHType t -> IO ()
 
 newtype EventHandlers = EventHandlers (TypeRepMap EventHandler)
 
-data EventHandlerWithID a = EventHandlerWithID
+data EventHandlerWithID (a :: Type) = EventHandlerWithID
   { ehID :: Integer
   , eh :: a
   }
 
+newtype CustomEHTypeStorage (a :: Type) = CustomEHTypeStorage
+  { unwrapCustomEHTypeStorage :: [EventHandlerWithID (a -> IO ())]
+  }
+  deriving newtype ( Monoid, Semigroup )
+
 type family EHStorageType (t :: EventType) where
-  EHStorageType ( 'CustomEvt s a) = LH.HashMap TypeRep (LH.HashMap TypeRep [EventHandlerWithID (StoredEHType ( 'CustomEvt s a))])
+  EHStorageType ( 'CustomEvt _) = TypeRepMap CustomEHTypeStorage
   EHStorageType t = [EventHandlerWithID (StoredEHType t)]
 
 newtype EventHandler (t :: EventType) = EH
@@ -284,7 +291,7 @@
         , WrapTypeable $ EH @'MessageReactionRemoveAllEvt []
         , WrapTypeable $ EH @'TypingStartEvt []
         , WrapTypeable $ EH @'UserUpdateEvt []
-        , WrapTypeable $ EH @('CustomEvt Void Dynamic) LH.empty
+        , WrapTypeable $ EH @('CustomEvt Void) TM.empty
         ]
 
 instance Semigroup EventHandlers where
@@ -296,18 +303,13 @@
 -- not sure what to think of this
 
 type family EHInstanceSelector (d :: EventType) :: Bool where
-  EHInstanceSelector ( 'CustomEvt _ _) = 'True
+  EHInstanceSelector ( 'CustomEvt _) = 'True
   EHInstanceSelector _ = 'False
 
-fromDynamicJust :: forall a. Typeable a => Dynamic -> a
-fromDynamicJust d = case fromDynamic d of
-  Just x -> x
-  Nothing -> error $ "Extracting dynamic failed, wanted: " <> (show . typeRep $ Proxy @a) <> ", got: " <> (show $ dynTypeRep d)
-
 {- | A helper typeclass that is used to decide how to register regular
  events, and custom events which require storing in a map at runtime.
 -}
-class InsertEventHandler a where
+class InsertEventHandler (a :: EventType) where
   makeEventHandlers :: Proxy a -> Integer -> StoredEHType a -> EventHandlers
 
 instance (EHInstanceSelector a ~ flag, InsertEventHandler' flag a) => InsertEventHandler a where
@@ -316,17 +318,11 @@
 class InsertEventHandler' (flag :: Bool) a where
   makeEventHandlers' :: Proxy flag -> Proxy a -> Integer -> StoredEHType a -> EventHandlers
 
-intoDynFn :: forall a. Typeable a => (a -> IO ()) -> (Dynamic -> IO ())
-intoDynFn fn = \d -> fn $ fromDynamicJust d
-
-instance
-  (Typeable a, Typeable s, Typeable (StoredEHType ( 'CustomEvt s a)), (EHType ( 'CustomEvt s a) -> IO ()) ~ (a -> IO ())) =>
-  InsertEventHandler' 'True ( 'CustomEvt s a)
-  where
+instance forall (x :: Type). (Typeable (EHType ('CustomEvt x))) => InsertEventHandler' 'True ( 'CustomEvt x) where
   makeEventHandlers' _ _ id' handler =
     EventHandlers . TM.one $
-      EH @( 'CustomEvt Void Dynamic)
-        (LH.singleton (typeRep $ Proxy @s) (LH.singleton (typeRep $ Proxy @a) [EventHandlerWithID id' (intoDynFn handler)]))
+      EH @( 'CustomEvt Void)
+        (TM.one @x $ CustomEHTypeStorage [EventHandlerWithID id' handler])
 
 instance (Typeable s, EHStorageType s ~ [EventHandlerWithID (StoredEHType s)], Typeable (StoredEHType s)) => InsertEventHandler' 'False s where
   makeEventHandlers' _ _ id' handler = EventHandlers . TM.one $ EH @s [EventHandlerWithID id' handler]
@@ -340,7 +336,7 @@
 class GetEventHandlers' (flag :: Bool) a where
   getEventHandlers' :: Proxy a -> Proxy flag -> EventHandlers -> [StoredEHType a]
 
-instance GetEventHandlers' 'True ( 'CustomEvt s a) where
+instance GetEventHandlers' 'True ( 'CustomEvt a) where
   getEventHandlers' _ _ _ = error "use getCustomEventHandlers instead"
 
 instance (Typeable s, Typeable (StoredEHType s), EHStorageType s ~ [EventHandlerWithID (StoredEHType s)]) => GetEventHandlers' 'False s where
@@ -357,15 +353,14 @@
 class RemoveEventHandler' (flag :: Bool) a where
   removeEventHandler' :: Proxy flag -> Proxy a -> Integer -> EventHandlers -> EventHandlers
 
-instance (Typeable s, Typeable a) => RemoveEventHandler' 'True ( 'CustomEvt s a) where
+instance forall (a :: Type). (Typeable a) => RemoveEventHandler' 'True ( 'CustomEvt a) where
   removeEventHandler' _ _ id' (EventHandlers handlers) =
     EventHandlers $
-      TM.adjust @( 'CustomEvt Void Dynamic)
+      TM.adjust @( 'CustomEvt Void)
         ( \(EH ehs) ->
             EH
-              ( LH.update
-                  (Just . LH.update (Just . filter ((/= id') . ehID)) (typeRep $ Proxy @a))
-                  (typeRep $ Proxy @s)
+              ( TM.adjust @a
+                  (CustomEHTypeStorage . filter ((/= id') . ehID) . unwrapCustomEHTypeStorage)
                   ehs
               )
         )
@@ -381,11 +376,9 @@
         (\(EH ehs) -> EH $ filter ((/= id') . ehID) ehs)
         handlers
 
-getCustomEventHandlers :: TypeRep -> TypeRep -> EventHandlers -> [Dynamic -> IO ()]
-getCustomEventHandlers s a (EventHandlers handlers) =
+getCustomEventHandlers :: forall a. Typeable a => EventHandlers -> [a -> IO ()]
+getCustomEventHandlers (EventHandlers handlers) =
   let handlerMap =
-        unwrapEventHandler @( 'CustomEvt Void Dynamic) $
-          fromMaybe
-            mempty
-            (TM.lookup handlers :: Maybe (EventHandler ( 'CustomEvt Void Dynamic)))
-   in map eh . concat $ LH.lookup s handlerMap >>= LH.lookup a
+        unwrapEventHandler @( 'CustomEvt Void) $
+          fromMaybe mempty (TM.lookup handlers :: Maybe (EventHandler ( 'CustomEvt Void)))
+   in maybe mempty (map eh . unwrapCustomEHTypeStorage) $ TM.lookup @a handlerMap
diff --git a/Calamity/Commands.hs b/Calamity/Commands.hs
--- a/Calamity/Commands.hs
+++ b/Calamity/Commands.hs
@@ -1,78 +1,101 @@
--- | Calamity commands
--- This module exports the DSL and core types for using commands
-module Calamity.Commands
-    ( module Calamity.Commands.Dsl
-    , module Calamity.Commands.Error
-    , module Calamity.Commands.Handler
-    , module Calamity.Commands.Utils
-    , module Calamity.Commands.ParsePrefix
-    , module Calamity.Commands.Parser
-    , module Calamity.Commands.Help
-    , module Calamity.Commands.Context
+{- | Calamity commands
+ This module exports the DSL and core types for using commands
+-}
+module Calamity.Commands (
+    module Calamity.Commands.Dsl,
+    module CalamityCommands.Error,
+    module CalamityCommands.Handler,
+    module Calamity.Commands.Help,
+    module CalamityCommands.ParsePrefix,
+    module CalamityCommands.Parser,
+    module Calamity.Commands.Utils,
+
     -- * Commands
     -- $commandDocs
-    ) where
+) where
 
-import           Calamity.Commands.Dsl
-import           Calamity.Commands.Error
-import           Calamity.Commands.Handler
-import           Calamity.Commands.Help
-import           Calamity.Commands.ParsePrefix
-import           Calamity.Commands.Parser
-import           Calamity.Commands.Utils
-import           Calamity.Commands.Context ( Context )
+import Calamity.Commands.CalamityParsers ()
+import Calamity.Commands.Dsl
+import Calamity.Commands.Help
+import Calamity.Commands.Utils
+import CalamityCommands.Error
+import CalamityCommands.Handler
+import CalamityCommands.ParsePrefix
+import CalamityCommands.Parser
 
--- $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 "Calamity.Commands.Dsl" for constructing commands
--- declaratively.
---
--- You can implement 'Parser' for your own types to allow them to be used in the
--- parameter list of commands.
---
--- A default help command is provided in "Calamity.Commands.Help" which can be
--- added just by using 'helpCommand' inside the command declaration DSL.
---
---
--- ==== Custom Events
---
--- The event handler registered by 'addCommands' will fire the following custom events:
---
---     1. @"command-error" ('Context', 'CommandError')@
---
---         Fired when a command returns an error.
---
---     2. @"command-not-found" ('Calamity.Types.Model.Channel.Message', ['Data.Text.Lazy.Text'])@
---
---         Fired when a valid prefix is used, but the command is not found.
---
---     3. @"command-invoked" 'Context'@
---
---         Fired when a command is successfully invoked.
---
--- ==== Registered Metrics
---
---     1. Counter: @"commands_invoked" [name]@
---
---         Incremented on each command invokation, the parameter @name@ is the
---         path/name of the invoked command.
---
---
--- ==== Examples
---
--- An example of using commands:
---
--- @
--- 'addCommands' $ do
---   'helpCommand'
---   'command' \@\'['Calamity.Types.Model.User.User'] "utest" $ \\ctx u \-\> do
---     'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx $ "got user: " '<>' 'TextShow.showtl' u
---   'group' "admin" $ do
---     'command' \@'[] "bye" $ \\ctx -> do
---       'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx "bye!"
---       'Calamity.Client.stopBot'
--- @
+{- $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 "Calamity.Commands.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 "Calamity.Commands.Help" which can be
+ added just by using 'helpCommand' inside the command declaration DSL.
+
+ This module is pretty much a wrapper/re-export of the package
+ "CalamityCommands", look there for more documentation.
+
+ To decide which context type you want to use, and how the command prefix
+ should be parsed, you need to handle the following effects:
+
+     1. 'CalamityCommands.ParsePrefix.ParsePrefix'
+
+         Handles parsing prefixes, "CalamityCommands" provides the
+         'CalamityCommands.ParsePrefix.useConstantPrefix' function for constant
+         prefixes.
+
+     2. 'CalamityCommands.Context.ConstructContext'
+
+         Handles constructing the context and also decides which context is
+         going to be used, calamity offers
+         'Calamity.Commands.Context.useFullContext' which makes the context
+         'Calamity.Commands.Context.FullContext', and
+         'Calamity.Commands.Context.useLightContext' which makes the context
+         'Calamity.Commands.Context.LightContext'.
+
+ ==== Custom Events
+
+ The event handler registered by 'addCommands' will fire the following custom events:
+
+     1. 'Calamity.Commands.Utils.CtxCommandError'
+
+         Fired when a command returns an error.
+
+     2. 'Calamity.Commands.Utils.CommandNotFound'
+
+         Fired when a valid prefix is used, but the command is not found.
+
+     3. 'Calamity.Commands.Utils.CommandInvoked'
+
+         Fired when a command is successfully invoked.
+
+
+ ==== Registered Metrics
+
+     1. Counter: @"commands_invoked" [name]@
+
+         Incremented on each command invokation, the parameter @name@ is the
+         path/name of the invoked command.
+
+
+ ==== Examples
+
+ An example of using commands:
+
+ @
+ 'addCommands' $ do
+   'helpCommand'
+   'command' \@\'['Calamity.Types.Model.User.User'] "utest" $ \\ctx u \-\> do
+     'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx $ "got user: " '<>' 'TextShow.showtl' u
+   'group' "admin" $ do
+     'command' \@'[] "bye" $ \\ctx -> do
+       'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx "bye!"
+       'Calamity.Client.stopBot'
+ @
+-}
diff --git a/Calamity/Commands/AliasType.hs b/Calamity/Commands/AliasType.hs
deleted file mode 100644
--- a/Calamity/Commands/AliasType.hs
+++ /dev/null
@@ -1,15 +0,0 @@
--- | Named boolean for determining if something is an alias or not
-module Calamity.Commands.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/Calamity/Commands/CalamityParsers.hs b/Calamity/Commands/CalamityParsers.hs
new file mode 100644
--- /dev/null
+++ b/Calamity/Commands/CalamityParsers.hs
@@ -0,0 +1,189 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+-- | 'ParameterParser' instances for calamity models
+module Calamity.Commands.CalamityParsers () where
+
+import Calamity.Cache.Eff
+import Calamity.Commands.Context
+import Calamity.Types.Model.Channel (Channel, GuildChannel)
+import Calamity.Types.Model.Guild (Emoji, Guild, Member, Partial (PartialEmoji), RawEmoji (..), Role)
+import Calamity.Types.Model.User (User)
+import Calamity.Types.Partial
+import Calamity.Types.Snowflake
+import CalamityCommands.ParameterInfo
+import CalamityCommands.Parser
+import Control.Lens hiding (Context)
+import Control.Monad
+import Control.Monad.Trans (lift)
+import Data.Maybe (fromMaybe, isJust)
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+import Data.Typeable
+import qualified Polysemy as P
+import qualified Polysemy.Reader as P
+import Text.Megaparsec hiding (parse)
+import Text.Megaparsec.Char.Lexer (decimal)
+import Text.Megaparsec.Error.Builder (errFancy, fancy)
+
+parserName :: forall a c r. ParameterParser a c r => S.Text
+parserName = let ParameterInfo (fromMaybe "" -> name) type_ _ = parameterInfo @a @c @r
+              in name <> ":" <> S.pack (show type_)
+
+instance Typeable (Snowflake a) => ParameterParser (Snowflake a) c r where
+  parse = parseMP (parserName @(Snowflake a)) snowflake
+  parameterDescription = "discord id"
+
+-- | Accepts both plain IDs and mentions
+instance {-# OVERLAPS #-}ParameterParser (Snowflake User) c r where
+  parse = parseMP (parserName @(Snowflake User)) (try (ping "@") <|> snowflake)
+  parameterDescription = "user mention or id"
+
+-- | Accepts both plain IDs and mentions
+instance {-# OVERLAPS #-}ParameterParser (Snowflake Member) c r where
+  parse = parseMP (parserName @(Snowflake Member)) (try (ping "@") <|> snowflake)
+  parameterDescription = "user mention or id"
+
+-- | Accepts both plain IDs and mentions
+instance {-# OVERLAPS #-}ParameterParser (Snowflake Channel) c r where
+  parse = parseMP (parserName @(Snowflake Channel)) (try (ping "#") <|> snowflake)
+  parameterDescription = "channel mention or id"
+
+-- | Accepts both plain IDs and mentions
+instance {-# OVERLAPS #-}ParameterParser (Snowflake Role) c r where
+  parse = parseMP (parserName @(Snowflake Role)) (try (ping "@&") <|> snowflake)
+  parameterDescription = "role mention or id"
+
+-- | Accepts both plain IDs and uses of emoji
+instance {-# OVERLAPS #-}ParameterParser (Snowflake Emoji) c r where
+  parse = parseMP (parserName @(Snowflake Emoji)) (try emoji <|> snowflake)
+  parameterDescription = "emoji or id"
+
+mapParserMaybeM :: (Monad m, Stream s) => ParsecT SpannedError s m a -> L.Text -> (a -> m (Maybe b)) -> ParsecT SpannedError s m b
+mapParserMaybeM m e f = do
+  offs <- getOffset
+  r <- m >>= lift . f
+  offe <- getOffset
+  case r of
+    Just r' -> pure r'
+    Nothing -> parseError . errFancy offs . fancy . ErrorCustom $ SpannedError e offs offe
+
+-- | ParameterParser for members in the guild the command was invoked in, this only looks
+-- in the cache. Use @'Snowflake' 'Member'@ and use
+-- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
+instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Member c r where
+  parse = parseMP (parserName @Member @c @r) $ mapParserMaybeM (try (ping "@") <|> snowflake)
+          "Couldn't find a Member with this id"
+          (\mid -> do
+              ctx <- P.ask
+              guild <- join <$> getGuild `traverse` ctxGuildID ctx
+              pure $ guild ^? _Just . #members . ix mid)
+  parameterDescription = "user mention or id"
+
+-- | ParameterParser for users, this only looks in the cache. Use @'Snowflake'
+-- 'User'@ and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow
+-- fetching from http.
+instance P.Member CacheEff r => ParameterParser User c r where
+  parse = parseMP (parserName @User @c @r) $ mapParserMaybeM (try (ping "@") <|> snowflake)
+          "Couldn't find a User with this id"
+          getUser
+  parameterDescription = "user mention or id"
+
+-- | ParameterParser for channels in the guild the command was invoked in, this only
+-- looks in the cache. Use @'Snowflake' 'Channel'@ and use
+-- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
+instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser GuildChannel c r where
+  parse = parseMP (parserName @GuildChannel @c @r) $ mapParserMaybeM (try (ping "#") <|> snowflake)
+          "Couldn't find a GuildChannel with this id"
+          (\cid -> do
+              ctx <- P.ask
+              guild <- join <$> getGuild `traverse` ctxGuildID ctx
+              pure $ guild ^? _Just . #channels . ix cid)
+  parameterDescription = "channel mention or id"
+
+-- | ParameterParser for guilds, this only looks in the cache. Use @'Snowflake' 'Guild'@
+-- and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching
+-- from http.
+instance P.Member CacheEff r => ParameterParser Guild c r where
+  parse = parseMP (parserName @Guild @c @r) $ mapParserMaybeM snowflake
+          "Couldn't find a Guild with this id"
+          getGuild
+  parameterDescription = "guild id"
+
+-- | ParameterParser for emojis in the guild the command was invoked in, this only
+-- looks in the cache. Use @'Snowflake' 'Emoji'@ and use
+-- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
+instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Emoji c r where
+  parse = parseMP (parserName @Emoji @c @r) $ mapParserMaybeM (try emoji <|> snowflake)
+          "Couldn't find an Emoji with this id"
+          (\eid -> do
+              ctx <- P.ask
+              guild <- join <$> getGuild `traverse` ctxGuildID ctx
+              pure $ guild ^? _Just . #emojis . ix eid)
+  parameterDescription = "emoji or id"
+
+-- | Parses both discord emojis, and unicode emojis
+instance ParameterParser RawEmoji c r where
+  parse = parseMP (parserName @RawEmoji) (try parseCustomEmoji <|> UnicodeEmoji <$> takeP (Just "A unicode emoji") 1)
+    where parseCustomEmoji = CustomEmoji <$> partialEmoji
+  parameterDescription = "emoji"
+
+-- | ParameterParser for roles in the guild the command was invoked in, this only
+-- looks in the cache. Use @'Snowflake' 'Role'@ and use
+-- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
+instance (P.Member CacheEff r, CalamityCommandContext c) => ParameterParser Role c r where
+  parse = parseMP (parserName @Role @c @r) $ mapParserMaybeM (try (ping "@&") <|> snowflake)
+          "Couldn't find an Emoji with this id"
+          (\rid -> do
+              ctx <- P.ask
+              guild <- join <$> getGuild `traverse` ctxGuildID ctx
+              pure $ guild ^? _Just . #roles . ix rid)
+  parameterDescription = "role mention or id"
+
+-- skipN :: (Stream s, Ord e) => Int -> ParsecT e s m ()
+-- skipN n = void $ takeP Nothing n
+
+ping :: MonadParsec e L.Text m => L.Text -> m (Snowflake a)
+ping c = chunk ("<" <> c) *> optional (chunk "!") *> snowflake <* chunk ">"
+
+ping' :: MonadParsec e L.Text m => m () -> m (Snowflake a)
+ping' m = chunk "<" *> m *> snowflake <* chunk ">"
+
+snowflake :: MonadParsec e L.Text m => m (Snowflake a)
+snowflake = Snowflake <$> decimal
+
+partialEmoji :: MonadParsec e L.Text m => m (Partial Emoji)
+partialEmoji = do
+  animated <- isJust <$> (chunk "<" *> optional (chunk "a"))
+  name <-  between (chunk ":") (chunk ":") (takeWhileP (Just "Emoji name") (/= ':'))
+  id <- snowflake
+  void $ chunk ">"
+  pure (PartialEmoji id name animated)
+
+emoji :: MonadParsec e L.Text m => m (Snowflake a)
+emoji = ping' (optional (chunk "a") *> between (chunk ":") (chunk ":") (void $ takeWhileP Nothing (/= ':')))
+
+-- 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") (/= '"'))
+
+-- -- manyNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
+-- -- manyNonWS = takeWhileP (Just "Any Non-Whitespace") (not . isSpace)
+
+-- someNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
+-- someNonWS = takeWhile1P (Just "any non-whitespace") (not . isSpace)
diff --git a/Calamity/Commands/Check.hs b/Calamity/Commands/Check.hs
deleted file mode 100644
--- a/Calamity/Commands/Check.hs
+++ /dev/null
@@ -1,51 +0,0 @@
--- | Command invokation preconditions
-module Calamity.Commands.Check
-    ( Check(..)
-    , buildCheck
-    , buildCheckPure
-    , runCheck ) where
-
-import {-# SOURCE #-} Calamity.Commands.Context
-import           Calamity.Commands.Error
-import           Calamity.Internal.RunIntoIO
-import           Calamity.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 = MkCheck
-  { name     :: S.Text
-    -- ^ The name of the check.
-  , callback :: Context -> IO (Maybe L.Text)
-    -- ^ The callback for the check, returns Nothing if it passes, otherwise
-    -- returns the reason for it not passing.
-  }
-  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 IO action.
-buildCheck :: P.Member (P.Final IO) r => S.Text -> (Context -> P.Sem r (Maybe L.Text)) -> P.Sem r Check
-buildCheck name cb = do
-  cb' <- bindSemToIO 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 :: S.Text -> (Context -> Maybe L.Text) -> Check
-buildCheckPure name cb = MkCheck name (pure . cb)
-
--- | Given an invokation 'Context', run a check and transform the result into an
--- @'Either' 'CommandError' ()@.
-runCheck :: P.Member (P.Embed IO) r => Context -> Check -> P.Sem r (Either CommandError ())
-runCheck ctx chk = P.embed (callback chk ctx) <&> justToEither . (CheckError (chk ^. #name) <$>)
diff --git a/Calamity/Commands/Command.hs b/Calamity/Commands/Command.hs
deleted file mode 100644
--- a/Calamity/Commands/Command.hs
+++ /dev/null
@@ -1,71 +0,0 @@
--- | Commands and stuff
-module Calamity.Commands.Command
-    ( Command(..) ) where
-
-import           Calamity.Commands.Check
-import           Calamity.Commands.Context
-import           Calamity.Commands.Error
-import           Calamity.Commands.Group
-
-import           Control.Lens              hiding ( (<.>), Context )
-
-import           Data.List.NonEmpty        ( NonEmpty )
-import           Data.Text                 as S
-import           Data.Text.Lazy            as L
-
-import           GHC.Generics
-
-import           TextShow
-import qualified TextShow.Generic          as TSG
-import qualified Data.List.NonEmpty as NE
-
--- | A command
-data Command = forall a. Command
-  { names    :: NonEmpty S.Text
-  , parent   :: Maybe Group
-  , hidden   :: Bool
-    -- ^ If this command is hidden
-  , checks   :: [Check]
-    -- ^ A list of checks that must pass for this command to be invoked
-  , params   :: [S.Text]
-    -- ^ A list of the parameters the command takes, only used for constructing
-    -- help messages.
-  , help     :: Context -> L.Text
-    -- ^ A function producing the \'help\' for the command.
-  , parser   :: Context -> IO (Either CommandError a)
-    -- ^ A function that parses the context for the command, producing the input
-    -- @a@ for the command.
-  , callback :: (Context, a) -> IO (Maybe L.Text)
-    -- ^ A function that given the context and the input (@a@) of the command,
-    -- performs the action of the command.
-  }
-
-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 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 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/Calamity/Commands/Command.hs-boot b/Calamity/Commands/Command.hs-boot
deleted file mode 100644
--- a/Calamity/Commands/Command.hs-boot
+++ /dev/null
@@ -1,11 +0,0 @@
--- | Commands and stuff
-module Calamity.Commands.Command
-    ( Command
-    ) where
-
-import TextShow
-
-data Command
-
-instance Show Command
-instance TextShow Command
diff --git a/Calamity/Commands/CommandUtils.hs b/Calamity/Commands/CommandUtils.hs
deleted file mode 100644
--- a/Calamity/Commands/CommandUtils.hs
+++ /dev/null
@@ -1,212 +0,0 @@
--- | Command utilities
-module Calamity.Commands.CommandUtils
-    ( TypedCommandC
-    , CommandForParsers
-    , buildCommand
-    , buildCommand'
-    , buildParser
-    , buildCallback
-    , runCommand
-    , invokeCommand
-    , groupPath
-    , commandPath
-    , commandParams ) where
-
-import           Calamity.Commands.Check
-import           Calamity.Commands.Command
-import           Calamity.Commands.Context
-import           Calamity.Commands.Error
-import           Calamity.Commands.Group
-import           Calamity.Commands.Parser
-import           Calamity.Internal.RunIntoIO
-import           Calamity.Internal.Utils
-
-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 -> [S.Text]
-groupPath Group { names, parent } = maybe [] groupPath parent ++ [NE.head names]
-
-commandPath :: Command -> [S.Text]
-commandPath Command { names, parent } = maybe [] groupPath parent ++ [NE.head names]
-
--- | Format a command's parameters
-commandParams :: Command -> 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 IO
--- actions.
-buildCommand' :: P.Member (P.Final IO) r
-              => NonEmpty S.Text
-              -- ^ The name (and aliases) of the command
-              -> Maybe Group
-              -- ^ The parent group of the command
-              -> Bool
-              -- ^ If the command is hidden
-              -> [Check]
-              -- ^ The checks for the command
-              -> [S.Text]
-              -- ^ The names of the command's parameters
-              -> (Context -> L.Text)
-              -- ^ The help generator for this command
-              -> (Context -> P.Sem r (Either CommandError a))
-              -- ^ The parser for this command
-              -> ((Context, a) -> P.Sem (P.Fail ': r) ())
-              -- ^ The callback for this command
-              -> P.Sem r Command
-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 bans a user by id.
---
--- @
--- 'buildCommand' @\'['Named' "user" ('Snowflake' 'User'), 'Named' "reason" ('KleeneStarConcat' 'S.Text')]
---    "ban" 'Nothing' [] ('const' "Ban a user") $ \\ctx uid r -> case (ctx 'Control.Lens.^.' #guild) of
---      'Just' guild -> do
---        'Control.Monad.void' . 'Calamity.HTTP.invoke' $ 'Calamity.HTTP.Guild.CreateGuildBan' guild uid ('Calamity.HTTP.Guild.CreateGuildBanData' 'Nothing' $ 'Just' r)
---        'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx ("Banned user `" '<>' 'TextShow.showt' uid '<>' "` with reason: " '<>' r)
---      'Nothing' -> 'void' $ 'Calamity.Types.Tellable.tell' @'L.Text' ctx "Can only ban users from guilds."
--- @
-buildCommand :: forall ps r.
-             (P.Member (P.Final IO) r, TypedCommandC ps r)
-             => NonEmpty S.Text
-             -- ^ The name (and aliases) of the command
-             -> Maybe Group
-             -- ^ The parent group of the command
-             -> Bool
-             -- ^ If the command is hidden
-             -> [Check]
-             -- ^ The checks for the command
-             -> (Context -> L.Text)
-             -- ^ The help generator for this command
-             -> (Context -> CommandForParsers ps r)
-             -- ^ The callback foor this command
-             -> P.Sem r Command
-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
--- IO action.
-buildParser :: P.Member (P.Final IO) r
-            => S.Text
-            -> (Context -> P.Sem r (Either CommandError a))
-            -> P.Sem r (Context -> IO (Either CommandError a))
-buildParser name cb = do
-  cb' <- bindSemToIO 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 IO action.
-buildCallback
-  :: P.Member (P.Final IO) r => ((Context, a) -> P.Sem (P.Fail ': r) ()) -> P.Sem r ((Context, a) -> IO (Maybe L.Text))
-buildCallback cb = do
-  cb' <- bindSemToIO (\x -> P.runFail (cb x) <&> \case
-                        Left e  -> Just $ L.pack e
-                        Right _ -> Nothing)
-  let cb'' = fromMaybe (Just "failed internally") <.> cb'
-  pure cb''
-
--- | Given an invokation 'Context', run a command. This does not perform the command's checks.
-runCommand :: P.Member (P.Embed IO) r => Context -> Command -> P.Sem r (Either CommandError ())
-runCommand ctx Command { names = name :| _, parser, callback } = P.embed (parser ctx) >>= \case
-  Left e   -> pure $ Left e
-  Right p' -> P.embed (callback (ctx, p')) <&> justToEither . (InvokeError name <$>)
-
--- | Given an invokation 'Context', first run all of the command's checks, then
--- run the command if they all pass.
-invokeCommand :: P.Member (P.Embed IO) r => Context -> Command -> P.Sem r (Either CommandError ())
-invokeCommand ctx cmd@Command { checks } = P.runError $ do
-  for_ checks (P.fromEither <=< runCheck ctx)
-  P.fromEither =<< runCommand ctx cmd
-
-type CommandSemType r = P.Sem (P.Fail ': r) ()
-
--- | Some constraints used for making parameter typed commands work
-type TypedCommandC ps r =
-  ( ApplyTupRes (ParserResult (ListToTup ps)) (CommandSemType r) ~ CommandForParsers ps r
-  , Parser (ListToTup ps) r
-  , ApplyTup (ParserResult (ListToTup ps)) (CommandSemType r)
-  , ParamNamesForParsers ps r
-  )
-
-buildTypedCommand
-  :: forall (ps :: [Type]) a r.
-  (TypedCommandC ps r, a ~ ParserResult (ListToTup ps))
-  => (Context -> CommandForParsers ps r)
-  -> ( Context
-         -> P.Sem r (Either CommandError a)
-     , (Context, a)
-         -> P.Sem (P.Fail ': r) ())
-buildTypedCommand cmd = let parser ctx = buildTypedCommandParser @ps ctx (ctx ^. #unparsedParams)
-                            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 (Parser 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]) r. Parser (ListToTup ps) r => Context -> 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', 'Calamity.Types.Snowflake' 'Calamity.Types.User', 'Calamity.Commands.Parser.Named' "something" 'L.Text' ] r ~
---   ('L.Text' -> 'Calamity.Types.Snowflake' 'Calamity.Types.User' -> 'L.Text' -> 'P.Sem' r ('P.Fail' ': r) ())
--- @
-type family CommandForParsers (ps :: [Type]) r where
-  CommandForParsers '[] r = P.Sem (P.Fail ': r) ()
-  CommandForParsers (x ': xs) r = ParserResult x -> CommandForParsers xs r
diff --git a/Calamity/Commands/Context.hs b/Calamity/Commands/Context.hs
--- a/Calamity/Commands/Context.hs
+++ b/Calamity/Commands/Context.hs
@@ -1,42 +1,136 @@
 -- | Command invokation context
-module Calamity.Commands.Context
-    ( Context(..) ) where
+module Calamity.Commands.Context (
+  CalamityCommandContext (..),
+  FullContext (..),
+  useFullContext,
+  LightContext (..),
+  useLightContext,
+) where
 
-import {-# SOURCE #-} Calamity.Commands.Command
-import           Calamity.Types.Model.Channel
-import           Calamity.Types.Model.Guild
-import           Calamity.Types.Model.User
-import           Calamity.Types.Snowflake
-import           Calamity.Types.Tellable
+import Calamity.Cache.Eff
+import Calamity.Commands.Types
+import Calamity.Internal.Utils
+import Calamity.Types.Model.Channel
+import Calamity.Types.Model.Guild
+import Calamity.Types.Model.User
+import Calamity.Types.Snowflake
+import Calamity.Types.Tellable
+import qualified CalamityCommands.Context as CC
+import Control.Lens hiding (Context)
+import Control.Monad
+import qualified Data.Text.Lazy as L
+import GHC.Generics
+import qualified Polysemy as P
+import qualified Polysemy.Fail as P
+import TextShow
+import qualified TextShow.Generic as TSG
 
-import qualified Data.Text.Lazy               as L
+class CommandContext c => CalamityCommandContext c where
+  -- | The id of the channel that invoked this command
+  ctxChannelID :: c -> Snowflake Channel
 
-import           GHC.Generics
+  -- | The id of the guild the command was invoked in, if in a guild
+  ctxGuildID :: c -> Maybe (Snowflake Guild)
 
-import           TextShow
-import qualified TextShow.Generic             as TSG
+  -- | The id of the user that invoked this command
+  ctxUserID :: c -> Snowflake User
 
+  -- | The message that triggered this command
+  ctxMessage :: c -> Message
+
 -- | Invokation context for commands
-data Context = Context
-  { message        :: Message
-    -- ^ The message that the command was invoked from
-  , guild          :: Maybe Guild
-    -- ^ If the command was sent in a guild, this will be present
-  , member         :: Maybe Member
-    -- ^ The member that invoked the command, if in a guild
-  , channel        :: Channel
-    -- ^ The channel the command was invoked from
-  , user           :: User
-    -- ^ The user that invoked the command
-  , command        :: Command
-    -- ^ The command that was invoked
-  , prefix         :: L.Text
-    -- ^ The prefix that was used to invoke the command
-  , unparsedParams :: L.Text
-    -- ^ The message remaining after consuming the prefix
+data FullContext = FullContext
+  { -- | The message that the command was invoked from
+    message :: Message
+  , -- | If the command was sent in a guild, this will be present
+    guild :: Maybe Guild
+  , -- | The member that invoked the command, if in a guild
+    member :: Maybe Member
+  , -- | The channel the command was invoked from
+    channel :: Channel
+  , -- | The user that invoked the command
+    user :: User
+  , -- | The command that was invoked
+    command :: Command FullContext
+  , -- | The prefix that was used to invoke the command
+    prefix :: L.Text
+  , -- | The message remaining after consuming the prefix
+    unparsedParams :: L.Text
   }
-  deriving ( Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric Context
+  deriving (Show, Generic)
+  deriving (TextShow) via TSG.FromGeneric FullContext
 
-instance Tellable Context where
-  getChannel Context { channel } = pure . getID $ channel
+instance CC.CommandContext IO FullContext () where
+  ctxPrefix = (^. #prefix)
+  ctxCommand = (^. #command)
+  ctxUnparsedParams = (^. #unparsedParams)
+
+instance CalamityCommandContext FullContext where
+  ctxChannelID = getID . (^. #channel)
+  ctxGuildID c = getID <$> c ^. #guild
+  ctxUserID = getID . (^. #user)
+  ctxMessage = (^. #message)
+
+instance Tellable FullContext where
+  getChannel = pure . ctxChannelID
+
+useFullContext :: P.Member CacheEff r => P.Sem (CC.ConstructContext Message FullContext IO () ': r) a -> P.Sem r a
+useFullContext =
+  P.interpret
+    ( \case
+        CC.ConstructContext (pre, cmd, up) msg -> buildContext msg pre cmd up
+    )
+
+buildContext :: P.Member CacheEff r => Message -> L.Text -> Command FullContext -> L.Text -> P.Sem r (Maybe FullContext)
+buildContext msg prefix command unparsed = (rightToMaybe <$>) . P.runFail $ do
+  guild <- join <$> getGuild `traverse` (msg ^. #guildID)
+  let member = guild ^? _Just . #members . ix (coerceSnowflake $ getID @User msg)
+  let gchan = guild ^? _Just . #channels . ix (coerceSnowflake $ getID @Channel msg)
+  Just channel <- case gchan of
+    Just chan -> pure . pure $ GuildChannel' chan
+    Nothing -> DMChannel' <<$>> getDM (coerceSnowflake $ getID @Channel msg)
+  Just user <- getUser $ getID msg
+
+  pure $ FullContext msg guild member channel user command prefix unparsed
+
+-- | A lightweight context that doesn't need any cache information
+data LightContext = LightContext
+  { -- | The message that the command was invoked from
+    message :: Message
+  , -- | If the command was sent in a guild, this will be present
+    guildID :: Maybe (Snowflake Guild)
+  , -- | The channel the command was invoked from
+    channelID :: Snowflake Channel
+  , -- | The user that invoked the command
+    userID :: Snowflake User
+  , -- | The command that was invoked
+    command :: Command LightContext
+  , -- | The prefix that was used to invoke the command
+    prefix :: L.Text
+  , -- | The message remaining after consuming the prefix
+    unparsedParams :: L.Text
+  }
+  deriving (Show, Generic)
+  deriving (TextShow) via TSG.FromGeneric LightContext
+
+instance CC.CommandContext IO LightContext () where
+  ctxPrefix = (^. #prefix)
+  ctxCommand = (^. #command)
+  ctxUnparsedParams = (^. #unparsedParams)
+
+instance CalamityCommandContext LightContext where
+  ctxChannelID = (^. #channelID)
+  ctxGuildID = (^. #guildID)
+  ctxUserID = (^. #userID)
+  ctxMessage = (^. #message)
+
+instance Tellable LightContext where
+  getChannel = pure . ctxChannelID
+
+useLightContext :: P.Sem (CC.ConstructContext Message LightContext IO () ': r) a -> P.Sem r a
+useLightContext =
+  P.interpret
+    ( \case
+        CC.ConstructContext (pre, cmd, up) msg ->
+          pure . Just $ LightContext msg (msg ^. #guildID) (msg ^. #channelID) (msg ^. #author) cmd pre up
+    )
diff --git a/Calamity/Commands/Context.hs-boot b/Calamity/Commands/Context.hs-boot
deleted file mode 100644
--- a/Calamity/Commands/Context.hs-boot
+++ /dev/null
@@ -1,10 +0,0 @@
--- | Command invokation context
-module Calamity.Commands.Context
-    ( Context ) where
-
-import           TextShow
-
-data Context
-
-instance Show Context
-instance TextShow Context
diff --git a/Calamity/Commands/Dsl.hs b/Calamity/Commands/Dsl.hs
--- a/Calamity/Commands/Dsl.hs
+++ b/Calamity/Commands/Dsl.hs
@@ -1,361 +1,327 @@
 {-# LANGUAGE RecursiveDo #-}
 
--- | A DSL for generating commands and groups
-module Calamity.Commands.Dsl
-    ( -- * Commands DSL
-      -- $dslTutorial
-      command
-    , command'
-    , commandA
-    , commandA'
-    , hide
-    , help
-    , requires
-    , requires'
-    , requiresPure
-    , group
-    , group'
-    , groupA
-    , groupA'
-    , DSLState
-    , raiseDSL
-    , fetchHandler ) where
+{- | A DSL for generating commands and groups
 
-import           Calamity.Commands.AliasType
-import           Calamity.Commands.Check
-import           Calamity.Commands.Command     hiding ( help )
-import           Calamity.Commands.CommandUtils
-import           Calamity.Commands.Context     hiding ( command )
-import           Calamity.Commands.Error
-import           Calamity.Commands.Group       hiding ( help )
-import           Calamity.Commands.Handler
-import           Calamity.Internal.LocalWriter
+ This is effectively just a re-export of "CalamityCommands.Dsl" but with
+ documentation more tuned for usage with Calamity.
+-}
+module Calamity.Commands.Dsl (
+    -- * Commands DSL
+    -- $dslTutorial
+    command,
+    command',
+    commandA,
+    commandA',
+    hide,
+    help,
+    requires,
+    requires',
+    requiresPure,
+    group,
+    group',
+    groupA,
+    groupA',
+    DSLState,
+    fetchHandler,
+) where
 
-import qualified Data.HashMap.Lazy             as LH
-import qualified Data.Text                     as S
-import qualified Data.Text.Lazy                as L
+import qualified CalamityCommands.Dsl as CC
+import CalamityCommands.ParameterInfo
 
-import qualified Polysemy                      as P
-import qualified Polysemy.Fail                 as P
-import qualified Polysemy.Tagged               as P
-import qualified Polysemy.Fixpoint             as P
-import qualified Polysemy.Reader               as P
-import Data.List.NonEmpty (NonEmpty(..))
+import Calamity.Commands.Types
 
--- $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:
--- 'Calamity.Commands.Help.helpCommand'
---
--- The 'Calamity.Commands.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 'Calamity.Commands.Utils.addCommands' function creates the command
--- handler for the commands registered in the passed action, it is what reads a
--- message to determine what command was invoked. It should be used to register the
--- commands with the bot by using it inside the setup action, for example:
---
--- @
--- 'Calamity.Client.runBotIO' ('Calamity.BotToken' token)
---   $ 'Calamity.Commands.Utils.addCommands' $ do
---     'Calamity.Commands.Help.helpCommand'
---     'Calamity.Commands.Dsl.command' \@'[] "test" \\ctx ->
---       'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' \@'L.Text' ctx "hi"
--- @
---
--- The above block will create a command with no parameters named \'test\',
--- along with a help command.
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
 
-type DSLState r =
-  ( LocalWriter (LH.HashMap S.Text (Command, AliasType))
-      ': LocalWriter (LH.HashMap S.Text (Group, AliasType))
-      ': P.Reader (Maybe Group)
-      ': P.Tagged "hidden" (P.Reader Bool)
-      ': P.Reader (Context -> L.Text)
-      ': P.Tagged "original-help" (P.Reader (Context -> L.Text))
-      ': P.Reader [Check]
-      ': P.Reader CommandHandler
-      ': P.Fixpoint
-      ': r
-  )
+import CalamityCommands.CommandUtils (CommandForParsers, TypedCommandC)
+import qualified CalamityCommands.Context as CC
+import CalamityCommands.Error (CommandError)
+import qualified Polysemy as P
+import qualified Polysemy.Fail as P
+import qualified Polysemy.Reader as P
+import qualified Polysemy.Tagged as P
 
-raiseDSL :: P.Sem r a -> P.Sem (DSLState r) a
-raiseDSL = P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise . P.raise
+{- $dslTutorial
 
--- | 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 IO actions. Then register the command.
---
--- The parent group, visibility, checks, and command help are drawn from the
--- reader context.
-command'
-  :: P.Member (P.Final IO) r
-  => S.Text
-  -- ^ The name of the command
-  -> [S.Text]
-  -- ^ The names of the command's parameters
-  -> (Context -> P.Sem r (Either CommandError a))
-  -- ^ The parser for this command
-  -> ((Context, a) -> P.Sem (P.Fail ': r) ())
-  -- ^ The callback for this command
-  -> P.Sem (DSLState r) Command
-command' name params parser cb = commandA' name [] params parser cb
+ This module provides a way of constructing bot commands in a declarative way.
 
--- | 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 IO actions. Then register the command.
---
--- The parent group, visibility, checks, and command help are drawn from the
--- reader context.
-commandA'
-  :: P.Member (P.Final IO) r
-  => S.Text
-  -- ^ The name of the command
-  -> [S.Text]
-  -- ^ The aliases for the command
-  -> [S.Text]
-  -- ^ The names of the command's parameters
-  -> (Context -> P.Sem r (Either CommandError a))
-  -- ^ The parser for this command
-  -> ((Context, a) -> P.Sem (P.Fail ': r) ())
-  -- ^ The callback for this command
-  -> P.Sem (DSLState r) Command
-commandA' name aliases params parser cb = do
-  parent <- P.ask @(Maybe Group)
-  hidden <- P.tag $ P.ask @Bool
-  checks <- P.ask @[Check]
-  help' <- P.ask @(Context -> 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
+ The main component of this is the 'Calamity.Commands.Dsl.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
+ 'Calamity.Commands.Dsl.command'', 'commandA' and 'commandA'', for when you want
+ to handle parsing of the input yourself, and/or want aliases of the command.
 
--- | 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
--- 'Calamity.Commands.Parser.parse' for the first
--- 'Calamity.Commands.Parser.Parser', then running the next parser on the
--- remaining input, and so on.
---
--- ==== Examples
---
--- Building a command that bans a user by id.
---
--- @
--- 'command' \@\'['Calamity.Commands.Parser.Named' "user" ('Calamity.Types.Snowflake' 'Calamity.Types.Model.User'),
---                'Calamity.Commands.Parser.Named' "reason" ('Calamity.Commands.Parser.KleeneStarConcat' 'S.Text')]
---    "ban" $ \\ctx uid r -> case (ctx 'Control.Lens.^.' #guild) of
---      'Just' guild -> do
---        'Control.Monad.void' . 'Calamity.HTTP.invoke' $ 'Calamity.HTTP.Guild.CreateGuildBan' guild uid ('Calamity.HTTP.Guild.CreateGuildBanData' 'Nothing' $ 'Just' r)
---        'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx ("Banned user `" '<>' 'TextShow.showt' uid '<>' "` with reason: " '<>' r)
---      'Nothing' -> 'void' $ 'Calamity.Types.Tellable.tell' @'L.Text' ctx "Can only ban users from guilds."
--- @
-command :: forall ps r.
-        ( P.Member (P.Final IO) r,
-          TypedCommandC ps r)
-        => S.Text
-        -- ^ The name of the command
-        -> (Context -> CommandForParsers ps r)
-        -- ^ The callback for this command
-        -> P.Sem (DSLState r) Command
-command name cmd = commandA @ps name [] cmd
+ 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:
 
--- | 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 bans a user by id.
---
--- @
--- 'commandA' \@\'['Calamity.Commands.Parser.Named' "user" ('Calamity.Types.Snowflake' 'Calamity.Types.Model.User'),
---                'Calamity.Commands.Parser.Named' "reason" ('Calamity.Commands.Parser.KleeneStarConcat' 'S.Text')]
---    "ban" [] $ \\ctx uid r -> case (ctx 'Control.Lens.^.' #guild) of
---      'Just' guild -> do
---        'Control.Monad.void' . 'Calamity.HTTP.invoke' $ 'Calamity.HTTP.Guild.CreateGuildBan' guild uid ('Calamity.HTTP.Guild.CreateGuildBanData' 'Nothing' $ 'Just' r)
---        'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx ("Banned user `" '<>' 'TextShow.showt' uid '<>' "` with reason: " '<>' r)
---      'Nothing' -> 'void' $ 'Calamity.Types.Tellable.tell' @'L.Text' ctx "Can only ban users from guilds."
--- @
-commandA :: forall ps r.
-        ( P.Member (P.Final IO) r,
-          TypedCommandC ps r)
-        => S.Text
-        -- ^ The name of the command
-        -> [S.Text]
-        -- ^ The aliases for the command
-        -> (Context -> CommandForParsers ps r)
-        -- ^ The callback for this command
-        -> P.Sem (DSLState r) Command
-commandA name aliases cmd = do
-  parent <- P.ask @(Maybe Group)
-  hidden <- P.tag $ P.ask @Bool
-  checks <- P.ask @[Check]
-  help' <- P.ask @(Context -> 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'
+ @
+ 'hide' '$' do
+   'Calamity.Commands.Dsl.command' \@'[] "test" \\ctx -> 'pure' ()
+ @
 
--- | 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 a
-     -> P.Sem r a
-hide = P.tag @"hidden" . P.local @Bool (const True) . P.raise
+ 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:
+ 'Calamity.Commands.Help.helpCommand'
 
+ The 'Calamity.Commands.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 'Calamity.Commands.Utils.addCommands' function creates the command
+ handler for the commands registered in the passed action, it is what reads a
+ message to determine what command was invoked. It should be used to register the
+ commands with the bot by using it inside the setup action, for example:
+
+ @
+ 'Calamity.Client.runBotIO' ('Calamity.BotToken' token)
+   $ 'Calamity.Commands.Utils.addCommands' $ do
+     'Calamity.Commands.Help.helpCommand'
+     'Calamity.Commands.Dsl.command' \@'[] "test" \\ctx ->
+       'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' \@'L.Text' ctx "hi"
+ @
+
+ The above block will create a command with no parameters named \'test\',
+ along with a help command.
+-}
+
+{- | 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 IO actions. Then register the command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+-}
+command' ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the command
+    S.Text ->
+    -- | The command's parameters
+    [ParameterInfo] ->
+    -- | The parser for this command
+    (c -> P.Sem r (Either CommandError a)) ->
+    -- | The callback for this command
+    ((c, a) -> P.Sem (P.Fail ': r) ()) ->
+    P.Sem (DSLState c r) (Command c)
+command' = CC.command'
+
+{- | 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 IO actions. Then register the command.
+
+ The parent group, visibility, checks, and command help are drawn from the
+ reader context.
+-}
+commandA' ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the command
+    S.Text ->
+    -- | The aliases for the command
+    [S.Text] ->
+    -- | The command's parameters
+    [ParameterInfo] ->
+    -- | The parser for this command
+    (c -> P.Sem r (Either CommandError a)) ->
+    -- | The callback for this command
+    ((c, a) -> P.Sem (P.Fail ': r) ()) ->
+    P.Sem (DSLState c r) (Command c)
+commandA' = CC.commandA'
+
+{- | 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.ParameterParser', then running the next parser on the
+ remaining input, and so on.
+
+ ==== Examples
+
+ Building a command that bans a user by id.
+
+ @
+ 'Calamity.Commands.Dsl.command' \@\'['CalamityCommands.Parser.Named' "user" ('Calamity.Types.Snowflake' 'Calamity.Types.Model.User'),
+                'CalamityCommands.Parser.Named' "reason" ('CalamityCommands.Parser.KleeneStarConcat' 'S.Text')]
+    "ban" $ \\ctx uid r -> case (ctx 'Control.Lens.^.' #guild) of
+      'Just' guild -> do
+        'Control.Monad.void' . 'Calamity.HTTP.invoke' $ 'Calamity.HTTP.Guild.CreateGuildBan' guild uid ('Calamity.HTTP.Guild.CreateGuildBanData' 'Nothing' $ 'Just' r)
+        'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx ("Banned user `" '<>' 'TextShow.showt' uid '<>' "` with reason: " '<>' r)
+      'Nothing' -> 'void' $ 'Calamity.Types.Tellable.tell' @'L.Text' ctx "Can only ban users from guilds."
+ @
+-}
+command ::
+    forall ps c r.
+    ( P.Member (P.Final IO) r
+    , CC.CommandContext IO c ()
+    , TypedCommandC ps c () r
+    ) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The callback for this command
+    (c -> CommandForParsers ps r ()) ->
+    P.Sem (DSLState c r) (Command c)
+command = CC.command @ps
+
+{- | 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 bans a user by id.
+
+ @
+ 'commandA' \@\'['CalamityCommands.Parser.Named' "user" ('Calamity.Types.Snowflake' 'Calamity.Types.Model.User'),
+                'CalamityCommands.Parser.Named' "reason" ('CalamityCommands.Parser.KleeneStarConcat' 'S.Text')]
+    "ban" [] $ \\ctx uid r -> case (ctx 'Control.Lens.^.' #guild) of
+      'Just' guild -> do
+        'Control.Monad.void' . 'Calamity.HTTP.invoke' $ 'Calamity.HTTP.Guild.CreateGuildBan' guild uid ('Calamity.HTTP.Guild.CreateGuildBanData' 'Nothing' $ 'Just' r)
+        'Control.Monad.void' $ 'Calamity.Types.Tellable.tell' ctx ("Banned user `" '<>' 'TextShow.showt' uid '<>' "` with reason: " '<>' r)
+      'Nothing' -> 'void' $ 'Calamity.Types.Tellable.tell' @'L.Text' ctx "Can only ban users from guilds."
+ @
+-}
+commandA ::
+    forall ps c r.
+    ( P.Member (P.Final IO) r
+    , CC.CommandContext IO c ()
+    , TypedCommandC ps c () r
+    ) =>
+    -- | The name of the command
+    S.Text ->
+    -- | The aliases for the command
+    [S.Text] ->
+    -- | The callback for this command
+    (c -> CommandForParsers ps r ()) ->
+    P.Sem (DSLState c r) (Command c)
+commandA = CC.commandA @ps
+
+{- | 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 a ->
+    P.Sem r a
+hide = CC.hide
+
 -- | Set the help for any groups or commands registered inside the given action.
-help :: P.Member (P.Reader (Context -> L.Text)) r
-     => (Context -> L.Text)
-     -> P.Sem r a
-     -> P.Sem r a
-help = P.local . const
+help ::
+    P.Member (P.Reader (c -> L.Text)) r =>
+    (c -> L.Text) ->
+    P.Sem r a ->
+    P.Sem r a
+help = CC.help
 
--- | Add to the list of checks for any commands registered inside the given
--- action.
-requires :: [Check]
-         -> P.Sem (DSLState r) a
-         -> P.Sem (DSLState r) a
-requires = P.local . (<>)
+{- | Add to the list of checks for any commands registered inside the given
+ action.
+-}
+requires ::
+    [Check c] ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+requires = CC.requires
 
--- | Construct a check and add it to the list of checks for any commands
--- registered inside the given action.
---
--- Refer to 'Calamity.Commands.Check.Check' for more info on checks.
-requires' :: P.Member (P.Final IO) r
-          => S.Text
-          -- ^ The name of the check
-          -> (Context -> P.Sem r (Maybe L.Text))
-          -- ^ The callback for the check
-          -> P.Sem (DSLState r) a
-          -> P.Sem (DSLState r) a
-requires' name cb m = do
-  check <- raiseDSL $ buildCheck name cb
-  requires [check] m
+{- | Construct a check and add it to the list of checks for any commands
+ registered inside the given action.
 
--- | Construct some pure checks and add them to the list of checks for any
--- commands registered inside the given action.
---
--- Refer to 'Calamity.Commands.Check.Check' for more info on checks.
-requiresPure :: [(S.Text, Context -> Maybe L.Text)]
-             -- A list of check names and check callbacks
-             -> P.Sem (DSLState r) a
-             -> P.Sem (DSLState r) a
-requiresPure checks = requires $ map (uncurry buildCheckPure) checks
+ Refer to 'CalamityCommands.Check.Check' for more info on checks.
+-}
+requires' ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the check
+    S.Text ->
+    -- | The callback for the check
+    (c -> P.Sem r (Maybe L.Text)) ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+requires' = CC.requires'
 
--- | 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 :: P.Member (P.Final IO) r
-      => S.Text
-      -- ^ The name of the group
-      -> P.Sem (DSLState r) a
-      -> P.Sem (DSLState r) a
-group name m = groupA name [] m
+{- | Construct some pure checks and add them to the list of checks for any
+ commands registered inside the given action.
 
--- | 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 :: P.Member (P.Final IO) r
-       => S.Text
-       -- ^ The name of the group
-       -> [S.Text]
-       -- ^ The aliases of the group
-       -> P.Sem (DSLState r) a
-       -> P.Sem (DSLState r) a
-groupA name aliases m = mdo
-  parent <- P.ask @(Maybe Group)
-  hidden <- P.tag $ P.ask @Bool
-  checks <- P.ask @[Check]
-  help'  <- P.ask @(Context -> L.Text)
-  origHelp <- fetchOrigHelp
-  let group' = Group (name :| aliases) parent hidden commands children help' checks
-  (children, (commands, res)) <- llisten @(LH.HashMap S.Text (Group, AliasType)) $
-                                 llisten @(LH.HashMap S.Text (Command, AliasType)) $
-                                 P.local @(Maybe Group) (const $ Just group') $
-                                 P.local @(Context -> L.Text) (const origHelp) m
-  ltell $ LH.singleton name (group', Original)
-  ltell $ LH.fromList [(name, (group', Alias)) | name <- aliases]
-  pure res
+ Refer to 'CalamityCommands.Check.Check' for more info on checks.
+-}
+requiresPure ::
+    [(S.Text, c -> Maybe L.Text)] ->
+    -- A list of check names and check callbacks
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+requiresPure = CC.requiresPure
 
-fetchOrigHelp :: P.Member (P.Tagged "original-help" (P.Reader (Context -> L.Text))) r => P.Sem r (Context -> L.Text)
-fetchOrigHelp = P.tag P.ask
+{- | Construct a group and place any commands registered in the given action
+ into the new group.
 
--- | 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 IO) r
-         => S.Text
-         -- The name of the group
-         -> P.Sem (DSLState r) a
-         -> P.Sem (DSLState r) a
-group' name m = groupA' name [] m
+ 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 ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the group
+    S.Text ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+group = CC.group
 
--- | 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' :: P.Member (P.Final IO) r
-         => S.Text
-         -- ^ The name of the group
-         -> [S.Text]
-         -- ^ The aliases of the group
-         -> P.Sem (DSLState r) a
-         -> P.Sem (DSLState r) a
-groupA' name aliases m = mdo
-  parent <- P.ask @(Maybe Group)
-  hidden <- P.tag $ P.ask @Bool
-  checks <- P.ask @[Check]
-  help'  <- P.ask @(Context -> L.Text)
-  let group' = Group (name :| aliases) parent hidden commands children help' checks
-  (children, (commands, res)) <- llisten @(LH.HashMap S.Text (Group, AliasType)) $
-                                 llisten @(LH.HashMap S.Text (Command, AliasType)) $
-                                 P.local @(Maybe Group) (const $ Just group') m
-  ltell $ LH.singleton name (group', Original)
-  ltell $ LH.fromList [(name, (group', Alias)) | name <- aliases]
-  pure res
+{- | 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 ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the group
+    S.Text ->
+    -- | The aliases of the group
+    [S.Text] ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+groupA = CC.groupA
+
+{- | 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 IO) r =>
+    -- | The name of the group
+    S.Text ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+group' = CC.group'
+
+{- | 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' ::
+    P.Member (P.Final IO) r =>
+    -- | The name of the group
+    S.Text ->
+    -- | The aliases of the group
+    [S.Text] ->
+    P.Sem (DSLState c r) a ->
+    P.Sem (DSLState c r) a
+groupA' = CC.groupA'
+
 -- | Retrieve the final command handler for this block
-fetchHandler :: P.Sem (DSLState r) CommandHandler
+fetchHandler :: P.Sem (DSLState c r) (CommandHandler c)
 fetchHandler = P.ask
diff --git a/Calamity/Commands/Error.hs b/Calamity/Commands/Error.hs
deleted file mode 100644
--- a/Calamity/Commands/Error.hs
+++ /dev/null
@@ -1,21 +0,0 @@
--- | Command errors
-module Calamity.Commands.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/Calamity/Commands/Group.hs b/Calamity/Commands/Group.hs
deleted file mode 100644
--- a/Calamity/Commands/Group.hs
+++ /dev/null
@@ -1,53 +0,0 @@
--- | Command groups
-module Calamity.Commands.Group
-    ( Group(..) ) where
-
-import           Calamity.Commands.AliasType
-import           Calamity.Commands.Check
-import {-# SOURCE #-} Calamity.Commands.Command
-import {-# SOURCE #-} Calamity.Commands.Context
-
-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           TextShow
-import qualified TextShow.Generic          as TSG
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.List.NonEmpty as NE
-
--- | A group of commands
-data Group = Group
-  { names    :: NonEmpty S.Text
-  , parent   :: Maybe Group
-  , hidden   :: Bool
-  , commands :: LH.HashMap S.Text (Command, AliasType)
-    -- ^ Any child commands of this group
-  , children :: LH.HashMap S.Text (Group, AliasType)
-    -- ^ Any child groups of this group
-  , help     :: Context -> L.Text
-    -- ^ A function producing the \'help' for the group
-  , checks   :: [Check]
-    -- ^ A list of checks that must pass
-  }
-  deriving ( Generic )
-
-data GroupS = GroupS
-  { names    :: NonEmpty S.Text
-  , parent   :: Maybe S.Text
-  , commands :: LH.HashMap S.Text (Command, AliasType)
-  , children :: LH.HashMap S.Text (Group, AliasType)
-  , hidden   :: Bool
-  }
-  deriving ( Generic, Show )
-  deriving ( TextShow ) via TSG.FromGeneric GroupS
-
-instance Show Group where
-  showsPrec d Group {names, parent, commands, children, hidden} = showsPrec d $ GroupS names (NE.head <$> parent ^? _Just . #names) commands children hidden
-
-instance TextShow Group where
-  showbPrec d Group {names, parent, commands, children, hidden} = showbPrec d $ GroupS names (NE.head <$> parent ^? _Just . #names) commands children hidden
diff --git a/Calamity/Commands/Handler.hs b/Calamity/Commands/Handler.hs
deleted file mode 100644
--- a/Calamity/Commands/Handler.hs
+++ /dev/null
@@ -1,24 +0,0 @@
--- | A command handler
-module Calamity.Commands.Handler
-    ( CommandHandler(..) ) where
-
-import           Calamity.Commands.AliasType
-import           Calamity.Commands.Command
-import           Calamity.Commands.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 = CommandHandler
-  { groups   :: LH.HashMap S.Text (Group, AliasType)
-    -- ^ Top level groups
-  , commands :: LH.HashMap S.Text (Command, AliasType)
-    -- ^ Top level commands
-  }
-  deriving ( Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric CommandHandler
diff --git a/Calamity/Commands/Help.hs b/Calamity/Commands/Help.hs
--- a/Calamity/Commands/Help.hs
+++ b/Calamity/Commands/Help.hs
@@ -1,193 +1,70 @@
 -- | A default help command implementation
-module Calamity.Commands.Help
-    ( helpCommand'
-    , helpCommand ) where
-
-import           Calamity.Client.Types
-import           Calamity.Commands.AliasType
-import           Calamity.Commands.Check
-import           Calamity.Commands.Command
-import           Calamity.Commands.CommandUtils
-import           Calamity.Commands.Context
-import           Calamity.Commands.Dsl
-import           Calamity.Commands.Group
-import           Calamity.Commands.Handler
-import           Calamity.Internal.LocalWriter
-import           Calamity.Types.Tellable
-
-import           Control.Applicative
-import           Control.Lens hiding ( Context(..) )
-import           Control.Monad
-
-import qualified Data.HashMap.Lazy              as LH
-import           Data.List.NonEmpty             ( NonEmpty(..) )
-import qualified Data.List.NonEmpty             as NE
-import           Data.Maybe                     ( catMaybes )
-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
-import Data.Maybe (mapMaybe)
-
-data CommandOrGroup
-  = Command' Command
-  | Group' Group [S.Text]
-
-helpCommandHelp :: Context -> L.Text
-helpCommandHelp _ = "Show help for a command or group."
-
-helpForCommand :: Context -> Command -> L.Text
-helpForCommand ctx (cmd@Command { names, checks, help }) = "```\nUsage: " <> prefix' <> path' <> " " <> params' <> "\n" <>
-                                                           aliasesFmt <>
-                                                           checksFmt <>
-                                                           help ctx <> "\n```"
-  where prefix' = ctx ^. #prefix
-        path'   = L.unwords . map L.fromStrict $ commandPath cmd
-        params' = commandParams cmd
-        aliases = map L.fromStrict $ NE.tail names
-        checks' = map L.fromStrict . map (^. #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"
+module Calamity.Commands.Help (
+  helpCommand',
+  helpCommand,
+) where
 
-fmtCommandWithParams :: Command -> L.Text
-fmtCommandWithParams cmd@Command { names } = formatWithAliases names <> " " <> commandParams cmd
+import Calamity.Client.Types (BotC)
+import Calamity.Commands.Types
+import Calamity.Types.Tellable
+import qualified CalamityCommands.Help as CC
+import Control.Monad (void)
+import qualified Polysemy as P
 
-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') <> "]"
+{- | 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' :: (Tellable c, BotC r, CommandContext c) => CommandHandler c -> Maybe (Group c) -> [Check c] -> P.Sem r (Command c)
+helpCommand' handler parent checks = CC.helpCommand' handler parent checks (\a b -> void $ tell a b)
 
-onlyOriginals :: [(a, AliasType)] -> [a]
-onlyOriginals = mapMaybe inner
-  where inner (_, Alias) = Nothing
-        inner (a, Original) = Just a
+{- | 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'@
 
-onlyVisibleC :: [Command] -> [Command]
-onlyVisibleC = catMaybes . map notHiddenC
+ 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:
 
-onlyVisibleG :: [Group] -> [Group]
-onlyVisibleG = catMaybes . map notHiddenG
+ @
+ The following groups exist:
+ - reanimate
+ - prefix[prefixes]
+ - alias[aliases]
+ - remind[reminder|reminders]
 
-helpForGroup :: Context -> Group -> 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 ("- " <>) . map fmtCommandWithParams $ commands)
-        aliases = map L.fromStrict . NE.tail $ grp ^. #names
-        checks' = map L.fromStrict . map (^. #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"
+ The following commands exist:
+ - help :[Text]
+ @
 
-rootHelp :: CommandHandler -> 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 ("- " <>) . map fmtCommandWithParams $ commands)
+ Both commands and groups are listed in the form: @\<name\>[\<alias 0\>|\<alias 1\>]@,
+ commands also have their parameter list shown.
 
-helpCommandCallback :: BotC r => CommandHandler -> Context -> [S.Text] -> P.Sem (P.Fail ': r) ()
-helpCommandCallback handler ctx path = do
-  case findCommandOrGroup handler path of
-    Just (Command' cmd@Command { names }) ->
-      void $ tell @L.Text ctx $ "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 void $ tell @L.Text ctx $ 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 void $ tell @L.Text ctx $ failedMsg <> rootHelp handler
+ 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:
 
--- | 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' :: BotC r => CommandHandler -> Maybe Group -> [Check] -> P.Sem r Command
-helpCommand' handler parent checks = buildCommand @'[[S.Text]] ("help" :| []) parent False checks helpCommandHelp
-  (helpCommandCallback handler)
+ @
+ Help for group remind:
+ Group: remind
+ Aliases: reminder reminders
+ Commands related to making reminders
 
--- | Create and register the default help command for all the commands
--- registered in the commands DSL this is used in.
---
--- The registered command will have the name \'help\', called with no parameters
--- it will print 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 :: BotC r => P.Sem (DSLState r) Command
-helpCommand = do
-  handler <- P.ask @CommandHandler
-  parent <- P.ask @(Maybe Group)
-  checks <- P.ask @[Check]
-  cmd <- raiseDSL $ helpCommand' handler parent checks
-  ltell $ LH.singleton "help" (cmd, Original)
-  pure cmd
+ The following child commands exist:
+ - list
+ - remove reminder_id:Text
+ - add :KleenePlusConcat Text
+ @
 
-notHiddenC :: Command -> Maybe Command
-notHiddenC c@(Command { hidden }) = if hidden then Nothing else Just c
+ If a command path is passed, the usage, checks and help for the command are
+ shown, for example:
 
-notHiddenG :: Group -> Maybe Group
-notHiddenG g@(Group { hidden }) = if hidden then Nothing else Just g
+ @
+ Help for command add:
+ Usage: c!prefix add prefix:Text
+ Checks: prefixLimit guildOnly
 
-findCommandOrGroup :: CommandHandler -> [S.Text] -> Maybe CommandOrGroup
-findCommandOrGroup handler path = go (handler ^. #commands, handler ^. #groups) path
-  where go :: (LH.HashMap S.Text (Command, AliasType), LH.HashMap S.Text (Group, AliasType))
-           -> [S.Text]
-           -> Maybe CommandOrGroup
-        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
+ Add a new prefix
+ @
+-}
+helpCommand :: (Tellable c, BotC r, CommandContext c) => P.Sem (DSLState c r) (Command c)
+helpCommand = CC.helpCommand (\a b -> void $ tell a b)
diff --git a/Calamity/Commands/ParsePrefix.hs b/Calamity/Commands/ParsePrefix.hs
deleted file mode 100644
--- a/Calamity/Commands/ParsePrefix.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-{-# LANGUAGE TemplateHaskell #-}
-
--- | Command prefix parsing effect
-module Calamity.Commands.ParsePrefix
-    ( ParsePrefix(..)
-    , parsePrefix
-    , useConstantPrefix ) where
-
-import           Calamity.Types.Model.Channel.Message
-
-import           Control.Lens
-
-import qualified Data.Text.Lazy                       as L
-
-import qualified Polysemy                             as P
-
--- | An effect for parsing the prefix of a command.
-data ParsePrefix m a where
-  -- | Parse a prefix in a message, returning a tuple of @(prefix, remaining message)@
-  ParsePrefix :: Message -> ParsePrefix 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 ': r) a -> P.Sem r a
-useConstantPrefix pre = P.interpret (\case
-                                       ParsePrefix m -> pure ((pre, ) <$> L.stripPrefix pre (m ^. #content)))
diff --git a/Calamity/Commands/Parser.hs b/Calamity/Commands/Parser.hs
deleted file mode 100644
--- a/Calamity/Commands/Parser.hs
+++ /dev/null
@@ -1,360 +0,0 @@
--- | Something that can parse user input
-module Calamity.Commands.Parser
-    ( Parser(..)
-    , Named
-    , KleeneStarConcat
-    , KleenePlusConcat
-    , ParserEffs
-    , runCommandParser ) where
-
-import           Calamity.Cache.Eff
-import           Calamity.Commands.Context
-import           Calamity.Types.Model.Channel  ( Channel, GuildChannel )
-import           Calamity.Types.Model.Guild    ( Emoji, RawEmoji(..), Partial(PartialEmoji), Guild, Member, Role )
-import           Calamity.Types.Model.User     ( User )
-import           Calamity.Types.Snowflake
-import           Calamity.Types.Partial
-
-import           Control.Lens                  hiding ( Context )
-import           Control.Monad
-import           Control.Monad.Trans           ( lift )
-
-import           Data.Char                     ( isSpace )
-import           Data.Kind
-import           Data.List.NonEmpty            ( NonEmpty(..) )
-import           Data.Maybe                    ( isJust )
-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           Text.Megaparsec               hiding ( parse )
-import           Text.Megaparsec.Char
-import           Text.Megaparsec.Error.Builder ( errFancy, fancy )
-import Text.Megaparsec.Char.Lexer (float, decimal, signed)
-import Numeric.Natural (Natural)
-
-data SpannedError = SpannedError L.Text !Int !Int
-  deriving ( Show, Eq, Ord )
-
-showTypeOf :: forall a. Typeable a => String
-showTypeOf = show . typeRep $ Proxy @a
-
-data ParserState = ParserState
-  { off :: Int
-  , msg :: L.Text
-  }
-  deriving ( Show, Generic )
-
-type ParserEffs r = P.State ParserState ': P.Error (S.Text, L.Text) ': P.Reader Context ': r
-type ParserCtxE r = P.Reader Context ': r
-
-runCommandParser :: Context -> L.Text -> P.Sem (ParserEffs r) a -> P.Sem r (Either (S.Text, L.Text) a)
-runCommandParser ctx t = P.runReader ctx . P.runError . P.evalState (ParserState 0 t)
-
-class Typeable a => Parser (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 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, Parser a r) => Parser (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)
-
-parseMP :: S.Text -> ParsecT SpannedError L.Text (P.Sem (ParserCtxE r)) a -> P.Sem (ParserEffs 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 Parser L.Text r where
-  parse = parseMP (parserName @L.Text) item
-
-instance Parser S.Text r where
-  parse = parseMP (parserName @S.Text) (L.toStrict <$> item)
-
-instance Parser Integer r where
-  parse = parseMP (parserName @Integer) (signed mempty decimal)
-
-instance Parser Natural r where
-  parse = parseMP (parserName @Natural) decimal
-
-instance Parser Int r where
-  parse = parseMP (parserName @Int) (signed mempty decimal)
-
-instance Parser Word r where
-  parse = parseMP (parserName @Word) decimal
-
-instance Parser Float r where
-  parse = parseMP (parserName @Float) (signed mempty (try float <|> decimal))
-
-instance Parser a r => Parser (Maybe a) r where
-  type ParserResult (Maybe a) = Maybe (ParserResult a)
-
-  parse = P.catch (Just <$> parse @a) (const $ pure Nothing)
-
-
-instance (Parser a r, Parser b r) => Parser (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 Parser a r => Parser [a] r where
-  type ParserResult [a] = [ParserResult a]
-
-  parse = go []
-    where go :: [ParserResult a] -> P.Sem (ParserEffs r) [ParserResult a]
-          go l = P.catch (Just <$> parse @a) (const $ pure Nothing) >>= \case
-            Just a -> go $ l ++ [a]
-            Nothing -> pure l
-
-instance (Parser a r, Typeable a) => Parser (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), Parser a r) => Parser (KleeneStarConcat a) r where
-  type ParserResult (KleeneStarConcat a) = ParserResult a
-
-  parse = mconcat <$> parse @[a]
-
-instance {-# OVERLAPS #-}Parser (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 #-}Parser (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), Parser a r) => Parser (KleenePlusConcat a) r where
-  type ParserResult (KleenePlusConcat a) = ParserResult a
-
-  parse = sconcat <$> parse @(NonEmpty a)
-
-instance {-# OVERLAPS #-}Parser (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 #-}Parser (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 Typeable (Snowflake a) => Parser (Snowflake a) r where
-  parse = parseMP (parserName @(Snowflake a)) snowflake
-
--- | Accepts both plain IDs and mentions
-instance {-# OVERLAPS #-}Parser (Snowflake User) r where
-  parse = parseMP (parserName @(Snowflake User)) (try (ping "@") <|> snowflake)
-
--- | Accepts both plain IDs and mentions
-instance {-# OVERLAPS #-}Parser (Snowflake Member) r where
-  parse = parseMP (parserName @(Snowflake Member)) (try (ping "@") <|> snowflake)
-
--- | Accepts both plain IDs and mentions
-instance {-# OVERLAPS #-}Parser (Snowflake Channel) r where
-  parse = parseMP (parserName @(Snowflake Channel)) (try (ping "#") <|> snowflake)
-
--- | Accepts both plain IDs and mentions
-instance {-# OVERLAPS #-}Parser (Snowflake Role) r where
-  parse = parseMP (parserName @(Snowflake Role)) (try (ping "@&") <|> snowflake)
-
--- | Accepts both plain IDs and uses of emoji
-instance {-# OVERLAPS #-}Parser (Snowflake Emoji) r where
-  parse = parseMP (parserName @(Snowflake Emoji)) (try emoji <|> snowflake)
-
--- mapParserMaybe :: Stream s => ParsecT SpannedError s m a -> Text -> (a -> Maybe b) -> ParsecT SpannedError s m b
--- mapParserMaybe m e f = do
---   offs <- getOffset
---   r <- f <$> m
---   offe <- getOffset
---   case r of
---     Just r' -> pure r'
---     _       -> parseError . errFancy offs . fancy . ErrorCustom $ SpannedError e offs offe
-
-mapParserMaybeM :: (Monad m, Stream s) => ParsecT SpannedError s m a -> L.Text -> (a -> m (Maybe b)) -> ParsecT SpannedError s m b
-mapParserMaybeM m e f = do
-  offs <- getOffset
-  r <- m >>= lift . f
-  offe <- getOffset
-  case r of
-    Just r' -> pure r'
-    Nothing -> parseError . errFancy offs . fancy . ErrorCustom $ SpannedError e offs offe
-
--- | Parser for members in the guild the command was invoked in, this only looks
--- in the cache. Use @'Snowflake' 'Member'@ and use
--- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-instance Parser Member r where
-  parse = parseMP (parserName @Member) $ mapParserMaybeM (try (ping "@") <|> snowflake)
-          "Couldn't find a Member with this id"
-          (\mid -> do
-              ctx <- P.ask
-              pure $ ctx ^? #guild . _Just . #members . ix mid)
-
--- | Parser for users, this only looks in the cache. Use @'Snowflake'
--- 'User'@ and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow
--- fetching from http.
-instance P.Member CacheEff r => Parser User r where
-  parse = parseMP (parserName @User @r) $ mapParserMaybeM (try (ping "@") <|> snowflake)
-          "Couldn't find a User with this id"
-          getUser
-
--- | Parser for channels in the guild the command was invoked in, this only
--- looks in the cache. Use @'Snowflake' 'Channel'@ and use
--- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-instance Parser GuildChannel r where
-  parse = parseMP (parserName @GuildChannel @r) $ mapParserMaybeM (try (ping "#") <|> snowflake)
-          "Couldn't find a GuildChannel with this id"
-          (\cid -> do
-              ctx <- P.ask
-              pure $ ctx ^? #guild . _Just . #channels . ix cid)
-
--- | Parser for guilds, this only looks in the cache. Use @'Snowflake' 'Guild'@
--- and use 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching
--- from http.
-instance P.Member CacheEff r => Parser Guild r where
-  parse = parseMP (parserName @Guild @r) $ mapParserMaybeM snowflake
-          "Couldn't find a Guild with this id"
-          getGuild
-
--- | Parser for emojis in the guild the command was invoked in, this only
--- looks in the cache. Use @'Snowflake' 'Emoji'@ and use
--- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-instance Parser Emoji r where
-  parse = parseMP (parserName @Emoji @r) $ mapParserMaybeM (try emoji <|> snowflake)
-          "Couldn't find an Emoji with this id"
-          (\eid -> do
-              ctx <- P.ask
-              pure $ ctx ^? #guild . _Just . #emojis . ix eid)
-
--- | Parses both discord emojis, and unicode emojis
-instance Parser RawEmoji r where
-  parse = parseMP (parserName @RawEmoji) (try parseCustomEmoji <|> (UnicodeEmoji <$> takeP (Just "A unicode emoji") 1))
-    where parseCustomEmoji = CustomEmoji <$> partialEmoji
-
--- | Parser for roles in the guild the command was invoked in, this only
--- looks in the cache. Use @'Snowflake' 'Role'@ and use
--- 'Calamity.Types.Upgradeable.upgrade' if you want to allow fetching from http.
-instance Parser Role r where
-  parse = parseMP (parserName @Role @r) $ mapParserMaybeM (try (ping "@&") <|> snowflake)
-          "Couldn't find an Emoji with this id"
-          (\rid -> do
-              ctx <- P.ask
-              pure $ ctx ^? #guild . _Just . #roles . ix rid)
-
-instance (Parser a r, Parser b r) => Parser (a, b) r where
-  type ParserResult (a, b) = (ParserResult a, ParserResult b)
-
-  parse = do
-    a <- parse @a
-    b <- parse @b
-    pure (a, b)
-
-instance Parser () 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
-
-ping :: MonadParsec e L.Text m => L.Text -> m (Snowflake a)
-ping c = chunk ("<" <> c) *> optional (chunk "!") *> snowflake <* chunk ">"
-
-ping' :: MonadParsec e L.Text m => m () -> m (Snowflake a)
-ping' m = chunk "<" *> m *> snowflake <* chunk ">"
-
-snowflake :: MonadParsec e L.Text m => m (Snowflake a)
-snowflake = Snowflake <$> decimal
-
-partialEmoji :: MonadParsec e L.Text m => m (Partial Emoji)
-partialEmoji = do
-  animated <- isJust <$> (chunk "<" *> optional (chunk "a"))
-  name <-  between (chunk ":") (chunk ":") (takeWhileP (Just "Emoji name") $ not . (== ':'))
-  id <- snowflake
-  void $ chunk ">"
-  pure (PartialEmoji id name animated)
-
-emoji :: MonadParsec e L.Text m => m (Snowflake a)
-emoji = ping' (optional (chunk "a") *> between (chunk ":") (chunk ":") (void $ takeWhileP Nothing $ not . (== ':')))
-
-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") $ not . (== '\''))) <|>
-               between (chunk "\"") (chunk "\"") (takeWhileP (Just "any character") $ not . (== '"'))
-
--- manyNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
--- manyNonWS = takeWhileP (Just "Any Non-Whitespace") (not . isSpace)
-
-someNonWS :: (Token s ~ Char, MonadParsec e s m) => m (Tokens s)
-someNonWS = takeWhile1P (Just "any non-whitespace") (not . isSpace)
diff --git a/Calamity/Commands/Types.hs b/Calamity/Commands/Types.hs
new file mode 100644
--- /dev/null
+++ b/Calamity/Commands/Types.hs
@@ -0,0 +1,27 @@
+{- | "CalamityCommands" types with their types filled in
+
+ If you're importing "CalamityCommands" make sure these get used instead of
+ the more generic variants.
+-}
+module Calamity.Commands.Types (
+    type Command,
+    type Group,
+    type CommandHandler,
+    type Check,
+    type DSLState,
+    type CommandContext,
+) where
+
+import qualified CalamityCommands.Check as CC
+import qualified CalamityCommands.Command as CC
+import qualified CalamityCommands.Dsl as CC
+import qualified CalamityCommands.Group as CC
+import qualified CalamityCommands.Handler as CC
+import qualified CalamityCommands.Context as CC
+
+type Command c = CC.Command IO c ()
+type Group c = CC.Group IO c ()
+type CommandHandler c = CC.CommandHandler IO c ()
+type Check c = CC.Check IO c
+type DSLState c r = CC.DSLState IO c () r
+type CommandContext c = CC.CommandContext IO c ()
diff --git a/Calamity/Commands/Utils.hs b/Calamity/Commands/Utils.hs
--- a/Calamity/Commands/Utils.hs
+++ b/Calamity/Commands/Utils.hs
@@ -1,55 +1,55 @@
+{-# LANGUAGE DerivingStrategies #-}
 {-# LANGUAGE RecursiveDo #-}
--- | Command handler utilities
-module Calamity.Commands.Utils
-    ( addCommands
-    , buildCommands
-    , buildContext
-    , handleCommands
-    , findCommand
-    , CmdInvokeFailReason(..) ) where
 
-import           Calamity.Cache.Eff
-import           Calamity.Metrics.Eff
-import           Calamity.Client.Client
-import           Calamity.Client.Types
-import           Calamity.Commands.AliasType
-import           Calamity.Commands.Command
-import           Calamity.Commands.CommandUtils
-import           Calamity.Commands.Context
-import           Calamity.Commands.Dsl
-import           Calamity.Commands.Handler
-import           Calamity.Commands.Error
-import           Calamity.Commands.Group
-import           Calamity.Commands.ParsePrefix
-import           Calamity.Internal.LocalWriter
-import           Calamity.Internal.Utils
-import           Calamity.Types.Model.Channel
-import           Calamity.Types.Model.User
-import           Calamity.Types.Snowflake
+-- | Command handler utilities
+module Calamity.Commands.Utils (
+  addCommands,
+  CmdInvokeFailReason (..),
+  CtxCommandError (..),
+  CommandNotFound (..),
+  CommandInvoked (..),
+) where
 
-import           Control.Lens                   hiding ( Context )
-import           Control.Monad
+import Calamity.Client.Client
+import Calamity.Client.Types
+import CalamityCommands.CommandUtils
+import qualified CalamityCommands.Error as CC
+import Calamity.Commands.Dsl
+import Calamity.Commands.Types
+import Calamity.Metrics.Eff
+import Calamity.Types.Model.Channel
+import qualified CalamityCommands.Utils as CC
+import Control.Monad
+import qualified Data.Text as S
+import qualified Data.Text.Lazy as L
+import GHC.Generics (Generic)
+import qualified Polysemy as P
+import qualified CalamityCommands.Context as CC
+import qualified CalamityCommands.ParsePrefix as CC
+import Data.Typeable
 
-import           Data.Char                      ( isSpace )
-import qualified Data.HashMap.Lazy              as LH
-import qualified Data.Text                      as S
-import qualified Data.Text.Lazy                 as L
+data CmdInvokeFailReason c
+  = NoContext
+  | NotFound [L.Text]
+  | CommandInvokeError c CC.CommandError
 
-import qualified Polysemy                       as P
-import qualified Polysemy.Error                 as P
-import qualified Polysemy.Fail                  as P
-import qualified Polysemy.Tagged                as P
-import qualified Polysemy.Fixpoint              as P
-import qualified Polysemy.Reader                as P
+data CtxCommandError c = CtxCommandError
+  { ctx :: c
+  , err :: CC.CommandError
+  }
+  deriving (Show, Generic)
 
-mapLeft :: (e -> e') -> Either e a -> Either e' a
-mapLeft f (Left x)  = Left $ f x
-mapLeft _ (Right x) = Right x
+data CommandNotFound = CommandNotFound
+  { msg :: Message
+  , -- | The groups that were successfully parsed
+    path :: [L.Text]
+  }
+  deriving (Show, Generic)
 
-data CmdInvokeFailReason
-  = NoContext
-  | NotFound [L.Text]
-  | CommandInvokeError Context CommandError
+newtype CommandInvoked c = CommandInvoked
+  { ctx :: c
+  }
+  deriving stock (Show, Generic)
 
 -- | Construct commands and groups from a command DSL, then registers an event
 -- handler on the bot that manages running those commands.
@@ -61,7 +61,7 @@
 --
 -- To determine if a command was invoked, and if so which command was invoked, the following happens:
 --
---     1. 'parsePrefix' is invoked, if no prefix is found: stop here.
+--     1. 'CalamityCommands.ParsePrefix.parsePrefix' is invoked, if no prefix is found: stop here.
 --
 --     2. The input is read a word at a time until a matching command is found,
 --        fire the \"command-not-found\" event if not.
@@ -72,123 +72,33 @@
 --
 -- This will fire the following events:
 --
---     1. @"command-error" ('Context', 'CommandError')@
+--     1. 'CtxCommandError'
 --
 --         Fired when a command returns an error.
 --
---     2. @"command-not-found" ('Calamity.Types.Model.Channel.Message', ['Data.Text.Lazy.Text'])@
+--     2. 'CommandNotFound'
 --
 --         Fired when a valid prefix is used, but the command is not found.
 --
---     3. @"command-invoked" 'Context'@
+--     3. 'CommandInvoked'
 --
 --         Fired when a command is successfully invoked.
 --
-addCommands :: (BotC r, P.Member ParsePrefix r) => P.Sem (DSLState r) a -> P.Sem r (P.Sem r (), CommandHandler, a)
+addCommands :: (BotC r, Typeable c, CommandContext c, P.Members [CC.ParsePrefix Message, CC.ConstructContext Message c IO ()] r)
+  => P.Sem (DSLState c r) a -> P.Sem r (P.Sem r (), CommandHandler c, a)
 addCommands m = do
-  (handler, res) <- buildCommands m
+  (handler, res) <- CC.buildCommands m
   remove <- react @'MessageCreateEvt $ \msg -> do
-    parsePrefix msg >>= \case
+    CC.parsePrefix msg >>= \case
       Just (prefix, cmd) -> do
-        r <- handleCommands handler msg prefix cmd
+        r <- CC.handleCommands handler msg prefix cmd
         case r of
-          Left (CommandInvokeError ctx e) -> fire $ customEvt @"command-error" (ctx, e)
-          Left (NotFound path)            -> fire $ customEvt @"command-not-found" (msg, path)
-          Left NoContext                  -> pure () -- ignore if context couldn't be built
-          Right ctx        -> do
-            cmdInvoke <- registerCounter "commands_invoked" [("name", S.unwords $ commandPath (ctx ^. #command))]
+          Left (CC.CommandInvokeError ctx e) -> fire . customEvt $ CtxCommandError ctx e
+          Left (CC.NotFound path)            -> fire . customEvt $ CommandNotFound msg path
+          Left CC.NoContext                  -> pure () -- ignore if context couldn't be built
+          Right (ctx, ())        -> do
+            cmdInvoke <- registerCounter "commands_invoked" [("name", S.unwords $ commandPath (CC.ctxCommand ctx))]
             void $ addCounter 1 cmdInvoke
-            fire $ customEvt @"command-invoked" ctx
+            fire . customEvt $ CommandInvoked ctx
       Nothing -> pure ()
   pure (remove, handler, res)
-
--- | Manages parsing messages and handling commands for a CommandHandler.
---
--- Returns Right if the command succeeded in parsing and running, Left with the
--- reason otherwise.
-handleCommands :: (BotC r, P.Member ParsePrefix r)
-               => CommandHandler
-               -> Message -- ^ The message that invoked the command
-               -> L.Text -- ^ The prefix used
-               -> L.Text -- ^ The command string, without a prefix
-               -> P.Sem r (Either CmdInvokeFailReason Context)
-handleCommands handler msg prefix cmd = P.runError $ do
-    (command, unparsedParams) <- P.fromEither $ mapLeft NotFound $ findCommand handler cmd
-    ctx <- P.note NoContext =<< buildContext msg prefix command unparsedParams
-    P.fromEither . mapLeft (CommandInvokeError ctx) =<< invokeCommand ctx (ctx ^. #command)
-    pure ctx
-
-
--- | Run a command DSL, returning the constructed 'CommandHandler'
-buildCommands :: forall r a. P.Member (P.Final IO) r
-              => P.Sem (DSLState r) a
-              -> P.Sem r (CommandHandler, a)
-buildCommands m = P.fixpointToFinal $ mdo
-  (groups, (cmds, a)) <- inner handler m
-  let handler = CommandHandler groups cmds
-  pure (handler, a)
-
-  where inner :: CommandHandler -> P.Sem (DSLState r) a
-              -> P.Sem (P.Fixpoint ': r) (LH.HashMap S.Text (Group, AliasType),
-                                          (LH.HashMap S.Text (Command, AliasType), a))
-        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, AliasType)) .
-          runLocalWriter @(LH.HashMap S.Text (Command, AliasType))
-        defaultHelp = (const "This command or group has no help.")
-
--- TODO: turn this into an effect
--- | Attempt to build the context for a command
-buildContext :: BotC r => Message -> L.Text -> Command -> L.Text -> P.Sem r (Maybe Context)
-buildContext msg prefix command unparsed = (rightToMaybe <$>) . P.runFail $ do
-  guild <- join <$> getGuild `traverse` (msg ^. #guildID)
-  let member = guild ^? _Just . #members . ix (coerceSnowflake $ getID @User msg)
-  let gchan = guild ^? _Just . #channels . ix (coerceSnowflake $ getID @Channel msg)
-  Just channel <- case gchan of
-    Just chan -> pure . pure $ GuildChannel' chan
-    Nothing   -> DMChannel' <<$>> getDM (coerceSnowflake $ getID @Channel msg)
-  Just user <- getUser $ getID msg
-
-  pure $ Context msg guild member channel user command prefix unparsed
-
-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 :: CommandHandler -> L.Text -> Either [L.Text] (Command, L.Text)
-findCommand handler msg = goH $ nextWord msg
-  where
-    goH :: (L.Text, L.Text) -> Either [L.Text] (Command, 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 -> Either [L.Text] (Command, 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 :: Maybe (a, b) -> Either [L.Text] a
-    attachInitial (Just (a, _)) = Right a
-    attachInitial Nothing = Left []
-
-    attachSoFar :: L.Text -> Either [L.Text] a -> Either [L.Text] a
-    attachSoFar cmd (Left xs) = Left (cmd:xs)
-    attachSoFar _ r = r
diff --git a/Calamity/Gateway/DispatchEvents.hs b/Calamity/Gateway/DispatchEvents.hs
--- a/Calamity/Gateway/DispatchEvents.hs
+++ b/Calamity/Gateway/DispatchEvents.hs
@@ -19,13 +19,13 @@
 import Calamity.Types.UnixTimestamp
 
 import Data.Aeson
-import Data.Dynamic
 import Data.Text.Lazy (Text)
 import Data.Time
-import Data.Typeable
 import Data.Vector.Unboxing (Vector)
 
 import GHC.Generics
+import Data.Kind (Type)
+import Data.Typeable (Typeable)
 
 data CalamityEvent
   = Dispatch
@@ -33,13 +33,9 @@
       -- ^ The shard that pushed this event
       DispatchData
       -- ^ The attached data
-  | Custom
-      TypeRep
-      -- ^ The name of the custom event
-      Dynamic
+  | forall (a :: Type). Typeable a => Custom a
       -- ^ The data sent to the custom event
   | ShutDown
-  deriving (Show, Generic)
 
 data DispatchData
   = Ready !ReadyData
diff --git a/Calamity/Types/Model/Channel/Embed.hs b/Calamity/Types/Model/Channel/Embed.hs
--- a/Calamity/Types/Model/Channel/Embed.hs
+++ b/Calamity/Types/Model/Channel/Embed.hs
@@ -1,142 +1,206 @@
 -- | Message embeds
-module Calamity.Types.Model.Channel.Embed
-    ( Embed(..)
-    , EmbedFooter(..)
-    , EmbedImage(..)
-    , EmbedThumbnail(..)
-    , EmbedVideo(..)
-    , EmbedProvider(..)
-    , EmbedAuthor(..)
-    , EmbedField(..) ) where
+module Calamity.Types.Model.Channel.Embed (
+    Embed (..),
+    embedFooter,
+    embedImage,
+    embedThumbnail,
+    embedAuthor,
+    embedAuthor',
+    embedField,
+    EmbedFooter (..),
+    EmbedImage (..),
+    EmbedThumbnail (..),
+    EmbedVideo (..),
+    EmbedProvider (..),
+    EmbedAuthor (..),
+    EmbedField (..),
+) where
 
-import           Calamity.Internal.AesonThings
-import           Calamity.Internal.IntColour   ()
-import           Calamity.Internal.Utils       ()
+import Calamity.Internal.AesonThings
+import Calamity.Internal.IntColour ()
+import Calamity.Internal.Utils ()
 
-import           Control.Lens
+import Control.Lens
 
-import           Data.Aeson
-import           Data.Colour                   ( Colour )
-import           Data.Default.Class
-import           Data.Generics.Labels          ()
-import           Data.Semigroup
-import           Data.Text.Lazy                ( Text )
-import           Data.Time
-import           Data.Word
+import Data.Aeson
+import Data.Colour (Colour)
+import Data.Default.Class
+import Data.Generics.Labels ()
+import Data.Semigroup
+import Data.Text.Lazy (Text)
+import Data.Time
+import Data.Word
 
-import           GHC.Generics
+import GHC.Generics
 
-import           TextShow
-import qualified TextShow.Generic              as TSG
+import TextShow
+import qualified TextShow.Generic as TSG
 
 data Embed = Embed
-  { title       :: Maybe Text
-  , type_       :: Maybe Text
-  , description :: Maybe Text
-  , url         :: Maybe Text
-  , timestamp   :: Maybe UTCTime
-  , color       :: Maybe (Colour Double)
-  , footer      :: Maybe EmbedFooter
-  , image       :: Maybe EmbedImage
-  , thumbnail   :: Maybe EmbedThumbnail
-  , video       :: Maybe EmbedVideo
-  , provider    :: Maybe EmbedProvider
-  , author      :: Maybe EmbedAuthor
-  , fields      :: [EmbedField]
-  }
-  deriving ( Eq, Show, Generic, Default )
-  deriving ( TextShow ) via TSG.FromGeneric Embed
-  deriving ( FromJSON ) via WithSpecialCases '[IfNoneThen "fields" DefaultToEmptyArray] Embed
-  deriving ( ToJSON ) via CalamityJSON Embed
+    { title :: Maybe Text
+    , type_ :: Maybe Text
+    , description :: Maybe Text
+    , url :: Maybe Text
+    , timestamp :: Maybe UTCTime
+    , color :: Maybe (Colour Double)
+    , footer :: Maybe EmbedFooter
+    , image :: Maybe EmbedImage
+    , thumbnail :: Maybe EmbedThumbnail
+    , video :: Maybe EmbedVideo
+    , provider :: Maybe EmbedProvider
+    , author :: Maybe EmbedAuthor
+    , fields :: [EmbedField]
+    }
+    deriving (Eq, Show, Generic, Default)
+    deriving (TextShow) via TSG.FromGeneric Embed
+    deriving (FromJSON) via WithSpecialCases '[IfNoneThen "fields" DefaultToEmptyArray] Embed
+    deriving (ToJSON) via CalamityJSON Embed
 
 instance Semigroup Embed where
-  l <> r = l
-    & #title       %~ (<> (r ^. #title))
-    & #type_       %~ getLast . (<> Last (r ^. #type_)) . Last
-    & #description %~ (<> (r ^. #description))
-    & #url         %~ getLast . (<> Last (r ^. #url)) . Last
-    & #timestamp   %~ getLast . (<> Last (r ^. #timestamp)) . Last
-    & #color       %~ getLast . (<> Last (r ^. #color)) . Last
-    & #footer      %~ (<> (r ^. #footer))
-    & #image       %~ getLast . (<> Last (r ^. #image)) . Last
-    & #thumbnail   %~ getLast . (<> Last (r ^. #thumbnail)) . Last
-    & #video       %~ getLast . (<> Last (r ^. #video)) . Last
-    & #provider    %~ getLast . (<> Last (r ^. #provider)) . Last
-    & #author      %~ getLast . (<> Last (r ^. #author)) . Last
-    & #fields      %~ (<> (r ^. #fields))
+    l <> r =
+        l
+            & #title %~ (<> (r ^. #title))
+            & #type_ %~ getLast . (<> Last (r ^. #type_)) . Last
+            & #description %~ (<> (r ^. #description))
+            & #url %~ getLast . (<> Last (r ^. #url)) . Last
+            & #timestamp %~ getLast . (<> Last (r ^. #timestamp)) . Last
+            & #color %~ getLast . (<> Last (r ^. #color)) . Last
+            & #footer %~ (<> (r ^. #footer))
+            & #image %~ getLast . (<> Last (r ^. #image)) . Last
+            & #thumbnail %~ getLast . (<> Last (r ^. #thumbnail)) . Last
+            & #video %~ getLast . (<> Last (r ^. #video)) . Last
+            & #provider %~ getLast . (<> Last (r ^. #provider)) . Last
+            & #author %~ getLast . (<> Last (r ^. #author)) . Last
+            & #fields %~ (<> (r ^. #fields))
 
 instance Monoid Embed where
-  mempty = def
+    mempty = def
 
 data EmbedFooter = EmbedFooter
-  { text         :: Text
-  , iconUrl      :: Maybe Text
-  , proxyIconUrl :: Maybe Text
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedFooter
-  deriving ( ToJSON, FromJSON ) via CalamityJSON EmbedFooter
+    { text :: Text
+    , iconUrl :: Maybe Text
+    , proxyIconUrl :: Maybe Text
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedFooter
+    deriving (ToJSON, FromJSON) via CalamityJSON EmbedFooter
 
+{- | Create an embed footer with a provided content
+
+ The remaining fields are set to Nothing
+-}
+embedFooter :: Text -> EmbedFooter
+embedFooter text = EmbedFooter text Nothing Nothing
+
 instance Semigroup EmbedFooter where
-  l <> r = l
-    & #text         %~ (<> (r ^. #text))
-    & #iconUrl      %~ getLast . (<> Last (r ^. #iconUrl)) . Last
-    & #proxyIconUrl %~ getLast . (<> Last (r ^. #proxyIconUrl)) . Last
+    l <> r =
+        l
+            & #text %~ (<> (r ^. #text))
+            & #iconUrl %~ getLast . (<> Last (r ^. #iconUrl)) . Last
+            & #proxyIconUrl %~ getLast . (<> Last (r ^. #proxyIconUrl)) . Last
 
 data EmbedImage = EmbedImage
-  { url      :: Text
-  , proxyUrl :: Text
-  , width    :: Word64
-  , height   :: Word64
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedImage
-  deriving ( FromJSON, ToJSON ) via CalamityJSON EmbedImage
+    { url :: Text
+    , proxyUrl :: Maybe Text
+    , width :: Maybe Word64
+    , height :: Maybe Word64
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedImage
+    deriving (FromJSON, ToJSON) via CalamityJSON EmbedImage
 
+{- | Create an embed image with a provided url
+
+ The remaining fields are set to Nothing
+-}
+embedImage :: Text -> EmbedImage
+embedImage url = EmbedImage url Nothing Nothing Nothing
+
 data EmbedThumbnail = EmbedThumbnail
-  { url      :: Text
-  , proxyUrl :: Text
-  , width    :: Word64
-  , height   :: Word64
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedThumbnail
-  deriving ( FromJSON, ToJSON ) via CalamityJSON EmbedThumbnail
+    { url :: Text
+    , proxyUrl :: Maybe Text
+    , width :: Maybe Word64
+    , height :: Maybe Word64
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedThumbnail
+    deriving (FromJSON, ToJSON) via CalamityJSON EmbedThumbnail
 
+{- | Create an embed thumbnail with a provided url
+
+ The remaining fields are set to Nothing
+-}
+embedThumbnail :: Text -> EmbedThumbnail
+embedThumbnail url = EmbedThumbnail url Nothing Nothing Nothing
+
 data EmbedVideo = EmbedVideo
-  { url    :: Text
-  , width  :: Word64
-  , height :: Word64
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedVideo
-  deriving ( FromJSON, ToJSON ) via CalamityJSON EmbedVideo
+    { url :: Maybe Text
+    , proxyUrl :: Maybe Text
+    , width :: Maybe Word64
+    , height :: Maybe Word64
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedVideo
+    deriving (FromJSON, ToJSON) via CalamityJSON EmbedVideo
 
 data EmbedProvider = EmbedProvider
-  { name :: Text
-  , url  :: Maybe Text
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedProvider
-  deriving ( ToJSON, FromJSON ) via CalamityJSON EmbedProvider
+    { name :: Maybe Text
+    , url :: Maybe Text
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedProvider
+    deriving (ToJSON, FromJSON) via CalamityJSON EmbedProvider
 
 data EmbedAuthor = EmbedAuthor
-  { name         :: Maybe Text
-  , url          :: Maybe Text
-  , iconUrl      :: Maybe Text
-  , proxyIconURL :: Maybe Text
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedAuthor
-  deriving ( ToJSON, FromJSON ) via CalamityJSON EmbedAuthor
+    { name :: Maybe Text
+    , url :: Maybe Text
+    , iconUrl :: Maybe Text
+    , proxyIconURL :: Maybe Text
+    }
+    deriving (Eq, Show, Generic, Default)
+    deriving (TextShow) via TSG.FromGeneric EmbedAuthor
+    deriving (ToJSON, FromJSON) via CalamityJSON EmbedAuthor
 
+{- | Create an embed author with the given name
+
+ The remaining fields are set to Nothing
+-}
+embedAuthor :: Text -> EmbedAuthor
+embedAuthor name = EmbedAuthor (Just name) Nothing Nothing Nothing
+
+{- | Create an embed author with the given name, url, and icon url
+
+ The remaining fields are set to Nothing
+-}
+embedAuthor' ::
+    -- | Name
+    Text ->
+    -- | Url
+    Text ->
+    -- | Icon url
+    Text ->
+    EmbedAuthor
+embedAuthor' name url iconUrl = EmbedAuthor (Just name) (Just url) (Just iconUrl) Nothing
+
 data EmbedField = EmbedField
-  { name   :: Text
-  , value  :: Text
-  , inline :: Bool
-  }
-  deriving ( Eq, Show, Generic )
-  deriving ( TextShow ) via TSG.FromGeneric EmbedField
-  deriving ( FromJSON ) via WithSpecialCases '[IfNoneThen "inline" DefaultToFalse]
-      EmbedField
-  deriving ( ToJSON ) via CalamityJSON EmbedField
+    { name :: Text
+    , value :: Text
+    , inline :: Bool
+    }
+    deriving (Eq, Show, Generic)
+    deriving (TextShow) via TSG.FromGeneric EmbedField
+    deriving
+        (FromJSON)
+        via WithSpecialCases
+                '[IfNoneThen "inline" DefaultToFalse]
+                EmbedField
+    deriving (ToJSON) via CalamityJSON EmbedField
+
+-- | Create a non-inline embed field
+embedField ::
+    -- | Name
+    Text ->
+    -- | Value
+    Text ->
+    EmbedField
+embedField name value = EmbedField name value False
diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,24 @@
 # Changelog for Calamity
 
+## 0.1.30.0
+
++ Removed the `Symbol` parameter of custom events, instead of `'CustomEvt
+  @"command-error" @(FullContext, CommandError)` it is now `'CustomEvt
+  (CtxCommandError FullContext)`, etc.
++ Added `embedFooter`, `embedImage`, `embedThumbnail`, `embedAuthor`, and
+  `embedField` utility functions.
++ Added `Default` instances for `EmbedAuthor`.
++ Corrected the nullability of `EmbedImage`, `EmbedThumbnail`, `EmbedVideo`, and
+  `EmbedProvider`.
++ Changed the command parameter machinery to hold more info about parameters.
++ Added a 'type cheatsheet' thing to command help in the default help command.
++ Calamity now uses and re-exports the Calamity-Commands package instead of
+  having all the code duplicated.
++ An extra effect now needs to be handled for commands: `ConstructContext`, this
+  also allows you to change which context your bot uses.
++ `Calamity.Commands.Context.Context` has been removed, instead use
+  `FullContext`, `LightContext`, or make your own.
+  
 ## 0.1.29.0
 
 + The minimum version of `base` has been upped to `4.13` as the library fails to
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -89,8 +89,8 @@
 
 import           Calamity
 import           Calamity.Cache.InMemory
-import           Calamity.Commands
-import qualified Calamity.Commands.Context   as CommandContext
+import Calamity.Commands
+import Calamity.Commands.Context (FullContext, useFullContext)
 import           Calamity.Metrics.Noop
 
 import           Control.Concurrent
@@ -139,11 +139,14 @@
 tellt :: (BotC r, Tellable t) => t -> L.Text -> P.Sem r (Either RestError Message)
 tellt t m = tell t $ L.toStrict m
 
+data MyCustomEvt = MyCustomEvt L.Text Message
+
 main :: IO ()
 main = do
   token <- L.pack <$> getEnv "BOT_TOKEN"
   Di.new $ \di ->
-    void . P.runFinal . P.embedToFinal . DiP.runDiToIO di . runCounterAtomic . runCacheInMemory . runMetricsNoop . useConstantPrefix "!"
+    void . P.runFinal . P.embedToFinal . DiP.runDiToIO di . runCounterAtomic 
+         . runCacheInMemory . runMetricsNoop . useConstantPrefix "!" . useFullContext
       $ runBotIO (BotToken token) defaultIntents $ do
       addCommands $ do
         helpCommand
@@ -173,7 +176,7 @@
           void $ tellt ctx "bye!"
           stopBot
         command @'[] "fire-evt" $ \ctx -> do
-          fire $ customEvt @"my-event" ("aha" :: L.Text, ctx ^. #message)
+          fire . customEvt $ MyCustomEvt "aha" (ctx ^. #message)
         command @'[L.Text] "wait-for" $ \ctx s -> do
           void $ tellt ctx ("waiting for !" <> s)
           waitUntil @'MessageCreateEvt (\msg -> msg ^. #content == ("!" <> s))
@@ -188,11 +191,11 @@
           void . invoke $ EditMessage (msg ^. #channelID) msg' (editMessageContent $ Just "lol")
           info "edited"
         _ -> pure ()
-      react @('CustomEvt "command-error" (CommandContext.Context, CommandError)) $ \(ctx, e) -> do
+      react @('CustomEvt (CtxCommandError FullContext)) \(CtxCommandError ctx e) -> do
         info $ "Command failed with reason: " <> showtl e
         case e of
           ParseError n r -> void . tellt ctx $ "Failed to parse parameter: `" <> L.fromStrict n <> "`, with reason: ```\n" <> r <> "```"
-      react @('CustomEvt "my-event" (L.Text, Message)) $ \(s, m) ->
+      react @('CustomEvt MyCustomEvt) $ \(MyCustomEvt s m) ->
         void $ tellt m ("Somebody told me to tell you about: " <> s)
 ```
 
diff --git a/calamity.cabal b/calamity.cabal
--- a/calamity.cabal
+++ b/calamity.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.18
 
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.4.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 89740b43cb19ef8c4833319daf4096283256327512903f70f9f82463297c7d07
+-- hash: 154662ee222dbfe7a26afc26afaa4243ceb89b1cccf3a71624f5b4c865436e46
 
 name:           calamity
-version:        0.1.29.0
+version:        0.1.30.0
 synopsis:       A library for writing discord bots in haskell
 description:    Please see the README on GitHub at <https://github.com/simmsb/calamity#readme>
 category:       Network, Web
@@ -18,7 +18,8 @@
 copyright:      2020 Ben Simms
 license:        MIT
 license-file:   LICENSE
-tested-with:    GHC == 8.10.4
+tested-with:
+    GHC == 8.10.4
 build-type:     Simple
 extra-source-files:
     README.md
@@ -40,18 +41,11 @@
       Calamity.Client.ShardManager
       Calamity.Client.Types
       Calamity.Commands
-      Calamity.Commands.AliasType
-      Calamity.Commands.Check
-      Calamity.Commands.Command
-      Calamity.Commands.CommandUtils
+      Calamity.Commands.CalamityParsers
       Calamity.Commands.Context
       Calamity.Commands.Dsl
-      Calamity.Commands.Error
-      Calamity.Commands.Group
-      Calamity.Commands.Handler
       Calamity.Commands.Help
-      Calamity.Commands.ParsePrefix
-      Calamity.Commands.Parser
+      Calamity.Commands.Types
       Calamity.Commands.Utils
       Calamity.Gateway
       Calamity.Gateway.DispatchEvents
@@ -132,45 +126,95 @@
   other-modules:
       Paths_calamity
   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
+      ./
+  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:
       aeson >=1.4 && <2
     , async >=2.2 && <3
     , base >=4.13 && <5
     , bytestring >=0.10 && <0.12
+    , calamity-commands >=0.1.2 && <0.2
     , colour >=2.3.5 && <2.4
-    , concurrent-extra >=0.7 && <0.8
+    , concurrent-extra ==0.7.*
     , connection >=0.2.6 && <0.4
-    , containers >=0.6 && <0.7
-    , data-default-class >=0.1 && <0.2
+    , containers ==0.6.*
+    , data-default-class ==0.1.*
     , data-flags >=0.0.3 && <0.1
     , deepseq >=1.4.4.0 && <2
-    , deque >=0.4 && <0.5
-    , df1 >=0.4 && <0.5
+    , deque ==0.4.*
+    , df1 ==0.4.*
     , di-core >=1.0.4 && <1.1
-    , di-polysemy >=0.2 && <0.3
-    , exceptions >=0.10 && <0.11
-    , fmt >=0.6 && <0.7
+    , di-polysemy ==0.2.*
+    , exceptions ==0.10.*
+    , fmt ==0.6.*
     , focus >=1.0 && <2
     , generic-lens >=2.0 && <3
     , hashable >=1.2 && <2
     , http-api-data >=0.4.3 && <0.5
     , http-client >=0.5 && <0.8
     , http-date >=0.0.8 && <0.1
-    , http-types >=0.12 && <0.13
+    , http-types ==0.12.*
     , lens >=4.18 && <6
     , lens-aeson >=1.1 && <2
     , megaparsec >=8 && <10
-    , mime-types >=0.1 && <0.2
+    , mime-types ==0.1.*
     , mtl >=2.2 && <3
     , polysemy >=1.5 && <2
-    , polysemy-plugin >=0.3 && <0.4
+    , polysemy-plugin ==0.3.*
     , reflection >=2.1 && <3
     , req >=3.1 && <3.10
     , safe-exceptions >=0.1 && <2
-    , scientific >=0.3 && <0.4
+    , scientific ==0.3.*
     , stm >=2.5 && <3
     , stm-chans >=3.0 && <4
     , stm-containers >=1.1 && <2
@@ -178,11 +222,11 @@
     , text-show >=3.8 && <4
     , time >=1.8 && <1.12
     , tls >=1.4 && <2
-    , typerep-map >=0.3 && <0.4
-    , unagi-chan >=0.4 && <0.5
-    , unboxing-vector >=0.2 && <0.3
-    , unordered-containers >=0.2 && <0.3
-    , vector >=0.12 && <0.13
-    , websockets >=0.12 && <0.13
+    , typerep-map ==0.3.*
+    , unagi-chan ==0.4.*
+    , unboxing-vector ==0.2.*
+    , unordered-containers ==0.2.*
+    , vector ==0.12.*
+    , websockets ==0.12.*
     , x509-system >=1.6.6 && <1.7
   default-language: Haskell2010
