Skip to main content

claude_code/commands/
print.rs

1use std::time::Duration;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ClaudeOutputFormat {
5    Text,
6    Json,
7    StreamJson,
8}
9
10impl ClaudeOutputFormat {
11    pub(crate) fn as_arg_value(&self) -> &'static str {
12        match self {
13            ClaudeOutputFormat::Text => "text",
14            ClaudeOutputFormat::Json => "json",
15            ClaudeOutputFormat::StreamJson => "stream-json",
16        }
17    }
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ClaudeInputFormat {
22    Text,
23    StreamJson,
24}
25
26impl ClaudeInputFormat {
27    pub(crate) fn as_arg_value(&self) -> &'static str {
28        match self {
29            ClaudeInputFormat::Text => "text",
30            ClaudeInputFormat::StreamJson => "stream-json",
31        }
32    }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ClaudeChromeMode {
37    Chrome,
38    NoChrome,
39}
40
41#[derive(Debug, Clone)]
42pub struct ClaudePrintRequest {
43    pub(crate) prompt: Option<String>,
44    pub(crate) stdin: Option<Vec<u8>>,
45    pub(crate) output_format: ClaudeOutputFormat,
46    pub(crate) input_format: Option<ClaudeInputFormat>,
47    pub(crate) json_schema: Option<String>,
48    pub(crate) model: Option<String>,
49    pub(crate) allowed_tools: Vec<String>,
50    pub(crate) disallowed_tools: Vec<String>,
51    pub(crate) permission_mode: Option<String>,
52    pub(crate) dangerously_skip_permissions: bool,
53    pub(crate) add_dirs: Vec<String>,
54    pub(crate) mcp_config: Option<String>,
55    pub(crate) strict_mcp_config: bool,
56    pub(crate) agent: Option<String>,
57    pub(crate) agents: Option<String>,
58    pub(crate) allow_dangerously_skip_permissions: bool,
59    pub(crate) append_system_prompt: Option<String>,
60    pub(crate) betas: Vec<String>,
61    pub(crate) chrome_mode: Option<ClaudeChromeMode>,
62    pub(crate) continue_session: bool,
63    pub(crate) debug: bool,
64    pub(crate) debug_file: Option<String>,
65    pub(crate) disable_slash_commands: bool,
66    pub(crate) fallback_model: Option<String>,
67    pub(crate) files: Vec<String>,
68    pub(crate) fork_session: bool,
69    pub(crate) from_pr: bool,
70    pub(crate) from_pr_value: Option<String>,
71    pub(crate) ide: bool,
72    pub(crate) include_partial_messages: bool,
73    pub(crate) max_budget_usd: Option<f64>,
74    pub(crate) mcp_debug: bool,
75    pub(crate) no_session_persistence: bool,
76    pub(crate) plugin_dirs: Vec<String>,
77    pub(crate) replay_user_messages: bool,
78    pub(crate) resume: bool,
79    pub(crate) resume_value: Option<String>,
80    pub(crate) session_id: Option<String>,
81    pub(crate) setting_sources: Option<String>,
82    pub(crate) settings: Option<String>,
83    pub(crate) system_prompt: Option<String>,
84    pub(crate) tools: Vec<String>,
85    pub(crate) verbose: bool,
86    pub(crate) timeout: Option<Duration>,
87    pub(crate) extra_args: Vec<String>,
88}
89
90impl ClaudePrintRequest {
91    pub fn new(prompt: impl Into<String>) -> Self {
92        Self {
93            prompt: Some(prompt.into()),
94            stdin: None,
95            output_format: ClaudeOutputFormat::Text,
96            input_format: None,
97            json_schema: None,
98            model: None,
99            allowed_tools: Vec::new(),
100            disallowed_tools: Vec::new(),
101            permission_mode: None,
102            dangerously_skip_permissions: false,
103            add_dirs: Vec::new(),
104            mcp_config: None,
105            strict_mcp_config: false,
106            agent: None,
107            agents: None,
108            allow_dangerously_skip_permissions: false,
109            append_system_prompt: None,
110            betas: Vec::new(),
111            chrome_mode: None,
112            continue_session: false,
113            debug: false,
114            debug_file: None,
115            disable_slash_commands: false,
116            fallback_model: None,
117            files: Vec::new(),
118            fork_session: false,
119            from_pr: false,
120            from_pr_value: None,
121            ide: false,
122            include_partial_messages: false,
123            max_budget_usd: None,
124            mcp_debug: false,
125            no_session_persistence: false,
126            plugin_dirs: Vec::new(),
127            replay_user_messages: false,
128            resume: false,
129            resume_value: None,
130            session_id: None,
131            setting_sources: None,
132            settings: None,
133            system_prompt: None,
134            tools: Vec::new(),
135            verbose: false,
136            timeout: None,
137            extra_args: Vec::new(),
138        }
139    }
140
141    pub fn no_prompt(mut self) -> Self {
142        self.prompt = None;
143        self
144    }
145
146    pub fn output_format(mut self, format: ClaudeOutputFormat) -> Self {
147        self.output_format = format;
148        self
149    }
150
151    pub fn input_format(mut self, format: ClaudeInputFormat) -> Self {
152        self.input_format = Some(format);
153        self
154    }
155
156    pub fn json_schema(mut self, schema: impl Into<String>) -> Self {
157        self.json_schema = Some(schema.into());
158        self
159    }
160
161    pub fn model(mut self, model: impl Into<String>) -> Self {
162        self.model = Some(model.into());
163        self
164    }
165
166    pub fn allowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
167        self.allowed_tools = tools.into_iter().map(Into::into).collect();
168        self
169    }
170
171    pub fn disallowed_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
172        self.disallowed_tools = tools.into_iter().map(Into::into).collect();
173        self
174    }
175
176    pub fn permission_mode(mut self, mode: impl Into<String>) -> Self {
177        self.permission_mode = Some(mode.into());
178        self
179    }
180
181    pub fn dangerously_skip_permissions(mut self, enabled: bool) -> Self {
182        self.dangerously_skip_permissions = enabled;
183        self
184    }
185
186    pub fn add_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<String>>) -> Self {
187        self.add_dirs = dirs.into_iter().map(Into::into).collect();
188        self
189    }
190
191    pub fn mcp_config(mut self, config: impl Into<String>) -> Self {
192        self.mcp_config = Some(config.into());
193        self
194    }
195
196    pub fn strict_mcp_config(mut self, enabled: bool) -> Self {
197        self.strict_mcp_config = enabled;
198        self
199    }
200
201    pub fn agent(mut self, agent: impl Into<String>) -> Self {
202        self.agent = Some(agent.into());
203        self
204    }
205
206    pub fn agents(mut self, json: impl Into<String>) -> Self {
207        self.agents = Some(json.into());
208        self
209    }
210
211    pub fn allow_dangerously_skip_permissions(mut self, enabled: bool) -> Self {
212        self.allow_dangerously_skip_permissions = enabled;
213        self
214    }
215
216    pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
217        self.append_system_prompt = Some(prompt.into());
218        self
219    }
220
221    pub fn betas(mut self, betas: impl IntoIterator<Item = impl Into<String>>) -> Self {
222        self.betas = betas.into_iter().map(Into::into).collect();
223        self
224    }
225
226    pub fn chrome(mut self) -> Self {
227        self.chrome_mode = Some(ClaudeChromeMode::Chrome);
228        self
229    }
230
231    pub fn no_chrome(mut self) -> Self {
232        self.chrome_mode = Some(ClaudeChromeMode::NoChrome);
233        self
234    }
235
236    pub fn continue_session(mut self, enabled: bool) -> Self {
237        self.continue_session = enabled;
238        self
239    }
240
241    pub fn debug(mut self, enabled: bool) -> Self {
242        self.debug = enabled;
243        self
244    }
245
246    pub fn debug_file(mut self, path: impl Into<String>) -> Self {
247        self.debug_file = Some(path.into());
248        self
249    }
250
251    pub fn disable_slash_commands(mut self, enabled: bool) -> Self {
252        self.disable_slash_commands = enabled;
253        self
254    }
255
256    pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
257        self.fallback_model = Some(model.into());
258        self
259    }
260
261    pub fn files(mut self, specs: impl IntoIterator<Item = impl Into<String>>) -> Self {
262        self.files = specs.into_iter().map(Into::into).collect();
263        self
264    }
265
266    pub fn fork_session(mut self, enabled: bool) -> Self {
267        self.fork_session = enabled;
268        self
269    }
270
271    pub fn from_pr(mut self, enabled: bool) -> Self {
272        self.from_pr = enabled;
273        self
274    }
275
276    pub fn from_pr_value(mut self, value: impl Into<String>) -> Self {
277        self.from_pr_value = Some(value.into());
278        self
279    }
280
281    pub fn ide(mut self, enabled: bool) -> Self {
282        self.ide = enabled;
283        self
284    }
285
286    pub fn include_partial_messages(mut self, enabled: bool) -> Self {
287        self.include_partial_messages = enabled;
288        self
289    }
290
291    pub fn max_budget_usd(mut self, amount: f64) -> Self {
292        self.max_budget_usd = Some(amount);
293        self
294    }
295
296    pub fn mcp_debug(mut self, enabled: bool) -> Self {
297        self.mcp_debug = enabled;
298        self
299    }
300
301    pub fn no_session_persistence(mut self, enabled: bool) -> Self {
302        self.no_session_persistence = enabled;
303        self
304    }
305
306    pub fn plugin_dirs(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
307        self.plugin_dirs = paths.into_iter().map(Into::into).collect();
308        self
309    }
310
311    pub fn replay_user_messages(mut self, enabled: bool) -> Self {
312        self.replay_user_messages = enabled;
313        self
314    }
315
316    pub fn resume(mut self, enabled: bool) -> Self {
317        self.resume = enabled;
318        self
319    }
320
321    pub fn resume_value(mut self, value: impl Into<String>) -> Self {
322        self.resume_value = Some(value.into());
323        self
324    }
325
326    pub fn session_id(mut self, id: impl Into<String>) -> Self {
327        self.session_id = Some(id.into());
328        self
329    }
330
331    pub fn setting_sources(mut self, sources: impl Into<String>) -> Self {
332        self.setting_sources = Some(sources.into());
333        self
334    }
335
336    pub fn settings(mut self, file_or_json: impl Into<String>) -> Self {
337        self.settings = Some(file_or_json.into());
338        self
339    }
340
341    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
342        self.system_prompt = Some(prompt.into());
343        self
344    }
345
346    pub fn tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
347        self.tools = tools.into_iter().map(Into::into).collect();
348        self
349    }
350
351    pub fn verbose(mut self, enabled: bool) -> Self {
352        self.verbose = enabled;
353        self
354    }
355
356    pub fn stdin_bytes(mut self, bytes: Vec<u8>) -> Self {
357        self.stdin = Some(bytes);
358        self
359    }
360
361    pub fn timeout(mut self, timeout: Duration) -> Self {
362        self.timeout = Some(timeout);
363        self
364    }
365
366    pub fn extra_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
367        self.extra_args = args.into_iter().map(Into::into).collect();
368        self
369    }
370
371    pub fn argv(&self) -> Vec<String> {
372        let mut out: Vec<String> = Vec::new();
373
374        out.push("--print".to_string());
375        out.push("--output-format".to_string());
376        out.push(self.output_format.as_arg_value().to_string());
377
378        if let Some(input_format) = self.input_format {
379            out.push("--input-format".to_string());
380            out.push(input_format.as_arg_value().to_string());
381        }
382
383        if let Some(schema) = self.json_schema.as_ref() {
384            out.push("--json-schema".to_string());
385            out.push(schema.clone());
386        }
387
388        if let Some(model) = self.model.as_ref() {
389            out.push("--model".to_string());
390            out.push(model.clone());
391        }
392
393        if !self.allowed_tools.is_empty() {
394            out.push("--allowedTools".to_string());
395            out.extend(self.allowed_tools.iter().cloned());
396        }
397
398        if !self.disallowed_tools.is_empty() {
399            out.push("--disallowedTools".to_string());
400            out.extend(self.disallowed_tools.iter().cloned());
401        }
402
403        if let Some(mode) = self.permission_mode.as_ref() {
404            out.push("--permission-mode".to_string());
405            out.push(mode.clone());
406        }
407
408        if self.dangerously_skip_permissions {
409            out.push("--dangerously-skip-permissions".to_string());
410        }
411
412        if self.allow_dangerously_skip_permissions {
413            out.push("--allow-dangerously-skip-permissions".to_string());
414        }
415
416        if !self.add_dirs.is_empty() {
417            out.push("--add-dir".to_string());
418            out.extend(self.add_dirs.iter().cloned());
419        }
420
421        if let Some(config) = self.mcp_config.as_ref() {
422            out.push("--mcp-config".to_string());
423            out.push(config.clone());
424        }
425
426        if self.strict_mcp_config {
427            out.push("--strict-mcp-config".to_string());
428        }
429
430        if let Some(agent) = self.agent.as_ref() {
431            out.push("--agent".to_string());
432            out.push(agent.clone());
433        }
434
435        if let Some(agents) = self.agents.as_ref() {
436            out.push("--agents".to_string());
437            out.push(agents.clone());
438        }
439
440        if let Some(prompt) = self.append_system_prompt.as_ref() {
441            out.push("--append-system-prompt".to_string());
442            out.push(prompt.clone());
443        }
444
445        if !self.betas.is_empty() {
446            out.push("--betas".to_string());
447            out.extend(self.betas.iter().cloned());
448        }
449
450        if let Some(mode) = self.chrome_mode {
451            match mode {
452                ClaudeChromeMode::Chrome => out.push("--chrome".to_string()),
453                ClaudeChromeMode::NoChrome => out.push("--no-chrome".to_string()),
454            }
455        }
456
457        if self.continue_session {
458            out.push("--continue".to_string());
459        }
460
461        if self.debug {
462            out.push("--debug".to_string());
463        }
464
465        if let Some(path) = self.debug_file.as_ref() {
466            out.push("--debug-file".to_string());
467            out.push(path.clone());
468        }
469
470        if self.disable_slash_commands {
471            out.push("--disable-slash-commands".to_string());
472        }
473
474        if let Some(model) = self.fallback_model.as_ref() {
475            out.push("--fallback-model".to_string());
476            out.push(model.clone());
477        }
478
479        if !self.files.is_empty() {
480            out.push("--file".to_string());
481            out.extend(self.files.iter().cloned());
482        }
483
484        if self.fork_session {
485            out.push("--fork-session".to_string());
486        }
487
488        if let Some(value) = self.from_pr_value.as_ref() {
489            out.push("--from-pr".to_string());
490            out.push(value.clone());
491        } else if self.from_pr {
492            out.push("--from-pr".to_string());
493        }
494
495        if self.ide {
496            out.push("--ide".to_string());
497        }
498
499        if self.include_partial_messages {
500            out.push("--include-partial-messages".to_string());
501        }
502
503        if let Some(amount) = self.max_budget_usd {
504            out.push("--max-budget-usd".to_string());
505            out.push(amount.to_string());
506        }
507
508        if self.mcp_debug {
509            out.push("--mcp-debug".to_string());
510        }
511
512        if self.no_session_persistence {
513            out.push("--no-session-persistence".to_string());
514        }
515
516        if !self.plugin_dirs.is_empty() {
517            out.push("--plugin-dir".to_string());
518            out.extend(self.plugin_dirs.iter().cloned());
519        }
520
521        if self.replay_user_messages {
522            out.push("--replay-user-messages".to_string());
523        }
524
525        if let Some(value) = self.resume_value.as_ref() {
526            out.push("--resume".to_string());
527            out.push(value.clone());
528        } else if self.resume {
529            out.push("--resume".to_string());
530        }
531
532        if let Some(id) = self.session_id.as_ref() {
533            out.push("--session-id".to_string());
534            out.push(id.clone());
535        }
536
537        if let Some(sources) = self.setting_sources.as_ref() {
538            out.push("--setting-sources".to_string());
539            out.push(sources.clone());
540        }
541
542        if let Some(settings) = self.settings.as_ref() {
543            out.push("--settings".to_string());
544            out.push(settings.clone());
545        }
546
547        if let Some(prompt) = self.system_prompt.as_ref() {
548            out.push("--system-prompt".to_string());
549            out.push(prompt.clone());
550        }
551
552        if !self.tools.is_empty() {
553            out.push("--tools".to_string());
554            out.extend(self.tools.iter().cloned());
555        }
556
557        // Upstream requires `--verbose` to be set when emitting stream-json output.
558        //
559        // This is surprising (the help text doesn't mention it), but without it the CLI exits
560        // non-zero and emits an error to stderr.
561        let stream_json_requires_verbose =
562            matches!(self.output_format, ClaudeOutputFormat::StreamJson);
563        if self.verbose || stream_json_requires_verbose {
564            out.push("--verbose".to_string());
565        }
566
567        out.extend(self.extra_args.iter().cloned());
568
569        if let Some(prompt) = self.prompt.as_ref() {
570            out.push(prompt.clone());
571        }
572
573        out
574    }
575}