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 +21/−0
- okf-cli.cabal +9/−2
- src/Okf/Cli.hs +66/−0
- src/Okf/Cli/Assist.hs +94/−0
- src/Okf/Cli/Config.hs +203/−0
- src/Okf/Cli/Help.hs +13/−1
- src/Okf/Cli/Kit.hs +68/−0
- src/Okf/Cli/Kit/Config.hs +25/−0
- test/Main.hs +139/−2
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 =