claw-code/rust/crates/rusty-claude-cli/src/main.rs

535 lines
18 KiB
Rust

#![allow(
dead_code,
unused_imports,
unused_variables,
clippy::unneeded_struct_pattern,
clippy::unnecessary_wraps,
clippy::unused_self,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::result_large_err
)]
mod args;
mod config;
mod constants;
mod diff;
mod doctor;
mod error;
mod export;
mod formatting;
mod status;
mod session;
mod init;
mod input;
mod live_cli;
mod model;
mod models;
mod permission;
mod api_client;
mod progress;
mod render;
mod tool_executor;
mod runtime_builder;
mod repl_commands;
mod setup_wizard;
mod steer;
pub(crate) use api_client::*;
pub(crate) use args::*;
pub(crate) use config::*;
pub(crate) use constants::*;
pub(crate) use diff::*;
pub(crate) use doctor::*;
pub(crate) use error::*;
pub(crate) use export::*;
pub(crate) use live_cli::*;
pub(crate) use repl_commands::*;
pub(crate) use formatting::*;
pub(crate) use status::*;
pub(crate) use session::*;
pub(crate) use init::*;
pub(crate) use model::*;
pub(crate) use models::*;
pub(crate) use permission::*;
pub(crate) use progress::*;
pub(crate) use tool_executor::*;
pub(crate) use runtime_builder::*;
pub(crate) use render::*;
pub(crate) use commands::*;
pub(crate) use runtime::*;
pub(crate) use steer::*;
use std::env;
use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use commands::SlashCommand;
use runtime::{PermissionMode, Session};
fn main() {
if let Err(error) = run() {
let message = format!(
"{:?}\n\nStack trace:\n{}",
error,
std::backtrace::Backtrace::force_capture()
);
let argv: Vec<String> = std::env::args().collect();
let json_output = argv
.windows(2)
.any(|w| w[0] == "--output-format" && w[1] == "json")
|| argv.iter().any(|a| a == "--output-format=json");
if json_output {
let kind = classify_error_kind(&message);
let (short_reason, hint) = split_error_hint(&message);
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": short_reason,
"kind": kind,
"hint": hint,
})
);
} else {
let kind = classify_error_kind(&message);
if message.contains("`claw --help`") {
eprintln!(
"[error-kind: {kind}]\nerror: {message}"
);
} else {
eprintln!(
"[error-kind: {kind}]\nerror: {message}\nRun `claw --help` for usage."
);
}
}
std::process::exit(1);
}
}
fn read_piped_stdin() -> Option<String> {
if io::stdin().is_terminal() {
return None;
}
let mut buffer = String::new();
if io::stdin().read_to_string(&mut buffer).is_err() {
return None;
}
if buffer.trim().is_empty() {
return None;
}
Some(buffer)
}
fn merge_prompt_with_stdin(prompt: &str, stdin_content: Option<&str>) -> String {
let Some(raw) = stdin_content else {
return prompt.to_string();
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return prompt.to_string();
}
if prompt.is_empty() {
return trimmed.to_string();
}
format!("{prompt}\n\n{trimmed}")
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
match parse_args(&args)? {
CliAction::DumpManifests {
output_format,
manifests_dir,
} => dump_manifests(manifests_dir.as_deref(), output_format)?,
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
CliAction::Agents {
args,
output_format,
} => LiveCli::print_agents(args.as_deref(), output_format)?,
CliAction::Mcp {
args,
output_format,
} => LiveCli::print_mcp(args.as_deref(), output_format)?,
CliAction::Skills {
args,
output_format,
} => LiveCli::print_skills(args.as_deref(), output_format)?,
CliAction::Plugins {
action,
target,
output_format,
} => LiveCli::print_plugins(action.as_deref(), target.as_deref(), output_format)?,
CliAction::PrintSystemPrompt {
cwd,
date,
output_format,
} => print_system_prompt(Some(cwd), Some(date), output_format)?,
CliAction::Version { output_format } => print_version(output_format)?,
CliAction::ResumeSession {
session_path,
commands,
output_format,
} => resume_session(&session_path, &commands, output_format),
CliAction::Status {
model,
model_flag_raw,
permission_mode,
output_format,
allowed_tools,
} => print_status_snapshot(
&model,
model_flag_raw.as_deref(),
permission_mode,
output_format,
allowed_tools.as_ref(),
)?,
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
CliAction::Prompt {
prompt,
model,
output_format,
allowed_tools,
permission_mode,
compact,
base_commit,
reasoning_effort,
allow_broad_cwd,
} => {
enforce_broad_cwd_policy(allow_broad_cwd, output_format)?;
run_stale_base_preflight(base_commit.as_deref());
let stdin_context = if matches!(permission_mode, PermissionMode::DangerFullAccess) {
read_piped_stdin()
} else {
None
};
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
let resolved_model = resolve_repl_model(model);
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
cli.set_reasoning_effort(reasoning_effort);
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
}
CliAction::Doctor { output_format } => run_doctor(output_format)?,
CliAction::Acp { output_format } => print_acp_status(output_format)?,
CliAction::State { output_format } => run_worker_state(output_format)?,
CliAction::Init { output_format } => run_init(output_format)?,
CliAction::Config {
section,
output_format,
} => match output_format {
CliOutputFormat::Text => {
println!("{}", render_config_report(section.as_deref())?);
}
CliOutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&render_config_json(section.as_deref())?)?
);
}
},
CliAction::Diff { output_format } => match output_format {
CliOutputFormat::Text => {
println!("{}", render_diff_report()?);
}
CliOutputFormat::Json => {
let cwd = env::current_dir()?;
println!(
"{}",
serde_json::to_string_pretty(&render_diff_json_for(&cwd)?)?
);
}
},
CliAction::Export {
session_reference,
output_path,
output_format,
} => run_export(&session_reference, output_path.as_deref(), output_format)?,
CliAction::Repl {
model,
allowed_tools,
permission_mode,
base_commit,
reasoning_effort,
allow_broad_cwd,
} => run_repl(
model,
allowed_tools,
permission_mode,
base_commit,
reasoning_effort,
allow_broad_cwd,
)?,
CliAction::HelpTopic(topic) => print_help_topic(topic),
CliAction::Help { output_format } => print_help(output_format)?,
}
Ok(())
}
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
let session_reference = session_path.display().to_string();
let (handle, session) = match load_session_reference(&session_reference) {
Ok(loaded) => loaded,
Err(error) => {
if output_format == CliOutputFormat::Json {
let full_message = format!("failed to restore session: {error}");
let kind = classify_error_kind(&full_message);
let (short_reason, hint) = split_error_hint(&full_message);
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": short_reason,
"kind": kind,
"hint": hint,
})
);
} else {
eprintln!("failed to restore session: {error}");
}
std::process::exit(1);
}
};
let resolved_path = handle.path.clone();
if commands.is_empty() {
if output_format == CliOutputFormat::Json {
println!(
"{}",
serde_json::json!({
"kind": "restored",
"session_id": session.session_id,
"path": handle.path.display().to_string(),
"message_count": session.messages.len(),
})
);
} else {
println!(
"Restored session from {} ({} messages).",
handle.path.display(),
session.messages.len()
);
}
return;
}
let mut session = session;
for raw_command in commands {
{
let cmd_root = raw_command
.trim_start_matches('/')
.split_whitespace()
.next()
.unwrap_or("");
if STUB_COMMANDS.contains(&cmd_root) {
if output_format == CliOutputFormat::Json {
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": format!("/{cmd_root} is not yet implemented in this build"),
"kind": "unsupported_command",
"command": raw_command,
})
);
} else {
eprintln!("/{cmd_root} is not yet implemented in this build");
}
std::process::exit(2);
}
}
let command = match SlashCommand::parse(raw_command) {
Ok(Some(command)) => command,
Ok(None) => {
if output_format == CliOutputFormat::Json {
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": format!("unsupported resumed command: {raw_command}"),
"kind": "unsupported_resumed_command",
"command": raw_command,
})
);
} else {
eprintln!("unsupported resumed command: {raw_command}");
}
std::process::exit(2);
}
Err(error) => {
if output_format == CliOutputFormat::Json {
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": error.to_string(),
"command": raw_command,
})
);
} else {
eprintln!("{error}");
}
std::process::exit(2);
}
};
match run_resume_command(&resolved_path, &session, &command) {
Ok(ResumeCommandOutcome {
session: next_session,
message,
json,
}) => {
session = next_session;
if output_format == CliOutputFormat::Json {
if let Some(value) = json {
println!(
"{}",
serde_json::to_string_pretty(&value)
.expect("resume command json output")
);
} else if let Some(message) = message {
println!("{message}");
}
} else if let Some(message) = message {
println!("{message}");
}
}
Err(error) => {
if output_format == CliOutputFormat::Json {
eprintln!(
"{}",
serde_json::json!({
"type": "error",
"error": error.to_string(),
"command": raw_command,
})
);
} else {
eprintln!("{error}");
}
std::process::exit(2);
}
}
}
}
pub(crate) fn write_mcp_server_fixture(script_path: &Path) {
let script = [
"#!/usr/bin/env python3",
"import json, sys",
"",
"def read_message():",
" header = b''",
r" while not header.endswith(b'\r\n\r\n'):",
" chunk = sys.stdin.buffer.read(1)",
" if not chunk:",
" return None",
" header += chunk",
" length = 0",
r" for line in header.decode().split('\r\n'):",
r" if line.lower().startswith('content-length:'):",
" length = int(line.split(':', 1)[1].strip())",
" payload = sys.stdin.buffer.read(length)",
" return json.loads(payload.decode())",
"",
"def send_message(message):",
" payload = json.dumps(message).encode()",
r" sys.stdout.buffer.write(f'Content-Length: {len(payload)}\r\n\r\n'.encode() + payload)",
" sys.stdout.buffer.flush()",
"",
"while True:",
" request = read_message()",
" if request is None:",
" break",
" method = request['method']",
" if method == 'initialize':",
" send_message({",
" 'jsonrpc': '2.0',",
" 'id': request['id'],",
" 'result': {",
" 'protocolVersion': request['params']['protocolVersion'],",
" 'capabilities': {'tools': {}, 'resources': {}},",
" 'serverInfo': {'name': 'fixture', 'version': '1.0.0'}",
" }",
" })",
" elif method == 'tools/list':",
" send_message({",
" 'jsonrpc': '2.0',",
" 'id': request['id'],",
" 'result': {",
" 'tools': [",
" {",
" 'name': 'fixture_tool',",
" 'description': 'A tool from the fixture server',",
" 'inputSchema': {",
" 'type': 'object',",
" 'properties': {",
" 'arg': {'type': 'string'}",
" },",
" 'required': ['arg']",
" }",
" }",
" ]",
" }",
" })",
" elif method == 'resources/list':",
" send_message({",
" 'jsonrpc': '2.0',",
" 'id': request['id'],",
" 'result': {",
" 'resources': [",
" {",
" 'uri': 'fixture://resource',",
" 'name': 'fixture_resource',",
" 'description': 'A resource from the fixture server',",
" 'mimeType': 'text/plain'",
" }",
" ]",
" }",
" })",
" elif method == 'resources/read':",
" send_message({",
" 'jsonrpc': '2.0',",
" 'id': request['id'],",
" 'result': {",
" 'contents': [",
" {",
" 'uri': request['params']['uri'],",
" 'mimeType': 'text/plain',",
" 'text': 'fixture resource content'",
" }",
" ]",
" }",
" })",
" elif method == 'tools/call':",
" send_message({",
" 'jsonrpc': '2.0',",
" 'id': request['id'],",
" 'result': {",
" 'content': [",
" {",
" 'type': 'text',",
" 'text': f\"fixture tool result for {request['params']['arguments']['arg']}\"",
" }",
" ]",
" }",
" })",
];
let content = script.join("\n");
fs::write(script_path, content).expect("write fixture script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(script_path, fs::Permissions::from_mode(0o755))
.expect("set fixture permissions");
}
}
#[cfg(test)]
mod main_tests;