535 lines
18 KiB
Rust
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;
|