claude_code_cli_acp/
cli.rs1use std::{ffi::OsString, io::IsTerminal, io::Read, io::Write, time::Duration};
2
3use clap::Parser;
4
5use crate::{acp::server::AcpServer, doctor, interactive, print_mode};
6
7#[derive(Debug, Parser)]
8#[command(name = "claude-code-cli-acp")]
9#[command(about = "ACP adapter and CLI wrapper for Claude Code")]
10struct PrintCommand {
11 #[arg()]
12 prompt: Vec<String>,
13 #[arg(long, default_value = "text")]
14 output_format: print_mode::OutputFormat,
15 #[arg(long)]
16 resume: Option<String>,
17 #[arg(long = "continue", default_value_t = false)]
18 continue_last: bool,
19 #[arg(long)]
20 session_id: Option<String>,
21 #[arg(long)]
22 cwd: Option<std::path::PathBuf>,
23 #[arg(long)]
24 model: Option<String>,
25 #[arg(long)]
26 permission_mode: Option<String>,
27 #[arg(long, default_value_t = 120)]
28 timeout: u64,
29 #[arg(long, default_value_t = false)]
30 attach_on_timeout: bool,
31 #[arg(long, default_value_t = false)]
32 attach_on_permission: bool,
33}
34
35pub async fn run(args: impl IntoIterator<Item = OsString>) -> anyhow::Result<()> {
36 let mut args = args.into_iter().collect::<Vec<_>>();
37 let _program = args
38 .first()
39 .cloned()
40 .unwrap_or_else(|| "claude-code-cli-acp".into());
41 if !args.is_empty() {
42 args.remove(0);
43 }
44
45 match first_arg(&args).as_deref() {
46 None if std::io::stdin().is_terminal() => {
47 let status = interactive::run(Vec::new()).await?;
48 std::process::exit(status);
49 }
50 None => AcpServer::new().serve_stdio().await.map_err(Into::into),
51 Some("acp") => AcpServer::new().serve_stdio().await.map_err(Into::into),
52 Some("interactive") => {
53 let forwarded = strip_command_and_separator(args);
54 let status = interactive::run(forwarded).await?;
55 std::process::exit(status);
56 }
57 Some("doctor") => {
58 let live_docs = args
59 .iter()
60 .any(|arg| arg == "--live-docs" || arg == "--check-upstream");
61 doctor::run(live_docs).await
62 }
63 Some("--version") | Some("-V") => {
64 let mut stdout = std::io::stdout().lock();
65 writeln!(
66 stdout,
67 "{} {}",
68 env!("CARGO_PKG_NAME"),
69 env!("CARGO_PKG_VERSION")
70 )?;
71 Ok(())
72 }
73 Some("print") => {
74 let print_args = std::iter::once(OsString::from("print"))
75 .chain(args.into_iter().skip(1))
76 .collect::<Vec<_>>();
77 let command = PrintCommand::parse_from(print_args);
78 let prompt = read_print_prompt(command.prompt)?;
79 let request = print_mode::PrintRequest {
80 prompt,
81 output_format: command.output_format,
82 resume: command.resume,
83 continue_last: command.continue_last,
84 session_id: command.session_id,
85 cwd: command.cwd,
86 model: command.model,
87 permission_mode: command.permission_mode,
88 timeout: Duration::from_secs(command.timeout),
89 attach_on_timeout: command.attach_on_timeout,
90 attach_on_permission: command.attach_on_permission,
91 };
92 print_mode::run(request).await
93 }
94 Some("--") => {
95 let status = interactive::run(args.into_iter().skip(1).collect()).await?;
96 std::process::exit(status);
97 }
98 Some(_) => {
99 let status = interactive::run(args).await?;
100 std::process::exit(status);
101 }
102 }
103}
104
105fn read_print_prompt(args: Vec<String>) -> anyhow::Result<String> {
106 let arg_prompt = args.join(" ");
107 if std::io::stdin().is_terminal() {
108 return Ok(arg_prompt);
109 }
110
111 let mut stdin_prompt = String::new();
112 std::io::stdin().read_to_string(&mut stdin_prompt)?;
113 let stdin_prompt = stdin_prompt.trim_end_matches(['\r', '\n']);
114 match (arg_prompt.is_empty(), stdin_prompt.is_empty()) {
115 (true, true) => Ok(String::new()),
116 (false, true) => Ok(arg_prompt),
117 (true, false) => Ok(stdin_prompt.to_string()),
118 (false, false) => Ok(format!("{arg_prompt}\n\n{stdin_prompt}")),
119 }
120}
121
122fn first_arg(args: &[OsString]) -> Option<String> {
123 args.first().map(|arg| arg.to_string_lossy().into_owned())
124}
125
126fn strip_command_and_separator(args: Vec<OsString>) -> Vec<OsString> {
127 let mut forwarded = args.into_iter().skip(1).collect::<Vec<_>>();
128 if forwarded.first().is_some_and(|arg| arg == "--") {
129 forwarded.remove(0);
130 }
131 forwarded
132}