#!/usr/bin/env node 'use strict'; const path = require('path'); const readline = require('readline'); const os = require('os'); const Agent = require('../index.js'); function parseArgs(argv) { const out = { model: 'qwen3.5-4b', cwd: process.cwd(), yes: false, device: 'auto', prompt: '', webFetch: false }; const rest = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--model' || a === '-m') out.model = argv[++i]; else if (a === '--cwd' || a === '-C') out.cwd = path.resolve(argv[++i] || '.'); else if (a === '--device') out.device = argv[++i] || 'auto'; else if (a === '--yes' || a === '-y') out.yes = true; else if (a === '--web-fetch') out.webFetch = true; else if (a === '--list-models') out.listModels = true; else if (a === '--help' || a === '-h') out.help = true; else rest.push(a); } out.prompt = rest.join(' ').trim(); return out; } function help() { process.stdout.write( [ 'agent-harness — local QVAC coding agent (no BridgeSwarm)', '', ' agent-harness [--model qwen3.5-4b] [--cwd DIR] [--yes] [prompt]', ' agent-harness --list-models', '', ' --yes always-approve writes and shell', ' --web-fetch enable web_fetch for public http(s)', ' --device auto|cpu|gpu (auto and gpu offload all layers; cpu to force CPU)', '', 'Default model: qwen3.5-4b. Catalog GGUFs only; arbitrary HF repos will not load.', 'Sessions: $AGENT_HARNESS_HOME or ~/.agent-harness', '', ].join('\n') ); } function askLine(rl, question) { return new Promise((resolve) => rl.question(question, resolve)); } async function bindInteractive(session, rl, yes) { session.on('agent_message_chunk', (ev) => { if (ev.text) process.stdout.write(ev.text); }); session.on('agent_thought_chunk', () => {}); session.on('tool_call', (ev) => { const name = ev.call && ev.call.name; process.stderr.write('\n▸ ' + (name || 'tool') + '\n'); }); session.on('permission', async (ev) => { if (yes) { session.permit(ev.jobId, ev.toolCallId, 'allow'); return; } const line = await askLine( rl, 'Allow ' + ev.tool + ' ' + JSON.stringify(ev.args || {}).slice(0, 120) + '? [y/N/always] ' ); const t = String(line || '').trim().toLowerCase(); if (t === 'always' || t === 'a') session.permit(ev.jobId, ev.toolCallId, 'always'); else if (t === 'y' || t === 'yes') session.permit(ev.jobId, ev.toolCallId, 'allow'); else session.permit(ev.jobId, ev.toolCallId, 'deny'); }); session.on('ask_user', async (ev) => { const opts = (ev.options || []).map((o, i) => ' ' + (i + 1) + ') ' + o).join('\n'); const line = await askLine(rl, (ev.question || 'Choose') + '\n' + opts + '\n> '); session.answer(ev.toolCallId, line); }); session.on('plan_approval', async (ev) => { process.stdout.write('\n--- plan.md ---\n' + (ev.plan || '') + '\n---------------\n'); if (yes) { session.planDecision('approve'); return; } const line = await askLine(rl, 'Approve plan? [y/N] '); const t = String(line || '').trim().toLowerCase(); session.planDecision(t === 'y' || t === 'yes' ? 'approve' : 'reject'); }); session.on('end', (ev) => { if (ev.reason && ev.reason !== 'stop') process.stderr.write('\n[' + ev.reason + ']\n'); else process.stdout.write('\n'); }); } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { help(); return; } if (args.listModels) { for (const m of Agent.catalog()) { process.stdout.write( m.id.padEnd(18) + (m.tools ? 'tools ' : ' ') + (m.vision ? 'vision ' : ' ') + (m.label || m.name) + '\n' ); } return; } const hw = { totalRamBytes: os.totalmem() }; const model = args.model || (Agent.suggest(hw) && Agent.suggest(hw).id) || 'qwen3.5-4b'; process.stderr.write('Loading ' + model + '…\n'); await Agent.engine.load({ model, device: args.device, tools: true, onProgress: (p) => { if (p && p.percent != null && process.stderr.isTTY) { process.stderr.write('\rdownload ' + Math.round(p.percent) + '% '); if (p.percent >= 100) process.stderr.write('\n'); } }, }); const loaded = Agent.engine.getLoaded(); process.stderr.write( 'Ready ' + (loaded.friendlyId || model) + ' · ' + (loaded.device || '') + (loaded.backend ? ' ' + loaded.backend : '') + '\n' ); const session = await Agent.create({ cwd: args.cwd, model, permissionMode: args.yes ? 'always-approve' : 'ask', webFetch: args.webFetch, }); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); await bindInteractive(session, rl, args.yes); async function one(text) { if (!String(text || '').trim()) return; await session.prompt(text); } if (args.prompt) { await one(args.prompt); rl.close(); await session.dispose(); await Agent.engine.close(); return; } process.stderr.write('Type a prompt. /quit to exit.\n'); const loopRead = async () => { const line = await askLine(rl, '> '); const t = String(line || '').trim(); if (!t || t === '/quit' || t === '/exit') return; await one(t); return loopRead(); }; try { await loopRead(); } finally { rl.close(); await session.dispose(); await Agent.engine.close(); } } main().catch((err) => { process.stderr.write(String((err && err.stack) || err) + '\n'); process.exit(1); });