okf-cli-0.1.2.0: src/Okf/Cli/Assist.hs
-- | 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]