Skip to main content

claude_code_cli_acp/
cli.rs

1use 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}