feat: Claude Code Router (CCR) executor (#196)
Implement Claude Code Router exector
This commit is contained in:
@@ -10,7 +10,8 @@ export const EXECUTOR_TYPES: string[] = [
|
||||
"claude",
|
||||
"amp",
|
||||
"gemini",
|
||||
"charmopencode"
|
||||
"charmopencode",
|
||||
"claude-code-router"
|
||||
];
|
||||
|
||||
export const EDITOR_TYPES: EditorType[] = [
|
||||
@@ -27,7 +28,8 @@ export const EXECUTOR_LABELS: Record<string, string> = {
|
||||
"claude": "Claude",
|
||||
"amp": "Amp",
|
||||
"gemini": "Gemini",
|
||||
"charmopencode": "Charm Opencode"
|
||||
"charmopencode": "Charm Opencode",
|
||||
"claude-code-router": "Claude Code Router"
|
||||
};
|
||||
|
||||
export const EDITOR_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -7,7 +7,7 @@ use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::executors::{
|
||||
AmpExecutor, CharmOpencodeExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor,
|
||||
AmpExecutor, CCRExecutor, CharmOpencodeExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor,
|
||||
SetupScriptExecutor,
|
||||
};
|
||||
|
||||
@@ -345,7 +345,12 @@ pub enum ExecutorConfig {
|
||||
Claude,
|
||||
Amp,
|
||||
Gemini,
|
||||
SetupScript { script: String },
|
||||
SetupScript {
|
||||
script: String,
|
||||
},
|
||||
#[serde(rename = "claude-code-router")]
|
||||
#[ts(rename = "claude-code-router")]
|
||||
ClaudeCodeRouter,
|
||||
CharmOpencode,
|
||||
// Future executors can be added here
|
||||
// Shell { command: String },
|
||||
@@ -370,6 +375,7 @@ impl FromStr for ExecutorConfig {
|
||||
"amp" => Ok(ExecutorConfig::Amp),
|
||||
"gemini" => Ok(ExecutorConfig::Gemini),
|
||||
"charmopencode" => Ok(ExecutorConfig::CharmOpencode),
|
||||
"claude-code-router" => Ok(ExecutorConfig::ClaudeCodeRouter),
|
||||
"setup_script" => Ok(ExecutorConfig::SetupScript {
|
||||
script: "setup script".to_string(),
|
||||
}),
|
||||
@@ -382,9 +388,10 @@ impl ExecutorConfig {
|
||||
pub fn create_executor(&self) -> Box<dyn Executor> {
|
||||
match self {
|
||||
ExecutorConfig::Echo => Box::new(EchoExecutor),
|
||||
ExecutorConfig::Claude => Box::new(ClaudeExecutor),
|
||||
ExecutorConfig::Claude => Box::new(ClaudeExecutor::new()),
|
||||
ExecutorConfig::Amp => Box::new(AmpExecutor),
|
||||
ExecutorConfig::Gemini => Box::new(GeminiExecutor),
|
||||
ExecutorConfig::ClaudeCodeRouter => Box::new(CCRExecutor::new()),
|
||||
ExecutorConfig::CharmOpencode => Box::new(CharmOpencodeExecutor),
|
||||
ExecutorConfig::SetupScript { script } => {
|
||||
Box::new(SetupScriptExecutor::new(script.clone()))
|
||||
@@ -398,7 +405,9 @@ impl ExecutorConfig {
|
||||
ExecutorConfig::CharmOpencode => {
|
||||
dirs::home_dir().map(|home| home.join(".opencode.json"))
|
||||
}
|
||||
ExecutorConfig::Claude => dirs::home_dir().map(|home| home.join(".claude.json")),
|
||||
ExecutorConfig::Claude | ExecutorConfig::ClaudeCodeRouter => {
|
||||
dirs::home_dir().map(|home| home.join(".claude.json"))
|
||||
}
|
||||
ExecutorConfig::Amp => {
|
||||
dirs::config_dir().map(|config| config.join("amp").join("settings.json"))
|
||||
}
|
||||
@@ -417,6 +426,7 @@ impl ExecutorConfig {
|
||||
ExecutorConfig::Claude => Some(vec!["mcpServers"]),
|
||||
ExecutorConfig::Amp => Some(vec!["amp", "mcpServers"]), // Nested path for Amp
|
||||
ExecutorConfig::Gemini => Some(vec!["mcpServers"]),
|
||||
ExecutorConfig::ClaudeCodeRouter => Some(vec!["mcpServers"]),
|
||||
ExecutorConfig::SetupScript { .. } => None, // Setup scripts don't support MCP
|
||||
}
|
||||
}
|
||||
@@ -437,6 +447,7 @@ impl ExecutorConfig {
|
||||
ExecutorConfig::Claude => "Claude",
|
||||
ExecutorConfig::Amp => "Amp",
|
||||
ExecutorConfig::Gemini => "Gemini",
|
||||
ExecutorConfig::ClaudeCodeRouter => "Claude Code Router",
|
||||
ExecutorConfig::SetupScript { .. } => "Setup Script",
|
||||
}
|
||||
}
|
||||
@@ -450,6 +461,7 @@ impl std::fmt::Display for ExecutorConfig {
|
||||
ExecutorConfig::Amp => "amp",
|
||||
ExecutorConfig::Gemini => "gemini",
|
||||
ExecutorConfig::CharmOpencode => "charmopencode",
|
||||
ExecutorConfig::ClaudeCodeRouter => "claude-code-router",
|
||||
ExecutorConfig::SetupScript { .. } => "setup_script",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
@@ -905,7 +917,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_claude_log_normalization() {
|
||||
let claude_executor = ClaudeExecutor;
|
||||
let claude_executor = ClaudeExecutor::new();
|
||||
let claude_logs = r#"{"type":"system","subtype":"init","cwd":"/private/tmp/mission-control-worktree-8ff34214-7bb4-4a5a-9f47-bfdf79e20368","session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9","tools":["Task","Bash","Glob","Grep","LS","exit_plan_mode","Read","Edit","MultiEdit","Write","NotebookRead","NotebookEdit","WebFetch","TodoRead","TodoWrite","WebSearch"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"bypassPermissions","apiKeySource":"none"}
|
||||
{"type":"assistant","message":{"id":"msg_014xUHgkAhs6cRx5WVT3s7if","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I'll help you list your projects using vibe-kanban. Let me first explore the codebase to understand how vibe-kanban works and find your projects."}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":13497,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9"}
|
||||
{"type":"assistant","message":{"id":"msg_014xUHgkAhs6cRx5WVT3s7if","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01Br3TvXdmW6RPGpB5NihTHh","name":"Task","input":{"description":"Find vibe-kanban projects","prompt":"I need to find and list projects using vibe-kanban."}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":13497,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"parent_tool_use_id":null,"session_id":"499dcce4-04aa-4a3e-9e0c-ea0228fa87c9"}"#;
|
||||
|
||||
115
backend/src/executors/ccr.rs
Normal file
115
backend/src/executors/ccr.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use async_trait::async_trait;
|
||||
use command_group::AsyncGroupChild;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
executor::{Executor, ExecutorError, NormalizedConversation},
|
||||
executors::{ClaudeExecutor, ClaudeFollowupExecutor},
|
||||
};
|
||||
|
||||
/// An executor that uses Claude Code Router (CCR) to process tasks
|
||||
/// This is a thin wrapper around ClaudeExecutor that uses Claude Code Router instead of Claude CLI
|
||||
pub struct CCRExecutor(ClaudeExecutor);
|
||||
|
||||
impl Default for CCRExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CCRExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self(ClaudeExecutor::with_command(
|
||||
"claude-code-router".to_string(),
|
||||
"npx -y @musistudio/claude-code-router code -p --dangerously-skip-permissions --verbose --output-format=stream-json".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Executor for CCRExecutor {
|
||||
async fn spawn(
|
||||
&self,
|
||||
pool: &sqlx::SqlitePool,
|
||||
task_id: Uuid,
|
||||
worktree_path: &str,
|
||||
) -> Result<AsyncGroupChild, ExecutorError> {
|
||||
self.0.spawn(pool, task_id, worktree_path).await
|
||||
}
|
||||
|
||||
fn normalize_logs(
|
||||
&self,
|
||||
logs: &str,
|
||||
worktree_path: &str,
|
||||
) -> Result<NormalizedConversation, String> {
|
||||
let filtered_logs = filter_ccr_service_messages(logs);
|
||||
let mut result = self.0.normalize_logs(&filtered_logs, worktree_path)?;
|
||||
result.executor_type = "claude-code-router".to_string();
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter out CCR service messages that appear in stdout but shouldn't be shown to users
|
||||
/// These are informational messages from the CCR wrapper itself
|
||||
fn filter_ccr_service_messages(logs: &str) -> String {
|
||||
logs.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
|
||||
// Filter out known CCR service messages
|
||||
if trimmed.eq("Service not running, starting service...")
|
||||
|| trimmed.eq("claude code router service has been successfully stopped.")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out system init JSON that contains misleading model information
|
||||
// CCR delegates to different models, so the init model info is incorrect
|
||||
if trimmed.starts_with(r#"{"type":"system","subtype":"init""#)
|
||||
&& trimmed.contains(r#""model":"#)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Claude Code Router followup executor - forwards to ClaudeFollowupExecutor with Claude Code Router command
|
||||
pub struct CCRFollowupExecutor(ClaudeFollowupExecutor);
|
||||
|
||||
impl CCRFollowupExecutor {
|
||||
pub fn new(session_id: String, prompt: String) -> Self {
|
||||
Self(ClaudeFollowupExecutor::with_command(
|
||||
session_id,
|
||||
prompt,
|
||||
"claude-code-router".to_string(),
|
||||
"npx -y @musistudio/claude-code-router code -p --dangerously-skip-permissions --verbose --output-format=stream-json".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Executor for CCRFollowupExecutor {
|
||||
async fn spawn(
|
||||
&self,
|
||||
pool: &sqlx::SqlitePool,
|
||||
task_id: Uuid,
|
||||
worktree_path: &str,
|
||||
) -> Result<AsyncGroupChild, ExecutorError> {
|
||||
self.0.spawn(pool, task_id, worktree_path).await
|
||||
}
|
||||
|
||||
fn normalize_logs(
|
||||
&self,
|
||||
logs: &str,
|
||||
worktree_path: &str,
|
||||
) -> Result<NormalizedConversation, String> {
|
||||
let filtered_logs = filter_ccr_service_messages(logs);
|
||||
let mut result = self.0.normalize_logs(&filtered_logs, worktree_path)?;
|
||||
result.executor_type = "claude-code-router".to_string();
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,68 @@ use crate::{
|
||||
};
|
||||
|
||||
/// An executor that uses Claude CLI to process tasks
|
||||
pub struct ClaudeExecutor;
|
||||
pub struct ClaudeExecutor {
|
||||
executor_type: String,
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl Default for ClaudeExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClaudeExecutor {
|
||||
/// Create a new ClaudeExecutor with default settings
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
executor_type: "Claude".to_string(),
|
||||
command: "npx -y @anthropic-ai/claude-code@latest -p --dangerously-skip-permissions --verbose --output-format=stream-json".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ClaudeExecutor with custom settings
|
||||
pub fn with_command(executor_type: String, command: String) -> Self {
|
||||
Self {
|
||||
executor_type,
|
||||
command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An executor that resumes a Claude session
|
||||
pub struct ClaudeFollowupExecutor {
|
||||
pub session_id: String,
|
||||
pub prompt: String,
|
||||
executor_type: String,
|
||||
command_base: String,
|
||||
}
|
||||
|
||||
impl ClaudeFollowupExecutor {
|
||||
/// Create a new ClaudeFollowupExecutor with default settings
|
||||
pub fn new(session_id: String, prompt: String) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
prompt,
|
||||
executor_type: "Claude".to_string(),
|
||||
command_base: "npx -y @anthropic-ai/claude-code@latest -p --dangerously-skip-permissions --verbose --output-format=stream-json".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ClaudeFollowupExecutor with custom settings
|
||||
pub fn with_command(
|
||||
session_id: String,
|
||||
prompt: String,
|
||||
executor_type: String,
|
||||
command_base: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
prompt,
|
||||
executor_type,
|
||||
command_base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -56,7 +112,7 @@ Task title: {}"#,
|
||||
// Use shell command for cross-platform compatibility
|
||||
let (shell_cmd, shell_arg) = get_shell_command();
|
||||
// Pass prompt via stdin instead of command line to avoid shell escaping issues
|
||||
let claude_command = "npx -y @anthropic-ai/claude-code@latest -p --dangerously-skip-permissions --verbose --output-format=stream-json";
|
||||
let claude_command = &self.command;
|
||||
|
||||
let mut command = Command::new(shell_cmd);
|
||||
command
|
||||
@@ -72,9 +128,9 @@ Task title: {}"#,
|
||||
let mut child = command
|
||||
.group_spawn() // Create new process group so we can kill entire tree
|
||||
.map_err(|e| {
|
||||
crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_task(task_id, Some(task.title.clone()))
|
||||
.with_context("Claude CLI execution for new task")
|
||||
.with_context(format!("{} CLI execution for new task", self.executor_type))
|
||||
.spawn_error(e)
|
||||
})?;
|
||||
|
||||
@@ -87,15 +143,20 @@ Task title: {}"#,
|
||||
prompt
|
||||
);
|
||||
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
|
||||
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
.with_task(task_id, Some(task.title.clone()))
|
||||
.with_context("Failed to write prompt to Claude CLI stdin");
|
||||
let context =
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_task(task_id, Some(task.title.clone()))
|
||||
.with_context(format!(
|
||||
"Failed to write prompt to {} CLI stdin",
|
||||
self.executor_type
|
||||
));
|
||||
ExecutorError::spawn_failed(e, context)
|
||||
})?;
|
||||
stdin.shutdown().await.map_err(|e| {
|
||||
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
.with_task(task_id, Some(task.title.clone()))
|
||||
.with_context("Failed to close Claude CLI stdin");
|
||||
let context =
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_task(task_id, Some(task.title.clone()))
|
||||
.with_context(format!("Failed to close {} CLI stdin", self.executor_type));
|
||||
ExecutorError::spawn_failed(e, context)
|
||||
})?;
|
||||
}
|
||||
@@ -508,10 +569,7 @@ impl Executor for ClaudeFollowupExecutor {
|
||||
// Use shell command for cross-platform compatibility
|
||||
let (shell_cmd, shell_arg) = get_shell_command();
|
||||
// Pass prompt via stdin instead of command line to avoid shell escaping issues
|
||||
let claude_command = format!(
|
||||
"npx -y @anthropic-ai/claude-code@latest -p --dangerously-skip-permissions --verbose --output-format=stream-json --resume={}",
|
||||
self.session_id
|
||||
);
|
||||
let claude_command = format!("{} --resume={}", self.command_base, self.session_id);
|
||||
|
||||
let mut command = Command::new(shell_cmd);
|
||||
command
|
||||
@@ -526,10 +584,10 @@ impl Executor for ClaudeFollowupExecutor {
|
||||
let mut child = command
|
||||
.group_spawn() // Create new process group so we can kill entire tree
|
||||
.map_err(|e| {
|
||||
crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_context(format!(
|
||||
"Claude CLI followup execution for session {}",
|
||||
self.session_id
|
||||
"{} CLI followup execution for session {}",
|
||||
self.executor_type, self.session_id
|
||||
))
|
||||
.spawn_error(e)
|
||||
})?;
|
||||
@@ -538,24 +596,27 @@ impl Executor for ClaudeFollowupExecutor {
|
||||
if let Some(mut stdin) = child.inner().stdin.take() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
tracing::debug!(
|
||||
"Writing prompt to Claude stdin for session {}: {:?}",
|
||||
"Writing prompt to {} stdin for session {}: {:?}",
|
||||
self.executor_type,
|
||||
self.session_id,
|
||||
self.prompt
|
||||
);
|
||||
stdin.write_all(self.prompt.as_bytes()).await.map_err(|e| {
|
||||
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
.with_context(format!(
|
||||
"Failed to write prompt to Claude CLI stdin for session {}",
|
||||
self.session_id
|
||||
));
|
||||
let context =
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_context(format!(
|
||||
"Failed to write prompt to {} CLI stdin for session {}",
|
||||
self.executor_type, self.session_id
|
||||
));
|
||||
ExecutorError::spawn_failed(e, context)
|
||||
})?;
|
||||
stdin.shutdown().await.map_err(|e| {
|
||||
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
|
||||
.with_context(format!(
|
||||
"Failed to close Claude CLI stdin for session {}",
|
||||
self.session_id
|
||||
));
|
||||
let context =
|
||||
crate::executor::SpawnContext::from_command(&command, &self.executor_type)
|
||||
.with_context(format!(
|
||||
"Failed to close {} CLI stdin for session {}",
|
||||
self.executor_type, self.session_id
|
||||
));
|
||||
ExecutorError::spawn_failed(e, context)
|
||||
})?;
|
||||
}
|
||||
@@ -569,7 +630,7 @@ impl Executor for ClaudeFollowupExecutor {
|
||||
worktree_path: &str,
|
||||
) -> Result<NormalizedConversation, String> {
|
||||
// Reuse the same logic as the main ClaudeExecutor
|
||||
let main_executor = ClaudeExecutor;
|
||||
let main_executor = ClaudeExecutor::new();
|
||||
main_executor.normalize_logs(logs, worktree_path)
|
||||
}
|
||||
}
|
||||
@@ -580,7 +641,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalize_logs_ignores_result_type() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
let logs = r#"{"type":"system","subtype":"init","cwd":"/private/tmp","session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114","tools":[],"model":"claude-sonnet-4-20250514"}
|
||||
{"type":"assistant","message":{"id":"msg_123","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Hello world"}],"stop_reason":null},"session_id":"e988eeea-3712-46a1-82d4-84fbfaa69114"}
|
||||
{"type":"result","subtype":"success","is_error":false,"duration_ms":6059,"result":"Final result"}
|
||||
@@ -606,7 +667,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_make_path_relative() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test with relative path (should remain unchanged)
|
||||
assert_eq!(
|
||||
@@ -623,7 +684,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_todo_tool_content_extraction() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test TodoWrite with actual todo list
|
||||
let todo_input = serde_json::json!({
|
||||
@@ -666,7 +727,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_todo_tool_empty_list() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test TodoWrite with empty todo list
|
||||
let empty_input = serde_json::json!({
|
||||
@@ -687,7 +748,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_todo_tool_no_todos_field() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test TodoWrite with no todos field
|
||||
let no_todos_input = serde_json::json!({
|
||||
@@ -708,7 +769,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_glob_tool_content_extraction() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test Glob with pattern and path
|
||||
let glob_input = serde_json::json!({
|
||||
@@ -730,7 +791,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_glob_tool_pattern_only() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test Glob with pattern only
|
||||
let glob_input = serde_json::json!({
|
||||
@@ -751,7 +812,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_ls_tool_content_extraction() {
|
||||
let executor = ClaudeExecutor;
|
||||
let executor = ClaudeExecutor::new();
|
||||
|
||||
// Test LS with path
|
||||
let ls_input = serde_json::json!({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod amp;
|
||||
pub mod ccr;
|
||||
pub mod charm_opencode;
|
||||
pub mod claude;
|
||||
pub mod dev_server;
|
||||
@@ -7,6 +8,7 @@ pub mod gemini;
|
||||
pub mod setup_script;
|
||||
|
||||
pub use amp::{AmpExecutor, AmpFollowupExecutor};
|
||||
pub use ccr::{CCRExecutor, CCRFollowupExecutor};
|
||||
pub use charm_opencode::{CharmOpencodeExecutor, CharmOpencodeFollowupExecutor};
|
||||
pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
|
||||
pub use dev_server::DevServerExecutor;
|
||||
|
||||
@@ -636,6 +636,7 @@ impl ProcessService {
|
||||
fn resolve_executor_config(executor_name: &Option<String>) -> crate::executor::ExecutorConfig {
|
||||
match executor_name.as_ref().map(|s| s.as_str()) {
|
||||
Some("claude") => crate::executor::ExecutorConfig::Claude,
|
||||
Some("claude-code-router") => crate::executor::ExecutorConfig::ClaudeCodeRouter,
|
||||
Some("amp") => crate::executor::ExecutorConfig::Amp,
|
||||
Some("gemini") => crate::executor::ExecutorConfig::Gemini,
|
||||
Some("charmopencode") => crate::executor::ExecutorConfig::CharmOpencode,
|
||||
@@ -779,17 +780,14 @@ impl ProcessService {
|
||||
prompt,
|
||||
} => {
|
||||
use crate::executors::{
|
||||
AmpFollowupExecutor, CharmOpencodeFollowupExecutor, ClaudeFollowupExecutor,
|
||||
GeminiFollowupExecutor,
|
||||
AmpFollowupExecutor, CCRFollowupExecutor, CharmOpencodeFollowupExecutor,
|
||||
ClaudeFollowupExecutor, GeminiFollowupExecutor,
|
||||
};
|
||||
|
||||
let executor: Box<dyn crate::executor::Executor> = match config {
|
||||
crate::executor::ExecutorConfig::Claude => {
|
||||
if let Some(sid) = session_id {
|
||||
Box::new(ClaudeFollowupExecutor {
|
||||
session_id: sid.clone(),
|
||||
prompt: prompt.clone(),
|
||||
})
|
||||
Box::new(ClaudeFollowupExecutor::new(sid.clone(), prompt.clone()))
|
||||
} else {
|
||||
return Err(TaskAttemptError::TaskNotFound); // No session ID for followup
|
||||
}
|
||||
@@ -825,6 +823,13 @@ impl ProcessService {
|
||||
return Err(TaskAttemptError::TaskNotFound); // No session ID for followup
|
||||
}
|
||||
}
|
||||
crate::executor::ExecutorConfig::ClaudeCodeRouter => {
|
||||
if let Some(sid) = session_id {
|
||||
Box::new(CCRFollowupExecutor::new(sid.clone(), prompt.clone()))
|
||||
} else {
|
||||
return Err(TaskAttemptError::TaskNotFound); // No session ID for followup
|
||||
}
|
||||
}
|
||||
crate::executor::ExecutorConfig::SetupScript { .. } => {
|
||||
// Setup scripts don't support followup, use regular setup script
|
||||
config.create_executor()
|
||||
|
||||
Reference in New Issue
Block a user