- claude-bridge.js: Node.js Bridge für Claude Code CLI - claude.rs: Child-Process Management, Event-Verarbeitung - events.ts: Frontend Event-Listener für Tauri-Events - Layout/ChatPanel: Echte Tauri-Commands statt Placeholder Events implementiert: - agent-started/stopped - tool-start/tool-end - claude-text (Streaming) - claude-result (Kosten/Token) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
193 lines
5.3 KiB
JavaScript
193 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Claude Desktop — Bridge zu Claude Code SDK
|
|
// Kommuniziert mit Rust-Backend über stdin/stdout (JSON)
|
|
|
|
const { spawn } = require('child_process');
|
|
const readline = require('readline');
|
|
|
|
// State
|
|
let claudeProcess = null;
|
|
let abortController = null;
|
|
|
|
// Event an Rust senden
|
|
function emit(event, payload) {
|
|
const msg = JSON.stringify({ type: 'event', event, payload });
|
|
process.stdout.write(msg + '\n');
|
|
}
|
|
|
|
// Antwort an Rust senden
|
|
function respond(id, result, error = null) {
|
|
const msg = JSON.stringify({ type: 'response', id, result, error });
|
|
process.stdout.write(msg + '\n');
|
|
}
|
|
|
|
// Claude Code als Subprocess starten
|
|
async function startClaude(message, requestId) {
|
|
abortController = new AbortController();
|
|
|
|
emit('agent-started', {
|
|
id: 'main',
|
|
type: 'Main Agent',
|
|
task: message.substring(0, 100)
|
|
});
|
|
|
|
try {
|
|
// Claude Code CLI aufrufen
|
|
claudeProcess = spawn('claude', [
|
|
'--output-format', 'stream-json',
|
|
'-p', message
|
|
], {
|
|
signal: abortController.signal,
|
|
env: { ...process.env, FORCE_COLOR: '0' }
|
|
});
|
|
|
|
let fullResponse = '';
|
|
let buffer = '';
|
|
|
|
claudeProcess.stdout.on('data', (data) => {
|
|
buffer += data.toString();
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop(); // Unvollständige Zeile behalten
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const event = JSON.parse(line);
|
|
handleClaudeEvent(event);
|
|
if (event.type === 'assistant' && event.message?.content) {
|
|
for (const block of event.message.content) {
|
|
if (block.type === 'text') {
|
|
fullResponse += block.text;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Kein JSON, ignorieren
|
|
}
|
|
}
|
|
});
|
|
|
|
claudeProcess.stderr.on('data', (data) => {
|
|
emit('log', { level: 'error', message: data.toString() });
|
|
});
|
|
|
|
claudeProcess.on('close', (code) => {
|
|
emit('agent-stopped', { id: 'main', code });
|
|
respond(requestId, fullResponse || 'Keine Antwort erhalten');
|
|
claudeProcess = null;
|
|
abortController = null;
|
|
});
|
|
|
|
claudeProcess.on('error', (err) => {
|
|
if (err.name === 'AbortError') {
|
|
respond(requestId, null, 'Abgebrochen durch Benutzer');
|
|
} else {
|
|
respond(requestId, null, err.message);
|
|
}
|
|
});
|
|
|
|
} catch (err) {
|
|
respond(requestId, null, err.message);
|
|
}
|
|
}
|
|
|
|
// Claude SDK Events verarbeiten
|
|
function handleClaudeEvent(event) {
|
|
switch (event.type) {
|
|
case 'tool_use':
|
|
emit('tool-start', {
|
|
id: event.tool_use_id,
|
|
tool: event.name,
|
|
input: event.input
|
|
});
|
|
break;
|
|
|
|
case 'tool_result':
|
|
emit('tool-end', {
|
|
id: event.tool_use_id,
|
|
success: !event.is_error,
|
|
output: typeof event.content === 'string'
|
|
? event.content.substring(0, 500)
|
|
: JSON.stringify(event.content).substring(0, 500)
|
|
});
|
|
break;
|
|
|
|
case 'subagent_start':
|
|
emit('subagent-start', {
|
|
id: event.subagent_id,
|
|
type: event.subagent_type,
|
|
task: event.prompt?.substring(0, 100)
|
|
});
|
|
break;
|
|
|
|
case 'subagent_stop':
|
|
emit('subagent-stop', {
|
|
id: event.subagent_id
|
|
});
|
|
break;
|
|
|
|
case 'assistant':
|
|
if (event.message?.content) {
|
|
for (const block of event.message.content) {
|
|
if (block.type === 'text') {
|
|
emit('text', { text: block.text });
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'result':
|
|
emit('result', {
|
|
cost: event.cost_usd,
|
|
tokens: {
|
|
input: event.input_tokens,
|
|
output: event.output_tokens
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Alle Prozesse stoppen
|
|
function stopAll() {
|
|
if (abortController) {
|
|
abortController.abort();
|
|
}
|
|
if (claudeProcess) {
|
|
claudeProcess.kill('SIGTERM');
|
|
}
|
|
emit('all-stopped', {});
|
|
}
|
|
|
|
// Stdin lesen
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
terminal: false
|
|
});
|
|
|
|
rl.on('line', (line) => {
|
|
try {
|
|
const msg = JSON.parse(line);
|
|
|
|
switch (msg.command) {
|
|
case 'message':
|
|
startClaude(msg.message, msg.id);
|
|
break;
|
|
case 'stop':
|
|
stopAll();
|
|
respond(msg.id, 'stopped');
|
|
break;
|
|
case 'ping':
|
|
respond(msg.id, 'pong');
|
|
break;
|
|
default:
|
|
respond(msg.id, null, `Unbekannter Befehl: ${msg.command}`);
|
|
}
|
|
} catch (e) {
|
|
emit('error', { message: e.message });
|
|
}
|
|
});
|
|
|
|
// Startup
|
|
emit('ready', { version: '0.1.0' });
|