packages feed

okf-cli 0.1.1.0 → 0.1.2.0

raw patch · 9 files changed

+638/−5 lines, 9 filesdep +baikaidep +baikai-kitdep +dhalldep ~okf-corePVP ok

version bump matches the API change (PVP)

Dependencies added: baikai, baikai-kit, dhall

Dependency ranges changed: okf-core

API changes (from Hackage documentation)

+ Okf.Cli: Assist :: AssistOptions -> Command
+ Okf.Cli: Config :: ConfigCommand -> Command
+ Okf.Cli: ConfigInit :: !Bool -> ConfigCommand
+ Okf.Cli: ConfigPath :: ConfigCommand
+ Okf.Cli: ConfigShow :: ConfigCommand
+ Okf.Cli: Kit :: KitCommand -> Command
+ Okf.Cli: data ConfigCommand
+ Okf.Cli: instance GHC.Classes.Eq Okf.Cli.ConfigCommand
+ Okf.Cli: instance GHC.Internal.Show.Show Okf.Cli.ConfigCommand
+ Okf.Cli.Assist: AssistOptions :: !Text -> !Maybe Text -> !Bool -> AssistOptions
+ Okf.Cli.Assist: [modelOverride] :: AssistOptions -> !Maybe Text
+ Okf.Cli.Assist: [printCommand] :: AssistOptions -> !Bool
+ Okf.Cli.Assist: [prompt] :: AssistOptions -> !Text
+ Okf.Cli.Assist: assistOptionsParser :: Parser AssistOptions
+ Okf.Cli.Assist: buildClaudeCommand :: OkfConfig -> [FilePath] -> AssistOptions -> [String]
+ Okf.Cli.Assist: data AssistOptions
+ Okf.Cli.Assist: handleAssistCommand :: OkfConfig -> AssistOptions -> IO ()
+ Okf.Cli.Assist: instance GHC.Classes.Eq Okf.Cli.Assist.AssistOptions
+ Okf.Cli.Assist: instance GHC.Internal.Show.Show Okf.Cli.Assist.AssistOptions
+ Okf.Cli.Config: AssistSettings :: !OkfProvider -> !Maybe Text -> !Maybe Text -> AssistSettings
+ Okf.Cli.Config: KitSettings :: !Text -> ![OkfProvider] -> KitSettings
+ Okf.Cli.Config: OkfConfig :: !KitSettings -> !AssistSettings -> OkfConfig
+ Okf.Cli.Config: ProviderClaude :: OkfProvider
+ Okf.Cli.Config: ProviderCodex :: OkfProvider
+ Okf.Cli.Config: SourceDefaults :: ConfigSource
+ Okf.Cli.Config: SourceDot :: !FilePath -> ConfigSource
+ Okf.Cli.Config: SourceEnv :: !FilePath -> ConfigSource
+ Okf.Cli.Config: SourceProject :: !FilePath -> ConfigSource
+ Okf.Cli.Config: SourceXdg :: !FilePath -> ConfigSource
+ Okf.Cli.Config: [assist] :: OkfConfig -> !AssistSettings
+ Okf.Cli.Config: [kit] :: OkfConfig -> !KitSettings
+ Okf.Cli.Config: [model] :: AssistSettings -> !Maybe Text
+ Okf.Cli.Config: [provider] :: AssistSettings -> !OkfProvider
+ Okf.Cli.Config: [providers] :: KitSettings -> ![OkfProvider]
+ Okf.Cli.Config: [repoUrl] :: KitSettings -> !Text
+ Okf.Cli.Config: [systemPrompt] :: AssistSettings -> !Maybe Text
+ Okf.Cli.Config: data AssistSettings
+ Okf.Cli.Config: data ConfigSource
+ Okf.Cli.Config: data KitSettings
+ Okf.Cli.Config: data OkfConfig
+ Okf.Cli.Config: data OkfProvider
+ Okf.Cli.Config: defaultOkfConfig :: OkfConfig
+ Okf.Cli.Config: dotConfigPath :: IO FilePath
+ Okf.Cli.Config: exampleConfigText :: Text
+ Okf.Cli.Config: findConfigSource :: IO ConfigSource
+ Okf.Cli.Config: instance Dhall.Marshal.Decode.FromDhall Okf.Cli.Config.AssistSettings
+ Okf.Cli.Config: instance Dhall.Marshal.Decode.FromDhall Okf.Cli.Config.KitSettings
+ Okf.Cli.Config: instance Dhall.Marshal.Decode.FromDhall Okf.Cli.Config.OkfConfig
+ Okf.Cli.Config: instance Dhall.Marshal.Decode.FromDhall Okf.Cli.Config.OkfProvider
+ Okf.Cli.Config: instance GHC.Classes.Eq Okf.Cli.Config.AssistSettings
+ Okf.Cli.Config: instance GHC.Classes.Eq Okf.Cli.Config.ConfigSource
+ Okf.Cli.Config: instance GHC.Classes.Eq Okf.Cli.Config.KitSettings
+ Okf.Cli.Config: instance GHC.Classes.Eq Okf.Cli.Config.OkfConfig
+ Okf.Cli.Config: instance GHC.Classes.Eq Okf.Cli.Config.OkfProvider
+ Okf.Cli.Config: instance GHC.Internal.Generics.Generic Okf.Cli.Config.AssistSettings
+ Okf.Cli.Config: instance GHC.Internal.Generics.Generic Okf.Cli.Config.KitSettings
+ Okf.Cli.Config: instance GHC.Internal.Generics.Generic Okf.Cli.Config.OkfConfig
+ Okf.Cli.Config: instance GHC.Internal.Generics.Generic Okf.Cli.Config.OkfProvider
+ Okf.Cli.Config: instance GHC.Internal.Show.Show Okf.Cli.Config.AssistSettings
+ Okf.Cli.Config: instance GHC.Internal.Show.Show Okf.Cli.Config.ConfigSource
+ Okf.Cli.Config: instance GHC.Internal.Show.Show Okf.Cli.Config.KitSettings
+ Okf.Cli.Config: instance GHC.Internal.Show.Show Okf.Cli.Config.OkfConfig
+ Okf.Cli.Config: instance GHC.Internal.Show.Show Okf.Cli.Config.OkfProvider
+ Okf.Cli.Config: loadOkfConfig :: IO (Either Text (OkfConfig, ConfigSource))
+ Okf.Cli.Config: okfConfigEnvVar :: String
+ Okf.Cli.Config: projectConfigPath :: IO FilePath
+ Okf.Cli.Config: renderConfig :: OkfConfig -> Text
+ Okf.Cli.Config: renderConfigSource :: ConfigSource -> Text
+ Okf.Cli.Config: xdgConfigPath :: IO FilePath
+ Okf.Cli.Kit: KitInstall :: !Text -> !KitScope -> KitCommand
+ Okf.Cli.Kit: KitList :: KitCommand
+ Okf.Cli.Kit: KitStatus :: KitCommand
+ Okf.Cli.Kit: KitUninstall :: !Text -> !KitScope -> KitCommand
+ Okf.Cli.Kit: KitUpdate :: !Maybe Text -> KitCommand
+ Okf.Cli.Kit: data KitCommand
+ Okf.Cli.Kit: handleKitCommand :: OkfConfig -> KitCommand -> IO ()
+ Okf.Cli.Kit: instance GHC.Classes.Eq Okf.Cli.Kit.KitCommand
+ Okf.Cli.Kit: instance GHC.Internal.Show.Show Okf.Cli.Kit.KitCommand
+ Okf.Cli.Kit: kitCommandParser :: Parser KitCommand
+ Okf.Cli.Kit.Config: kitConfig :: OkfConfig -> KitConfig

