#![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; type RuntimePluginStateBuildOutput = ( Option>>, Vec, ); 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 { 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> { let args: Vec = 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, output_format: CliOutputFormat, }, Mcp { args: Option, output_format: CliOutputFormat, }, Skills { args: Option, output_format: CliOutputFormat, }, Plugins { action: Option, target: Option, output_format: CliOutputFormat, }, PrintSystemPrompt { cwd: PathBuf, date: String, output_format: CliOutputFormat, }, Version { output_format: CliOutputFormat, }, ResumeSession { session_path: PathBuf, commands: Vec, 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, permission_mode: PermissionMode, compact: bool, base_commit: Option, reasoning_effort: Option, }, 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, output_format: CliOutputFormat, }, Repl { model: String, allowed_tools: Option, permission_mode: PermissionMode, base_commit: Option, reasoning_effort: Option, }, 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 { 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> { // Let `runtime::mcp_server` resolve tools from Lean when this is empty. // (See `CLAW_LEAN_MCP_MANIFEST_CMD`.) let tools: Vec = 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, String>> = OnceLock::new(); move |tool_name: &str, input: &Value| -> Result { // 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(()) }