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.
- package/package.json +10 -3
- package/src/cli.mjs +181 -5
- package/src/commands/autostart.mjs +342 -0
- package/src/commands/chat.mjs +14 -8
- package/src/commands/microsoft-auth.mjs +29 -0
- package/src/commands/ops.mjs +37 -0
- package/src/commands/plugin.mjs +481 -0
- package/src/commands/ui.mjs +28 -7
- package/src/commands/voice.mjs +845 -0
- package/src/config.mjs +61 -0
- package/src/constants.mjs +9 -1
- package/src/services/llm.mjs +22 -1
- package/src/services/mail-router.mjs +298 -0
- package/src/services/memory.mjs +627 -0
- package/src/services/message-responder.mjs +778 -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 +620 -11
- package/src/services/ops-pipeline.mjs +7 -8
- package/src/services/token-store.mjs +41 -14
- package/src/services/tool-executor.mjs +392 -0
- package/src/services/web-ui.mjs +187 -1
package/src/commands/chat.mjs
CHANGED
|
@@ -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
|
-
// ──
|
|
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/
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/ops.mjs
CHANGED
|
@@ -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
|
+
}
|