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/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, API_BASE } from './constants.mjs';
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, cmdAgentCreate, cmdAgentList, cmdAgentDelete } from './commands/ask.mjs';
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 'browse':
132
- case 'browser':
133
- return cmdBrowse(args);
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://localhost:3847)`);
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`);
@@ -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)) { fail(`File not found: ${attachFile}`); process.exit(1); }
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
- info(`Attached: ${path.basename(filePath)} (${Math.round(content.length / 1024)} KB)`);
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
- }