claude-desktop/src-tauri/scripts/claude-bridge.js
Eddy 5003fb9996 Phase 2: Claude SDK Integration + Event-System
- 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>
2026-04-13 14:43:34 +02:00

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' });