nothumanallowed 4.1.0 → 5.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops. Ask agents directly, plan your day with 5 specialist agents, manage tasks, connect Gmail + Calendar.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -15,7 +15,10 @@ import { cmdOps } from './commands/ops.mjs';
15
15
  import { cmdChat } from './commands/chat.mjs';
16
16
  import { cmdUI } from './commands/ui.mjs';
17
17
  import { cmdGoogle } from './commands/google-auth.mjs';
18
+ import { cmdMicrosoft } from './commands/microsoft-auth.mjs';
18
19
  import { cmdScan } from './commands/scan.mjs';
20
+ import { cmdVoice } from './commands/voice.mjs';
21
+ import { cmdPlugin, findPluginForCommand } from './commands/plugin.mjs';
19
22
  import { banner, info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, M, B, R } from './ui.mjs';
20
23
 
21
24
  export async function main(argv) {
@@ -66,9 +69,21 @@ export async function main(argv) {
66
69
  case 'google':
67
70
  return cmdGoogle(args);
68
71
 
72
+ case 'microsoft':
73
+ case 'ms':
74
+ case 'outlook':
75
+ return cmdMicrosoft(args);
76
+
69
77
  case 'scan':
70
78
  return cmdScan(args);
71
79
 
80
+ case 'voice':
81
+ return cmdVoice(args);
82
+
83
+ case 'plugin':
84
+ case 'plugins':
85
+ return cmdPlugin(args);
86
+
72
87
  case 'pif':
73
88
  return cmdPif(args);
74
89
 
@@ -103,9 +118,16 @@ export async function main(argv) {
103
118
  case '-h':
104
119
  return cmdHelp();
105
120
 
106
- default:
121
+ default: {
122
+ // Check if a plugin handles this command before falling through to Legion
123
+ const pluginMatch = await findPluginForCommand(cmd);
124
+ if (pluginMatch && pluginMatch.plugin.run) {
125
+ const { cmdPlugin: runPlugin } = await import('./commands/plugin.mjs');
126
+ return runPlugin(['run', cmd, ...args]);
127
+ }
107
128
  // Try as Legion command passthrough
108
129
  return spawnCore('legion', [cmd, ...args]);
130
+ }
109
131
  }
110
132
  }
111
133
 
@@ -228,6 +250,8 @@ function cmdConfig(args) {
228
250
  console.log('');
229
251
  info('Keys: provider, key, openai-key, gemini-key, deepseek-key, grok-key, model, timeout');
230
252
  info(' verbose, immersive, deliberation, rounds, convergence, tribunal, knowledge');
253
+ info(' google-client-id, google-client-secret');
254
+ info(' microsoft-client-id, microsoft-client-secret, microsoft-tenant');
231
255
  return;
232
256
  }
233
257
  const success = setConfigValue(key, value);
@@ -273,10 +297,31 @@ function cmdConfig(args) {
273
297
  console.log(` ${D}(not registered — run "nha pif register")${NC}`);
274
298
  }
275
299
 
300
+ console.log(`\n ${C}Integrations${NC}`);
301
+ console.log(` Google: ${config.google?.clientId ? G + 'configured' : D + '(not set)'}${NC}`);
302
+ console.log(` Microsoft: ${config.microsoft?.clientId ? G + 'configured' : D + '(not set)'}${NC}`);
303
+ if (config.microsoft?.tenantId && config.microsoft.tenantId !== 'common') {
304
+ console.log(` MS Tenant: ${D}${config.microsoft.tenantId}${NC}`);
305
+ }
306
+
276
307
  console.log(`\n ${C}Features${NC}`);
277
308
  for (const [k, v] of Object.entries(config.features)) {
278
309
  console.log(` ${k.padEnd(28)} ${v ? G + '✓' : D + '○'}${NC}`);
279
310
  }
311
+
312
+ if (config.plugins) {
313
+ console.log(`\n ${C}Plugins${NC}`);
314
+ console.log(` Auto-run: ${config.plugins.autoRun ? G + 'yes' : D + 'no'}${NC}`);
315
+ if (config.plugins.directory) console.log(` Directory: ${D}${config.plugins.directory}${NC}`);
316
+ }
317
+
318
+ if (config.voice) {
319
+ console.log(`\n ${C}Voice${NC}`);
320
+ console.log(` Prefer Whisper: ${config.voice.preferWhisper ? G + 'yes' : D + 'no'}${NC}`);
321
+ console.log(` Speech Synth: ${config.voice.speechSynthesis ? G + 'yes' : D + 'no'}${NC}`);
322
+ if (config.voice.language) console.log(` Language: ${D}${config.voice.language}${NC}`);
323
+ }
324
+
280
325
  console.log('');
281
326
  }
