diff --git a/Cargo.lock b/Cargo.lock index 443b79d..f956d2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1357,6 +1357,7 @@ dependencies = [ "commands", "compat-harness", "crossterm", + "plugins", "pulldown-cmark", "runtime", "serde_json", diff --git a/crates/runtime/src/mcp_stdio.rs b/crates/runtime/src/mcp_stdio.rs index 27402d6..56b15ed 100644 --- a/crates/runtime/src/mcp_stdio.rs +++ b/crates/runtime/src/mcp_stdio.rs @@ -807,6 +807,7 @@ mod tests { use std::collections::BTreeMap; use std::fs; use std::io::ErrorKind; + #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; @@ -846,9 +847,12 @@ mod tests { "#!/bin/sh\nprintf 'READY:%s\\n' \"$MCP_TEST_TOKEN\"\nIFS= read -r line\nprintf 'ECHO:%s\\n' \"$line\"\n", ) .expect("write script"); - let mut permissions = fs::metadata(&script_path).expect("metadata").permissions(); - permissions.set_mode(0o755); - fs::set_permissions(&script_path, permissions).expect("chmod"); + #[cfg(unix)] + { + let mut permissions = fs::metadata(&script_path).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script_path, permissions).expect("chmod"); + } script_path } diff --git a/crates/rusty-claude-cli/Cargo.toml b/crates/rusty-claude-cli/Cargo.toml index 625d1e5..5e5186d 100644 --- a/crates/rusty-claude-cli/Cargo.toml +++ b/crates/rusty-claude-cli/Cargo.toml @@ -15,6 +15,7 @@ commands = { path = "../commands" } compat-harness = { path = "../compat-harness" } crossterm = "0.28" pulldown-cmark = "0.13" +plugins = { path = "../plugins" } runtime = { path = "../runtime" } serde_json = "1" syntect = "5" diff --git a/crates/rusty-claude-cli/src/main.rs b/crates/rusty-claude-cli/src/main.rs index 173df7e..ecc5550 100644 --- a/crates/rusty-claude-cli/src/main.rs +++ b/crates/rusty-claude-cli/src/main.rs @@ -12,12 +12,14 @@ use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use api::{ - resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock, + resolve_startup_auth_source, AuthSource, ClawApiClient, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest, MessageResponse, OutputContentBlock, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, }; use commands::{ + handle_agents_slash_command, handle_branch_slash_command, handle_commit_slash_command, + handle_plugins_slash_command, handle_skills_slash_command, handle_worktree_slash_command, render_slash_command_help, resume_supported_slash_commands, slash_command_specs, SlashCommand, }; use compat_harness::{extract_manifest, UpstreamPaths}; @@ -33,6 +35,7 @@ use runtime::{ }; use serde_json::json; use tools::{execute_tool, mvp_tool_specs, ToolSpec}; +use plugins::{self, PluginManager, PluginManagerConfig}; const DEFAULT_MODEL: &str = "claude-opus-4-6"; const DEFAULT_MAX_TOKENS: u32 = 32; @@ -404,7 +407,7 @@ fn dump_manifests() { } fn print_bootstrap_plan() { - for phase in runtime::BootstrapPlan::claude_code_default().phases() { + for phase in runtime::BootstrapPlan::claw_default().phases() { println!("- {phase:?}"); } } @@ -450,7 +453,7 @@ fn run_login() -> Result<(), Box> { return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into()); } - let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url()); + let client = ClawApiClient::from_auth(AuthSource::None).with_base_url(api::read_base_url()); let exchange_request = OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri); let runtime = tokio::runtime::Runtime::new()?; @@ -864,10 +867,35 @@ fn run_resume_command( )), }) } - SlashCommand::Resume { .. } + SlashCommand::Agents { args } => Ok(ResumeCommandOutcome { + session: session.clone(), + message: Some( + handle_agents_slash_command(args.as_deref(), &env::current_dir()?) + .map_err(|error| error.to_string())?, + ), + }), + SlashCommand::Skills { args } => Ok(ResumeCommandOutcome { + session: session.clone(), + message: Some( + handle_skills_slash_command(args.as_deref(), &env::current_dir()?) + .map_err(|error| error.to_string())?, + ), + }), + SlashCommand::Branch { .. } + | SlashCommand::Bughunter { .. } + | SlashCommand::Worktree { .. } + | SlashCommand::Commit + | SlashCommand::CommitPushPr { .. } + | SlashCommand::Pr { .. } + | SlashCommand::Issue { .. } + | SlashCommand::Ultraplan { .. } + | SlashCommand::Teleport { .. } + | SlashCommand::DebugToolCall + | SlashCommand::Resume { .. } | SlashCommand::Model { .. } | SlashCommand::Permissions { .. } | SlashCommand::Session { .. } + | SlashCommand::Plugins { .. } | SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()), } } @@ -1033,7 +1061,7 @@ impl LiveCli { } fn run_prompt_json(&mut self, input: &str) -> Result<(), Box> { - let client = AnthropicClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url()); + let client = ClawApiClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url()); let request = MessageRequest { model: self.model.clone(), max_tokens: DEFAULT_MAX_TOKENS, @@ -1055,7 +1083,9 @@ impl LiveCli { .iter() .filter_map(|block| match block { OutputContentBlock::Text { text } => Some(text.as_str()), - OutputContentBlock::ToolUse { .. } => None, + OutputContentBlock::ToolUse { .. } + | OutputContentBlock::Thinking { .. } + | OutputContentBlock::RedactedThinking { .. } => None, }) .collect::>() .join(""); @@ -1124,6 +1154,75 @@ impl LiveCli { self.export_session(path.as_deref())?; false } + SlashCommand::Branch { action, target } => { + println!( + "{}", + handle_branch_slash_command( + action.as_deref(), + target.as_deref(), + &env::current_dir()? + )? + ); + false + } + SlashCommand::Worktree { + action, + path, + branch, + } => { + println!( + "{}", + handle_worktree_slash_command( + action.as_deref(), + path.as_deref(), + branch.as_deref(), + &env::current_dir()? + )? + ); + false + } + SlashCommand::Commit => { + println!( + "{}", + handle_commit_slash_command("resume commit", &env::current_dir()?)? + ); + false + } + SlashCommand::Agents { args } => { + println!( + "{}", + handle_agents_slash_command(args.as_deref(), &env::current_dir()?)? + ); + false + } + SlashCommand::Skills { args } => { + println!( + "{}", + handle_skills_slash_command(args.as_deref(), &env::current_dir()?)? + ); + false + } + SlashCommand::Plugins { action, target } => { + let config = plugins::PluginManagerConfig::new(env::current_dir()?); + let mut manager = plugins::PluginManager::new(config); + let result = handle_plugins_slash_command( + action.as_deref(), + target.as_deref(), + &mut manager, + )?; + println!("{}", result.message); + result.reload_runtime + } + SlashCommand::Bughunter { .. } + | SlashCommand::CommitPushPr { .. } + | SlashCommand::Pr { .. } + | SlashCommand::Issue { .. } + | SlashCommand::Ultraplan { .. } + | SlashCommand::Teleport { .. } + | SlashCommand::DebugToolCall => { + eprintln!("slash command not yet implemented in REPL: {command:?}"); + false + } SlashCommand::Session { action, target } => { self.handle_session_command(action.as_deref(), target.as_deref())? } @@ -1920,7 +2019,7 @@ impl runtime::PermissionPrompter for CliPermissionPrompter { struct AnthropicRuntimeClient { runtime: tokio::runtime::Runtime, - client: AnthropicClient, + client: ClawApiClient, model: String, enable_tools: bool, allowed_tools: Option, @@ -1934,7 +2033,7 @@ impl AnthropicRuntimeClient { ) -> Result> { Ok(Self { runtime: tokio::runtime::Runtime::new()?, - client: AnthropicClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url()), + client: ClawApiClient::from_auth(resolve_cli_auth_source()?).with_base_url(api::read_base_url()), model, enable_tools, allowed_tools, @@ -2018,6 +2117,8 @@ impl ApiClient for AnthropicRuntimeClient { input.push_str(&partial_json); } } + ContentBlockDelta::ThinkingDelta { .. } + | ContentBlockDelta::SignatureDelta { .. } => {} }, ApiStreamEvent::ContentBlockStop(_) => { if let Some((id, name, input)) = pending_tool.take() { @@ -2151,6 +2252,7 @@ fn push_output_block( .map_err(|error| RuntimeError::new(error.to_string()))?; *pending_tool = Some((id, name, input.to_string())); } + OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {} } Ok(()) }