- @anthropic-ai/claude-agent-sdk statt raw API oder CLI-Spawn - query() Funktion mit async generator für Streaming - OAuth-Auth funktioniert automatisch (Claude Max Abo) - Opus 4.6 als Default, kein Rate-Limit, ~5s Antwort - AbortController für STOPP-Button - Kein CLI-Overhead, keine Hooks, kein MCP-Init Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
189 lines
4.7 KiB
JavaScript
189 lines
4.7 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;
|
|
const MODEL = process.env.CLAUDE_MODEL || 'opus';
|
|
|
|
// ============ 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) {
|
|
currentAgentId = randomUUID();
|
|
activeAbort = new AbortController();
|
|
|
|
sendEvent('agent-started', {
|
|
id: currentAgentId,
|
|
type: 'Main',
|
|
task: message.substring(0, 100),
|
|
});
|
|
|
|
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet' });
|
|
|
|
const startTime = Date.now();
|
|
let fullText = '';
|
|
let usedModel = MODEL;
|
|
|
|
try {
|
|
const conversation = query({
|
|
prompt: message,
|
|
options: {
|
|
model: MODEL,
|
|
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;
|
|
}
|
|
sendMessage(msg.message, msg.id);
|
|
break;
|
|
|
|
case 'stop':
|
|
if (activeAbort) {
|
|
activeAbort.abort();
|
|
}
|
|
sendResponse(msg.id, { status: 'gestoppt' });
|
|
break;
|
|
|
|
case 'status':
|
|
sendResponse(msg.id, {
|
|
model: MODEL,
|
|
isProcessing: !!currentAgentId,
|
|
});
|
|
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.0.0', pid: process.pid, model: MODEL });
|