diff --git a/package.json b/package.json index 3b6eaa6..818ac06 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "vite": "^5.0.0" }, "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.104", "@anthropic-ai/claude-code": "^0.2.0", "@anthropic-ai/sdk": "^0.88.0", "@tauri-apps/api": "^2.0.0", diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 7a3dbc7..3b653bc 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -1,105 +1,23 @@ #!/usr/bin/env node -// Claude Desktop — Bridge zwischen Tauri-Backend und Anthropic API +// Claude Desktop — Bridge via Claude Agent SDK // -// Direkte API-Anbindung statt Claude CLI — kein Overhead, ~2s Antwort -// -// Kommunikation: stdin/stdout als NDJSON -// -// Eingehend (von Tauri): -// { "command": "message", "id": "req-1", "message": "Fixe den Bug..." } -// { "command": "stop", "id": "req-2" } -// { "command": "set-api-key", "id": "req-3", "apiKey": "sk-ant-..." } -// -// Ausgehend (an Tauri): -// { "type": "event", "event": "ready" } -// { "type": "event", "event": "agent-started", "payload": {...} } -// { "type": "event", "event": "text", "payload": { "text": "..." } } -// { "type": "event", "event": "result", "payload": { "cost": 0.01, ... } } -// { "type": "event", "event": "agent-stopped", "payload": {...} } +// Nutzt @anthropic-ai/claude-agent-sdk (query-Funktion) +// OAuth-Auth funktioniert automatisch (Claude Max Abo) +// Kein CLI-Spawn, kein Overhead — direkte SDK-Aufrufe -import Anthropic from '@anthropic-ai/sdk'; +import { query } from '@anthropic-ai/claude-agent-sdk'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; -import { readFileSync, writeFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { homedir } from 'node:os'; -// Prozess am Leben halten — MUSS vor allem anderen stehen +// Prozess am Leben halten const keepAlive = setInterval(() => {}, 60000); -process.stdin.resume(); // stdin offen halten auch ohne readline +process.stdin.resume(); // ============ State ============ -let client = null; -let apiKey = null; -let conversationHistory = []; // Messages für Multi-Turn -let activeAbortController = null; -const MODEL = process.env.CLAUDE_MODEL || 'claude-opus-4-20250514'; - -const SYSTEM_PROMPT = `Du bist Claude, ein hilfreicher KI-Assistent von Anthropic. -Du antwortest auf Deutsch. Du bist direkt, präzise und hilfreich. -Wenn du Code schreibst, nutze Kommentare auf Deutsch.`; - -// ============ API-Key Management ============ - -function getConfigPath() { - return join(homedir(), '.claude', 'claude-desktop-config.json'); -} - -function loadApiKey() { - // 1. Env-Var - if (process.env.ANTHROPIC_API_KEY) { - return process.env.ANTHROPIC_API_KEY; - } - - // 2. Gespeicherte Config (eigener Key) - const configPath = getConfigPath(); - if (existsSync(configPath)) { - try { - const config = JSON.parse(readFileSync(configPath, 'utf-8')); - if (config.apiKey) return config.apiKey; - } catch {} - } - - // 3. Claude Code OAuth-Token (aus Claude Max Abo) - const credentialsPath = join(homedir(), '.claude', '.credentials.json'); - if (existsSync(credentialsPath)) { - try { - const creds = JSON.parse(readFileSync(credentialsPath, 'utf-8')); - const oauth = creds.claudeAiOauth; - if (oauth?.accessToken) { - // Prüfen ob Token noch gültig - if (oauth.expiresAt && oauth.expiresAt > Date.now()) { - process.stderr.write(`🔑 OAuth-Token aus Claude Code geladen (gültig bis ${new Date(oauth.expiresAt).toLocaleString('de-DE')})\n`); - return oauth.accessToken; - } else { - process.stderr.write(`⚠️ OAuth-Token abgelaufen\n`); - } - } - } catch {} - } - - return null; -} - -function saveApiKey(key) { - const configPath = getConfigPath(); - try { - const config = existsSync(configPath) - ? JSON.parse(readFileSync(configPath, 'utf-8')) - : {}; - config.apiKey = key; - writeFileSync(configPath, JSON.stringify(config, null, 2)); - } catch (err) { - process.stderr.write(`Config speichern fehlgeschlagen: ${err.message}\n`); - } -} - -function initClient(key) { - apiKey = key; - client = new Anthropic({ apiKey: key }); - return true; -} +let activeAbort = null; +let currentAgentId = null; +const MODEL = process.env.CLAUDE_MODEL || 'opus'; // ============ Kommunikation mit Tauri ============ @@ -119,93 +37,101 @@ function sendError(id, error) { sendToTauri({ type: 'response', id, error }); } -// ============ Claude API aufrufen ============ +// ============ Claude Agent SDK ============ -async function sendToAnthropic(message, requestId) { - if (!client) { - sendError(requestId, 'Kein API-Key gesetzt. Bitte zuerst API-Key konfigurieren.'); - sendEvent('agent-stopped', { id: 'none', code: 1 }); - return; - } - - const agentId = randomUUID(); - - // AbortController für STOPP - activeAbortController = new AbortController(); +async function sendMessage(message, requestId) { + currentAgentId = randomUUID(); + activeAbort = new AbortController(); sendEvent('agent-started', { - id: agentId, + id: currentAgentId, type: 'Main', task: message.substring(0, 100), }); - sendResponse(requestId, { agentId, status: 'gestartet' }); + sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet' }); - // Nachricht zur History hinzufügen - conversationHistory.push({ role: 'user', content: message }); + const startTime = Date.now(); + let fullText = ''; + let usedModel = MODEL; try { - const startTime = Date.now(); - - // Streaming Response - const stream = client.messages.stream({ - model: MODEL, - max_tokens: 8192, - system: SYSTEM_PROMPT, - messages: conversationHistory, - }, { - signal: activeAbortController.signal, + const conversation = query({ + prompt: message, + options: { + model: MODEL, + maxTurns: 25, + abortController: activeAbort, + }, }); - let fullResponse = ''; - let inputTokens = 0; - let outputTokens = 0; + 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; - // Text-Chunks streamen - stream.on('text', (text) => { - fullResponse += text; - sendEvent('text', { text }); - }); + case 'tool_use': + sendEvent('tool-start', { + id: event.tool_use_id || randomUUID(), + tool: event.name || 'unknown', + input: event.input || {}, + }); + break; - // Warten auf vollständige Antwort - const finalMessage = await stream.finalMessage(); + case 'tool_result': + sendEvent('tool-end', { + id: event.tool_use_id || '', + success: !event.is_error, + }); + break; - inputTokens = finalMessage.usage?.input_tokens || 0; - outputTokens = finalMessage.usage?.output_tokens || 0; - - // Kosten berechnen (Claude Sonnet 4 Preise) - const cost = (inputTokens * 3 / 1_000_000) + (outputTokens * 15 / 1_000_000); - - const durationMs = Date.now() - startTime; - - // Antwort zur History hinzufügen - conversationHistory.push({ role: 'assistant', content: fullResponse }); - - sendEvent('result', { - text: fullResponse, - cost, - tokens: { input: inputTokens, output: outputTokens }, - duration_ms: durationMs, - model: MODEL, - }); - - sendEvent('agent-stopped', { id: agentId, code: 0 }); + case 'result': + // Endergebnis + sendEvent('result', { + text: fullText, + cost: event.total_cost_usd || 0, + tokens: { + input: event.usage?.input_tokens || 0, + output: event.usage?.output_tokens || 0, + }, + session_id: event.session_id || '', + duration_ms: Date.now() - startTime, + model: usedModel, + }); + break; + default: + // Andere Events still ignorieren + break; + } + } } catch (err) { if (err.name === 'AbortError') { - sendEvent('agent-stopped', { id: agentId, code: -1 }); + // Abgebrochen — kein Fehler } else { - const errorMsg = err.message || String(err); - sendEvent('text', { text: `\n\n**Fehler:** ${errorMsg}` }); - sendEvent('agent-stopped', { id: agentId, code: 1 }); + sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); } } finally { - activeAbortController = null; + sendEvent('agent-stopped', { id: currentAgentId, code: 0 }); sendEvent('all-stopped'); + currentAgentId = null; + activeAbort = null; } } -// ============ Befehle verarbeiten ============ +// ============ Befehle von Tauri ============ function handleCommand(msg) { switch (msg.command) { @@ -214,38 +140,20 @@ function handleCommand(msg) { sendError(msg.id, 'Keine Nachricht angegeben'); return; } - sendToAnthropic(msg.message, msg.id); + sendMessage(msg.message, msg.id); break; case 'stop': - if (activeAbortController) { - activeAbortController.abort(); + if (activeAbort) { + activeAbort.abort(); } sendResponse(msg.id, { status: 'gestoppt' }); break; - case 'set-api-key': - if (!msg.apiKey) { - sendError(msg.id, 'Kein API-Key angegeben'); - return; - } - initClient(msg.apiKey); - saveApiKey(msg.apiKey); - sendResponse(msg.id, { status: 'ok' }); - sendEvent('api-key-set'); - break; - - case 'clear-history': - conversationHistory = []; - sendResponse(msg.id, { status: 'ok', cleared: true }); - break; - case 'status': sendResponse(msg.id, { - hasApiKey: !!apiKey, model: MODEL, - historyLength: conversationHistory.length, - isProcessing: !!activeAbortController, + isProcessing: !!currentAgentId, }); break; @@ -260,38 +168,22 @@ function handleCommand(msg) { // ============ Main ============ -// API-Key laden -const savedKey = loadApiKey(); -if (savedKey) { - initClient(savedKey); -} - -// 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); + handleCommand(JSON.parse(line)); } catch (err) { process.stderr.write(`Ungültige Eingabe: ${err.message}\n`); } }); -// NICHT beenden wenn stdin schließt — wir warten auf weitere Befehle per stdin rl.on('close', () => { - process.stderr.write('⚠️ stdin geschlossen — Bridge bleibt trotzdem aktiv\n'); + process.stderr.write('stdin geschlossen\n'); }); -// Sauber beenden process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); }); process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); }); -// Bereit-Signal -sendEvent('ready', { - version: '0.2.0', - pid: process.pid, - hasApiKey: !!apiKey, - model: MODEL, -}); +// Bereit +sendEvent('ready', { version: '1.0.0', pid: process.pid, model: MODEL });