Skip to main content

forge_sdk/
argv.rs

1//! Pure-function argv composition. Takes an [`Options`] and produces
2//! the `Vec<String>` to pass to the `claude` subprocess. Kept separate
3//! from [`transport::process`](crate::transport::process) so the
4//! transport layer doesn't import MCP orchestration types.
5
6use crate::Error;
7use crate::mcp::orchestration::McpHosts;
8use crate::options::{Options, PermissionMode, SdkPluginConfig, SystemPromptKind};
9
10/// Build the subprocess argv from [`Options`], SDK's
11/// `_build_command` byte-for-byte where possible. Exposed `pub` so
12/// advanced callers can inspect the argv without spawning.
13pub fn build_args(options: &Options) -> Result<Vec<String>, Error> {
14    let mut args: Vec<String> = Vec::new();
15
16    // the CLI leads every invocation with these three flags
17    //.
18    args.push("--output-format".into());
19    args.push("stream-json".into());
20    args.push("--verbose".into());
21
22    // system_prompt - the CLI always emits one of four flag forms,
23    // including an explicit
24    // `--system-prompt ""` when the option is unset so the CLI
25    // doesn't fall back to its builtin prompt. Match byte-for-byte.
26    match options.system_prompt.as_ref() {
27        None => {
28            args.push("--system-prompt".into());
29            args.push(String::new());
30        }
31        Some(SystemPromptKind::Inline(text)) => {
32            args.push("--system-prompt".into());
33            args.push(text.clone());
34        }
35        Some(SystemPromptKind::File(path)) => {
36            args.push("--system-prompt-file".into());
37            args.push(path.to_string_lossy().into_owned());
38        }
39        Some(SystemPromptKind::Preset { append, .. }) => {
40            // `exclude_dynamic_sections` is delivered in the initialize
41            // control_request body (client.rs), NOT on argv.
42            if let Some(text) = append {
43                args.push("--append-system-prompt".into());
44                args.push(text.clone());
45            }
46        }
47    }
48
49    // --allowedTools (camelCase on the wire).
50    if !options.allowed_tools.is_empty() {
51        args.push("--allowedTools".into());
52        args.push(options.allowed_tools.join(","));
53    }
54
55    if let Some(n) = options.max_turns {
56        args.push("--max-turns".into());
57        args.push(n.to_string());
58    }
59    if let Some(model) = &options.model {
60        args.push("--model".into());
61        args.push(model.clone());
62    }
63    if let Some(name) = &options.permission_prompt_tool_name {
64        args.push("--permission-prompt-tool".into());
65        args.push(name.clone());
66    }
67    // the CLI only emits `--permission-mode` when the caller set
68    // one explicitly. We mirror that: the CLI default is already
69    // `default`, so omitting the flag on the default variant avoids
70    // argv drift and also lets the CLI honour any user-level override.
71    if options.permission_mode != PermissionMode::Ask {
72        args.push("--permission-mode".into());
73        args.push(options.permission_mode.as_cli_arg().into());
74    }
75    if let Some(resume) = &options.resume {
76        args.push("--resume".into());
77        args.push(resume.clone());
78    }
79    if let Some(sid) = &options.session_id {
80        args.push("--session-id".into());
81        args.push(sid.clone());
82    }
83
84    // MCP: pass --mcp-config '<inline-json>' when servers are registered.
85    // the CLI uses inline JSON (not a temp file) with {"type": "sdk"}
86    // entries to signal in-process hosting; external servers carry their
87    // own stdio / SSE / HTTP config verbatim.
88    let hosts = McpHosts::new(options.mcp_servers.clone(), options.external_mcp_servers.clone());
89    if !hosts.is_empty() {
90        args.push("--mcp-config".into());
91        args.push(hosts.config_argv());
92    }
93
94    for plugin in &options.plugins {
95        match plugin {
96            SdkPluginConfig::Local { path } => {
97                args.push("--plugin-dir".into());
98                args.push(path.to_string_lossy().into_owned());
99            }
100        }
101    }
102
103    // extra_args - arbitrary CLI flags. `None` value = bare flag.
104    for (flag, maybe_val) in &options.extra_args {
105        args.push(format!("--{flag}"));
106        if let Some(v) = maybe_val {
107            args.push(v.clone());
108        }
109    }
110
111    if let Some(effort) = &options.effort {
112        args.push("--effort".into());
113        args.push(effort.as_cli_arg());
114    }
115
116    // Always use streaming mode with stdin (matching TypeScript SDK).
117    // This allows agents and other large configs to be sent via
118    // `initialize` request rather than argv.
119    args.push("--input-format".into());
120    args.push("stream-json".into());
121
122    Ok(args)
123}