Some checks failed
Build AppImage / build (push) Failing after 2m14s
Die Bridge hat bisher set-permission-mode komplett ignoriert. Jetzt wird --permission-mode als CLI-Flag an claude übergeben. Auch context und resumeSessionId werden jetzt korrekt durchgereicht (--append-system-prompt und --resume). Behebt: Yolo-Modus hatte keinen Effekt, Claude fragte trotzdem nach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
233 lines
7 KiB
JavaScript
233 lines
7 KiB
JavaScript
#!/usr/bin/env node
|
|
// Claude Desktop — Bridge zu Claude Code SDK
|
|
// Kommuniziert mit Rust-Backend über stdin/stdout (JSON)
|
|
|
|
const { spawn } = require('child_process');
|
|
const readline = require('readline');
|
|
|
|
// State
|
|
let claudeProcess = null;
|
|
let abortController = null;
|
|
let currentPermissionMode = 'default';
|
|
let currentResumeSessionId = null;
|
|
|
|
// Event an Rust senden
|
|
function emit(event, payload) {
|
|
const msg = JSON.stringify({ type: 'event', event, payload });
|
|
process.stdout.write(msg + '\n');
|
|
}
|
|
|
|
// Antwort an Rust senden
|
|
function respond(id, result, error = null) {
|
|
const msg = JSON.stringify({ type: 'response', id, result, error });
|
|
process.stdout.write(msg + '\n');
|
|
}
|
|
|
|
// Claude Code als Subprocess starten
|
|
async function startClaude(message, requestId, context, resumeSessionId) {
|
|
abortController = new AbortController();
|
|
|
|
emit('agent-started', {
|
|
id: 'main',
|
|
type: 'Main Agent',
|
|
task: message.substring(0, 100)
|
|
});
|
|
|
|
try {
|
|
// CLI-Args aufbauen
|
|
const args = ['--output-format', 'stream-json'];
|
|
|
|
// Permission-Modus: direkt als CLI-Flag durchreichen
|
|
if (currentPermissionMode && currentPermissionMode !== 'default') {
|
|
args.push('--permission-mode', currentPermissionMode);
|
|
}
|
|
|
|
// Resume-Session: vorherige Claude-Session fortsetzen
|
|
const sid = resumeSessionId || currentResumeSessionId;
|
|
if (sid) {
|
|
args.push('--resume', sid);
|
|
}
|
|
|
|
// Context als System-Prompt anhängen
|
|
if (context) {
|
|
args.push('--append-system-prompt', context);
|
|
}
|
|
|
|
// Nachricht als Prompt
|
|
args.push('-p', message);
|
|
|
|
emit('log', {
|
|
level: 'info',
|
|
message: `CLI args: claude ${args.filter(a => a !== context).join(' ')} [permMode=${currentPermissionMode}]`
|
|
});
|
|
|
|
// Claude Code CLI aufrufen
|
|
claudeProcess = spawn('claude', args, {
|
|
signal: abortController.signal,
|
|
env: { ...process.env, FORCE_COLOR: '0' }
|
|
});
|
|
|
|
let fullResponse = '';
|
|
let buffer = '';
|
|
|
|
claudeProcess.stdout.on('data', (data) => {
|
|
buffer += data.toString();
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop(); // Unvollständige Zeile behalten
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const event = JSON.parse(line);
|
|
handleClaudeEvent(event);
|
|
if (event.type === 'assistant' && event.message?.content) {
|
|
for (const block of event.message.content) {
|
|
if (block.type === 'text') {
|
|
fullResponse += block.text;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Kein JSON, ignorieren
|
|
}
|
|
}
|
|
});
|
|
|
|
claudeProcess.stderr.on('data', (data) => {
|
|
emit('log', { level: 'error', message: data.toString() });
|
|
});
|
|
|
|
claudeProcess.on('close', (code) => {
|
|
emit('agent-stopped', { id: 'main', code });
|
|
respond(requestId, fullResponse || 'Keine Antwort erhalten');
|
|
claudeProcess = null;
|
|
abortController = null;
|
|
});
|
|
|
|
claudeProcess.on('error', (err) => {
|
|
if (err.name === 'AbortError') {
|
|
respond(requestId, null, 'Abgebrochen durch Benutzer');
|
|
} else {
|
|
respond(requestId, null, err.message);
|
|
}
|
|
});
|
|
|
|
} catch (err) {
|
|
respond(requestId, null, err.message);
|
|
}
|
|
}
|
|
|
|
// Claude SDK Events verarbeiten
|
|
function handleClaudeEvent(event) {
|
|
switch (event.type) {
|
|
case 'tool_use':
|
|
emit('tool-start', {
|
|
id: event.tool_use_id,
|
|
tool: event.name,
|
|
input: event.input
|
|
});
|
|
break;
|
|
|
|
case 'tool_result':
|
|
emit('tool-end', {
|
|
id: event.tool_use_id,
|
|
success: !event.is_error,
|
|
output: typeof event.content === 'string'
|
|
? event.content.substring(0, 500)
|
|
: JSON.stringify(event.content).substring(0, 500)
|
|
});
|
|
break;
|
|
|
|
case 'subagent_start':
|
|
emit('subagent-start', {
|
|
id: event.subagent_id,
|
|
type: event.subagent_type,
|
|
task: event.prompt?.substring(0, 100)
|
|
});
|
|
break;
|
|
|
|
case 'subagent_stop':
|
|
emit('subagent-stop', {
|
|
id: event.subagent_id
|
|
});
|
|
break;
|
|
|
|
case 'assistant':
|
|
if (event.message?.content) {
|
|
for (const block of event.message.content) {
|
|
if (block.type === 'text') {
|
|
emit('text', { text: block.text });
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'result':
|
|
emit('result', {
|
|
cost: event.cost_usd,
|
|
tokens: {
|
|
input: event.input_tokens,
|
|
output: event.output_tokens
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Alle Prozesse stoppen
|
|
function stopAll() {
|
|
if (abortController) {
|
|
abortController.abort();
|
|
}
|
|
if (claudeProcess) {
|
|
claudeProcess.kill('SIGTERM');
|
|
}
|
|
emit('all-stopped', {});
|
|
}
|
|
|
|
// Stdin lesen
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
terminal: false
|
|
});
|
|
|
|
rl.on('line', (line) => {
|
|
try {
|
|
const msg = JSON.parse(line);
|
|
|
|
switch (msg.command) {
|
|
case 'message':
|
|
startClaude(msg.message, msg.id, msg.context, msg.resumeSessionId);
|
|
break;
|
|
case 'set-permission-mode':
|
|
currentPermissionMode = msg.mode || 'default';
|
|
emit('log', { level: 'info', message: `Permission-Modus: ${currentPermissionMode}` });
|
|
respond(msg.id, `permission-mode=${currentPermissionMode}`);
|
|
break;
|
|
case 'set-mode':
|
|
// Agent-Modus (solo/handlanger/experten/auto) — wird aktuell nicht an CLI weitergegeben
|
|
respond(msg.id, `mode=${msg.mode}`);
|
|
break;
|
|
case 'set-model':
|
|
// Modell-Wechsel — wird aktuell nicht an CLI weitergegeben
|
|
respond(msg.id, `model=${msg.model}`);
|
|
break;
|
|
case 'stop':
|
|
stopAll();
|
|
respond(msg.id, 'stopped');
|
|
break;
|
|
case 'ping':
|
|
respond(msg.id, 'pong');
|
|
break;
|
|
default:
|
|
emit('log', { level: 'warn', message: `Unbekannter Befehl: ${msg.command}` });
|
|
respond(msg.id, null, `Unbekannter Befehl: ${msg.command}`);
|
|
}
|
|
} catch (e) {
|
|
emit('error', { message: e.message });
|
|
}
|
|
});
|
|
|
|
// Startup
|
|
emit('ready', { version: '0.1.0' });
|