mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
428 lines
14 KiB
Rust
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(())
|
|
}
|