#!/usr/bin/env node /** * BooCoder CLI client. * * Usage: * boocode run "task description" [--agent opencode] [--model claude-opus-4-7] [--project ] * boocode ls [--state pending|running|completed|failed] * boocode attach * boocode send "message" */ import { WebSocket } from 'ws'; const BASE_URL = process.env.BOOCODER_URL ?? 'http://100.114.205.53:9502'; // ─── Arg parsing ───────────────────────────────────────────────────────────── function getFlag(args: string[], name: string): string | undefined { const idx = args.indexOf(name); if (idx === -1 || idx + 1 >= args.length) return undefined; return args[idx + 1]; } function hasFlag(args: string[], name: string): boolean { return args.includes(name); } // ─── HTTP helpers ──────────────────────────────────────────────────────────── async function api(method: string, path: string, body?: unknown): Promise { const url = `${BASE_URL}${path}`; const res = await fetch(url, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`${method} ${path} → ${res.status}: ${text}`); } return res.json(); } // ─── WS streaming ──────────────────────────────────────────────────────────── function streamSession(sessionId: string): void { const wsUrl = BASE_URL.replace(/^http/, 'ws') + `/api/ws/sessions/${sessionId}`; const ws = new WebSocket(wsUrl); ws.on('message', (data) => { try { const frame = JSON.parse(data.toString()) as { type: string; content?: string; name?: string; arguments?: string }; if (frame.type === 'delta' && frame.content) { process.stdout.write(frame.content); } else if (frame.type === 'tool_call') { process.stdout.write(`\n[tool: ${frame.name ?? '?'}(${(frame.arguments ?? '').slice(0, 80)})]\n`); } else if (frame.type === 'tool_result') { process.stdout.write(`[tool_result]\n`); } else if (frame.type === 'status' || frame.type === 'chat_status') { // Silent } } catch { // Non-JSON frame, ignore } }); ws.on('error', (err) => { process.stderr.write(`WS error: ${err.message}\n`); }); ws.on('close', () => { process.stdout.write('\n'); process.exit(0); }); process.on('SIGINT', () => { ws.close(); process.exit(0); }); } // ─── Commands ──────────────────────────────────────────────────────────────── async function cmdRun(args: string[]): Promise { const input = args.find((a) => !a.startsWith('--')); if (!input) { process.stderr.write('Usage: boocode run "task description" [--agent X] [--model X] [--project X]\n'); process.exit(1); } const agent = getFlag(args, '--agent'); const model = getFlag(args, '--model'); const project_id = getFlag(args, '--project'); if (!project_id) { process.stderr.write('Error: --project is required\n'); process.exit(1); } const result = (await api('POST', '/api/tasks', { project_id, input, ...(agent && { agent }), ...(model && { model }), })) as { id: string; state: string }; process.stdout.write(`Task created: ${result.id} (state: ${result.state})\n`); // Poll until task has session_id, then stream; or poll until terminal state const POLL_MS = 2000; for (;;) { await sleep(POLL_MS); const task = (await api('GET', `/api/tasks/${result.id}`)) as { id: string; state: string; session_id?: string; output_summary?: string; }; if (task.session_id) { process.stdout.write(`Streaming session ${task.session_id}...\n`); streamSession(task.session_id); return; // streamSession handles exit } if (task.state === 'completed') { process.stdout.write(`\nCompleted: ${task.output_summary ?? '(no summary)'}\n`); return; } if (task.state === 'failed') { process.stderr.write(`\nFailed: ${task.output_summary ?? '(no summary)'}\n`); process.exit(1); } if (task.state === 'cancelled') { process.stderr.write(`\nCancelled.\n`); process.exit(1); } } } async function cmdLs(args: string[]): Promise { const state = getFlag(args, '--state'); const query = state ? `?state=${state}` : ''; const tasks = (await api('GET', `/api/tasks${query}`)) as Array<{ id: string; state: string; agent: string | null; input: string; created_at: string; }>; if (tasks.length === 0) { process.stdout.write('No tasks.\n'); return; } // Table header process.stdout.write( pad('ID', 38) + pad('STATE', 12) + pad('AGENT', 14) + pad('INPUT', 52) + 'CREATED\n', ); process.stdout.write('-'.repeat(120) + '\n'); for (const t of tasks) { process.stdout.write( pad(t.id, 38) + pad(t.state, 12) + pad(t.agent ?? '-', 14) + pad(t.input.slice(0, 50), 52) + (t.created_at?.slice(0, 19) ?? '') + '\n', ); } } async function cmdAttach(args: string[]): Promise { const taskId = args[0]; if (!taskId) { process.stderr.write('Usage: boocode attach \n'); process.exit(1); } const task = (await api('GET', `/api/tasks/${taskId}`)) as { session_id?: string }; if (!task.session_id) { process.stderr.write('Task has no session yet (still pending?).\n'); process.exit(1); } streamSession(task.session_id); } async function cmdSend(args: string[]): Promise { const taskId = args[0]; const message = args[1]; if (!taskId || !message) { process.stderr.write('Usage: boocode send "message"\n'); process.exit(1); } const task = (await api('GET', `/api/tasks/${taskId}`)) as { session_id?: string }; if (!task.session_id) { process.stderr.write('Task has no session yet.\n'); process.exit(1); } // Find active chat const sessionId = task.session_id; // POST message to the session's chat (the messages route expects session_id in path) await api('POST', `/api/sessions/${sessionId}/messages`, { content: message }); // Then attach to stream the response streamSession(sessionId); } // ─── Utils ─────────────────────────────────────────────────────────────────── function pad(s: string, width: number): string { return s.length >= width ? s.slice(0, width) : s + ' '.repeat(width - s.length); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } // ─── Main ──────────────────────────────────────────────────────────────────── const [cmd, ...rest] = process.argv.slice(2); switch (cmd) { case 'run': cmdRun(rest).catch(fatal); break; case 'ls': cmdLs(rest).catch(fatal); break; case 'attach': cmdAttach(rest).catch(fatal); break; case 'send': cmdSend(rest).catch(fatal); break; default: process.stdout.write( 'BooCoder CLI\n\n' + 'Commands:\n' + ' run "task" [--agent X] [--model X] [--project ] Create and stream a task\n' + ' ls [--state pending|running|completed|failed] List tasks\n' + ' attach Stream a running task\n' + ' send "message" Send input to a task\n' + '\n' + `Base URL: ${BASE_URL} (set BOOCODER_URL to override)\n`, ); if (cmd && cmd !== '--help' && cmd !== '-h') process.exit(1); } function fatal(err: unknown): void { process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`); process.exit(1); }