Files
gnome-jarvis/vendor/agent-harness/bin/cli.js
T
2026-09-11 13:41:22 -04:00

181 lines
5.5 KiB
JavaScript
Executable File

#!/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);
});