#!/usr/bin/env node /* * Chrome native messaging host for JoboCop. * Chrome starts this process, sends one JSON message on stdin, reads one reply on stdout. * * { type: "ping", cwd? } -> { ok, version, cwd } * { type: "ask", prompt, cwd?, model? } -> { ok, text } * * The prompt is passed to `claude -p` (headless mode) on stdin. In headless mode * Claude Code can read files in the working folder but cannot run commands or * edit files without permission, so it cannot change anything on this computer. */ const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const CONFIG = (() => { try { return JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8').replace(/^\uFEFF/, '')); } catch { return {}; } })(); const CLAUDE = CONFIG.claudePath || 'claude'; const TIMEOUT_MS = 180_000; // ---------- native messaging framing (4-byte little-endian length + JSON) ---------- function send(obj) { const data = Buffer.from(JSON.stringify(obj), 'utf8'); const header = Buffer.alloc(4); header.writeUInt32LE(data.length, 0); process.stdout.write(Buffer.concat([header, data])); } let buf = Buffer.alloc(0); process.stdin.on('data', (chunk) => { buf = Buffer.concat([buf, chunk]); while (buf.length >= 4) { const len = buf.readUInt32LE(0); if (buf.length < 4 + len) break; const raw = buf.subarray(4, 4 + len).toString('utf8'); buf = buf.subarray(4 + len); let msg; try { msg = JSON.parse(raw); } catch { send({ ok: false, error: 'Bad message from the extension.' }); continue; } handle(msg).then(send, (e) => send({ ok: false, error: String(e?.message || e) })); } }); process.stdin.on('end', () => setTimeout(() => process.exit(0), 50)); // ---------- running claude ---------- function workingDir(cwd) { const dir = cwd ? cwd.replace(/^~(?=$|[\\/])/, os.homedir()) : ''; try { if (dir && fs.statSync(dir).isDirectory()) return dir; } catch {} return os.homedir(); } function runClaude(args, input, cwd) { return new Promise((resolve) => { const isWin = process.platform === 'win32'; const viaShell = isWin && /\.(cmd|bat)$/i.test(CLAUDE); const env = { ...process.env, ...(CONFIG.path ? { PATH: CONFIG.path } : {}) }; let child; try { child = spawn(viaShell ? `"${CLAUDE}"` : CLAUDE, args, { cwd, env, shell: viaShell, windowsHide: true }); } catch (e) { return resolve({ ok: false, error: e.message }); } let out = ''; let err = ''; let finished = false; const done = (r) => { if (finished) return; finished = true; clearTimeout(timer); resolve(r); }; const timer = setTimeout(() => { child.kill(); done({ ok: false, error: 'Claude Code took longer than 3 minutes. Try fewer questions at once.' }); }, TIMEOUT_MS); child.stdout.on('data', (d) => (out += d)); child.stderr.on('data', (d) => (err += d)); child.on('error', (e) => done({ ok: false, error: e.code === 'ENOENT' ? `Claude Code was not found at "${CLAUDE}". Run the installer again.` : e.message }) ); child.on('close', (code) => done({ ok: code === 0, code, out, err })); child.stdin.on('error', () => {}); child.stdin.end(input ?? ''); }); } async function handle(msg) { const cwd = workingDir(msg.cwd); if (msg.type === 'ping') { const r = await runClaude(['--version'], '', cwd); if (!r.ok) return { ok: false, error: r.error || r.err.trim() || 'Claude Code did not start.' }; return { ok: true, version: r.out.trim(), cwd }; } if (msg.type === 'ask') { if (!msg.prompt) return { ok: false, error: 'Empty prompt.' }; const args = ['-p', '--output-format', 'json']; if (msg.model) args.push('--model', String(msg.model)); const r = await runClaude(args, msg.prompt, cwd); if (r.error) return { ok: false, error: r.error }; let parsed = null; try { parsed = JSON.parse(r.out); } catch {} if (!r.ok || !parsed || parsed.is_error) { const detail = parsed?.result || r.err.trim() || r.out.trim().slice(0, 300); const login = /log ?in|auth|api key/i.test(detail) ? ' Open a terminal and run `claude` once to sign in.' : ''; return { ok: false, error: `Claude Code failed: ${detail || `exit code ${r.code}`}.${login}` }; } return { ok: true, text: String(parsed.result ?? '') }; } return { ok: false, error: `Unknown request: ${msg.type}` }; }