diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# Changelog
+
+All notable changes to `tricorder-types` will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to the [PVP](https://pvp.haskell.org/).
+
+## [Unreleased]
+
+## [0.1.0.0] - 2026-08-24
+
+### Added
+
+- Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Tweag
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# tricorder-types
+
+Shared domain types for [Tricorder](../tricorder/README.md) and
+[Tricorder's MCP server](../tricorder-mcp/README.md).
+
+`tricorder-types` intentionally has no dependency on `tricorder` itself, so
+consumers that only need to construct or render these types — without pulling
+in the full daemon, TUI, or build machinery — can depend on it directly.
+
+## License
+
+MIT — see [LICENSE](LICENSE).
diff --git a/src/Tricorder/CLI/Command.hs b/src/Tricorder/CLI/Command.hs
new file mode 100644
--- /dev/null
+++ b/src/Tricorder/CLI/Command.hs
@@ -0,0 +1,136 @@
+module Tricorder.CLI.Command
+    ( Command (..)
+    , EvalCommentsOptions (..)
+    , FollowMode (..)
+    , Force (..)
+    , LogMode (..)
+    , OutputFormat (..)
+    , StatusOptions (..)
+    , TestOptions (..)
+    , Verbosity (..)
+    , WaitMode (..)
+    , commandToArgs
+    )
+where
+
+import Tricorder.SourceLookup.SourceQuery (SourceQuery, renderSourceQuery)
+
+
+data Force = Force | NoForce
+
+
+data WaitMode
+    = ShowCurrent
+    | WaitForBuild
+    deriving stock (Eq)
+
+
+data OutputFormat
+    = TextOutput
+    | JsonOutput
+    deriving stock (Eq)
+
+
+data Verbosity
+    = Concise
+    | Verbose
+    deriving stock (Eq)
+
+
+data FollowMode
+    = NoFollow
+    | Follow
+    deriving stock (Eq)
+
+
+data LogMode
+    = ShowLog FollowMode
+    | ShowLogPath
+
+
+data StatusOptions = StatusOptions
+    { wait :: WaitMode
+    , format :: OutputFormat
+    , verbosity :: Verbosity
+    , expand :: Maybe Int
+    }
+
+
+data TestOptions = TestOptions
+    { failedOnly :: Bool
+    , wait :: WaitMode
+    }
+
+
+data EvalCommentsOptions = EvalCommentsOptions
+    { wait :: WaitMode
+    , format :: OutputFormat
+    }
+
+
+data Command
+    = Start
+    | Stop Force
+    | Status StatusOptions
+    | Test TestOptions
+    | UI
+    | Log LogMode
+    | Source [SourceQuery]
+    | Restart Force
+    | EvalComments EvalCommentsOptions
+
+
+-- | Render a 'Command' to the argument list the @tricorder@ CLI expects
+-- (subcommand name followed by flags) — the inverse of the parser in
+-- "Tricorder.CLI.Arguments". Kept next to 'Command' so a new field or
+-- constructor forces both the parser and this renderer to be updated
+-- together; this is what @tricorder-mcp@ uses to invoke @tricorder@ without
+-- duplicating flag names.
+commandToArgs :: Command -> [String]
+commandToArgs Start = ["start"]
+commandToArgs (Stop doForce) = "stop" : forceArgs doForce
+commandToArgs (Status (StatusOptions {wait, format, verbosity, expand})) =
+    "status"
+        : waitArgs wait
+            <> formatArgs format
+            <> verbosityArgs verbosity
+            <> maybe [] (\n -> ["--expand", show n]) expand
+commandToArgs (Test (TestOptions {failedOnly, wait})) =
+    "test-results" : failedArgs failedOnly <> waitArgs wait
+commandToArgs UI = ["ui"]
+commandToArgs (Log ShowLogPath) = ["log", "--print-path"]
+commandToArgs (Log (ShowLog follow)) = "log" : followArgs follow
+commandToArgs (Source queries) = "source" : map renderSourceQuery queries
+commandToArgs (Restart doForce) = "restart" : forceArgs doForce
+commandToArgs (EvalComments (EvalCommentsOptions {wait, format})) =
+    "eval-comments" : waitArgs wait <> formatArgs format
+
+
+forceArgs :: Force -> [String]
+forceArgs Force = ["--force"]
+forceArgs NoForce = []
+
+
+waitArgs :: WaitMode -> [String]
+waitArgs WaitForBuild = ["--wait"]
+waitArgs ShowCurrent = []
+
+
+formatArgs :: OutputFormat -> [String]
+formatArgs JsonOutput = ["--json"]
+formatArgs TextOutput = []
+
+
+verbosityArgs :: Verbosity -> [String]
+verbosityArgs Verbose = ["--verbose"]
+verbosityArgs Concise = []
+
+
+followArgs :: FollowMode -> [String]
+followArgs Follow = ["--follow"]
+followArgs NoFollow = []
+
+
+failedArgs :: Bool -> [String]
+failedArgs True = ["--failed"]
+failedArgs False = []
diff --git a/src/Tricorder/SourceLookup/SourceQuery.hs b/src/Tricorder/SourceLookup/SourceQuery.hs
new file mode 100644
--- /dev/null
+++ b/src/Tricorder/SourceLookup/SourceQuery.hs
@@ -0,0 +1,45 @@
+module Tricorder.SourceLookup.SourceQuery
+    ( SourceQuery (..)
+    , ModuleName (..)
+    , parseSourceQuery
+    , renderSourceQuery
+    )
+where
+
+import Data.Aeson (FromJSON, ToJSON)
+
+import Data.Text qualified as T
+
+
+-- | A query for module source: optionally scoped to a single top-level symbol.
+data SourceQuery = SourceQuery
+    { moduleName :: ModuleName
+    , function :: Maybe Text
+    -- ^ The symbol to slice: 'Nothing' is the whole module; @'Just' name@ is a
+    -- single top-level declaration — a value binding, or (by initial casing) a
+    -- type, class, or constructor.
+    }
+    deriving stock (Eq, Generic, Show)
+    deriving anyclass (FromJSON, Hashable, ToJSON)
+
+
+-- | Parse the CLI/MCP query syntax @MODULE[#FUNCTION]@ into a 'SourceQuery'.
+parseSourceQuery :: Text -> SourceQuery
+parseSourceQuery t =
+    let (m, rest) = T.break (== '#') t
+    in  SourceQuery
+            { moduleName = ModuleName m
+            , function = if T.null rest then Nothing else Just (T.tail rest)
+            }
+
+
+-- | Render a 'SourceQuery' back to its @MODULE[#FUNCTION]@ argument form, the
+-- inverse of 'parseSourceQuery'.
+renderSourceQuery :: SourceQuery -> String
+renderSourceQuery (SourceQuery {moduleName, function}) =
+    toString (unModuleName moduleName) <> maybe "" (\f -> "#" <> toString f) function
+
+
+-- | A dotted Haskell module name, e.g. @"Data.Map.Strict"@.
+newtype ModuleName = ModuleName {unModuleName :: Text}
+    deriving newtype (Eq, FromJSON, Hashable, IsString, Ord, Show, ToJSON)
diff --git a/tricorder-types.cabal b/tricorder-types.cabal
new file mode 100644
--- /dev/null
+++ b/tricorder-types.cabal
@@ -0,0 +1,73 @@
+cabal-version: 2.0
+
+-- This file has been generated from package.yaml by hpack version 0.38.3.
+--
+-- see: https://github.com/sol/hpack
+
+name:           tricorder-types
+version:        0.1.0.0
+synopsis:       Shared domain types for various Tricorder components
+description:    Shared domain types for various Tricorder components like
+                [Tricorder itself](https://hackage.haskell.org/package/tricorder) and
+                [tricorder-mcp](https://hackage.haskell.org/package/tricorder-mcp).
+category:       Development
+homepage:       https://github.com/tweag/tricorder#readme
+bug-reports:    https://github.com/tweag/tricorder/issues
+author:         Victor Nascimento Bakke
+maintainer:     victor.bakke@tweag.io
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+tested-with:
+    GHC == 9.10.3
+  , GHC == 9.6.7
+  , GHC == 9.8.4
+  , GHC == 9.12.4
+extra-doc-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/tweag/tricorder
+
+library
+  exposed-modules:
+      Tricorder.CLI.Command
+      Tricorder.SourceLookup.SourceQuery
+  other-modules:
+      Paths_tricorder_types
+  autogen-modules:
+      Paths_tricorder_types
+  hs-source-dirs:
+      src
+  default-extensions:
+      BlockArguments
+      DataKinds
+      DeriveAnyClass
+      DerivingStrategies
+      DerivingVia
+      DuplicateRecordFields
+      FlexibleContexts
+      GADTs
+      LambdaCase
+      MultiWayIf
+      OverloadedLabels
+      OverloadedRecordDot
+      OverloadedStrings
+      StrictData
+      TemplateHaskell
+      TypeFamilies
+  ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded
+  build-depends:
+      aeson >=2.2 && <2.4
+    , atelier-prelude >=0.1 && <0.3
+    , base >=4.18 && <4.23
+    , effectful-core ==2.6.*
+    , effectful-plugin >=2.0 && <2.2
+    , text ==2.1.*
+  mixins:
+      base hiding (Prelude)
+  default-language: GHC2021
+  if impl(GHC >= 9.8)
+    ghc-options: -Wno-missing-poly-kind-signatures -Wno-missing-role-annotations
