packages feed

baikai-claude-0.3.0.0: src/Baikai/Provider/Claude/Interactive.hs

-- | Launch real interactive Claude Code sessions from Baikai's
-- provider-neutral interactive request type.
--
-- This module is intentionally separate from
-- "Baikai.Provider.Claude.Cli": that module drives @claude -p@ as a
-- batch completion provider, while this module starts the interactive
-- terminal UI and returns only after the CLI exits.
module Baikai.Provider.Claude.Interactive
  ( ClaudeInteractiveConfig,
    executable,
    extraArgs,
    defaultClaudeInteractiveConfig,
    claudeInteractiveCommand,
    launchClaudeInteractive,
  )
where

import Baikai.Interactive
  ( InteractiveLaunchRequest,
    InteractiveLaunchResult,
    InteractiveProvider (..),
    InteractiveSafety (..),
    interactiveLaunchResult,
  )
import Baikai.Prelude
import Cradle (addArgs, cmd, run, setWorkingDir)
import Data.Generics.Labels ()
import Data.Text qualified as Text

-- | Configuration for the interactive @claude@ process.
data ClaudeInteractiveConfig = ClaudeInteractiveConfig
  { executable :: !FilePath,
    extraArgs :: ![Text]
  }
  deriving stock (Eq, Show, Generic)

defaultClaudeInteractiveConfig :: ClaudeInteractiveConfig
defaultClaudeInteractiveConfig =
  ClaudeInteractiveConfig
    { executable = "claude",
      extraArgs = mempty
    }

-- | Render the executable and arguments for an interactive Claude
-- Code launch. The final positional argument is the initial user
-- prompt. The prompt is preceded by @--@ because Claude's
-- @--allowedTools@ and @--add-dir@ flags are variadic.
claudeInteractiveCommand ::
  ClaudeInteractiveConfig -> InteractiveLaunchRequest -> (FilePath, [String])
claudeInteractiveCommand cfg req =
  ( cfg ^. #executable,
    modelArgs req
      <> systemPromptArgs req
      <> extraDirArgs req
      <> safetyArgs req
      <> fmap Text.unpack (cfg ^. #extraArgs)
      <> fmap Text.unpack (req ^. #extraArgs)
      <> ["--", Text.unpack (req ^. #userPrompt)]
  )

-- | Launch Claude Code with inherited stdin, stdout, and stderr so
-- the local CLI owns the interactive terminal experience.
launchClaudeInteractive ::
  ClaudeInteractiveConfig -> InteractiveLaunchRequest -> IO InteractiveLaunchResult
launchClaudeInteractive cfg req = do
  let (exe, args) = claudeInteractiveCommand cfg req
  code <-
    run $
      cmd exe
        & addArgs args
        & maybe id setWorkingDir (req ^. #workingDir)
  pure (interactiveLaunchResult InteractiveClaude code)

modelArgs :: InteractiveLaunchRequest -> [String]
modelArgs req = case Text.strip <$> req ^. #modelId of
  Nothing -> []
  Just "" -> []
  Just mid -> ["--model", Text.unpack mid]

systemPromptArgs :: InteractiveLaunchRequest -> [String]
systemPromptArgs req = case Text.strip <$> req ^. #systemPrompt of
  Nothing -> []
  Just "" -> []
  Just prompt -> ["--system-prompt", Text.unpack prompt]

extraDirArgs :: InteractiveLaunchRequest -> [String]
extraDirArgs req =
  concatMap (\dir -> ["--add-dir", dir]) (req ^. #extraDirs)

safetyArgs :: InteractiveLaunchRequest -> [String]
safetyArgs req = case req ^. #safety of
  ClaudeAllowedTools [] -> []
  ClaudeAllowedTools tools ->
    ["--allowedTools", Text.unpack (Text.intercalate "," tools)]
  DefaultSafety -> []
  CodexSandbox _ _ -> []