#!/usr/bin/env node // Claude Desktop — Bridge zwischen Tauri-Backend und Claude CLI // // Kommunikation: stdin/stdout als NDJSON (eine JSON-Zeile pro Nachricht) // // Eingehend (von Tauri): // { "command": "message", "id": "req-1", "message": "Fixe den Bug..." } // { "command": "stop", "id": "req-2" } // // Ausgehend (an Tauri): // { "type": "event", "event": "ready" } // { "type": "event", "event": "agent-started", "payload": { "id": "...", "type": "Main" } } // { "type": "event", "event": "text", "payload": { "text": "..." } } // { "type": "event", "event": "tool-start", "payload": { "id": "...", "tool": "Read", "input": {...} } } // { "type": "event", "event": "tool-end", "payload": { "id": "...", "success": true } } // { "type": "event", "event": "result", "payload": { "cost": 0.01, "tokens": {...} } } // { "type": "event", "event": "agent-stopped", "payload": { "id": "..." } } import { spawn } from 'node:child_process'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; // Aktive Claude-Prozesse const activeProcesses = new Map(); // Session-ID für --resume let currentSessionId = null; // ============ Kommunikation mit Tauri ============ function sendToTauri(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); } function sendEvent(event, payload = {}) { sendToTauri({ type: 'event', event, payload }); } function sendResponse(id, result) { sendToTauri({ type: 'response', id, result }); } function sendError(id, error) { sendToTauri({ type: 'response', id, error }); } // ============ Claude CLI aufrufen ============ function spawnClaude(message, requestId) { const agentId = randomUUID(); const args = [ '-p', // Print-Modus (nicht interaktiv) '--output-format', 'stream-json', // Streaming JSON Events '--allowedTools', 'Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write', 'WebFetch', 'WebSearch', 'Agent', ]; // Bei Fortsetzung letzte Session verwenden if (currentSessionId) { args.push('--resume', currentSessionId); } // Nachricht als Argument args.push(message); // Claude CLI Pfad — npx oder global installiert const claudePath = process.env.CLAUDE_PATH || 'claude'; const proc = spawn(claudePath, args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, // Keine interaktive UI CLAUDE_CODE_HEADLESS: '1', }, }); activeProcesses.set(agentId, { proc, requestId }); sendEvent('agent-started', { id: agentId, type: 'Main', task: message.substring(0, 100), }); // NDJSON-Zeilen von stdout parsen let fullText = ''; let toolCounter = 0; const rl = createInterface({ input: proc.stdout }); rl.on('line', (line) => { if (!line.trim()) return; try { const event = JSON.parse(line); handleClaudeEvent(event, agentId, requestId, { fullText: () => fullText, addText: (t) => { fullText += t; } }); } catch { // Nicht-JSON Zeile — als Text weiterleiten fullText += line; sendEvent('text', { text: line }); } }); // stderr für Debug-Infos const stderrRl = createInterface({ input: proc.stderr }); stderrRl.on('line', (line) => { if (process.env.CLAUDE_DEBUG) { process.stderr.write(`[claude-stderr] ${line}\n`); } }); proc.on('close', (code) => { activeProcesses.delete(agentId); sendEvent('agent-stopped', { id: agentId, code, }); if (activeProcesses.size === 0) { sendEvent('all-stopped'); } }); proc.on('error', (err) => { sendError(requestId, `Claude konnte nicht gestartet werden: ${err.message}`); activeProcesses.delete(agentId); }); return agentId; } // ============ Claude Stream-JSON Events verarbeiten ============ function handleClaudeEvent(event, agentId, requestId, textState) { // Das stream-json Format hat verschiedene Event-Typen: // { "type": "assistant", "message": { "content": [...], ... } } // { "type": "content_block_start", "content_block": { "type": "text", "text": "..." } } // { "type": "content_block_delta", "delta": { "type": "text_delta", "text": "..." } } // { "type": "content_block_start", "content_block": { "type": "tool_use", "name": "Read", ... } } // { "type": "content_block_delta", "delta": { "type": "input_json_delta", ... } } // { "type": "result", "result": "...", "cost_usd": 0.01, ... } // { "type": "system", "subtype": "session_id", "session_id": "..." } const type = event.type; switch (type) { case 'system': if (event.subtype === 'session_id' && event.session_id) { currentSessionId = event.session_id; } break; case 'assistant': // Vollständige Nachricht — Inhalt extrahieren if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text') { sendEvent('text', { text: block.text }); } } } break; case 'content_block_start': if (event.content_block?.type === 'text') { // Text-Block startet if (event.content_block.text) { sendEvent('text', { text: event.content_block.text }); } } else if (event.content_block?.type === 'tool_use') { // Tool-Aufruf startet sendEvent('tool-start', { id: event.content_block.id || randomUUID(), tool: event.content_block.name, input: event.content_block.input || {}, }); } break; case 'content_block_delta': if (event.delta?.type === 'text_delta' && event.delta.text) { sendEvent('text', { text: event.delta.text }); } break; case 'content_block_stop': // Block fertig — wenn es ein Tool war, Tool-Ende senden break; case 'tool_result': case 'tool_use_result': sendEvent('tool-end', { id: event.tool_use_id || event.id || '', success: !event.is_error, output: typeof event.content === 'string' ? event.content : JSON.stringify(event.content)?.substring(0, 500), }); break; case 'result': // Endergebnis mit Kosten sendEvent('result', { text: event.result || '', cost: event.cost_usd || event.cost || 0, tokens: { input: event.input_tokens || 0, output: event.output_tokens || 0, }, session_id: event.session_id || currentSessionId, duration_ms: event.duration_ms || 0, }); // Session-ID für Fortsetzung merken if (event.session_id) { currentSessionId = event.session_id; } break; default: // Unbekannte Events durchreichen if (event.subagent_id || event.agent_id) { // Sub-Agent Events sendEvent('subagent-event', { agentId: event.subagent_id || event.agent_id, type, data: event, }); } break; } } // ============ Alle Prozesse stoppen ============ function stopAll() { for (const [agentId, { proc }] of activeProcesses) { try { proc.kill('SIGTERM'); // Nach 2 Sekunden hart killen setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 2000); } catch {} } activeProcesses.clear(); sendEvent('all-stopped'); } // ============ Eingehende Befehle verarbeiten ============ function handleCommand(msg) { switch (msg.command) { case 'message': if (!msg.message) { sendError(msg.id, 'Keine Nachricht angegeben'); return; } const agentId = spawnClaude(msg.message, msg.id); sendResponse(msg.id, { agentId, status: 'gestartet' }); break; case 'stop': stopAll(); sendResponse(msg.id, { status: 'gestoppt' }); break; case 'status': const agents = []; for (const [id, { proc }] of activeProcesses) { agents.push({ id, running: !proc.killed, pid: proc.pid, }); } sendResponse(msg.id, { agents, sessionId: currentSessionId }); break; case 'ping': sendResponse(msg.id, { pong: true }); break; default: sendError(msg.id, `Unbekannter Befehl: ${msg.command}`); } } // ============ Main ============ // stdin zeilenweise lesen const rl = createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; try { const msg = JSON.parse(line); handleCommand(msg); } catch (err) { process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); } }); // Sauber beenden process.on('SIGTERM', () => { stopAll(); process.exit(0); }); process.on('SIGINT', () => { stopAll(); process.exit(0); }); // Bereit-Signal senden sendEvent('ready', { version: '0.1.0', pid: process.pid });