#!/usr/bin/env node // Claude Desktop — Bridge via Claude Agent SDK // // Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion) // OAuth-Auth funktioniert automatisch (Claude Max Abo) // Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe import { query } from '@anthropic-ai/claude-agent-sdk'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; // Prozess am Leben halten const keepAlive = setInterval(() => {}, 60000); process.stdin.resume(); // ============ State ============ let activeAbort = null; let currentAgentId = null; let currentModel = process.env.CLAUDE_MODEL || 'opus'; // Agent-Modus (solo | handlanger | experten | auto) let agentMode = 'solo'; // Sticky Context (Schicht 1) — wird bei JEDEM API-Call injiziert let stickyContext = ''; // ============ Orchestrator Prompts ============ const ORCHESTRATOR_PROMPTS = { handlanger: ` Du bist der HAUPT-AGENT und arbeitest im HANDLANGER-MODUS. WICHTIG: Du denkst und planst, aber Sub-Agents führen aus! Wenn du eine Aufgabe bekommst: 1. ANALYSIERE was nötig ist 2. DELEGIERE an passende Sub-Agents mit EXAKTEN Anweisungen 3. Sub-Agents führen GENAU aus, was du sagst — sie denken NICHT selbst 4. Du erhältst ZUSAMMENFASSUNGEN zurück (keine Rohdaten) 5. Du entscheidest den nächsten Schritt Beispiel-Delegationen: - "Lies Datei X, gib mir Zeilen 10-50 zurück" - "Suche nach 'handleError' in src/, liste die Dateien" - "Führe 'npm test' aus, berichte nur ob passed/failed" Halte deinen Context klein — lass Sub-Agents die Details bearbeiten! `, experten: ` Du bist der HAUPT-AGENT und arbeitest im EXPERTEN-MODUS. WICHTIG: Du koordinierst autonome Experten-Agents! Deine Experten: - **Research**: Durchsucht Code, findet Informationen, PLANT SELBST wie er sucht - **Implement**: Schreibt Code, ENTSCHEIDET SELBST wie er es macht (Best Practices) - **Test**: Schreibt und führt Tests aus, WÄHLT SELBST passende Testfälle - **Review**: Prüft Code, FINDET SELBST Probleme Wenn du eine Aufgabe bekommst: 1. TEILE sie in Experten-Bereiche auf 2. DELEGIERE an den passenden Experten mit dem WAS, nicht dem WIE 3. Der Experte arbeitet AUTONOM und liefert eine Zusammenfassung 4. Du INTEGRIERST die Ergebnisse Beispiel-Delegationen: - Research: "Finde heraus wie Authentication in diesem Projekt implementiert ist" - Implement: "Füge OAuth2-Support hinzu" (ohne exakte Code-Vorgabe) - Test: "Teste die neue Auth-Funktionalität" - Review: "Prüfe die OAuth-Implementierung auf Sicherheitsprobleme" `, auto: ` Du analysierst Aufgaben und wählst den optimalen Arbeitsmodus. Entscheide basierend auf: - SOLO: Einfache, schnelle Aufgaben (Typo fix, Code erklären, einzelne Datei ändern) - HANDLANGER: Koordinations-intensive Aufgaben (viele Dateien lesen, Bug in großer Codebase) - EXPERTEN: Komplexe Features (neues System implementieren, großes Refactoring) Teile deine Wahl am Anfang mit: "[Modus: X] Begründung" `, }; // Subagent-Tracking // Map: toolUseId → { agentId, parentId, type, task, depth } const activeSubagents = new Map(); // Verfügbare Modelle const AVAILABLE_MODELS = [ { id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' }, { id: 'sonnet', name: 'Claude Sonnet', description: 'Ausgewogen' }, { id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' }, ]; // Tools die Subagents spawnen const SUBAGENT_TOOLS = ['Task', 'Agent', 'spawn_agent', 'launch_agent']; // Subagent-Typ aus Tool-Input ermitteln function getSubagentType(toolName, input) { if (input?.subagent_type) return input.subagent_type.toLowerCase(); if (input?.agent_type) return input.agent_type.toLowerCase(); // Fallback basierend auf description/prompt const desc = (input?.description || input?.prompt || '').toLowerCase(); if (desc.includes('explore') || desc.includes('search') || desc.includes('find')) return 'explore'; if (desc.includes('plan') || desc.includes('design')) return 'plan'; if (desc.includes('bash') || desc.includes('command') || desc.includes('terminal')) return 'bash'; if (desc.includes('code') || desc.includes('implement') || desc.includes('write')) return 'code'; if (desc.includes('test') || desc.includes('verify')) return 'test'; if (desc.includes('review') || desc.includes('check')) return 'review'; return 'explore'; // Default } // ============ 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 }); } // ============ Monitor-Events ============ // Sendet ein Event für den System-Monitor function sendMonitorEvent(type, summary, details = {}, options = {}) { sendEvent('monitor', { type, // 'api' | 'hook' | 'tool' | 'mcp' | 'agent' | 'error' | 'debug' summary, // Einzeiler für Kompakt-Ansicht details, // Vollständige Daten agentId: options.agentId || currentAgentId, durationMs: options.durationMs, error: options.error, }); } // Tool-Input für Logging kürzen (sensitive Daten maskieren) function summarizeToolInput(tool, input) { if (!input) return ''; // Bestimmte Tools speziell behandeln if (tool === 'Read') { return input.file_path || ''; } if (tool === 'Edit' || tool === 'Write') { const path = input.file_path || ''; const size = input.content ? `(${input.content.length} chars)` : ''; return `${path} ${size}`; } if (tool === 'Grep') { return `"${input.pattern}" in ${input.path || '.'}`; } if (tool === 'Bash') { const cmd = input.command || ''; return cmd.length > 50 ? cmd.substring(0, 50) + '...' : cmd; } if (tool === 'Task') { return input.description || input.prompt || ''; } // Default: Erstes String-Feld nehmen const firstString = Object.values(input).find(v => typeof v === 'string'); if (firstString) { return firstString.length > 50 ? firstString.substring(0, 50) + '...' : firstString; } return ''; } // ============ Claude Agent SDK ============ async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) { // Modell für diese Anfrage (Parameter > State > Default) const useModel = model || currentModel; // Context für diese Anfrage (Parameter > State) const useContext = contextOverride || stickyContext; currentAgentId = randomUUID(); activeAbort = new AbortController(); const isResuming = !!resumeSessionId; sendEvent('agent-started', { id: currentAgentId, type: 'Main', task: message.substring(0, 100), model: useModel, resuming: isResuming, }); // Monitor: Agent gestartet const resumeInfo = isResuming ? ' (Fortsetzung)' : ''; sendMonitorEvent('agent', `Main Agent gestartet (${useModel})${resumeInfo}`, { agentId: currentAgentId, model: useModel, task: message.substring(0, 100), contextTokens: useContext ? Math.ceil(useContext.length / 4) : 0, resumeSessionId: resumeSessionId || null, }); // Monitor: API-Request const contextInfo = useContext ? ` +${Math.ceil(useContext.length / 4)} ctx` : ''; sendMonitorEvent('api', `→ ${useModel}${contextInfo}${resumeInfo}`, { model: useModel, promptLength: message.length, contextLength: useContext?.length || 0, maxTurns: 25, resumeSessionId: resumeSessionId || null, }); sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel, resuming: isResuming, mode: agentMode }); // Orchestrator-Prompt für nicht-Solo Modi let orchestratorPrompt = ''; if (agentMode !== 'solo' && ORCHESTRATOR_PROMPTS[agentMode]) { orchestratorPrompt = ORCHESTRATOR_PROMPTS[agentMode]; sendMonitorEvent('agent', `Orchestrator-Modus: ${agentMode}`, { mode: agentMode }); } // Nachricht mit Context und Orchestrator kombinieren let fullPrompt = message; if (orchestratorPrompt) { fullPrompt = `${orchestratorPrompt}\n\n---\n\n${message}`; } if (useContext) { fullPrompt = `${useContext}\n\n---\n\n${fullPrompt}`; } const startTime = Date.now(); let fullText = ''; let usedModel = useModel; try { // Query-Optionen zusammenstellen const queryOptions = { model: useModel, maxTurns: 25, abortController: activeAbort, }; // Session-ID für Fortsetzung hinzufügen wenn vorhanden if (resumeSessionId) { queryOptions.sessionId = resumeSessionId; } const conversation = query({ prompt: fullPrompt, options: queryOptions, }); for await (const event of conversation) { switch (event.type) { case 'assistant': // Text aus der Nachricht extrahieren if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text' && block.text) { fullText += block.text; sendEvent('text', { text: block.text }); } } } if (event.message?.model) { usedModel = event.message.model; } break; case 'tool_use': { const toolId = event.tool_use_id || randomUUID(); const toolName = event.name || 'unknown'; const toolInput = event.input || {}; // Prüfen ob dieses Tool einen Subagent startet if (SUBAGENT_TOOLS.includes(toolName)) { const subagentId = randomUUID(); const subagentType = getSubagentType(toolName, toolInput); const subagentTask = toolInput.description || toolInput.prompt || toolInput.task || 'Subagent-Aufgabe'; const subagentModel = toolInput.model || useModel; // Tiefe berechnen (Main = 0, erster Sub = 1, etc.) // Für jetzt: immer depth 1 (direkter Subagent vom Main) const depth = 1; activeSubagents.set(toolId, { agentId: subagentId, parentId: currentAgentId, type: subagentType, task: subagentTask, depth, model: subagentModel, }); sendEvent('subagent-started', { id: subagentId, parentAgentId: currentAgentId, type: subagentType, task: subagentTask.substring(0, 100), depth, model: subagentModel, toolUseId: toolId, }); } sendEvent('tool-start', { id: toolId, tool: toolName, input: toolInput, agentId: currentAgentId, }); // Monitor: Tool gestartet const toolSummary = summarizeToolInput(toolName, toolInput); sendMonitorEvent('tool', `${toolName} ${toolSummary}`, { toolId, tool: toolName, input: toolInput, }); break; } case 'tool_result': { const toolId = event.tool_use_id || ''; // Prüfen ob dieser Tool-Call ein Subagent war if (activeSubagents.has(toolId)) { const subagent = activeSubagents.get(toolId); sendEvent('subagent-stopped', { id: subagent.agentId, parentAgentId: subagent.parentId, success: !event.is_error, toolUseId: toolId, }); activeSubagents.delete(toolId); } sendEvent('tool-end', { id: toolId, success: !event.is_error, agentId: currentAgentId, }); break; } case 'result': { // Endergebnis const durationMs = Date.now() - startTime; const inputTokens = event.usage?.input_tokens || 0; const outputTokens = event.usage?.output_tokens || 0; const cost = event.total_cost_usd || 0; sendEvent('result', { text: fullText, cost, tokens: { input: inputTokens, output: outputTokens }, session_id: event.session_id || '', duration_ms: durationMs, model: usedModel, }); // Monitor: API-Response const tokenK = ((inputTokens + outputTokens) / 1000).toFixed(1); sendMonitorEvent('api', `← ${usedModel} [${durationMs}ms] ${tokenK}k tok $${cost.toFixed(4)}`, { model: usedModel, inputTokens, outputTokens, cost, sessionId: event.session_id, }, { durationMs }); break; } default: // Andere Events still ignorieren break; } } } catch (err) { if (err.name === 'AbortError') { // Abgebrochen — kein Fehler sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' }); } else { sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); // Monitor: Fehler sendMonitorEvent('error', `${err.message || err}`, { name: err.name, message: err.message, stack: err.stack, }, { error: err.message || String(err) }); } } finally { // Alle noch aktiven Subagents stoppen for (const [toolId, subagent] of activeSubagents) { sendEvent('subagent-stopped', { id: subagent.agentId, parentAgentId: subagent.parentId, success: false, // Vorzeitig beendet toolUseId: toolId, }); } activeSubagents.clear(); sendEvent('agent-stopped', { id: currentAgentId, code: 0 }); sendEvent('all-stopped'); currentAgentId = null; activeAbort = null; } } // ============ Befehle von Tauri ============ function handleCommand(msg) { switch (msg.command) { case 'message': if (!msg.message) { sendError(msg.id, 'Keine Nachricht angegeben'); return; } // Modell, Context und Resume-Session-ID können pro Anfrage überschrieben werden sendMessage(msg.message, msg.id, msg.model, msg.context, msg.resumeSessionId); break; case 'set-context': // Sticky Context setzen (wird bei allen folgenden Nachrichten verwendet) stickyContext = msg.context || ''; const ctxTokens = stickyContext ? Math.ceil(stickyContext.length / 4) : 0; sendResponse(msg.id, { status: 'Context gesetzt', tokens: ctxTokens }); sendMonitorEvent('hook', `Sticky Context gesetzt (~${ctxTokens} Token)`, { contextLength: stickyContext.length, estimatedTokens: ctxTokens, }); break; case 'get-context': sendResponse(msg.id, { context: stickyContext, tokens: stickyContext ? Math.ceil(stickyContext.length / 4) : 0, }); break; case 'clear-context': stickyContext = ''; sendResponse(msg.id, { status: 'Context gelöscht' }); sendMonitorEvent('hook', 'Sticky Context gelöscht', {}); break; case 'stop': if (activeAbort) { activeAbort.abort(); } sendResponse(msg.id, { status: 'gestoppt' }); break; case 'set-model': if (!msg.model) { sendError(msg.id, 'Kein Modell angegeben'); return; } const validModels = AVAILABLE_MODELS.map(m => m.id); if (!validModels.includes(msg.model)) { sendError(msg.id, `Ungültiges Modell: ${msg.model}. Verfügbar: ${validModels.join(', ')}`); return; } currentModel = msg.model; sendResponse(msg.id, { model: currentModel, status: 'Modell geändert' }); sendEvent('model-changed', { model: currentModel }); break; case 'get-models': sendResponse(msg.id, { current: currentModel, available: AVAILABLE_MODELS, }); break; case 'set-mode': // Agent-Modus setzen (solo, handlanger, experten, auto) const validModes = ['solo', 'handlanger', 'experten', 'auto']; if (!msg.mode || !validModes.includes(msg.mode)) { sendError(msg.id, `Ungültiger Modus: ${msg.mode}. Verfügbar: ${validModes.join(', ')}`); return; } agentMode = msg.mode; sendResponse(msg.id, { mode: agentMode, status: 'Modus geändert' }); sendEvent('mode-changed', { mode: agentMode }); sendMonitorEvent('agent', `Agent-Modus geändert: ${agentMode}`, { mode: agentMode }); break; case 'get-mode': sendResponse(msg.id, { mode: agentMode }); break; case 'status': sendResponse(msg.id, { model: currentModel, mode: agentMode, isProcessing: !!currentAgentId, availableModels: AVAILABLE_MODELS, }); break; case 'ping': sendResponse(msg.id, { pong: true }); break; default: sendError(msg.id, `Unbekannter Befehl: ${msg.command}`); } } // ============ Main ============ const rl = createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; try { handleCommand(JSON.parse(line)); } catch (err) { process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); } }); rl.on('close', () => { process.stderr.write('stdin geschlossen\n'); }); process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); }); process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); }); // Bereit sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });