nothumanallowed 4.0.2 → 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 +1 -1
- package/src/cli.mjs +70 -2
- package/src/commands/chat.mjs +2 -6
- package/src/commands/microsoft-auth.mjs +29 -0
- package/src/commands/plugin.mjs +481 -0
- package/src/commands/ui.mjs +6 -2
- package/src/commands/voice.mjs +845 -0
- package/src/config.mjs +23 -0
- package/src/constants.mjs +2 -1
- package/src/services/mail-router.mjs +298 -0
- package/src/services/microsoft-calendar.mjs +319 -0
- package/src/services/microsoft-mail.mjs +308 -0
- package/src/services/microsoft-oauth.mjs +345 -0
- package/src/services/ops-daemon.mjs +159 -5
- package/src/services/ops-pipeline.mjs +7 -8
- package/src/services/token-store.mjs +41 -14
- package/src/services/web-ui.mjs +238 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "
|
|
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`);
|
package/src/commands/chat.mjs
CHANGED
|
@@ -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
|
-
// ──
|
|
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/
|
|
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
|
+
}
|