claude-desktop/src-tauri/scripts/claude-bridge.js
Eddy 6a41e2c8c7
Some checks failed
Build AppImage / build (push) Failing after 2m14s
fix: Permission-Mode (Yolo) an Claude CLI durchreichen [appimage]
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>
2026-05-02 23:03:29 +02:00

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' });