nothumanallowed 9.5.1 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -305
- package/bin/nha.mjs +34 -3
- package/package.json +2 -2
- package/src/cli.mjs +105 -153
- package/src/commands/ask.mjs +18 -206
- package/src/commands/chat.mjs +64 -482
- package/src/commands/ui.mjs +30 -804
- package/src/config.mjs +0 -2
- package/src/constants.mjs +1 -1
- package/src/services/google-oauth.mjs +21 -12
- package/src/services/llm.mjs +0 -138
- package/src/services/ops-daemon.mjs +236 -0
- package/src/services/screen-capture.mjs +160 -0
- package/src/services/tool-executor.mjs +88 -335
- package/src/services/web-ui.mjs +126 -401
- package/src/services/browser-engine.mjs +0 -1240
- package/src/services/conversations.mjs +0 -277
- package/src/services/web-tools.mjs +0 -430
package/src/cli.mjs
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { VERSION, NHA_DIR, AGENTS_DIR, EXTENSIONS_DIR, AGENTS, EXTENSIONS, BASE_URL
|
|
5
|
+
import { VERSION, NHA_DIR, AGENTS_DIR, EXTENSIONS_DIR, AGENTS, EXTENSIONS, BASE_URL } from './constants.mjs';
|
|
6
6
|
import { needsBootstrap, bootstrap } from './bootstrap.mjs';
|
|
7
7
|
import { spawnCore } from './spawn.mjs';
|
|
8
8
|
import { loadConfig, setConfigValue } from './config.mjs';
|
|
9
9
|
import { checkForUpdates, runUpdate, checkNpmVersion } from './updater.mjs';
|
|
10
10
|
import { download } from './downloader.mjs';
|
|
11
|
-
import { cmdAsk
|
|
11
|
+
import { cmdAsk } from './commands/ask.mjs';
|
|
12
12
|
import { cmdPlan } from './commands/plan.mjs';
|
|
13
13
|
import { cmdTasks } from './commands/tasks.mjs';
|
|
14
14
|
import { cmdOps } from './commands/ops.mjs';
|
|
@@ -22,22 +22,6 @@ import { cmdVoice } from './commands/voice.mjs';
|
|
|
22
22
|
import { cmdPlugin, findPluginForCommand } from './commands/plugin.mjs';
|
|
23
23
|
import { banner, info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, M, B, R } from './ui.mjs';
|
|
24
24
|
|
|
25
|
-
// Show web UI tip once after first real command
|
|
26
|
-
export function showUiTipOnce(cmd) {
|
|
27
|
-
const skipCmds = ['ui', 'help', 'version', 'setup', 'update', 'config', 'doctor', '--help', '-h'];
|
|
28
|
-
if (skipCmds.includes(cmd)) return;
|
|
29
|
-
try {
|
|
30
|
-
const marker = path.join(NHA_DIR, '.ui-tip-shown');
|
|
31
|
-
if (fs.existsSync(marker)) return;
|
|
32
|
-
console.log('');
|
|
33
|
-
console.log(` ${G}Tip:${NC} Run ${C}nha ui${NC} to open the full web dashboard in your browser.`);
|
|
34
|
-
console.log(` Use ${C}nha ui --lan${NC} to access it from your phone or tablet on the same Wi-Fi.`);
|
|
35
|
-
console.log(` ${D}Docs: https://nothumanallowed.com/docs/web-dashboard${NC}`);
|
|
36
|
-
fs.mkdirSync(NHA_DIR, { recursive: true });
|
|
37
|
-
fs.writeFileSync(marker, new Date().toISOString());
|
|
38
|
-
} catch {}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
25
|
export async function main(argv) {
|
|
42
26
|
const cmd = argv[0] || 'help';
|
|
43
27
|
const args = argv.slice(1);
|
|
@@ -58,13 +42,6 @@ export async function main(argv) {
|
|
|
58
42
|
}
|
|
59
43
|
}).catch(() => {});
|
|
60
44
|
|
|
61
|
-
// Anonymous usage ping — fire-and-forget, no user data
|
|
62
|
-
fetch(`${API_BASE}/telemetry/ping`, {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify({ platform: 'npm-cli', version: VERSION, command: cmd }),
|
|
66
|
-
}).catch(() => {});
|
|
67
|
-
|
|
68
45
|
// npm version check (non-blocking)
|
|
69
46
|
checkNpmVersion().then(result => {
|
|
70
47
|
if (result?.updateAvailable) {
|
|
@@ -80,15 +57,6 @@ export async function main(argv) {
|
|
|
80
57
|
case 'ask':
|
|
81
58
|
return cmdAsk(args);
|
|
82
59
|
|
|
83
|
-
case 'agent:create':
|
|
84
|
-
return cmdAgentCreate(args);
|
|
85
|
-
|
|
86
|
-
case 'agent:list':
|
|
87
|
-
return cmdAgentList();
|
|
88
|
-
|
|
89
|
-
case 'agent:delete':
|
|
90
|
-
return cmdAgentDelete(args);
|
|
91
|
-
|
|
92
60
|
case 'run':
|
|
93
61
|
return cmdRun(args);
|
|
94
62
|
|
|
@@ -128,9 +96,15 @@ export async function main(argv) {
|
|
|
128
96
|
case 'voice':
|
|
129
97
|
return cmdVoice(args);
|
|
130
98
|
|
|
131
|
-
case '
|
|
132
|
-
|
|
133
|
-
|
|
99
|
+
case 'cron':
|
|
100
|
+
return cmdCron(args);
|
|
101
|
+
|
|
102
|
+
case 'heartbeat':
|
|
103
|
+
return cmdHeartbeat(args);
|
|
104
|
+
|
|
105
|
+
case 'daemon':
|
|
106
|
+
// Alias for nha ops (friendlier name)
|
|
107
|
+
return cmdOps(args.length ? args : ['start']);
|
|
134
108
|
|
|
135
109
|
case 'plugin':
|
|
136
110
|
case 'plugins':
|
|
@@ -181,7 +155,6 @@ export async function main(argv) {
|
|
|
181
155
|
return spawnCore('legion', [cmd, ...args]);
|
|
182
156
|
}
|
|
183
157
|
}
|
|
184
|
-
|
|
185
158
|
}
|
|
186
159
|
|
|
187
160
|
// ── nha responder ─────────────────────────────────────────────────────────
|
|
@@ -251,111 +224,6 @@ async function cmdResponder(args) {
|
|
|
251
224
|
}
|
|
252
225
|
}
|
|
253
226
|
|
|
254
|
-
// ── nha browse ────────────────────────────────────────────────────────────
|
|
255
|
-
async function cmdBrowse(args) {
|
|
256
|
-
const sub = args[0];
|
|
257
|
-
|
|
258
|
-
if (sub === 'open' && args[1]) {
|
|
259
|
-
const { browserOpen, browserInfo } = await import('./services/browser-engine.mjs');
|
|
260
|
-
info(`Opening ${args[1]}...`);
|
|
261
|
-
const result = await browserOpen(args[1]);
|
|
262
|
-
if (result.error) {
|
|
263
|
-
fail(result.message);
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
ok(`${result.title}`);
|
|
267
|
-
info(`URL: ${result.url}`);
|
|
268
|
-
|
|
269
|
-
// If there are more args, execute them as a sequence
|
|
270
|
-
if (args.length <= 2) {
|
|
271
|
-
info('Browser is running. Use "nha browse screenshot", "nha browse extract", etc.');
|
|
272
|
-
info('Or use "nha chat" — browser tools are available in chat.');
|
|
273
|
-
}
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (sub === 'screenshot') {
|
|
278
|
-
const { browserScreenshot, isBrowserRunning } = await import('./services/browser-engine.mjs');
|
|
279
|
-
if (!isBrowserRunning()) {
|
|
280
|
-
fail('No browser open. Run: nha browse open <url>');
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
const saveTo = args[1] || path.join(os.homedir(), `nha-screenshot-${Date.now()}.png`);
|
|
284
|
-
info('Capturing screenshot...');
|
|
285
|
-
const result = await browserScreenshot({ saveTo });
|
|
286
|
-
if (result.error) {
|
|
287
|
-
fail(result.message);
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
ok(`Screenshot saved: ${result.savedTo}`);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (sub === 'extract') {
|
|
295
|
-
const { browserExtract, isBrowserRunning } = await import('./services/browser-engine.mjs');
|
|
296
|
-
if (!isBrowserRunning()) {
|
|
297
|
-
fail('No browser open. Run: nha browse open <url>');
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
const selector = args[1] || 'body';
|
|
301
|
-
const result = await browserExtract({ selector, mode: 'text' });
|
|
302
|
-
if (result.error) {
|
|
303
|
-
fail(result.message);
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
console.log(result.content);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (sub === 'js' && args[1]) {
|
|
311
|
-
const { browserEval, isBrowserRunning } = await import('./services/browser-engine.mjs');
|
|
312
|
-
if (!isBrowserRunning()) {
|
|
313
|
-
fail('No browser open. Run: nha browse open <url>');
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
const code = args.slice(1).join(' ');
|
|
317
|
-
const result = await browserEval(code);
|
|
318
|
-
if (result.error) {
|
|
319
|
-
fail(result.message);
|
|
320
|
-
process.exit(1);
|
|
321
|
-
}
|
|
322
|
-
console.log(result.result);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (sub === 'close') {
|
|
327
|
-
const { browserClose } = await import('./services/browser-engine.mjs');
|
|
328
|
-
const result = await browserClose();
|
|
329
|
-
ok(result.message);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Help
|
|
334
|
-
console.log(`
|
|
335
|
-
${BOLD}${C}NHA Browser${NC} ${D}— Headless Chrome automation${NC}
|
|
336
|
-
|
|
337
|
-
${C}Usage:${NC}
|
|
338
|
-
nha browse open <url> Open a page in headless Chrome
|
|
339
|
-
nha browse screenshot Save screenshot to ~/
|
|
340
|
-
nha browse screenshot out.png Save to specific path
|
|
341
|
-
nha browse extract Extract all text from page
|
|
342
|
-
nha browse extract "h1" Extract text from CSS selector
|
|
343
|
-
nha browse js "code" Execute JavaScript in page
|
|
344
|
-
nha browse close Close the browser
|
|
345
|
-
|
|
346
|
-
${C}In Chat:${NC} All browser tools are available in ${W}nha chat${NC}:
|
|
347
|
-
"Open google.com and search for NHA"
|
|
348
|
-
"Take a screenshot of the page"
|
|
349
|
-
"Click the submit button"
|
|
350
|
-
"Fill the email field with test@example.com"
|
|
351
|
-
"Extract all links from the page"
|
|
352
|
-
|
|
353
|
-
${D}Requires Chrome or Chromium installed. Set CHROME_PATH to override.${NC}
|
|
354
|
-
${D}SSRF-protected — blocks localhost and private IPs.${NC}
|
|
355
|
-
${D}Zero npm dependencies — pure CDP WebSocket.${NC}
|
|
356
|
-
`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
227
|
// ── nha run ────────────────────────────────────────────────────────────────
|
|
360
228
|
async function cmdRun(args) {
|
|
361
229
|
if (args.length === 0) {
|
|
@@ -585,6 +453,92 @@ function cmdConfig(args) {
|
|
|
585
453
|
console.log('');
|
|
586
454
|
}
|
|
587
455
|
|
|
456
|
+
// ── nha cron ──────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
async function cmdCron(args) {
|
|
459
|
+
const { addCronJob, removeCronJob, listCronJobs } = await import('./services/ops-daemon.mjs');
|
|
460
|
+
const sub = args[0];
|
|
461
|
+
|
|
462
|
+
if (!sub || sub === 'list' || sub === 'ls') {
|
|
463
|
+
const jobs = listCronJobs();
|
|
464
|
+
if (jobs.length === 0) {
|
|
465
|
+
info('No cron jobs configured.');
|
|
466
|
+
info('Add one: nha cron add "every monday 9am" "check my open PRs"');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
console.log(`\n ${BOLD}Scheduled Jobs (${jobs.length})${NC}\n`);
|
|
470
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
471
|
+
const j = jobs[i];
|
|
472
|
+
const status = j.enabled ? `${G}active${NC}` : `${R}paused${NC}`;
|
|
473
|
+
const lastRun = j.lastRun ? new Date(j.lastRun).toLocaleString() : 'never';
|
|
474
|
+
console.log(` ${Y}${i + 1}.${NC} ${C}${j.schedule}${NC} → ${j.prompt}`);
|
|
475
|
+
console.log(` Status: ${status} Runs: ${j.runCount} Last: ${lastRun}`);
|
|
476
|
+
if (j.lastResult) console.log(` Result: ${D}${j.lastResult.slice(0, 80)}...${NC}`);
|
|
477
|
+
console.log('');
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (sub === 'add') {
|
|
483
|
+
const schedule = args[1];
|
|
484
|
+
const prompt = args.slice(2).join(' ');
|
|
485
|
+
if (!schedule || !prompt) {
|
|
486
|
+
fail('Usage: nha cron add "every monday 9am" "check open PRs on my repos"');
|
|
487
|
+
info('Schedules: "every 5m", "every 2h", "every monday 9am", "daily 8:30", "at 14:00"');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const result = addCronJob(schedule, prompt);
|
|
491
|
+
if (result.ok) {
|
|
492
|
+
ok(`Cron job added: ${schedule} → ${prompt}`);
|
|
493
|
+
info('The daemon will execute it automatically. Start daemon: nha ops start');
|
|
494
|
+
} else {
|
|
495
|
+
fail(result.error);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
501
|
+
const id = args[1];
|
|
502
|
+
if (!id) { fail('Usage: nha cron remove <number>'); return; }
|
|
503
|
+
const result = removeCronJob(id);
|
|
504
|
+
if (result.ok) {
|
|
505
|
+
ok(`Removed: ${result.removed.schedule} → ${result.removed.prompt}`);
|
|
506
|
+
} else {
|
|
507
|
+
fail(result.error);
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fail(`Unknown subcommand: ${sub}`);
|
|
513
|
+
info('Usage: nha cron [list|add|remove]');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── nha heartbeat ─────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
async function cmdHeartbeat(args) {
|
|
519
|
+
const { addHeartbeat } = await import('./services/ops-daemon.mjs');
|
|
520
|
+
const interval = args[0];
|
|
521
|
+
const prompt = args.slice(1).join(' ');
|
|
522
|
+
|
|
523
|
+
if (!interval || !prompt) {
|
|
524
|
+
info('Create a recurring background task:');
|
|
525
|
+
console.log(`\n ${C}nha heartbeat "2h" "summarize new emails"${NC}`);
|
|
526
|
+
console.log(` ${C}nha heartbeat "30m" "check GitHub notifications"${NC}`);
|
|
527
|
+
console.log(` ${C}nha heartbeat "1h" "monitor server health"${NC}\n`);
|
|
528
|
+
info('List all scheduled tasks: nha cron list');
|
|
529
|
+
info('Remove a task: nha cron remove <number>');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const result = addHeartbeat(interval, prompt);
|
|
534
|
+
if (result.ok) {
|
|
535
|
+
ok(`Heartbeat created: every ${interval} → ${prompt}`);
|
|
536
|
+
info('The daemon will execute it automatically. Start daemon: nha ops start');
|
|
537
|
+
} else {
|
|
538
|
+
fail(result.error);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
588
542
|
// ── nha doctor ─────────────────────────────────────────────────────────────
|
|
589
543
|
async function cmdDoctor() {
|
|
590
544
|
console.log(`\n ${BOLD}NHA Health Check${NC}\n`);
|
|
@@ -677,17 +631,8 @@ function cmdHelp() {
|
|
|
677
631
|
console.log(` run "prompt" Multi-agent collaboration (server-routed)`);
|
|
678
632
|
console.log(` run "prompt" ${D}--agents saber,zero${NC} Collaborate with specific agents\n`);
|
|
679
633
|
|
|
680
|
-
console.log(` ${C}Browser Automation${NC} ${D}(headless Chrome, zero dependencies)${NC}`);
|
|
681
|
-
console.log(` browse open <url> Open page in headless Chrome (CDP)`);
|
|
682
|
-
console.log(` browse screenshot Capture screenshot to file`);
|
|
683
|
-
console.log(` browse extract Extract page text (CSS selector)`);
|
|
684
|
-
console.log(` browse js "code" Execute JavaScript in page`);
|
|
685
|
-
console.log(` browse close Close browser`);
|
|
686
|
-
console.log(` ${D}Also available as tools in nha chat (browser_open, browser_click, etc.)${NC}\n`);
|
|
687
|
-
|
|
688
634
|
console.log(` ${C}Daily Operations${NC} ${D}(Gmail + Calendar + Tasks)${NC}`);
|
|
689
|
-
console.log(` ui Open local web dashboard (http://
|
|
690
|
-
console.log(` ui --lan Access from phone/tablet on the same Wi-Fi`);
|
|
635
|
+
console.log(` ui Open local web dashboard (http://127.0.0.1:3847)`);
|
|
691
636
|
console.log(` ui --port=4000 Custom port ui --no-browser Don't auto-open`);
|
|
692
637
|
console.log(` chat Interactive chat — manage email/calendar/tasks naturally`);
|
|
693
638
|
console.log(` voice Voice-powered chat (opens browser with mic interface)`);
|
|
@@ -701,6 +646,13 @@ function cmdHelp() {
|
|
|
701
646
|
console.log(` ops start Start background daemon (auto-alerts + WebSocket)`);
|
|
702
647
|
console.log(` ops stop Stop daemon`);
|
|
703
648
|
console.log(` ops status Daemon status`);
|
|
649
|
+
console.log(` daemon Alias for ops start\n`);
|
|
650
|
+
console.log(` ${C}Scheduled Tasks${NC}`);
|
|
651
|
+
console.log(` cron list List all scheduled jobs`);
|
|
652
|
+
console.log(` cron add "schedule" "prompt" Add a recurring task`);
|
|
653
|
+
console.log(` cron remove 1 Remove a scheduled job`);
|
|
654
|
+
console.log(` heartbeat "2h" "prompt" Quick recurring task\n`);
|
|
655
|
+
console.log(` ${C}Autostart${NC}`);
|
|
704
656
|
console.log(` autostart enable Auto-start daemon on login (launchd/systemd)`);
|
|
705
657
|
console.log(` autostart disable Remove OS autostart`);
|
|
706
658
|
console.log(` autostart status Check autostart configuration\n`);
|
package/src/commands/ask.mjs
CHANGED
|
@@ -18,12 +18,9 @@ export async function cmdAsk(args) {
|
|
|
18
18
|
if (!agentName || agentName.startsWith('-')) {
|
|
19
19
|
fail('Usage: nha ask <agent> "your question"');
|
|
20
20
|
fail(' nha ask saber "Audit this Express app for OWASP Top 10"');
|
|
21
|
-
fail(' nha ask oracle "Analyze this CSV" --file data.csv');
|
|
22
|
-
fail(' nha ask forge "What\'s in this?" --image screenshot.png');
|
|
21
|
+
fail(' nha ask oracle "Analyze this CSV for trends" --file data.csv');
|
|
23
22
|
console.log('');
|
|
24
23
|
info('Available agents: ' + AGENTS.join(', '));
|
|
25
|
-
info('Custom agents in ~/.nha/agents/ are also available.');
|
|
26
|
-
info('Create one: nha agent:create myagent "Expert in X" "You are..."');
|
|
27
24
|
process.exit(1);
|
|
28
25
|
}
|
|
29
26
|
|
|
@@ -31,7 +28,6 @@ export async function cmdAsk(args) {
|
|
|
31
28
|
if (!fs.existsSync(agentFile)) {
|
|
32
29
|
fail(`Agent "${agentName}" not found in ~/.nha/agents/`);
|
|
33
30
|
info('Available: ' + AGENTS.join(', '));
|
|
34
|
-
info('Create custom: nha agent:create <name> <tagline> <system-prompt>');
|
|
35
31
|
process.exit(1);
|
|
36
32
|
}
|
|
37
33
|
|
|
@@ -40,33 +36,32 @@ export async function cmdAsk(args) {
|
|
|
40
36
|
let model = null;
|
|
41
37
|
let stream = true;
|
|
42
38
|
let attachFile = null;
|
|
43
|
-
let attachImage = null;
|
|
44
39
|
|
|
45
40
|
for (let i = 1; i < args.length; i++) {
|
|
46
41
|
if (args[i] === '--provider' && args[i + 1]) { provider = args[++i]; continue; }
|
|
47
42
|
if (args[i] === '--model' && args[i + 1]) { model = args[++i]; continue; }
|
|
48
43
|
if (args[i] === '--no-stream') { stream = false; continue; }
|
|
49
44
|
if (args[i] === '--file' && args[i + 1]) { attachFile = args[++i]; continue; }
|
|
50
|
-
if (args[i] === '--image' && args[i + 1]) { attachImage = args[++i]; continue; }
|
|
51
45
|
promptParts.push(args[i]);
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
let userMessage = promptParts.join(' ');
|
|
49
|
+
if (!userMessage) {
|
|
50
|
+
fail('No prompt provided.');
|
|
51
|
+
fail('Usage: nha ask saber "your question here"');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
55
54
|
|
|
56
55
|
if (attachFile) {
|
|
57
56
|
const filePath = path.resolve(attachFile);
|
|
58
|
-
if (!fs.existsSync(filePath)) {
|
|
57
|
+
if (!fs.existsSync(filePath)) {
|
|
58
|
+
fail(`File not found: ${attachFile}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
59
61
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
60
62
|
const maxChars = 100_000;
|
|
61
63
|
const truncated = content.length > maxChars ? content.slice(0, maxChars) + '\n\n[... truncated ...]' : content;
|
|
62
|
-
|
|
63
|
-
userMessage = (userMessage || 'Analyze this file') + `\n\n--- Attached file: ${path.basename(filePath)} ---\n${truncated}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!userMessage && !attachImage) {
|
|
67
|
-
fail('No prompt provided.');
|
|
68
|
-
fail('Usage: nha ask saber "your question here"');
|
|
69
|
-
process.exit(1);
|
|
64
|
+
userMessage += `\n\n--- Attached file: ${path.basename(filePath)} ---\n${truncated}`;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
const config = loadConfig();
|
|
@@ -90,92 +85,16 @@ export async function cmdAsk(args) {
|
|
|
90
85
|
console.log(`\n ${BOLD}${card?.displayName || agentName.toUpperCase()}${NC} ${D}(${card?.tagline || card?.category || 'agent'})${NC}`);
|
|
91
86
|
console.log(` ${D}Provider: ${provider}${model ? ' / ' + model : ''} | Direct call — no server${NC}\n`);
|
|
92
87
|
|
|
88
|
+
const callFn = getProviderCall(provider);
|
|
89
|
+
if (!callFn) {
|
|
90
|
+
fail(`Unknown provider: ${provider}`);
|
|
91
|
+
info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
93
95
|
const startTime = Date.now();
|
|
94
96
|
|
|
95
97
|
try {
|
|
96
|
-
// Image attachment — use vision API
|
|
97
|
-
if (attachImage) {
|
|
98
|
-
const imagePath = path.resolve(attachImage);
|
|
99
|
-
if (!fs.existsSync(imagePath)) { fail(`Image not found: ${attachImage}`); process.exit(1); }
|
|
100
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
101
|
-
const base64 = imageBuffer.toString('base64');
|
|
102
|
-
const ext = path.extname(imagePath).toLowerCase();
|
|
103
|
-
const mimeMap = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.gif': 'image/gif' };
|
|
104
|
-
const mimeType = mimeMap[ext] || 'image/jpeg';
|
|
105
|
-
info(`Attached image: ${path.basename(imagePath)} (${Math.round(base64.length * 3 / 4 / 1024)} KB)`);
|
|
106
|
-
|
|
107
|
-
const imagePrompt = userMessage || 'Describe this image in detail. Extract any text, data, or important information.';
|
|
108
|
-
let response = '';
|
|
109
|
-
|
|
110
|
-
if (provider === 'anthropic') {
|
|
111
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
114
|
-
body: JSON.stringify({
|
|
115
|
-
model: model || 'claude-sonnet-4-20250514', max_tokens: 8192, system: systemPrompt,
|
|
116
|
-
messages: [{ role: 'user', content: [
|
|
117
|
-
{ type: 'image', source: { type: 'base64', media_type: mimeType, data: base64 } },
|
|
118
|
-
{ type: 'text', text: imagePrompt },
|
|
119
|
-
]}],
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
if (!res.ok) throw new Error(`Anthropic ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
123
|
-
const data = await res.json();
|
|
124
|
-
response = data.content?.[0]?.text || '';
|
|
125
|
-
} else if (provider === 'openai') {
|
|
126
|
-
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
127
|
-
method: 'POST',
|
|
128
|
-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
129
|
-
body: JSON.stringify({
|
|
130
|
-
model: model || 'gpt-4o-mini', max_tokens: 8192,
|
|
131
|
-
messages: [
|
|
132
|
-
{ role: 'system', content: systemPrompt },
|
|
133
|
-
{ role: 'user', content: [
|
|
134
|
-
{ type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64}` } },
|
|
135
|
-
{ type: 'text', text: imagePrompt },
|
|
136
|
-
]},
|
|
137
|
-
],
|
|
138
|
-
}),
|
|
139
|
-
});
|
|
140
|
-
if (!res.ok) throw new Error(`OpenAI ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
141
|
-
const data = await res.json();
|
|
142
|
-
response = data.choices?.[0]?.message?.content || '';
|
|
143
|
-
} else if (provider === 'gemini') {
|
|
144
|
-
const m = model || 'gemini-2.0-flash';
|
|
145
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m}:generateContent?key=${apiKey}`, {
|
|
146
|
-
method: 'POST',
|
|
147
|
-
headers: { 'Content-Type': 'application/json' },
|
|
148
|
-
body: JSON.stringify({
|
|
149
|
-
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
150
|
-
contents: [{ parts: [
|
|
151
|
-
{ inline_data: { mime_type: mimeType, data: base64 } },
|
|
152
|
-
{ text: imagePrompt },
|
|
153
|
-
]}],
|
|
154
|
-
generationConfig: { maxOutputTokens: 8192 },
|
|
155
|
-
}),
|
|
156
|
-
});
|
|
157
|
-
if (!res.ok) throw new Error(`Gemini ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
158
|
-
const data = await res.json();
|
|
159
|
-
response = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
160
|
-
} else {
|
|
161
|
-
fail(`Vision not supported for "${provider}". Use anthropic, openai, or gemini.`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
console.log(response);
|
|
166
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
167
|
-
console.log(`\n ${D}${elapsed}s | ${provider}${model ? ' / ' + model : ''} | ${card?.displayName || agentName}${NC}\n`);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Text / file — standard LLM call
|
|
172
|
-
const callFn = getProviderCall(provider);
|
|
173
|
-
if (!callFn) {
|
|
174
|
-
fail(`Unknown provider: ${provider}`);
|
|
175
|
-
info('Supported: anthropic, openai, gemini, deepseek, grok, mistral, cohere');
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
98
|
const useStream = stream && (provider === 'anthropic' || provider === 'openai' || provider === 'deepseek' || provider === 'grok' || provider === 'mistral');
|
|
180
99
|
const result = await callFn(apiKey, model, systemPrompt, userMessage, useStream);
|
|
181
100
|
|
|
@@ -190,110 +109,3 @@ export async function cmdAsk(args) {
|
|
|
190
109
|
process.exit(1);
|
|
191
110
|
}
|
|
192
111
|
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* nha agent:create <name> <tagline> <system-prompt>
|
|
196
|
-
* Creates a custom agent file in ~/.nha/agents/
|
|
197
|
-
*/
|
|
198
|
-
/**
|
|
199
|
-
* nha agent:list — Show all available agents (built-in + custom)
|
|
200
|
-
*/
|
|
201
|
-
export async function cmdAgentList() {
|
|
202
|
-
info('Built-in agents:');
|
|
203
|
-
for (const name of AGENTS) {
|
|
204
|
-
const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
|
|
205
|
-
if (fs.existsSync(agentFile)) {
|
|
206
|
-
const source = fs.readFileSync(agentFile, 'utf-8');
|
|
207
|
-
const { card } = parseAgentFile(source, name);
|
|
208
|
-
console.log(` ${C}${name.padEnd(16)}${NC} ${D}${card?.tagline || card?.category || ''}${NC}`);
|
|
209
|
-
} else {
|
|
210
|
-
console.log(` ${D}${name.padEnd(16)} (not downloaded)${NC}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Custom agents
|
|
215
|
-
if (fs.existsSync(AGENTS_DIR)) {
|
|
216
|
-
const custom = fs.readdirSync(AGENTS_DIR)
|
|
217
|
-
.filter(f => f.endsWith('.mjs'))
|
|
218
|
-
.map(f => f.replace('.mjs', ''))
|
|
219
|
-
.filter(n => !AGENTS.includes(n));
|
|
220
|
-
if (custom.length > 0) {
|
|
221
|
-
console.log(`\n${Y}Custom agents:${NC}`);
|
|
222
|
-
for (const name of custom) {
|
|
223
|
-
const source = fs.readFileSync(path.join(AGENTS_DIR, `${name}.mjs`), 'utf-8');
|
|
224
|
-
const { card } = parseAgentFile(source, name);
|
|
225
|
-
console.log(` ${Y}${name.padEnd(16)}${NC} ${D}${card?.tagline || ''}${NC}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
console.log(`\n ${D}Invoke: nha ask <agent> "prompt"${NC}`);
|
|
230
|
-
console.log(` ${D}Create: nha agent:create <name> "<tagline>" "<system prompt>"${NC}`);
|
|
231
|
-
console.log(` ${D}Delete: nha agent:delete <name>${NC}\n`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* nha agent:delete <name> — Delete a custom agent
|
|
236
|
-
*/
|
|
237
|
-
export async function cmdAgentDelete(args) {
|
|
238
|
-
const name = (args[0] || '').toLowerCase();
|
|
239
|
-
if (!name) {
|
|
240
|
-
fail('Usage: nha agent:delete <agent-name>');
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
if (AGENTS.includes(name)) {
|
|
244
|
-
fail(`"${name}" is a built-in agent and cannot be deleted.`);
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
|
|
248
|
-
if (!fs.existsSync(agentFile)) {
|
|
249
|
-
fail(`Agent "${name}" not found.`);
|
|
250
|
-
process.exit(1);
|
|
251
|
-
}
|
|
252
|
-
fs.unlinkSync(agentFile);
|
|
253
|
-
ok(`Agent "${name}" deleted.`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export async function cmdAgentCreate(args) {
|
|
257
|
-
const name = (args[0] || '').toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
258
|
-
const tagline = args[1] || '';
|
|
259
|
-
const sysPrompt = args.slice(2).join(' ') || '';
|
|
260
|
-
|
|
261
|
-
if (!name || !tagline || !sysPrompt) {
|
|
262
|
-
fail('Usage: nha agent:create <name> "<tagline>" "<system prompt>"');
|
|
263
|
-
console.log('');
|
|
264
|
-
info('Example:');
|
|
265
|
-
info(' nha agent:create reviewer "Code review expert" "You are a senior code reviewer..."');
|
|
266
|
-
console.log('');
|
|
267
|
-
info('The agent will be available as: nha ask reviewer "review this code"');
|
|
268
|
-
process.exit(1);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const agentFile = path.join(AGENTS_DIR, `${name}.mjs`);
|
|
272
|
-
if (fs.existsSync(agentFile)) {
|
|
273
|
-
fail(`Agent "${name}" already exists at ${agentFile}`);
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const content = `// NHA Custom Agent: ${name}
|
|
278
|
-
// Created: ${new Date().toISOString()}
|
|
279
|
-
|
|
280
|
-
export const CARD = {
|
|
281
|
-
name: '${name}',
|
|
282
|
-
displayName: '${name.toUpperCase()}',
|
|
283
|
-
category: 'custom',
|
|
284
|
-
tagline: '${tagline.replace(/'/g, "\\'")}',
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export const SYSTEM_PROMPT = \`${sysPrompt.replace(/`/g, '\\`')}\`;
|
|
288
|
-
`;
|
|
289
|
-
|
|
290
|
-
if (!fs.existsSync(AGENTS_DIR)) {
|
|
291
|
-
fs.mkdirSync(AGENTS_DIR, { recursive: true });
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
fs.writeFileSync(agentFile, content, 'utf-8');
|
|
295
|
-
ok(`Agent "${name}" created at ${agentFile}`);
|
|
296
|
-
info(`Invoke it: nha ask ${name} "your question"`);
|
|
297
|
-
info(`With file: nha ask ${name} "analyze" --file report.csv`);
|
|
298
|
-
info(`With image: nha ask ${name} "what is this?" --image photo.jpg`);
|
|
299
|
-
}
|