Files

CHANGELOG.md view
@@ -7,6 +7,27 @@  ## [Unreleased] +## [0.1.2.0] - 2026-07-14++### Added++- `okf kit` command for installing reusable AI-agent skills and subagents from a+  configured `okf-kit` git repository (`list`, `install`, `update`, `uninstall`,+  `status`), with user and project (`--project`) scopes.+- `okf assist` command that launches an interactive Claude session seeded with a+  prompt and your installed okf skills on its path; `--print-command` prints the+  command line without launching.+- `okf config` command for managing the optional agent-assistance settings+  (`show`, `path`, `init`, `init --global`), sourced from `okf-config.dhall`,+  `~/.config/okf/config.dhall`, or `OKF_CONFIG` with built-in defaults.+- `okf help` topics for `kit`, `config`, and `agents` documenting the kit,+  configuration, and assist workflows.++### Changed++- Wired the baikai kit and agent-assist dependencies into the build.+- Updated the bundled baikai packages.+ ## [0.1.1.0] - 2026-06-28  ### Added
okf-cli.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.4 name:            okf-cli-version:         0.1.1.0+version:         0.1.2.0 synopsis:        Command-line interface for Open Knowledge Format bundles description:   okf-cli provides the @okf@ executable for working with Open Knowledge Format@@ -35,24 +35,31 @@   hs-source-dirs:  src   exposed-modules:     Okf.Cli+    Okf.Cli.Assist     Okf.Cli.Completions+    Okf.Cli.Config     Okf.Cli.Help+    Okf.Cli.Kit+    Okf.Cli.Kit.Config     Okf.Cli.Version    other-modules:   Paths_okf_cli   autogen-modules: Paths_okf_cli   build-depends:     , aeson                 >=2.2      && <2.4+    , baikai                ^>=0.3.0+    , baikai-kit            ^>=0.1.0.1     , base                  >=4.20     && <5     , bytestring            >=0.11     && <0.13     , containers            >=0.6      && <0.8+    , dhall                 >=1.41     && <1.43     , directory             >=1.3      && <1.4     , file-embed            >=0.0.15   && <0.1     , filepath              >=1.4      && <1.6     , generic-lens          >=2.2      && <2.4     , githash               ^>=0.1     , lens                  ^>=5.3-    , okf-core              ^>=0.1.1.0+    , okf-core              ^>=0.1.2.0     , optparse-applicative  >=0.18     && <0.20     , process               >=1.6      && <1.7     , text                  ^>=2.1
src/Okf/Cli.hs view
@@ -3,6 +3,7 @@   ( Command (..),     GraphOptions (..),     IndexOptions (..),+    ConfigCommand (..),     LogAddOptions (..),     LogOptions (..),     LogSub (..),@@ -26,8 +27,11 @@ import Data.Text.IO qualified as Text.IO import Data.Time (defaultTimeLocale, formatTime, getCurrentTime, utctDay) import Okf.Bundle+import Okf.Cli.Assist (AssistOptions, assistOptionsParser, handleAssistCommand) import Okf.Cli.Completions (CompletionsShell, completionsParser, handleCompletions)+import Okf.Cli.Config import Okf.Cli.Help (HelpCommand, handleHelpCommand, helpCommandParser)+import Okf.Cli.Kit (KitCommand, handleKitCommand, kitCommandParser) import Okf.Cli.Version (appVersionWithGit) import Okf.ConceptId import Okf.Document (DocumentParseError (..), body)@@ -51,6 +55,9 @@   | Log LogOptions   | GraphCommand GraphOptions   | ShowConcept ShowOptions+  | Config ConfigCommand+  | Kit KitCommand+  | Assist AssistOptions   | Completions CompletionsShell   | Help HelpCommand   deriving stock (Show, Eq)@@ -103,6 +110,12 @@   }   deriving stock (Show, Eq) +data ConfigCommand+  = ConfigShow+  | ConfigPath+  | ConfigInit !Bool+  deriving stock (Show, Eq)+ data Options = Options   { cmd :: !Command   }@@ -139,6 +152,9 @@         <> command "log" (info (Log <$> logOptionsParser <**> helper) (progDesc "Preview and check log.md files"))         <> command "graph" (info (GraphCommand <$> graphOptionsParser <**> helper) (progDesc "Print a bundle graph"))         <> command "show" (info (ShowConcept <$> showOptionsParser <**> helper) (progDesc "Show one concept"))+        <> command "config" (info (Config <$> configCommandParser <**> helper) (progDesc "Show and manage okf configuration"))+        <> command "kit" (info (Kit <$> kitCommandParser <**> helper) (progDesc "Install and manage agent skills and subagents"))+        <> command "assist" (info (Assist <$> assistOptionsParser <**> helper) (progDesc "Launch an interactive agent session with installed okf skills"))         <> command "completions" (info (Completions <$> completionsParser <**> helper) (progDesc "Generate a shell completion script (bash, zsh, fish)"))         <> command "help" (info (Help <$> helpCommandParser <**> helper) (progDesc "Show conceptual help topics"))     )@@ -245,6 +261,20 @@     <$> bundleArgument     <*> (Text.pack <$> strArgument (metavar "CONCEPT_ID" <> help "Concept ID such as tables/users")) +configCommandParser :: Parser ConfigCommand+configCommandParser =+  hsubparser+    ( command "show" (info (pure ConfigShow) (progDesc "Print the effective configuration and its source"))+        <> command "path" (info (pure ConfigPath) (progDesc "Print the path the configuration was loaded from"))+        <> command+          "init"+          ( info+              (ConfigInit <$> switch (long "global" <> help "Write to ~/.config/okf/config.dhall instead of ./okf-config.dhall"))+              (progDesc "Write a commented example okf-config.dhall")+          )+    )+    <|> pure ConfigShow+ bundleArgument :: Parser FilePath bundleArgument =   strArgument (metavar "BUNDLE" <> help "Path to an OKF bundle directory")@@ -256,8 +286,44 @@   Log options -> runLog options   GraphCommand options -> runGraph options   ShowConcept options -> runShow options+  Config configCommand -> runConfig configCommand+  Kit kitCommand -> do+    config <- loadConfigOrDie+    handleKitCommand config kitCommand+  Assist assistOptions -> do+    config <- loadConfigOrDie+    handleAssistCommand config assistOptions   Completions shell -> handleCompletions shell   Help helpCommand -> handleHelpCommand helpCommand++runConfig :: ConfigCommand -> IO ()+runConfig = \case+  ConfigShow -> do+    (config, configSource) <- loadConfigWithSourceOrDie+    Text.IO.putStrLn ("source: " <> renderConfigSource configSource)+    Text.IO.putStr (renderConfig config)+  ConfigPath -> do+    configSource <- findConfigSource+    Text.IO.putStrLn (renderConfigSource configSource)+  ConfigInit global -> do+    target <- if global then xdgConfigPath else projectConfigPath+    exists <- doesFileExist target+    if exists+      then dieText ("Refusing to overwrite existing config: " <> Text.pack target)+      else do+        createDirectoryIfMissing True (FilePath.takeDirectory target)+        Text.IO.writeFile target exampleConfigText+        Text.IO.putStrLn ("Wrote " <> Text.pack target)++loadConfigOrDie :: IO OkfConfig+loadConfigOrDie = fst <$> loadConfigWithSourceOrDie++loadConfigWithSourceOrDie :: IO (OkfConfig, ConfigSource)+loadConfigWithSourceOrDie = do+  result <- loadOkfConfig+  case result of+    Left err -> dieText ("Failed to load config: " <> err)+    Right loaded -> pure loaded  runValidate :: ValidateOptions -> IO () runValidate ValidateOptions {bundlePath, strictMode, profilePath, profileEnforce, logEnforce} = do
+ src/Okf/Cli/Assist.hs view
@@ -0,0 +1,94 @@+-- | The @okf assist@ command: launch an interactive agent session with+-- installed okf skills on its path.+module Okf.Cli.Assist+  ( AssistOptions (..),+    assistOptionsParser,+    handleAssistCommand,+    buildClaudeCommand,+  )+where++import Baikai.Kit.Session (agentDirsForSession)+import Control.Exception (IOException, try)+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.IO qualified as Text.IO+import Okf.Cli.Config (AssistSettings (..), OkfConfig (..), OkfProvider (..))+import Okf.Cli.Kit.Config (kitConfig)+import Options.Applicative+import System.Exit (ExitCode (..), exitWith)+import System.IO (hPutStrLn, stderr)+import System.Process (createProcess, delegate_ctlc, proc, waitForProcess)++data AssistOptions = AssistOptions+  { prompt :: !Text,+    modelOverride :: !(Maybe Text),+    printCommand :: !Bool+  }+  deriving stock (Show, Eq)++assistOptionsParser :: Parser AssistOptions+assistOptionsParser =+  AssistOptions+    <$> (Text.pack <$> strArgument (metavar "PROMPT" <> help "The task or question to start the agent session with"))+    <*> optional+      ( Text.pack+          <$> strOption (long "model" <> metavar "MODEL" <> help "Override the assist model from config")+      )+    <*> switch (long "print-command" <> help "Print the agent command line instead of launching it")++-- | Build the @claude@ argv from config, discovered kit agent dirs, and command+-- options. The prompt is the final positional argument.+buildClaudeCommand :: OkfConfig -> [FilePath] -> AssistOptions -> [String]+buildClaudeCommand+  OkfConfig {assist = AssistSettings {model = configModel, systemPrompt}}+  agentDirs+  AssistOptions {prompt, modelOverride} =+    concatMap (\dir -> ["--add-dir", dir]) agentDirs+      ++ modelArgs+      ++ systemPromptArgs+      ++ [Text.unpack prompt]+    where+      chosenModel = case modelOverride of+        Nothing -> configModel+        Just override -> Just override+      modelArgs = maybe [] (\model -> ["--model", Text.unpack model]) chosenModel+      systemPromptArgs =+        maybe [] (\systemPromptText -> ["--append-system-prompt", Text.unpack systemPromptText]) systemPrompt++handleAssistCommand :: OkfConfig -> AssistOptions -> IO ()+handleAssistCommand config options =+  case provider (assist config) of+    ProviderCodex -> do+      hPutStrLn stderr "okf assist: the Codex provider is not yet supported; set assist.provider = Claude."+      exitWith (ExitFailure 2)+    ProviderClaude -> do+      agentDirs <- agentDirsForSession (kitConfig config)+      let argv = buildClaudeCommand config agentDirs options+      if printCommand options+        then Text.IO.putStrLn (Text.pack (unwords ("claude" : map quoteArg argv)))+        else launchClaude argv++launchClaude :: [String] -> IO ()+launchClaude argv = do+  result <- try @IOException $ do+    (_, _, _, processHandle) <- createProcess (proc "claude" argv) {delegate_ctlc = True}+    waitForProcess processHandle+  case result of+    Left exception -> do+      hPutStrLn stderr $+        "okf assist: failed to launch claude: "+          <> show exception+          <> "\nInstall Claude Code or run `okf assist --print-command ...` to inspect the command."+      exitWith (ExitFailure 127)+    Right exitCode -> exitWith exitCode++quoteArg :: String -> String+quoteArg arg+  | null arg = "''"+  | any needsQuote arg = "'" <> concatMap escapeSingleQuote arg <> "'"+  | otherwise = arg+  where+    needsQuote c = c == ' ' || c == '\t' || c == '\'' || c == '"'+    escapeSingleQuote '\'' = "'\\''"+    escapeSingleQuote c = [c]
+ src/Okf/Cli/Config.hs view
@@ -0,0 +1,203 @@+-- | Project and global configuration for the okf CLI, loaded from Dhall.+module Okf.Cli.Config+  ( OkfConfig (..),+    KitSettings (..),+    AssistSettings (..),+    OkfProvider (..),+    ConfigSource (..),+    defaultOkfConfig,+    loadOkfConfig,+    findConfigSource,+    renderConfigSource,+    exampleConfigText,+    renderConfig,+    okfConfigEnvVar,+    projectConfigPath,+    xdgConfigPath,+    dotConfigPath,+  )+where++import Control.Exception (SomeException, catch)+import Data.Text qualified as Text+import Dhall (FromDhall (..), auto, genericAutoWith)+import Dhall qualified+import Okf.Prelude+import System.Directory (doesFileExist, getCurrentDirectory, getHomeDirectory)+import System.Environment (lookupEnv)+import System.FilePath ((</>))++-- | Which interactive agent provider a setting refers to. This okf-local enum+-- keeps config loading free of a direct dependency on Baikai provider types.+data OkfProvider+  = ProviderClaude+  | ProviderCodex+  deriving stock (Generic, Eq, Show)++instance FromDhall OkfProvider where+  autoWith _normalizer =+    genericAutoWith+      (Dhall.defaultInterpretOptions {Dhall.constructorModifier = stripProviderPrefix})+    where+      stripProviderPrefix name = fromMaybe name (Text.stripPrefix "Provider" name)++-- | Kit-related settings: where to fetch skills/subagents and which providers+-- to install for.+data KitSettings = KitSettings+  { repoUrl :: !Text,+    providers :: ![OkfProvider]+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromDhall)++-- | Assist-related settings: which provider to launch and optional overrides.+data AssistSettings = AssistSettings+  { provider :: !OkfProvider,+    model :: !(Maybe Text),+    systemPrompt :: !(Maybe Text)+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromDhall)++-- | The whole okf configuration.+data OkfConfig = OkfConfig+  { kit :: !KitSettings,+    assist :: !AssistSettings+  }+  deriving stock (Generic, Eq, Show)+  deriving anyclass (FromDhall)++-- | Where the effective configuration came from.+data ConfigSource+  = SourceEnv !FilePath+  | SourceProject !FilePath+  | SourceXdg !FilePath+  | SourceDot !FilePath+  | SourceDefaults+  deriving stock (Eq, Show)++defaultOkfConfig :: OkfConfig+defaultOkfConfig =+  OkfConfig+    { kit =+        KitSettings+          { repoUrl = "https://github.com/shinzui/okf-kit.git",+            providers = [ProviderClaude]+          },+      assist =+        AssistSettings+          { provider = ProviderClaude,+            model = Nothing,+            systemPrompt = Nothing+          }+    }++okfConfigEnvVar :: String+okfConfigEnvVar = "OKF_CONFIG"++projectConfigPath :: IO FilePath+projectConfigPath = (</> "okf-config.dhall") <$> getCurrentDirectory++xdgConfigPath :: IO FilePath+xdgConfigPath = (\home -> home </> ".config" </> "okf" </> "config.dhall") <$> getHomeDirectory++dotConfigPath :: IO FilePath+dotConfigPath = (\home -> home </> ".okf" </> "config.dhall") <$> getHomeDirectory++-- | Resolve which config file to use. The first existing file wins; when none+-- exists, okf uses built-in defaults.+findConfigSource :: IO ConfigSource+findConfigSource = do+  mEnv <- lookupEnv okfConfigEnvVar+  case mEnv of+    Just path -> do+      exists <- doesFileExist path+      if exists then pure (SourceEnv path) else searchFiles+    Nothing -> searchFiles+  where+    searchFiles = do+      projectPath <- projectConfigPath+      xdgPath <- xdgConfigPath+      dotPath <- dotConfigPath+      firstExisting+        [ (SourceProject, projectPath),+          (SourceXdg, xdgPath),+          (SourceDot, dotPath)+        ]++    firstExisting [] = pure SourceDefaults+    firstExisting ((mkSource, path) : rest) = do+      exists <- doesFileExist path+      if exists then pure (mkSource path) else firstExisting rest++-- | Load the effective configuration and report its source. A parse or type+-- error in a found file is returned as 'Left'; a missing file yields defaults.+loadOkfConfig :: IO (Either Text (OkfConfig, ConfigSource))+loadOkfConfig = do+  configSource <- findConfigSource+  case sourcePath configSource of+    Nothing -> pure (Right (defaultOkfConfig, configSource))+    Just path ->+      ( do+          config <- Dhall.inputFile auto path+          pure (Right (config, configSource))+      )+        `catch` \(exception :: SomeException) ->+          pure (Left (Text.pack (show exception)))++sourcePath :: ConfigSource -> Maybe FilePath+sourcePath = \case+  SourceEnv path -> Just path+  SourceProject path -> Just path+  SourceXdg path -> Just path+  SourceDot path -> Just path+  SourceDefaults -> Nothing++renderConfigSource :: ConfigSource -> Text+renderConfigSource = \case+  SourceEnv path -> "OKF_CONFIG=" <> Text.pack path+  SourceProject path -> Text.pack path+  SourceXdg path -> Text.pack path+  SourceDot path -> Text.pack path+  SourceDefaults -> "(built-in defaults)"++-- | Human-readable dump of the effective configuration.+renderConfig :: OkfConfig -> Text+renderConfig+  OkfConfig+    { kit = KitSettings {repoUrl, providers},+      assist = AssistSettings {provider, model, systemPrompt}+    } =+    Text.unlines+      [ "kit.repoUrl     = " <> repoUrl,+        "kit.providers   = " <> renderProviders providers,+        "assist.provider = " <> renderProvider provider,+        "assist.model    = " <> fromMaybe "(unset)" model,+        "assist.systemPrompt = " <> fromMaybe "(unset)" systemPrompt+      ]++renderProviders :: [OkfProvider] -> Text+renderProviders providers = "[" <> Text.intercalate ", " (map renderProvider providers) <> "]"++renderProvider :: OkfProvider -> Text+renderProvider = \case+  ProviderClaude -> "claude"+  ProviderCodex -> "codex"++-- | The commented example written by @okf config init@.+exampleConfigText :: Text+exampleConfigText =+  Text.unlines+    [ "-- okf configuration. See `okf config show` for the effective values.",+      "let Provider = < Claude | Codex >",+      "in  { kit =",+      "        { repoUrl = \"https://github.com/shinzui/okf-kit.git\"",+      "        , providers = [ Provider.Claude ]",+      "        }",+      "    , assist =",+      "        { provider = Provider.Claude",+      "        , model = None Text",+      "        , systemPrompt = None Text",+      "        }",+      "    }"+    ]
src/Okf/Cli/Help.hs view
@@ -47,7 +47,10 @@   [ HelpTopic "okf" "What the Open Knowledge Format is" okfTopicContent,     HelpTopic "format" "Bundle layout, concept IDs, frontmatter, and links" formatTopicContent,     HelpTopic "validation" "How bundles are validated and referential integrity" validationTopicContent,-    HelpTopic "profiles" "Checking a bundle against house conventions" profilesTopicContent+    HelpTopic "profiles" "Checking a bundle against house conventions" profilesTopicContent,+    HelpTopic "config" "Config files, defaults, and agent settings" configTopicContent,+    HelpTopic "kit" "Installing and publishing agent skills and subagents" kitTopicContent,+    HelpTopic "agents" "Installing agent skills and launching assist" agentsTopicContent   ]  okfTopicContent :: Text@@ -61,6 +64,15 @@  profilesTopicContent :: Text profilesTopicContent = $(embedStringFile "help/profiles.md")++configTopicContent :: Text+configTopicContent = $(embedStringFile "help/config.md")++kitTopicContent :: Text+kitTopicContent = $(embedStringFile "help/kit.md")++agentsTopicContent :: Text+agentsTopicContent = $(embedStringFile "help/agents.md")  -- | Parse @help [TOPIC]@. With no argument, 'pure ListTopics' wins via '<|>'. helpCommandParser :: Parser HelpCommand
+ src/Okf/Cli/Kit.hs view
@@ -0,0 +1,68 @@+-- | The @okf kit@ command group: install and manage agent skills/subagents.+module Okf.Cli.Kit+  ( KitCommand (..),+    kitCommandParser,+    handleKitCommand,+  )+where++import Baikai.Kit.Command qualified as Engine+import Baikai.Kit.Config (KitScope (..))+import Data.Text (Text)+import Data.Text qualified as Text+import Okf.Cli.Config (OkfConfig)+import Okf.Cli.Kit.Config (kitConfig)+import Options.Applicative++-- | okf-local mirror of the engine's kit command. This derives 'Eq' so okf's+-- top-level command type can keep deriving 'Eq'.+data KitCommand+  = KitList+  | KitInstall !Text !KitScope+  | KitUpdate !(Maybe Text)+  | KitUninstall !Text !KitScope+  | KitStatus+  deriving stock (Show, Eq)++kitCommandParser :: Parser KitCommand+kitCommandParser =+  hsubparser+    ( command "list" (info (pure KitList) (progDesc "List available skills and subagents"))+        <> command "install" (info installParser (progDesc "Install a skill or subagent"))+        <> command "update" (info updateParser (progDesc "Update installed skills and subagents"))+        <> command "uninstall" (info uninstallParser (progDesc "Uninstall a skill or subagent"))+        <> command "status" (info (pure KitStatus) (progDesc "Show installed skills and subagents"))+    )+    <|> pure KitList+  where+    installParser =+      KitInstall+        <$> textArgument (metavar "NAME" <> help "Name of the skill or subagent to install")+        <*> scopeParser "Install to project scope (.okf/agents) instead of user scope"++    updateParser =+      KitUpdate+        <$> optional (textArgument (metavar "NAME" <> help "Name of a specific item to update (default: all)"))++    uninstallParser =+      KitUninstall+        <$> textArgument (metavar "NAME" <> help "Name of the skill or subagent to uninstall")+        <*> scopeParser "Uninstall from project scope (.okf/agents) instead of user scope"++    scopeParser helpText = flag UserScope ProjectScope (long "project" <> help helpText)++    textArgument modifiers = Text.pack <$> strArgument modifiers++-- | Translate the parsed okf command into the engine command and run it against+-- the kit configuration derived from the loaded okf config.+handleKitCommand :: OkfConfig -> KitCommand -> IO ()+handleKitCommand config kitCommand =+  Engine.runKit (kitConfig config) (toEngineCommand kitCommand)++toEngineCommand :: KitCommand -> Engine.KitCommand+toEngineCommand = \case+  KitList -> Engine.KitList+  KitInstall name scope -> Engine.KitInstall name scope+  KitUpdate name -> Engine.KitUpdate name+  KitUninstall name scope -> Engine.KitUninstall name scope+  KitStatus -> Engine.KitStatus
+ src/Okf/Cli/Kit/Config.hs view
@@ -0,0 +1,25 @@+-- | Bridge okf configuration to the baikai-kit engine's KitConfig.+module Okf.Cli.Kit.Config+  ( kitConfig,+  )+where++import Baikai.Interactive (InteractiveProvider (..))+import Baikai.Kit.Config (KitConfig (..))+import Okf.Cli.Config (KitSettings (..), OkfConfig (..), OkfProvider (..))++-- | Build the baikai-kit configuration from okf's loaded configuration. The+-- tool name "okf" fixes the on-disk layout for caches, installed assets, and+-- sidecar files.+kitConfig :: OkfConfig -> KitConfig+kitConfig OkfConfig {kit = KitSettings {repoUrl = url, providers = providerList}} =+  KitConfig+    { toolName = "okf",+      repoUrl = url,+      providers = map toInteractive providerList+    }++toInteractive :: OkfProvider -> InteractiveProvider+toInteractive = \case+  ProviderClaude -> InteractiveClaude+  ProviderCodex -> InteractiveCodex
test/Main.hs view
@@ -1,12 +1,16 @@ module Main (main) where +import Control.Exception (bracket) import Control.Monad (unless) import Data.Text qualified as Text import Data.Text.IO qualified as Text.IO import Okf.Cli+import Okf.Cli.Assist (AssistOptions (..), buildClaudeCommand)+import Okf.Cli.Config (AssistSettings (..), ConfigSource (..), KitSettings (..), OkfConfig (..), OkfProvider (..), defaultOkfConfig, exampleConfigText, findConfigSource, loadOkfConfig, okfConfigEnvVar, projectConfigPath) import Okf.Cli.Help (HelpTopic (..), helpTopics) import Options.Applicative-import System.Directory (createDirectoryIfMissing, getTemporaryDirectory, removeDirectoryRecursive)+import System.Directory (createDirectoryIfMissing, getCurrentDirectory, getTemporaryDirectory, removeDirectoryRecursive, withCurrentDirectory)+import System.Environment (lookupEnv, setEnv, unsetEnv) import System.Exit (exitFailure) import System.FilePath ((</>)) import System.IO.Temp (createTempDirectory)@@ -14,6 +18,12 @@ main :: IO () main = do   logAddWrites <- testLogAddWritesFile+  configDefaults <- testConfigDefaults+  configProjectPrecedence <- testConfigProjectPrecedence+  configEnvPrecedence <- testConfigEnvPrecedence+  configInvalidDhall <- testConfigInvalidDhall+  assistCommandBuilder <- testAssistCommandBuilder+  assistModelOverride <- testAssistModelOverride   let results =         [ parseSucceeds ["validate", "bundle"],           parseSucceeds ["validate", "bundle", "--strict"],@@ -78,6 +88,23 @@           parseSucceeds ["completions", "bash"],           parseSucceeds ["completions", "zsh"],           parseSucceeds ["completions", "fish"],+          parseSucceeds ["config"],+          parseSucceeds ["config", "show"],+          parseSucceeds ["config", "path"],+          parseSucceeds ["config", "init"],+          parseSucceeds ["config", "init", "--global"],+          parseSucceeds ["kit"],+          parseSucceeds ["kit", "list"],+          parseSucceeds ["kit", "install", "demo-skill"],+          parseSucceeds ["kit", "install", "demo-skill", "--project"],+          parseSucceeds ["kit", "update"],+          parseSucceeds ["kit", "update", "demo-skill"],+          parseSucceeds ["kit", "uninstall", "demo-skill"],+          parseSucceeds ["kit", "uninstall", "demo-skill", "--project"],+          parseSucceeds ["kit", "status"],+          parseSucceeds ["assist", "Summarize this bundle"],+          parseSucceeds ["assist", "--print-command", "Summarize this bundle"],+          parseSucceeds ["assist", "--model", "claude-opus-4-5", "Summarize this bundle"],           parseFails ["completions", "elvish"],           parseSucceeds ["help"],           parseSucceeds ["help", "okf"],@@ -86,7 +113,13 @@           all (not . Text.null . topicContent) helpTopics,           parseShowsInfo ["--version"],           parseFails ["hello"],-          logAddWrites+          logAddWrites,+          configDefaults,+          configProjectPrecedence,+          configEnvPrecedence,+          configInvalidDhall,+          assistCommandBuilder,+          assistModelOverride         ]   unless (and results) exitFailure @@ -149,6 +182,110 @@     ( "## 2026-06-23" `Text.isInfixOf` written         && "* **Update**: Refreshed schema" `Text.isInfixOf` written     )++testConfigDefaults :: IO Bool+testConfigDefaults =+  withIsolatedConfigEnv "okf-cli-config-defaults" $ do+    configSource <- findConfigSource+    loaded <- loadOkfConfig+    pure (configSource == SourceDefaults && loaded == Right (defaultOkfConfig, SourceDefaults))++testConfigProjectPrecedence :: IO Bool+testConfigProjectPrecedence =+  withIsolatedConfigEnv "okf-cli-config-project" $ do+    projectPath <- projectConfigPath+    Text.IO.writeFile projectPath exampleConfigText+    configSource <- findConfigSource+    loaded <- loadOkfConfig+    pure (configSource == SourceProject projectPath && loaded == Right (defaultOkfConfig, SourceProject projectPath))++testConfigEnvPrecedence :: IO Bool+testConfigEnvPrecedence =+  withIsolatedConfigEnv "okf-cli-config-env" $ do+    projectPath <- projectConfigPath+    Text.IO.writeFile projectPath exampleConfigText+    envPath <- (</> "env-config.dhall") <$> getCurrentDirectory+    Text.IO.writeFile envPath exampleConfigText+    setEnv okfConfigEnvVar envPath+    configSource <- findConfigSource+    loaded <- loadOkfConfig+    pure (configSource == SourceEnv envPath && loaded == Right (defaultOkfConfig, SourceEnv envPath))++testConfigInvalidDhall :: IO Bool+testConfigInvalidDhall =+  withIsolatedConfigEnv "okf-cli-config-invalid" $ do+    projectPath <- projectConfigPath+    Text.IO.writeFile projectPath "this is not valid Dhall"+    loaded <- loadOkfConfig+    pure $+      case loaded of+        Left message -> not (Text.null (Text.strip message))+        Right _ -> False++testAssistCommandBuilder :: IO Bool+testAssistCommandBuilder =+  pure $+    buildClaudeCommand assistTestConfig ["/a", "/b"] (AssistOptions "do work" Nothing False)+      == [ "--add-dir",+           "/a",+           "--add-dir",+           "/b",+           "--model",+           "claude-opus-4-5",+           "--append-system-prompt",+           "Be concise",+           "do work"+         ]++testAssistModelOverride :: IO Bool+testAssistModelOverride =+  pure $+    buildClaudeCommand assistTestConfig [] (AssistOptions "do work" (Just "override-model") True)+      == [ "--model",+           "override-model",+           "--append-system-prompt",+           "Be concise",+           "do work"+         ]++assistTestConfig :: OkfConfig+assistTestConfig =+  defaultOkfConfig+    { assist =+        AssistSettings+          { provider = ProviderClaude,+            model = Just "claude-opus-4-5",+            systemPrompt = Just "Be concise"+          },+      kit =+        KitSettings+          { repoUrl = "file:///tmp/okf-kit",+            providers = [ProviderClaude]+          }+    }++withIsolatedConfigEnv :: String -> IO Bool -> IO Bool+withIsolatedConfigEnv name runTest = do+  temporaryDirectory <- getTemporaryDirectory+  originalCwd <- getCurrentDirectory+  originalOkfConfig <- lookupEnv okfConfigEnvVar+  originalHome <- lookupEnv "HOME"+  bracket+    (createTempDirectory temporaryDirectory name)+    ( \root -> do+        setMaybeEnv okfConfigEnvVar originalOkfConfig+        setMaybeEnv "HOME" originalHome+        withCurrentDirectory originalCwd (removeDirectoryRecursive root)+    )+    ( \root -> do+        unsetEnv okfConfigEnvVar+        setEnv "HOME" root+        withCurrentDirectory root runTest+    )+  where+    setMaybeEnv key = \case+      Nothing -> unsetEnv key+      Just envValue -> setEnv key envValue  parseFails :: [String] -> Bool parseFails args =