282
327
 
@@ -304,6 +349,14 @@ async function cmdDoctor() {
304
349
  const extCount = EXTENSIONS.filter(e => fs.existsSync(path.join(EXTENSIONS_DIR, `${e}.mjs`))).length;
305
350
  console.log(` Extensions: ${D}${extCount}/${EXTENSIONS.length} installed${NC}`);
306
351
 
352
+ // Check plugins
353
+ const pluginsDir = path.join(NHA_DIR, 'plugins');
354
+ let pluginCount = 0;
355
+ if (fs.existsSync(pluginsDir)) {
356
+ pluginCount = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.mjs')).length;
357
+ }
358
+ console.log(` Plugins: ${D}${pluginCount} installed${NC}`);
359
+
307
360
  // Check API key
308
361
  console.log(` API Key: ${config.llm.apiKey ? G + 'configured (' + config.llm.provider + ')' : R + 'NOT SET'}${NC}`);
309
362
 
@@ -368,13 +421,15 @@ function cmdHelp() {
368
421
  console.log(` ui Open local web dashboard (http://127.0.0.1:3847)`);
369
422
  console.log(` ui --port=4000 Custom port ui --no-browser Don't auto-open`);
370
423
  console.log(` chat Interactive chat — manage email/calendar/tasks naturally`);
424
+ console.log(` voice Voice-powered chat (opens browser with mic interface)`);
425
+ console.log(` voice ${D}--port=3849${NC} Custom port voice ${D}--no-browser${NC}`);
371
426
  console.log(` plan Generate daily plan (5 agents analyze your day)`);
372
427
  console.log(` plan --refresh Regenerate today's plan`);
373
428
  console.log(` tasks List today's tasks`);
374
429
  console.log(` tasks add "desc" Add a task`);
375
430
  console.log(` tasks done 3 Complete task #3`);
376
431
  console.log(` tasks week Week overview`);
377
- console.log(` ops start Start background daemon (auto-alerts)`);
432
+ console.log(` ops start Start background daemon (auto-alerts + WebSocket)`);
378
433
  console.log(` ops stop Stop daemon`);
379
434
  console.log(` ops status Daemon status\n`);
380
435
 
@@ -383,11 +438,24 @@ function cmdHelp() {
383
438
  console.log(` google status Connection status`);
384
439
  console.log(` google revoke Disconnect\n`);
385
440
 
441
+ console.log(` ${C}Microsoft Integration${NC} ${D}(Outlook Mail + Calendar)${NC}`);
442
+ console.log(` microsoft auth Connect Outlook + Calendar`);
443
+ console.log(` microsoft status Connection status`);
444
+ console.log(` microsoft revoke Disconnect`);
445
+ console.log(` ${D}Aliases: ms, outlook${NC}\n`);
446
+
386
447
  console.log(` ${C}Extensions${NC} ${D}(downloadable agent modules)${NC}`);
387
448
  console.log(` install <name> Install an extension agent`);
388
449
  console.log(` install --all Install all ${EXTENSIONS.length} extensions`);
389
450
  console.log(` extensions List installed extensions\n`);
390
451
 
452
+ console.log(` ${C}Plugins${NC} ${D}(user-extensible commands)${NC}`);
453
+ console.log(` plugin list List installed & available plugins`);
454
+ console.log(` plugin install <name> Download a plugin from NHA server`);
455
+ console.log(` plugin run <name> Execute a plugin`);
456
+ console.log(` plugin create <name> Scaffold a new plugin from template`);
457
+ console.log(` plugin remove <name> Remove an installed plugin\n`);
458
+
391
459
  console.log(` ${C}Social Network${NC} ${D}(NHA platform)${NC}`);
392
460
  console.log(` pif register Register your agent identity`);
393
461
  console.log(` pif post Post content`);
@@ -13,23 +13,19 @@ import { loadConfig } from '../config.mjs';
13
13
  import { callLLM } from '../services/llm.mjs';
14
14
  import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, M, R, B } from '../ui.mjs';
15
15
 
16
- // ── Gmail imports ────────────────────────────────────────────────────────────
16
+ // ── Mail + Calendar imports (unified router — Google or Microsoft) ───────────
17
17
  import {
18
18
  listMessages,
19
19
  getMessage,
20
20
  getUnreadImportant,
21
21
  sendEmail,
22
22
  createDraft,
23
- } from '../services/google-gmail.mjs';
24
-
25
- // ── Calendar imports ─────────────────────────────────────────────────────────
26
- import {
27
23
  getTodayEvents,
28
24
  getUpcomingEvents,
29
25
  getEventsForDate,
30
26
  createEvent,
31
27
  updateEvent,
32
- } from '../services/google-calendar.mjs';
28
+ } from '../services/mail-router.mjs';
33
29
 
34
30
  // ── Task imports ─────────────────────────────────────────────────────────────
35
31
  import {
@@ -0,0 +1,29 @@
1
+ /** nha microsoft auth|status|revoke — Microsoft account management */
2
+
3
+ import { runMicrosoftAuthFlow, showMicrosoftStatus, revokeMicrosoftAuth } from '../services/microsoft-oauth.mjs';
4
+ import { loadConfig } from '../config.mjs';
5
+ import { fail, info } from '../ui.mjs';
6
+
7
+ export async function cmdMicrosoft(args) {
8
+ const sub = args[0] || 'auth';
9
+ const config = loadConfig();
10
+
11
+ switch (sub) {
12
+ case 'auth':
13
+ case 'login':
14
+ case 'connect':
15
+ return runMicrosoftAuthFlow(config);
16
+
17
+ case 'status':
18
+ return showMicrosoftStatus();
19
+
20
+ case 'revoke':
21
+ case 'disconnect':
22
+ case 'logout':
23
+ return revokeMicrosoftAuth();
24
+
25
+ default:
26
+ fail(`Unknown: nha microsoft ${sub}`);
27
+ info('Commands: auth, status, revoke');
28
+ }
29
+ }
@@ -0,0 +1,481 @@
1
+ /**
2
+ * nha plugin — Plugin system for user-extensible commands.
3
+ *
4
+ * Plugins are .mjs files stored in ~/.nha/plugins/. Each exports a
5
+ * PLUGIN_CARD object and a run() function. The plugin system gives plugins
6
+ * access to NHA services (LLM, Gmail, Calendar, Tasks, notifications)
7
+ * through a context object.
8
+ *
9
+ * Subcommands:
10
+ * nha plugin list — List installed + available plugins
11
+ * nha plugin install <name> — Download plugin from NHA server
12
+ * nha plugin run <name> [args] — Execute a plugin
13
+ * nha plugin create <name> — Scaffold a new plugin from template
14
+ * nha plugin remove <name> — Remove an installed plugin
15
+ * nha plugin info <name> — Show plugin details
16
+ *
17
+ * Zero npm dependencies — Node.js 22 native only.
18
+ */
19
+
20
+ import fs from 'fs';
21
+ import path from 'path';
22
+ import { loadConfig } from '../config.mjs';
23
+ import { callLLM, callAgent } from '../services/llm.mjs';
24
+ import { NHA_DIR, PLUGINS_DIR, BASE_URL, VERSION } from '../constants.mjs';
25
+ import { download } from '../downloader.mjs';
26
+ import { info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
27
+
28
+ // ── Constants ──────────────────────────────────────────────────────────────
29
+
30
+ const PLUGINS_REGISTRY_URL = `${BASE_URL}/plugins/registry.json`;
31
+
32
+ // ── Plugin Loader ──────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Load a plugin by name from the plugins directory.
36
+ * Returns { card, run } or null if not found.
37
+ */
38
+ export async function loadPlugin(name) {
39
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
40
+ const pluginFile = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
41
+
42
+ if (!fs.existsSync(pluginFile)) return null;
43
+
44
+ try {
45
+ const mod = await import(`file://${pluginFile}`);
46
+ return {
47
+ card: mod.PLUGIN_CARD || { name: sanitized, version: '0.0.0', description: '', commands: [] },
48
+ run: typeof mod.run === 'function' ? mod.run : null,
49
+ filePath: pluginFile,
50
+ };
51
+ } catch (err) {
52
+ fail(`Failed to load plugin "${sanitized}": ${err.message}`);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Load all installed plugins. Returns an array of { card, run, filePath }.
59
+ */
60
+ export async function loadAllPlugins() {
61
+ if (!fs.existsSync(PLUGINS_DIR)) return [];
62
+
63
+ const files = fs.readdirSync(PLUGINS_DIR).filter(f => f.endsWith('.mjs'));
64
+ const plugins = [];
65
+
66
+ for (const file of files) {
67
+ const name = file.replace(/\.mjs$/, '');
68
+ const plugin = await loadPlugin(name);
69
+ if (plugin) plugins.push(plugin);
70
+ }
71
+
72
+ return plugins;
73
+ }
74
+
75
+ /**
76
+ * Check if any installed plugin handles the given command name.
77
+ * Returns { plugin, command } or null.
78
+ */
79
+ export async function findPluginForCommand(commandName) {
80
+ const plugins = await loadAllPlugins();
81
+
82
+ for (const plugin of plugins) {
83
+ const commands = plugin.card.commands || [];
84
+ if (commands.includes(commandName) || plugin.card.name === commandName) {
85
+ return { plugin, command: commandName };
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ /**
93
+ * Build the context object passed to plugin run() functions.
94
+ * Gives plugins access to all NHA services.
95
+ */
96
+ async function buildPluginContext(config) {
97
+ const gmail = await import('../services/google-gmail.mjs').catch(() => null);
98
+ const calendar = await import('../services/google-calendar.mjs').catch(() => null);
99
+ const taskStore = await import('../services/task-store.mjs').catch(() => null);
100
+ const notifier = await import('../services/notification.mjs').catch(() => null);
101
+
102
+ return {
103
+ config,
104
+ version: VERSION,
105
+ pluginsDir: PLUGINS_DIR,
106
+ nhaDir: NHA_DIR,
107
+
108
+ // LLM
109
+ callLLM: (systemPrompt, userMessage, opts) => callLLM(config, systemPrompt, userMessage, opts),
110
+ callAgent: (agentName, userMessage, opts) => callAgent(config, agentName, userMessage, opts),
111
+
112
+ // Gmail
113
+ gmail: gmail ? {
114
+ listMessages: (query, max) => gmail.listMessages(config, query, max),
115
+ getMessage: (id) => gmail.getMessage(config, id),
116
+ getUnreadImportant: (max) => gmail.getUnreadImportant(config, max),
117
+ sendEmail: (to, subject, body, opts) => gmail.sendEmail(config, to, subject, body, opts),
118
+ createDraft: (to, subject, body) => gmail.createDraft(config, to, subject, body),
119
+ } : null,
120
+
121
+ // Calendar
122
+ calendar: calendar ? {
123
+ getTodayEvents: () => calendar.getTodayEvents(config),
124
+ getUpcomingEvents: (hours) => calendar.getUpcomingEvents(config, hours),
125
+ getEventsForDate: (date) => calendar.getEventsForDate(config, date),
126
+ createEvent: (data) => calendar.createEvent(config, data),
127
+ updateEvent: (calId, eventId, data) => calendar.updateEvent(config, calId, eventId, data),
128
+ } : null,
129
+
130
+ // Tasks
131
+ tasks: taskStore ? {
132
+ getTasks: () => taskStore.getTasks(),
133
+ addTask: (data) => taskStore.addTask(data),
134
+ completeTask: (id) => taskStore.completeTask(id),
135
+ moveTask: (id, from, to) => taskStore.moveTask(id, from, to),
136
+ getDayStats: () => taskStore.getDayStats(),
137
+ } : null,
138
+
139
+ // Notifications
140
+ notify: notifier ? (title, message) => notifier.notify(title, message, config) : null,
141
+
142
+ // Filesystem helpers (sandboxed to plugins dir)
143
+ fs: {
144
+ readFile: (name) => fs.readFileSync(path.join(PLUGINS_DIR, name), 'utf-8'),
145
+ writeFile: (name, content) => fs.writeFileSync(path.join(PLUGINS_DIR, name), content, 'utf-8'),
146
+ exists: (name) => fs.existsSync(path.join(PLUGINS_DIR, name)),
147
+ },
148
+
149
+ // Fetch (for plugins that need HTTP)
150
+ fetch: globalThis.fetch,
151
+ };
152
+ }
153
+
154
+ // ── Registry (available plugins from server) ────────────────────────────────
155
+
156
+ async function fetchRegistry() {
157
+ try {
158
+ const res = await fetch(PLUGINS_REGISTRY_URL, { signal: AbortSignal.timeout(10000) });
159
+ if (!res.ok) return [];
160
+ const data = await res.json();
161
+ return Array.isArray(data.plugins) ? data.plugins : [];
162
+ } catch {
163
+ return [];
164
+ }
165
+ }
166
+
167
+ // ── Plugin Template ─────────────────────────────────────────────────────────
168
+
169
+ function generateTemplate(name) {
170
+ const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
171
+
172
+ return `/**
173
+ * NHA Plugin: ${name}
174
+ *
175
+ * This plugin was scaffolded by "nha plugin create ${name}".
176
+ * Edit the PLUGIN_CARD and run() function to add your functionality.
177
+ *
178
+ * Usage:
179
+ * nha plugin run ${name}
180
+ * nha plugin run ${name} --help
181
+ */
182
+
183
+ export const PLUGIN_CARD = {
184
+ name: '${name}',
185
+ version: '1.0.0',
186
+ description: 'Describe what this plugin does',
187
+ author: '',
188
+ commands: ['${name}'],
189
+ };
190
+
191
+ /**
192
+ * Main entry point for the plugin.
193
+ *
194
+ * @param {string[]} args - Command-line arguments passed after the plugin name
195
+ * @param {object} context - NHA services context
196
+ * @param {function} context.callLLM - Call any LLM provider: callLLM(systemPrompt, userMessage, opts?)
197
+ * @param {function} context.callAgent - Call a named agent: callAgent('saber', 'Audit this code')
198
+ * @param {object|null} context.gmail - Gmail service (listMessages, getMessage, sendEmail, createDraft)
199
+ * @param {object|null} context.calendar - Calendar service (getTodayEvents, getUpcomingEvents, createEvent)
200
+ * @param {object|null} context.tasks - Tasks service (getTasks, addTask, completeTask, moveTask)
201
+ * @param {function|null} context.notify - Send a desktop notification: notify(title, message)
202
+ * @param {object} context.config - NHA config object
203
+ * @param {function} context.fetch - Global fetch for HTTP requests
204
+ * @param {object} context.fs - Sandboxed fs: readFile(name), writeFile(name, content), exists(name)
205
+ * @returns {Promise<string>} Result to display to the user
206
+ */
207
+ export async function run(args, context) {
208
+ if (args.includes('--help')) {
209
+ return [
210
+ 'Usage: nha plugin run ${name} [options]',
211
+ '',
212
+ 'Options:',
213
+ ' --help Show this help message',
214
+ '',
215
+ 'Description:',
216
+ ' ' + PLUGIN_CARD.description,
217
+ ].join('\\n');
218
+ }
219
+
220
+ // Example: call an agent
221
+ // const analysis = await context.callAgent('saber', 'Analyze this for security issues');
222
+
223
+ // Example: call LLM directly
224
+ // const response = await context.callLLM('You are a helpful assistant.', 'Hello!');
225
+
226
+ // Example: list tasks
227
+ // if (context.tasks) {
228
+ // const tasks = context.tasks.getTasks();
229
+ // return 'Your tasks: ' + tasks.map(t => t.description).join(', ');
230
+ // }
231
+
232
+ return 'Plugin ${name} executed successfully. Edit ~/.nha/plugins/${name}.mjs to add your logic.';
233
+ }
234
+ `;
235
+ }
236
+
237
+ // ── Subcommand Handlers ─────────────────────────────────────────────────────
238
+
239
+ async function cmdList() {
240
+ console.log(`\n ${BOLD}NHA Plugins${NC}\n`);
241
+
242
+ // Installed plugins
243
+ const installed = await loadAllPlugins();
244
+
245
+ if (installed.length > 0) {
246
+ console.log(` ${C}Installed${NC} ${D}(~/.nha/plugins/)${NC}\n`);
247
+ for (const p of installed) {
248
+ const card = p.card;
249
+ console.log(` ${G}*${NC} ${W}${card.name}${NC} ${D}v${card.version}${NC}`);
250
+ if (card.description) console.log(` ${D}${card.description}${NC}`);
251
+ if (card.commands && card.commands.length > 0) {
252
+ console.log(` ${D}Commands: ${card.commands.join(', ')}${NC}`);
253
+ }
254
+ }
255
+ } else {
256
+ console.log(` ${D}No plugins installed.${NC}`);
257
+ }
258
+
259
+ // Available from server
260
+ console.log('');
261
+ info('Checking server for available plugins...');
262
+ const registry = await fetchRegistry();
263
+
264
+ if (registry.length > 0) {
265
+ console.log(`\n ${C}Available from Server${NC}\n`);
266
+ const installedNames = new Set(installed.map(p => p.card.name));
267
+ for (const p of registry) {
268
+ const status = installedNames.has(p.name) ? `${G}installed${NC}` : `${D}available${NC}`;
269
+ console.log(` ${D}*${NC} ${W}${p.name}${NC} ${D}v${p.version}${NC} — ${status}`);
270
+ if (p.description) console.log(` ${D}${p.description}${NC}`);
271
+ }
272
+ } else {
273
+ console.log(`\n ${D}No plugins available from server (or server unreachable).${NC}`);
274
+ console.log(` ${D}Create your own: nha plugin create my-plugin${NC}`);
275
+ }
276
+
277
+ console.log('');
278
+ }
279
+
280
+ async function cmdInstall(name) {
281
+ if (!name) {
282
+ fail('Usage: nha plugin install <name>');
283
+ return;
284
+ }
285
+
286
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '').replace(/\.mjs$/, '');
287
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
288
+
289
+ const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
290
+ const url = `${BASE_URL}/plugins/${sanitized}.mjs`;
291
+
292
+ info(`Installing plugin "${sanitized}" from ${url}...`);
293
+
294
+ const success = await download(url, dest, { timeout: 15000 });
295
+ if (success) {
296
+ // Validate the downloaded plugin
297
+ const plugin = await loadPlugin(sanitized);
298
+ if (plugin && plugin.run) {
299
+ ok(`Plugin "${sanitized}" installed to ~/.nha/plugins/`);
300
+ if (plugin.card.description) {
301
+ info(plugin.card.description);
302
+ }
303
+ if (plugin.card.commands && plugin.card.commands.length > 0) {
304
+ info(`Commands: ${plugin.card.commands.join(', ')}`);
305
+ }
306
+ } else if (plugin) {
307
+ warn(`Plugin "${sanitized}" installed but has no run() function.`);
308
+ } else {
309
+ warn(`Plugin "${sanitized}" downloaded but could not be loaded. Check the file.`);
310
+ }
311
+ } else {
312
+ fail(`Could not download plugin "${sanitized}".`);
313
+ info(`Try: nha plugin create ${sanitized} (to create it locally)`);
314
+ }
315
+ }
316
+
317
+ async function cmdRun(name, args) {
318
+ if (!name) {
319
+ fail('Usage: nha plugin run <name> [args...]');
320
+ return;
321
+ }
322
+
323
+ const plugin = await loadPlugin(name);
324
+ if (!plugin) {
325
+ fail(`Plugin "${name}" not found. Install it first: nha plugin install ${name}`);
326
+ return;
327
+ }
328
+
329
+ if (!plugin.run) {
330
+ fail(`Plugin "${name}" has no run() function.`);
331
+ return;
332
+ }
333
+
334
+ const config = loadConfig();
335
+ const context = await buildPluginContext(config);
336
+
337
+ info(`Running plugin "${name}" v${plugin.card.version}...`);
338
+ console.log('');
339
+
340
+ try {
341
+ const result = await plugin.run(args, context);
342
+ if (result) {
343
+ console.log(result);
344
+ }
345
+ console.log('');
346
+ ok(`Plugin "${name}" completed.`);
347
+ } catch (err) {
348
+ fail(`Plugin "${name}" error: ${err.message}`);
349
+ if (err.stack) {
350
+ console.log(` ${D}${err.stack.split('\n').slice(1, 4).join('\n ')}${NC}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ async function cmdCreate(name) {
356
+ if (!name) {
357
+ fail('Usage: nha plugin create <name>');
358
+ return;
359
+ }
360
+
361
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '');
362
+ if (!sanitized) {
363
+ fail('Invalid plugin name. Use only letters, numbers, hyphens, underscores.');
364
+ return;
365
+ }
366
+
367
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
368
+
369
+ const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
370
+ if (fs.existsSync(dest)) {
371
+ fail(`Plugin "${sanitized}" already exists at ${dest}`);
372
+ info('Edit it directly or remove it first: nha plugin remove ' + sanitized);
373
+ return;
374
+ }
375
+
376
+ const template = generateTemplate(sanitized);
377
+ fs.writeFileSync(dest, template, 'utf-8');
378
+
379
+ ok(`Plugin "${sanitized}" created at ~/.nha/plugins/${sanitized}.mjs`);
380
+ console.log('');
381
+ info('Edit the file to add your logic:');
382
+ console.log(` ${D}${dest}${NC}`);
383
+ console.log('');
384
+ info('Then run it:');
385
+ console.log(` ${D}nha plugin run ${sanitized}${NC}`);
386
+ console.log('');
387
+ }
388
+
389
+ async function cmdRemove(name) {
390
+ if (!name) {
391
+ fail('Usage: nha plugin remove <name>');
392
+ return;
393
+ }
394
+
395
+ const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '').replace(/\.mjs$/, '');
396
+ const dest = path.join(PLUGINS_DIR, `${sanitized}.mjs`);
397
+
398
+ if (!fs.existsSync(dest)) {
399
+ fail(`Plugin "${sanitized}" not found.`);
400
+ return;
401
+ }
402
+
403
+ fs.rmSync(dest, { force: true });
404
+ ok(`Plugin "${sanitized}" removed.`);
405
+ }
406
+
407
+ async function cmdInfo(name) {
408
+ if (!name) {
409
+ fail('Usage: nha plugin info <name>');
410
+ return;
411
+ }
412
+
413
+ const plugin = await loadPlugin(name);
414
+ if (!plugin) {
415
+ fail(`Plugin "${name}" not found.`);
416
+ return;
417
+ }
418
+
419
+ const card = plugin.card;
420
+ console.log(`\n ${BOLD}${W}${card.name}${NC} ${D}v${card.version}${NC}\n`);
421
+ if (card.description) console.log(` ${card.description}\n`);
422
+ if (card.author) console.log(` ${D}Author:${NC} ${card.author}`);
423
+ if (card.commands && card.commands.length > 0) console.log(` ${D}Commands:${NC} ${card.commands.join(', ')}`);
424
+ console.log(` ${D}File:${NC} ${plugin.filePath}`);
425
+ console.log(` ${D}Has run():${NC} ${plugin.run ? G + 'yes' : R + 'no'}${NC}`);
426
+ console.log('');
427
+ }
428
+
429
+ // ── Main Command Router ─────────────────────────────────────────────────────
430
+
431
+ export async function cmdPlugin(args) {
432
+ const sub = args[0];
433
+ const rest = args.slice(1);
434
+
435
+ switch (sub) {
436
+ case 'list':
437
+ case 'ls':
438
+ case undefined:
439
+ return cmdList();
440
+
441
+ case 'install':
442
+ case 'add':
443
+ return cmdInstall(rest[0]);
444
+
445
+ case 'run':
446
+ case 'exec':
447
+ return cmdRun(rest[0], rest.slice(1));
448
+
449
+ case 'create':
450
+ case 'new':
451
+ case 'init':
452
+ return cmdCreate(rest[0]);
453
+
454
+ case 'remove':
455
+ case 'rm':
456
+ case 'uninstall':
457
+ return cmdRemove(rest[0]);
458
+
459
+ case 'info':
460
+ case 'show':
461
+ return cmdInfo(rest[0]);
462
+
463
+ default:
464
+ // If the subcommand matches a plugin name, run it directly
465
+ const plugin = await loadPlugin(sub);
466
+ if (plugin && plugin.run) {
467
+ return cmdRun(sub, rest);
468
+ }
469
+
470
+ fail(`Unknown plugin subcommand: ${sub}`);
471
+ console.log('');
472
+ info('Usage:');
473
+ console.log(` ${D}nha plugin list${NC} List installed & available plugins`);
474
+ console.log(` ${D}nha plugin install <name>${NC} Download a plugin from NHA server`);
475
+ console.log(` ${D}nha plugin run <name> [args]${NC} Execute a plugin`);
476
+ console.log(` ${D}nha plugin create <name>${NC} Scaffold a new plugin`);
477
+ console.log(` ${D}nha plugin remove <name>${NC} Remove an installed plugin`);
478
+ console.log(` ${D}nha plugin info <name>${NC} Show plugin details`);
479
+ console.log('');
480
+ }
481
+ }