Research-Stack/1-Distributed-Systems/agents/claw/rust/crates/rusty-claude-cli/src/main.rs

428 lines
14 KiB
Rust

#![allow(
dead_code,
unused_imports,
unused_variables,
clippy::unneeded_struct_pattern,
clippy::unnecessary_wraps,
clippy::unused_self
)]
mod init;
mod input;
mod render;
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::{self, IsTerminal, Read, Write};
use std::net::TcpListener;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant, UNIX_EPOCH};
use api::{
detect_provider_kind, oauth_token_is_expired, resolve_startup_auth_source, AnthropicClient,
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
MessageResponse, OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient,
ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
ToolResultContentBlock,
};
use commands::{
classify_skills_slash_command, handle_agents_slash_command, handle_agents_slash_command_json,
handle_mcp_slash_command, handle_mcp_slash_command_json, handle_plugins_slash_command,
handle_skills_slash_command, handle_skills_slash_command_json, render_slash_command_help,
resume_supported_slash_commands, slash_command_specs, validate_slash_command_input,
SkillSlashDispatch, SlashCommand,
};
use compat_harness::{extract_manifest, UpstreamPaths};
use init::initialize_repo;
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{
check_base_commit, clear_oauth_credentials, format_stale_base_warning, format_usd,
generate_pkce_pair, generate_state, load_oauth_credentials, load_system_prompt,
parse_oauth_callback_request_target, pricing_for_model, resolve_expected_base,
resolve_sandbox_status, save_oauth_credentials, ApiClient, ApiRequest, AssistantEvent,
CompactionConfig, ConfigLoader, ConfigSource, ContentBlock, ConversationMessage,
ConversationRuntime, McpServer, McpServerManager, McpServerSpec, McpTool, MessageRole,
ModelPricing, OAuthAuthorizationRequest, OAuthConfig, OAuthTokenExchangeRequest,
PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode,
RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use tools::{
execute_tool, mvp_tool_specs, GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput,
};
const DEFAULT_MODEL: &str = "claude-opus-4-6";
fn max_tokens_for_model(model: &str) -> u32 {
if model.contains("opus") {
32_000
} else {
64_000
}
}
// Build-time constants injected by build.rs (fall back to static values when
// build.rs hasn't run, e.g. in doc-test or unusual toolchain environments).
const DEFAULT_DATE: &str = match option_env!("BUILD_DATE") {
Some(d) => d,
None => "unknown",
};
const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
const POST_TOOL_STALL_TIMEOUT: Duration = Duration::from_secs(10);
const PRIMARY_SESSION_EXTENSION: &str = "jsonl";
const LEGACY_SESSION_EXTENSION: &str = "json";
const LATEST_SESSION_REFERENCE: &str = "latest";
const SESSION_REFERENCE_ALIASES: &[&str] = &[LATEST_SESSION_REFERENCE, "last", "recent"];
const CLI_OPTION_SUGGESTIONS: &[&str] = &[
"--help",
"-h",
"--version",
"-V",
"--model",
"--output-format",
"--permission-mode",
"--dangerously-skip-permissions",
"--allowedTools",
"--allowed-tools",
"--resume",
"--print",
"--compact",
"--base-commit",
"-p",
];
type AllowedToolSet = BTreeSet<String>;
type RuntimePluginStateBuildOutput = (
Option<Arc<Mutex<RuntimeMcpState>>>,
Vec<RuntimeToolDefinition>,
);
fn main() {
if let Err(error) = run() {
let message = error.to_string();
if message.contains("`claw --help`") {
eprintln!("error: {message}");
} else {
eprintln!(
"error: {message}
Run `claw --help` for usage."
);
}
std::process::exit(1);
}
}
/// Read piped stdin content when stdin is not a terminal.
///
/// Returns `None` when stdin is attached to a terminal (interactive REPL use),
/// when reading fails, or when the piped content is empty after trimming.
/// Returns `Some(raw_content)` when a pipe delivered non-empty content.
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)
}
/// Merge a piped stdin payload into a prompt argument.
///
/// When `stdin_content` is `None` or empty after trimming, the prompt is
/// returned unchanged. Otherwise the trimmed stdin content is appended to the
/// prompt separated by a blank line so the model sees the prompt first and the
/// piped context immediately after it.
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 } => dump_manifests(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(cwd, 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,
permission_mode,
output_format,
} => print_status_snapshot(&model, permission_mode, output_format)?,
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,
} => {
run_stale_base_preflight(base_commit.as_deref());
// Only consume piped stdin as prompt context when the permission
// mode is fully unattended. In modes where the permission
// prompter may invoke CliPermissionPrompter::decide(), stdin
// must remain available for interactive approval; otherwise the
// prompter's read_line() would hit EOF and deny every request.
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 mut cli = LiveCli::new(model, true, allowed_tools, permission_mode)?;
cli.set_reasoning_effort(reasoning_effort);
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
}
CliAction::Login { output_format } => run_login(output_format)?,
CliAction::Logout { output_format } => run_logout(output_format)?,
CliAction::Doctor { output_format } => run_doctor(output_format)?,
CliAction::State { output_format } => run_worker_state(output_format)?,
CliAction::Init { output_format } => run_init(output_format)?,
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,
} => run_repl(
model,
allowed_tools,
permission_mode,
base_commit,
reasoning_effort,
)?,
CliAction::HelpTopic(topic) => print_help_topic(topic),
CliAction::Help { output_format } => print_help(output_format)?,
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CliAction {
DumpManifests {
output_format: CliOutputFormat,
},
BootstrapPlan {
output_format: CliOutputFormat,
},
Agents {
args: Option<String>,
output_format: CliOutputFormat,
},
Mcp {
args: Option<String>,
output_format: CliOutputFormat,
},
Skills {
args: Option<String>,
output_format: CliOutputFormat,
},
Plugins {
action: Option<String>,
target: Option<String>,
output_format: CliOutputFormat,
},
PrintSystemPrompt {
cwd: PathBuf,
date: String,
output_format: CliOutputFormat,
},
Version {
output_format: CliOutputFormat,
},
ResumeSession {
session_path: PathBuf,
commands: Vec<String>,
output_format: CliOutputFormat,
},
Status {
model: String,
permission_mode: PermissionMode,
output_format: CliOutputFormat,
},
Sandbox {
output_format: CliOutputFormat,
},
Prompt {
prompt: String,
model: String,
output_format: CliOutputFormat,
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
compact: bool,
base_commit: Option<String>,
reasoning_effort: Option<String>,
},
Login {
output_format: CliOutputFormat,
},
Logout {
output_format: CliOutputFormat,
},
Doctor {
output_format: CliOutputFormat,
},
State {
output_format: CliOutputFormat,
},
Init {
output_format: CliOutputFormat,
},
Export {
session_reference: String,
output_path: Option<PathBuf>,
output_format: CliOutputFormat,
},
Repl {
model: String,
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
base_commit: Option<String>,
reasoning_effort: Option<String>,
},
HelpTopic(LocalHelpTopic),
// prompt-mode formatting is only supported for non-interactive runs
Help {
output_format: CliOutputFormat,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LocalHelpTopic {
Status,
Sandbox,
Doctor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CliOutputFormat {
Text,
Json,
}
impl CliOutputFormat {
fn parse(value: &str) -> Result<Self, String> {
match value {
"text" => Ok(Self::Text),
"json" => Ok(Self::Json),
other => Err(format!(
"unsupported value for --output-format: {other} (expected text or json)"
)),
}
}
}
// ... (rest of file unchanged)
/// the same surface the in-process agent loop uses.
fn run_mcp_serve() -> Result<(), Box<dyn std::error::Error>> {
// Let `runtime::mcp_server` resolve tools from Lean when this is empty.
// (See `CLAW_LEAN_MCP_MANIFEST_CMD`.)
let tools: Vec<McpTool> = Vec::new();
// Build a closure that preserves built-in tool behavior while allowing
// Lean-defined surfaces to dispatch to external providers.
let tool_handler = {
use runtime::mcp_surface_router::{dispatch_with_map, ToolCommandMap};
use std::sync::OnceLock;
static ROUTER_MAP: OnceLock<Result<Option<ToolCommandMap>, String>> = OnceLock::new();
move |tool_name: &str, input: &Value| -> Result<String, String> {
// 1) Built-in tools first.
if let Ok(result) = execute_tool(tool_name, input) {
return Ok(result);
}
// 2) External routing map (transitional pool).
let map = ROUTER_MAP
.get_or_init(|| ToolCommandMap::from_env())
.clone()
.map_err(|error| error)?;
if let Some(map) = map {
if map.handles(tool_name) {
return dispatch_with_map(&map, tool_name, input);
}
}
// 3) Fall through: original built-in error message.
execute_tool(tool_name, input)
}
};
let spec = McpServerSpec {
server_name: "claw".to_string(),
server_version: VERSION.to_string(),
tools,
tool_handler: Box::new(tool_handler),
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async move {
let mut server = McpServer::new(spec);
server.run().await
})?;
Ok(())
}