Bridge (scripts/claude-bridge.js): - allowedTools je nach Agent-Modus erzwingt Delegation - Handlanger: nur Task + TodoWrite - Experten: Task + TodoWrite + Read + Grep + Glob - Solo/Auto: unveraendert Backend (src-tauri/src/claude.rs): - Mode-Persistenz: nach bridge-ready wird gespeicherter Modus gesetzt - Catch-all Event-Handler: leitet unbekannte Bridge-Events generisch ans Frontend weiter (subagent-started, monitor-event, mode-changed, ...) UI (routes/+layout.svelte, stores/events.ts): - Modus-Badge im Footer (Handlanger orange, Experten lila, Auto cyan) - mode-changed Event-Listener synchronisiert agentMode Store Bugfix voice.rs: - reqwest::multipart::Part::file existiert nicht → auf Part::bytes umgestellt - keine Temp-Datei mehr noetig Bugfix knowledge.rs: - Type-Annotation bei category Option<&str> fuer exec_map Inference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
562 lines
18 KiB
JavaScript
562 lines
18 KiB
JavaScript
#!/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;
|
|
}
|
|
|
|
// Tool-Filterung je nach Agent-Modus — erzwingt Delegation
|
|
// Handlanger: Main darf NUR delegieren (Task) und planen (TodoWrite)
|
|
// Experten: Main darf zusätzlich lesen/suchen, aber nicht schreiben
|
|
if (agentMode === 'handlanger') {
|
|
queryOptions.allowedTools = ['Task', 'TodoWrite'];
|
|
sendMonitorEvent('agent', 'Handlanger-Modus: Main darf nur Task+TodoWrite', {
|
|
mode: agentMode,
|
|
allowedTools: queryOptions.allowedTools,
|
|
});
|
|
} else if (agentMode === 'experten') {
|
|
queryOptions.allowedTools = ['Task', 'TodoWrite', 'Read', 'Grep', 'Glob'];
|
|
sendMonitorEvent('agent', 'Experten-Modus: Main darf lesen+delegieren', {
|
|
mode: agentMode,
|
|
allowedTools: queryOptions.allowedTools,
|
|
});
|
|
}
|
|
// solo + auto: keine Einschränkung
|
|
|
|
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 });
|