181 lines
5.5 KiB
JavaScript
Executable File
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);
|
|
});
|