claude-desktop/scripts/claude-bridge.js
Eddy 433e2de2b6 Modell-Auswahl in Settings implementiert
- Neues SettingsPanel mit Modell-Auswahl (Haiku/Sonnet/Opus)
- Modell wird in SQLite persistiert (claude_model Setting)
- Bridge unterstützt set-model und get-models Commands
- Modell kann zur Laufzeit gewechselt werden
- Preisanzeige pro Modell im Settings-Panel
- Aktuelles Modell wird beim App-Start geladen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-14 09:32:26 +02:00

224 lines
6 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';
// 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' },
];
// ============ 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 Agent SDK ============
async function sendMessage(message, requestId, model = null) {
// Modell für diese Anfrage (Parameter > State > Default)
const useModel = model || currentModel;
currentAgentId = randomUUID();
activeAbort = new AbortController();
sendEvent('agent-started', {
id: currentAgentId,
type: 'Main',
task: message.substring(0, 100),
model: useModel,
});
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
const startTime = Date.now();
let fullText = '';
let usedModel = useModel;
try {
const conversation = query({
prompt: message,
options: {
model: useModel,
maxTurns: 25,
abortController: activeAbort,
},
});
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':
sendEvent('tool-start', {
id: event.tool_use_id || randomUUID(),
tool: event.name || 'unknown',
input: event.input || {},
});
break;
case 'tool_result':
sendEvent('tool-end', {
id: event.tool_use_id || '',
success: !event.is_error,
});
break;
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') {
// Abgebrochen — kein Fehler
} else {
sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` });
}
} finally {
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 kann pro Anfrage überschrieben werden
sendMessage(msg.message, msg.id, msg.model);
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 'status':
sendResponse(msg.id, {
model: currentModel,
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 });