Signed-off-by: 未知时光 <732857315@qq.com>
This commit is contained in:
19
plugins/claudecode/Cargo.toml
Normal file
19
plugins/claudecode/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "agentsd-plugin-claudecode"
|
||||
version = "2.1.169"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "agentsd-plugin-claudecode"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
agentsd-plugin-sdk = { path = "../plugin-sdk" }
|
||||
agentsd-proto = { path = "../../core/proto" }
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
254
plugins/claudecode/src/main.rs
Normal file
254
plugins/claudecode/src/main.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use agentsd_plugin_sdk::{decode_json, err_json, export, ok_json, plugin_info_with_endpoint};
|
||||
use agentsd_proto::agentsd::{
|
||||
plugin_bridge_server::{PluginBridge, PluginBridgeServer},
|
||||
CallRequest, CallResponse, HeartbeatRequest, HeartbeatResponse, PluginInfo, RegisterResponse,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tonic::{transport::Server, Request, Response, Status};
|
||||
|
||||
const PLUGIN_ID: &str = "claudecode";
|
||||
const DEFAULT_PORT: u16 = 50053;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallArgs {
|
||||
#[serde(default)]
|
||||
prompt: String,
|
||||
#[serde(default)]
|
||||
text: String,
|
||||
#[serde(default)]
|
||||
cwd: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CallResult {
|
||||
result: String,
|
||||
}
|
||||
|
||||
fn find_claude_bin() -> Option<String> {
|
||||
if let Ok(bin) = std::env::var("CLAUDE_BIN") {
|
||||
return Some(bin);
|
||||
}
|
||||
|
||||
let candidates = vec![
|
||||
"claude",
|
||||
"claude.cmd",
|
||||
&std::env::var("APPDATA")
|
||||
.map(|p| format!("{}\\npm\\claude.cmd", p))
|
||||
.unwrap_or_default(),
|
||||
];
|
||||
|
||||
for candidate in candidates {
|
||||
if std::process::Command::new(candidate)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok()
|
||||
{
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn call_claude(
|
||||
claude_bin: &str,
|
||||
prompt: &str,
|
||||
cwd: Option<&str>,
|
||||
max_turns: Option<u32>,
|
||||
allowed_tools: &[&str],
|
||||
) -> Result<String> {
|
||||
let mut cmd = tokio::process::Command::new(claude_bin);
|
||||
cmd.arg("-p").arg("--output-format").arg("text");
|
||||
|
||||
if let Some(turns) = max_turns {
|
||||
cmd.arg("--max-turns").arg(turns.to_string());
|
||||
}
|
||||
|
||||
if !allowed_tools.is_empty() {
|
||||
cmd.arg("--allowedTools");
|
||||
for tool in allowed_tools {
|
||||
cmd.arg(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dir) = cwd {
|
||||
if !dir.is_empty() {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.env("CLAUDE_CODE_SIMPLE", "1");
|
||||
cmd.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let mut child = cmd.spawn().context("spawn claude process")?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(prompt.as_bytes())
|
||||
.await
|
||||
.context("write prompt to stdin")?;
|
||||
stdin.flush().await.context("flush stdin")?;
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
let output = tokio::time::timeout(std::time::Duration::from_secs(300), child.wait_with_output())
|
||||
.await
|
||||
.context("claude timeout (300s)")??;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("claude failed: {}", stderr);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
struct ClaudeCodePlugin {
|
||||
claude_bin: String,
|
||||
}
|
||||
|
||||
impl ClaudeCodePlugin {
|
||||
fn new(claude_bin: String) -> Self {
|
||||
Self { claude_bin }
|
||||
}
|
||||
|
||||
async fn handle_call(&self, fn_name: &str, args: CallArgs) -> CallResponse {
|
||||
let prompt = if !args.prompt.is_empty() {
|
||||
args.prompt
|
||||
} else {
|
||||
args.text
|
||||
};
|
||||
|
||||
if prompt.is_empty() {
|
||||
return err_json("missing prompt or text");
|
||||
}
|
||||
|
||||
let cwd = if args.cwd.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(args.cwd.as_str())
|
||||
};
|
||||
|
||||
let result = match fn_name {
|
||||
"chat" => call_claude(&self.claude_bin, &prompt, cwd, Some(1), &[]).await,
|
||||
"code" => {
|
||||
call_claude(
|
||||
&self.claude_bin,
|
||||
&prompt,
|
||||
cwd,
|
||||
None,
|
||||
&["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
|
||||
)
|
||||
.await
|
||||
}
|
||||
"review" => {
|
||||
let review_prompt =
|
||||
format!("Review the following code or changes. Be concise and actionable:\n\n{}", prompt);
|
||||
call_claude(
|
||||
&self.claude_bin,
|
||||
&review_prompt,
|
||||
cwd,
|
||||
Some(1),
|
||||
&["Read", "Glob", "Grep"],
|
||||
)
|
||||
.await
|
||||
}
|
||||
"ask" => {
|
||||
call_claude(
|
||||
&self.claude_bin,
|
||||
&prompt,
|
||||
cwd,
|
||||
Some(3),
|
||||
&["Read", "Glob", "Grep"],
|
||||
)
|
||||
.await
|
||||
}
|
||||
"run" => call_claude(&self.claude_bin, &prompt, cwd, None, &[]).await,
|
||||
_ => {
|
||||
return err_json(format!("unknown function: {}", fn_name));
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(output) => ok_json(&CallResult { result: output }),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl PluginBridge for ClaudeCodePlugin {
|
||||
async fn register(&self, _req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> {
|
||||
Err(Status::unimplemented("not a bridge"))
|
||||
}
|
||||
|
||||
async fn call(&self, req: Request<CallRequest>) -> Result<Response<CallResponse>, Status> {
|
||||
let r = req.into_inner();
|
||||
let args: CallArgs = decode_json(&r.args).unwrap_or_default();
|
||||
let response = self.handle_call(&r.r#fn, args).await;
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamCallStream = futures_core::stream::Empty<Result<CallResponse, Status>>;
|
||||
|
||||
async fn stream_call(&self, _req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> {
|
||||
Err(Status::unimplemented("stream_call not supported"))
|
||||
}
|
||||
|
||||
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> {
|
||||
Ok(Response::new(HeartbeatResponse { ok: true }))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let claude_bin = find_claude_bin().context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
|
||||
tracing::info!("Found Claude Code CLI: {}", claude_bin);
|
||||
|
||||
let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT);
|
||||
let listen_addr = agentsd_plugin_sdk::listen_addr_from_env(DEFAULT_PORT);
|
||||
|
||||
let channel = agentsd_plugin_sdk::connect().await?;
|
||||
let register_resp = agentsd_plugin_sdk::register(
|
||||
channel,
|
||||
plugin_info_with_endpoint(
|
||||
PLUGIN_ID,
|
||||
"Claude Code",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"standard",
|
||||
&["process.spawn", "display.text", "fs.read", "fs.write", "network.request"],
|
||||
&["hermes"],
|
||||
vec![
|
||||
export("chat", "Send prompt to Claude Code, get text response (no tools)"),
|
||||
export("code", "Generate or edit code with Claude Code (Bash/Read/Edit/Write)"),
|
||||
export("review", "Review code changes (Read/Glob/Grep only)"),
|
||||
export("ask", "Ask about the codebase (Read/Glob/Grep, max 3 turns)"),
|
||||
export("run", "Full Claude Code access (all tools, unlimited turns)"),
|
||||
],
|
||||
&endpoint,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Registered with agentsd: session={}", register_resp.session_id);
|
||||
|
||||
let plugin = ClaudeCodePlugin::new(claude_bin);
|
||||
let addr = listen_addr.parse()?;
|
||||
|
||||
tracing::info!("Claude Code plugin server listening on {}", addr);
|
||||
|
||||
Server::builder()
|
||||
.add_service(PluginBridgeServer::new(plugin))
|
||||
.serve(addr)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user