nothumanallowed 4.1.0 → 6.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.
@@ -11,25 +11,22 @@
11
11
  import readline from 'readline';
12
12
  import { loadConfig } from '../config.mjs';
13
13
  import { callLLM } from '../services/llm.mjs';
14
+ import { loadChatHistory, saveChatHistory, extractMemory } from '../services/memory.mjs';
14
15
  import { fail, info, ok, warn, C, G, Y, D, W, BOLD, NC, M, R, B } from '../ui.mjs';
15
16
 
16
- // ── Gmail imports ────────────────────────────────────────────────────────────
17
+ // ── Mail + Calendar imports (unified router — Google or Microsoft) ───────────
17
18
  import {
18
19
  listMessages,
19
20
  getMessage,
20
21
  getUnreadImportant,
21
22
  sendEmail,
22
23
  createDraft,
23
- } from '../services/google-gmail.mjs';
24
-
25
- // ── Calendar imports ─────────────────────────────────────────────────────────
26
- import {
27
24
  getTodayEvents,
28
25
  getUpcomingEvents,
29
26
  getEventsForDate,
30
27
  createEvent,
31
28
  updateEvent,
32
- } from '../services/google-calendar.mjs';
29
+ } from '../services/mail-router.mjs';
33
30
 
34
31
  // ── Task imports ─────────────────────────────────────────────────────────────
35
32
  import {
@@ -497,8 +494,9 @@ async function handleSlashCommand(input, config, history) {
497
494
 
498
495
  if (trimmed === '/clear') {
499
496
  history.length = 0;
497
+ try { saveChatHistory([]); } catch { /* non-critical */ }
500
498
  console.clear();
501
- console.log(` ${G}Conversation cleared.${NC}`);
499
+ console.log(` ${G}Conversation cleared (memory preserved, chat history reset).${NC}`);
502
500
  return true;
503
501
  }
504
502
 
@@ -586,7 +584,11 @@ export async function cmdChat(args) {
586
584
  terminal: true,
587
585
  });
588
586
 
589
- const history = [];
587
+ // Load persisted chat history from previous sessions
588
+ const history = loadChatHistory();
589
+ if (history.length > 0) {
590
+ ok(`Loaded ${Math.floor(history.length / 2)} previous conversation turns from memory.`);
591
+ }
590
592
  const systemPrompt = buildSystemPrompt(initialContext);
591
593
 
592
594
  // ── Graceful exit ───────────────────────────────────────────────────────
@@ -699,6 +701,10 @@ export async function cmdChat(args) {
699
701
  history.shift();
700
702
  history.shift();
701
703
  }
704
+
705
+ // Persist chat history and extract episodic memory
706
+ try { saveChatHistory(history); } catch { /* non-critical */ }
707
+ try { extractMemory('chat', input, response); } catch { /* non-critical */ }
702
708
  } catch (err) {
703
709
  process.stdout.write('\r' + ' '.repeat(40) + '\r');
704
710
  console.log(`\n ${R}LLM error: ${err.message}${NC}\n`);
@@ -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
+ }
@@ -17,6 +17,20 @@ export async function cmdOps(args) {
17
17
  if (result.ok) {
18
18
  ok(`PAO daemon started (PID ${result.pid})`);
19
19
  info('Monitoring Gmail + Calendar. Notifications enabled.');
20
+
21
+ const config = loadConfig();
22
+ const hasTelegram = !!config.responder?.telegram?.token;
23
+ const hasDiscord = !!config.responder?.discord?.token;
24
+ if (hasTelegram || hasDiscord) {
25
+ const platforms = [hasTelegram && 'Telegram', hasDiscord && 'Discord'].filter(Boolean).join(' + ');
26
+ info(`Message responder active: ${platforms}`);
27
+ }
28
+
29
+ const proactive = config.ops?.proactive?.enabled !== false;
30
+ if (proactive) {
31
+ info('Proactive intelligence enabled (follow-ups, meeting prep, deadlines).');
32
+ }
33
+
20
34
  info('Run "nha ops status" to check. "nha ops stop" to halt.');
21
35
  } else {
22
36
  warn(result.message);
@@ -36,6 +50,8 @@ export async function cmdOps(args) {
36
50
 
37
51
  case 'status': {
38
52
  const status = getDaemonStatus();
53
+ const config = loadConfig();
54
+
39
55
  console.log(`\n ${BOLD}PAO Daemon Status${NC}\n`);
40
56
  console.log(` Running: ${status.running ? G + 'yes' + NC + ` (PID ${status.pid})` : R + 'no' + NC}`);
41
57
  if (status.startedAt) console.log(` Started: ${D}${status.startedAt}${NC}`);
@@ -43,6 +59,27 @@ export async function cmdOps(args) {
43
59
  if (status.lastCalendarCheck) console.log(` Last cal check: ${D}${status.lastCalendarCheck}${NC}`);
44
60
  if (status.lastPlanGenerated) console.log(` Last plan: ${D}${status.lastPlanGenerated}${NC}`);
45
61
  if (status.errors > 0) console.log(` Errors: ${Y}${status.errors}${NC}`);
62
+
63
+ // Proactive Intelligence Engine status
64
+ const proactive = status.proactive || {};
65
+ console.log(`\n ${BOLD}Proactive Intelligence${NC}\n`);
66
+ console.log(` Enabled: ${proactive.enabled !== false ? G + 'yes' + NC : D + 'no' + NC}`);
67
+ console.log(` Email follow-up: ${proactive.emailFollowUp !== false ? G + 'on' + NC : D + 'off' + NC}`);
68
+ console.log(` Meeting prep: ${proactive.meetingPrep !== false ? G + 'on' + NC : D + 'off' + NC}`);
69
+ console.log(` Pattern detect: ${proactive.patterns !== false ? G + 'on' + NC : D + 'off' + NC}`);
70
+ console.log(` Deadline alerts: ${proactive.deadlines !== false ? G + 'on' + NC : D + 'off' + NC}`);
71
+ if (status.lastProactiveCheck) console.log(` Last check: ${D}${status.lastProactiveCheck}${NC}`);
72
+ if (status.lastPatternDetection) console.log(` Last patterns: ${D}${status.lastPatternDetection}${NC}`);
73
+
74
+ // Message Responder status
75
+ const responder = status.responder || {};
76
+ const telegramConfigured = !!config.responder?.telegram?.token;
77
+ const discordConfigured = !!config.responder?.discord?.token;
78
+ console.log(`\n ${BOLD}Message Responder${NC}\n`);
79
+ console.log(` Telegram: ${responder.telegram ? G + 'active' + NC : telegramConfigured ? Y + 'configured (daemon restart needed)' + NC : D + 'not configured' + NC}`);
80
+ console.log(` Discord: ${responder.discord ? G + 'active' + NC : discordConfigured ? Y + 'configured (daemon restart needed)' + NC : D + 'not configured' + NC}`);
81
+ console.log(` Auto-route: ${config.responder?.autoRoute !== false ? G + 'keyword routing' + NC : D + 'CONDUCTOR only' + NC}`);
82
+
46
83
  console.log('');
47
84
  return;
48
85
  }
@@ -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
+ }