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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops.
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"description": "NotHumanAllowed — 38 AI agents for security, code, DevOps, data & daily ops. Per-agent memory, Telegram + Discord auto-responder, proactive intelligence daemon, voice chat, plugin system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nha": "./bin/nha.mjs",
|
|
@@ -31,7 +31,14 @@
|
|
|
31
31
|
"anthropic",
|
|
32
32
|
"openai",
|
|
33
33
|
"gemini",
|
|
34
|
-
"deepseek"
|
|
34
|
+
"deepseek",
|
|
35
|
+
"telegram-bot",
|
|
36
|
+
"discord-bot",
|
|
37
|
+
"voice-assistant",
|
|
38
|
+
"daily-ops",
|
|
39
|
+
"gmail",
|
|
40
|
+
"calendar",
|
|
41
|
+
"proactive"
|
|
35
42
|
],
|
|
36
43
|
"author": "Nicola Cucurachi <ados.labsproject@gmail.com>",
|
|
37
44
|
"license": "MIT",
|
package/src/cli.mjs
CHANGED
|
@@ -12,10 +12,14 @@ import { cmdAsk } from './commands/ask.mjs';
|
|
|
12
12
|
import { cmdPlan } from './commands/plan.mjs';
|
|
13
13
|
import { cmdTasks } from './commands/tasks.mjs';
|
|
14
14
|
import { cmdOps } from './commands/ops.mjs';
|
|
15
|
+
import { cmdAutostart } from './commands/autostart.mjs';
|
|
15
16
|
import { cmdChat } from './commands/chat.mjs';
|
|
16
17
|
import { cmdUI } from './commands/ui.mjs';
|
|
17
18
|
import { cmdGoogle } from './commands/google-auth.mjs';
|
|
19
|
+
import { cmdMicrosoft } from './commands/microsoft-auth.mjs';
|
|
18
20
|
import { cmdScan } from './commands/scan.mjs';
|
|
21
|
+
import { cmdVoice } from './commands/voice.mjs';
|
|
22
|
+
import { cmdPlugin, findPluginForCommand } from './commands/plugin.mjs';
|
|
19
23
|
import { banner, info, ok, warn, fail, C, G, Y, D, W, BOLD, NC, M, B, R } from './ui.mjs';
|
|
20
24
|
|
|
21
25
|
export async function main(argv) {
|
|
@@ -57,6 +61,12 @@ export async function main(argv) {
|
|
|
57
61
|
case 'ops':
|
|
58
62
|
return cmdOps(args);
|
|
59
63
|
|
|
64
|
+
case 'autostart':
|
|
65
|
+
return cmdAutostart(args);
|
|
66
|
+
|
|
67
|
+
case 'responder':
|
|
68
|
+
return cmdResponder(args);
|
|
69
|
+
|
|
60
70
|
case 'chat':
|
|
61
71
|
return cmdChat(args);
|
|
62
72
|
|
|
@@ -66,9 +76,21 @@ export async function main(argv) {
|
|
|
66
76
|
case 'google':
|
|
67
77
|
return cmdGoogle(args);
|
|
68
78
|
|
|
79
|
+
case 'microsoft':
|
|
80
|
+
case 'ms':
|
|
81
|
+
case 'outlook':
|
|
82
|
+
return cmdMicrosoft(args);
|
|
83
|
+
|
|
69
84
|
case 'scan':
|
|
70
85
|
return cmdScan(args);
|
|
71
86
|
|
|
87
|
+
case 'voice':
|
|
88
|
+
return cmdVoice(args);
|
|
89
|
+
|
|
90
|
+
case 'plugin':
|
|
91
|
+
case 'plugins':
|
|
92
|
+
return cmdPlugin(args);
|
|
93
|
+
|
|
72
94
|
case 'pif':
|
|
73
95
|
return cmdPif(args);
|
|
74
96
|
|
|
@@ -103,9 +125,83 @@ export async function main(argv) {
|
|
|
103
125
|
case '-h':
|
|
104
126
|
return cmdHelp();
|
|
105
127
|
|
|
106
|
-
default:
|
|
128
|
+
default: {
|
|
129
|
+
// Check if a plugin handles this command before falling through to Legion
|
|
130
|
+
const pluginMatch = await findPluginForCommand(cmd);
|
|
131
|
+
if (pluginMatch && pluginMatch.plugin.run) {
|
|
132
|
+
const { cmdPlugin: runPlugin } = await import('./commands/plugin.mjs');
|
|
133
|
+
return runPlugin(['run', cmd, ...args]);
|
|
134
|
+
}
|
|
107
135
|
// Try as Legion command passthrough
|
|
108
136
|
return spawnCore('legion', [cmd, ...args]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── nha responder ─────────────────────────────────────────────────────────
|
|
142
|
+
async function cmdResponder(args) {
|
|
143
|
+
const sub = args[0] || 'status';
|
|
144
|
+
const config = loadConfig();
|
|
145
|
+
|
|
146
|
+
switch (sub) {
|
|
147
|
+
case 'start': {
|
|
148
|
+
const { isRunning } = await import('./services/ops-daemon.mjs');
|
|
149
|
+
if (!isRunning()) {
|
|
150
|
+
warn('Daemon is not running. The responder runs inside the daemon.');
|
|
151
|
+
info('Start it with: nha ops start');
|
|
152
|
+
info('Or enable autostart: nha autostart enable');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
info('The responder starts automatically with the daemon when tokens are configured.');
|
|
156
|
+
info('Configure tokens:');
|
|
157
|
+
console.log(' nha config set telegram-bot-token YOUR_BOT_TOKEN');
|
|
158
|
+
console.log(' nha config set discord-bot-token YOUR_BOT_TOKEN');
|
|
159
|
+
info('Then restart the daemon: nha ops stop && nha ops start');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'stop': {
|
|
164
|
+
info('The responder stops when the daemon stops.');
|
|
165
|
+
info('Run: nha ops stop');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'status': {
|
|
170
|
+
console.log(`\n ${BOLD}Message Responder Status${NC}\n`);
|
|
171
|
+
|
|
172
|
+
const telegramToken = config.responder?.telegram?.token;
|
|
173
|
+
const discordToken = config.responder?.discord?.token;
|
|
174
|
+
const autoRoute = config.responder?.autoRoute !== false;
|
|
175
|
+
|
|
176
|
+
console.log(` Telegram: ${telegramToken ? G + 'configured' + NC : D + '(not set)' + NC}`);
|
|
177
|
+
if (telegramToken) {
|
|
178
|
+
const chatIds = config.responder?.telegram?.allowedChatIds || [];
|
|
179
|
+
console.log(` Chat filter: ${chatIds.length > 0 ? Y + chatIds.join(', ') + NC : D + 'all chats' + NC}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(` Discord: ${discordToken ? G + 'configured' + NC : D + '(not set)' + NC}`);
|
|
183
|
+
if (discordToken) {
|
|
184
|
+
const channelIds = config.responder?.discord?.allowedChannelIds || [];
|
|
185
|
+
console.log(` Channel filter: ${channelIds.length > 0 ? Y + channelIds.join(', ') + NC : D + 'all channels (mention/command only)' + NC}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(` Auto-route: ${autoRoute ? G + 'keyword routing' + NC : D + 'CONDUCTOR only' + NC}`);
|
|
189
|
+
|
|
190
|
+
const { isRunning: isDaemonRunning } = await import('./services/ops-daemon.mjs');
|
|
191
|
+
console.log(` Daemon: ${isDaemonRunning() ? G + 'running' + NC : R + 'stopped' + NC}`);
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
if (!telegramToken && !discordToken) {
|
|
195
|
+
info('Configure a bot token to enable:');
|
|
196
|
+
console.log(' nha config set telegram-bot-token YOUR_TOKEN');
|
|
197
|
+
console.log(' nha config set discord-bot-token YOUR_TOKEN');
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
default:
|
|
203
|
+
fail(`Unknown: nha responder ${sub}`);
|
|
204
|
+
info('Commands: start, stop, status');
|
|
109
205
|
}
|
|
110
206
|
}
|
|
111
207
|
|
|
@@ -228,6 +324,10 @@ function cmdConfig(args) {
|
|
|
228
324
|
console.log('');
|
|
229
325
|
info('Keys: provider, key, openai-key, gemini-key, deepseek-key, grok-key, model, timeout');
|
|
230
326
|
info(' verbose, immersive, deliberation, rounds, convergence, tribunal, knowledge');
|
|
327
|
+
info(' google-client-id, google-client-secret');
|
|
328
|
+
info(' microsoft-client-id, microsoft-client-secret, microsoft-tenant');
|
|
329
|
+
info(' telegram-bot-token, discord-bot-token, responder-auto-route');
|
|
330
|
+
info(' proactive, proactive-email, proactive-meeting, proactive-patterns, proactive-deadlines');
|
|
231
331
|
return;
|
|
232
332
|
}
|
|
233
333
|
const success = setConfigValue(key, value);
|
|
@@ -240,9 +340,8 @@ function cmdConfig(args) {
|
|
|
240
340
|
}
|
|
241
341
|
|
|
242
342
|
if (sub === 'reset') {
|
|
243
|
-
const { saveConfig } = loadConfig; // re-import
|
|
244
343
|
fs.rmSync(path.join(NHA_DIR, 'config.json'), { force: true });
|
|
245
|
-
ok('Config reset to defaults');
|
|
344
|
+
ok('Config reset to defaults. Run any command to regenerate.');
|
|
246
345
|
return;
|
|
247
346
|
}
|
|
248
347
|
|
|
@@ -273,10 +372,48 @@ function cmdConfig(args) {
|
|
|
273
372
|
console.log(` ${D}(not registered — run "nha pif register")${NC}`);
|
|
274
373
|
}
|
|
275
374
|
|
|
375
|
+
console.log(`\n ${C}Integrations${NC}`);
|
|
376
|
+
console.log(` Google: ${config.google?.clientId ? G + 'configured' : D + '(not set)'}${NC}`);
|
|
377
|
+
console.log(` Microsoft: ${config.microsoft?.clientId ? G + 'configured' : D + '(not set)'}${NC}`);
|
|
378
|
+
if (config.microsoft?.tenantId && config.microsoft.tenantId !== 'common') {
|
|
379
|
+
console.log(` MS Tenant: ${D}${config.microsoft.tenantId}${NC}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
276
382
|
console.log(`\n ${C}Features${NC}`);
|
|
277
383
|
for (const [k, v] of Object.entries(config.features)) {
|
|
278
384
|
console.log(` ${k.padEnd(28)} ${v ? G + '✓' : D + '○'}${NC}`);
|
|
279
385
|
}
|
|
386
|
+
|
|
387
|
+
if (config.plugins) {
|
|
388
|
+
console.log(`\n ${C}Plugins${NC}`);
|
|
389
|
+
console.log(` Auto-run: ${config.plugins.autoRun ? G + 'yes' : D + 'no'}${NC}`);
|
|
390
|
+
if (config.plugins.directory) console.log(` Directory: ${D}${config.plugins.directory}${NC}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (config.voice) {
|
|
394
|
+
console.log(`\n ${C}Voice${NC}`);
|
|
395
|
+
console.log(` Prefer Whisper: ${config.voice.preferWhisper ? G + 'yes' : D + 'no'}${NC}`);
|
|
396
|
+
console.log(` Speech Synth: ${config.voice.speechSynthesis ? G + 'yes' : D + 'no'}${NC}`);
|
|
397
|
+
if (config.voice.language) console.log(` Language: ${D}${config.voice.language}${NC}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (config.responder) {
|
|
401
|
+
console.log(`\n ${C}Message Responder${NC}`);
|
|
402
|
+
console.log(` Telegram: ${config.responder.telegram?.token ? G + 'configured' : D + '(not set)'}${NC}`);
|
|
403
|
+
console.log(` Discord: ${config.responder.discord?.token ? G + 'configured' : D + '(not set)'}${NC}`);
|
|
404
|
+
console.log(` Auto-route: ${config.responder.autoRoute !== false ? G + 'yes' : D + 'no'}${NC}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const proactive = config.ops?.proactive;
|
|
408
|
+
if (proactive) {
|
|
409
|
+
console.log(`\n ${C}Proactive Intelligence${NC}`);
|
|
410
|
+
console.log(` Enabled: ${proactive.enabled !== false ? G + 'yes' : D + 'no'}${NC}`);
|
|
411
|
+
console.log(` Email follow-up:${proactive.emailFollowUp !== false ? G + ' on' : D + ' off'}${NC}`);
|
|
412
|
+
console.log(` Meeting prep: ${proactive.meetingPrep !== false ? G + 'on' : D + 'off'}${NC}`);
|
|
413
|
+
console.log(` Patterns: ${proactive.patterns !== false ? G + 'on' : D + 'off'}${NC}`);
|
|
414
|
+
console.log(` Deadlines: ${proactive.deadlines !== false ? G + 'on' : D + 'off'}${NC}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
280
417
|
console.log('');
|
|
281
418
|
}
|
|
282
419
|
|
|
@@ -304,6 +441,14 @@ async function cmdDoctor() {
|
|
|
304
441
|
const extCount = EXTENSIONS.filter(e => fs.existsSync(path.join(EXTENSIONS_DIR, `${e}.mjs`))).length;
|
|
305
442
|
console.log(` Extensions: ${D}${extCount}/${EXTENSIONS.length} installed${NC}`);
|
|
306
443
|
|
|
444
|
+
// Check plugins
|
|
445
|
+
const pluginsDir = path.join(NHA_DIR, 'plugins');
|
|
446
|
+
let pluginCount = 0;
|
|
447
|
+
if (fs.existsSync(pluginsDir)) {
|
|
448
|
+
pluginCount = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.mjs')).length;
|
|
449
|
+
}
|
|
450
|
+
console.log(` Plugins: ${D}${pluginCount} installed${NC}`);
|
|
451
|
+
|
|
307
452
|
// Check API key
|
|
308
453
|
console.log(` API Key: ${config.llm.apiKey ? G + 'configured (' + config.llm.provider + ')' : R + 'NOT SET'}${NC}`);
|
|
309
454
|
|
|
@@ -368,26 +513,57 @@ function cmdHelp() {
|
|
|
368
513
|
console.log(` ui Open local web dashboard (http://127.0.0.1:3847)`);
|
|
369
514
|
console.log(` ui --port=4000 Custom port ui --no-browser Don't auto-open`);
|
|
370
515
|
console.log(` chat Interactive chat — manage email/calendar/tasks naturally`);
|
|
516
|
+
console.log(` voice Voice-powered chat (opens browser with mic interface)`);
|
|
517
|
+
console.log(` voice ${D}--port=3849${NC} Custom port voice ${D}--no-browser${NC}`);
|
|
371
518
|
console.log(` plan Generate daily plan (5 agents analyze your day)`);
|
|
372
519
|
console.log(` plan --refresh Regenerate today's plan`);
|
|
373
520
|
console.log(` tasks List today's tasks`);
|
|
374
521
|
console.log(` tasks add "desc" Add a task`);
|
|
375
522
|
console.log(` tasks done 3 Complete task #3`);
|
|
376
523
|
console.log(` tasks week Week overview`);
|
|
377
|
-
console.log(` ops start Start background daemon (auto-alerts)`);
|
|
524
|
+
console.log(` ops start Start background daemon (auto-alerts + WebSocket)`);
|
|
378
525
|
console.log(` ops stop Stop daemon`);
|
|
379
|
-
console.log(` ops status Daemon status
|
|
526
|
+
console.log(` ops status Daemon status`);
|
|
527
|
+
console.log(` autostart enable Auto-start daemon on login (launchd/systemd)`);
|
|
528
|
+
console.log(` autostart disable Remove OS autostart`);
|
|
529
|
+
console.log(` autostart status Check autostart configuration\n`);
|
|
530
|
+
|
|
531
|
+
console.log(` ${C}Message Responder${NC} ${D}(Telegram + Discord auto-reply)${NC}`);
|
|
532
|
+
console.log(` responder status Show responder configuration`);
|
|
533
|
+
console.log(` config set telegram-bot-token TOKEN`);
|
|
534
|
+
console.log(` config set discord-bot-token TOKEN`);
|
|
535
|
+
console.log(` ${D}Routes messages to agents via keyword matching (zero LLM overhead)${NC}\n`);
|
|
536
|
+
|
|
537
|
+
console.log(` ${C}Proactive Intelligence${NC} ${D}(runs inside daemon)${NC}`);
|
|
538
|
+
console.log(` ${D}Email follow-ups, meeting prep, pattern detection, deadline tracking${NC}`);
|
|
539
|
+
console.log(` config set proactive true/false Toggle all proactive features`);
|
|
540
|
+
console.log(` config set proactive-email true/false Email follow-up reminders`);
|
|
541
|
+
console.log(` config set proactive-meeting true/false Auto meeting briefs`);
|
|
542
|
+
console.log(` config set proactive-deadlines true/false Deadline alerts\n`);
|
|
380
543
|
|
|
381
544
|
console.log(` ${C}Google Integration${NC}`);
|
|
382
545
|
console.log(` google auth Connect Gmail + Calendar`);
|
|
383
546
|
console.log(` google status Connection status`);
|
|
384
547
|
console.log(` google revoke Disconnect\n`);
|
|
385
548
|
|
|
549
|
+
console.log(` ${C}Microsoft Integration${NC} ${D}(Outlook Mail + Calendar)${NC}`);
|
|
550
|
+
console.log(` microsoft auth Connect Outlook + Calendar`);
|
|
551
|
+
console.log(` microsoft status Connection status`);
|
|
552
|
+
console.log(` microsoft revoke Disconnect`);
|
|
553
|
+
console.log(` ${D}Aliases: ms, outlook${NC}\n`);
|
|
554
|
+
|
|
386
555
|
console.log(` ${C}Extensions${NC} ${D}(downloadable agent modules)${NC}`);
|
|
387
556
|
console.log(` install <name> Install an extension agent`);
|
|
388
557
|
console.log(` install --all Install all ${EXTENSIONS.length} extensions`);
|
|
389
558
|
console.log(` extensions List installed extensions\n`);
|
|
390
559
|
|
|
560
|
+
console.log(` ${C}Plugins${NC} ${D}(user-extensible commands)${NC}`);
|
|
561
|
+
console.log(` plugin list List installed & available plugins`);
|
|
562
|
+
console.log(` plugin install <name> Download a plugin from NHA server`);
|
|
563
|
+
console.log(` plugin run <name> Execute a plugin`);
|
|
564
|
+
console.log(` plugin create <name> Scaffold a new plugin from template`);
|
|
565
|
+
console.log(` plugin remove <name> Remove an installed plugin\n`);
|
|
566
|
+
|
|
391
567
|
console.log(` ${C}Social Network${NC} ${D}(NHA platform)${NC}`);
|
|
392
568
|
console.log(` pif register Register your agent identity`);
|
|
393
569
|
console.log(` pif post Post content`);
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nha autostart — OS-level daemon autostart management.
|
|
3
|
+
*
|
|
4
|
+
* macOS: launchd plist at ~/Library/LaunchAgents/com.nha.daemon.plist
|
|
5
|
+
* Linux: systemd user service at ~/.config/systemd/user/nha-daemon.service
|
|
6
|
+
*
|
|
7
|
+
* Zero dependencies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { execSync, spawnSync } from 'child_process';
|
|
14
|
+
import { NHA_DIR, DAEMON_SCRIPT } from '../constants.mjs';
|
|
15
|
+
import { info, ok, fail, warn, C, G, Y, D, W, BOLD, NC, R } from '../ui.mjs';
|
|
16
|
+
|
|
17
|
+
const PLATFORM = os.platform();
|
|
18
|
+
const HOME = os.homedir();
|
|
19
|
+
|
|
20
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
// DAEMON_SCRIPT is resolved relative to the installed package (via constants.mjs),
|
|
23
|
+
// so it works correctly both in development and after npm install -g.
|
|
24
|
+
const DAEMON_SCRIPT_ABS = DAEMON_SCRIPT;
|
|
25
|
+
const NODE_BIN = process.execPath;
|
|
26
|
+
|
|
27
|
+
const LAUNCHD_LABEL = 'com.nha.daemon';
|
|
28
|
+
const LAUNCHD_PLIST = path.join(HOME, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
29
|
+
const LAUNCHD_LOG_DIR = path.join(NHA_DIR, 'ops', 'daemon');
|
|
30
|
+
|
|
31
|
+
const SYSTEMD_DIR = path.join(HOME, '.config', 'systemd', 'user');
|
|
32
|
+
const SYSTEMD_UNIT = 'nha-daemon.service';
|
|
33
|
+
const SYSTEMD_FILE = path.join(SYSTEMD_DIR, SYSTEMD_UNIT);
|
|
34
|
+
|
|
35
|
+
// ── launchd (macOS) ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function generateLaunchdPlist() {
|
|
38
|
+
// launchd XML plist — KeepAlive restarts on crash, RunAtLoad starts on login
|
|
39
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
|
+
<plist version="1.0">
|
|
42
|
+
<dict>
|
|
43
|
+
<key>Label</key>
|
|
44
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
45
|
+
|
|
46
|
+
<key>ProgramArguments</key>
|
|
47
|
+
<array>
|
|
48
|
+
<string>${NODE_BIN}</string>
|
|
49
|
+
<string>${DAEMON_SCRIPT_ABS}</string>
|
|
50
|
+
<string>--daemon-loop</string>
|
|
51
|
+
</array>
|
|
52
|
+
|
|
53
|
+
<key>RunAtLoad</key>
|
|
54
|
+
<true/>
|
|
55
|
+
|
|
56
|
+
<key>KeepAlive</key>
|
|
57
|
+
<dict>
|
|
58
|
+
<key>SuccessfulExit</key>
|
|
59
|
+
<false/>
|
|
60
|
+
</dict>
|
|
61
|
+
|
|
62
|
+
<key>ThrottleInterval</key>
|
|
63
|
+
<integer>10</integer>
|
|
64
|
+
|
|
65
|
+
<key>StandardOutPath</key>
|
|
66
|
+
<string>${path.join(LAUNCHD_LOG_DIR, 'daemon.log')}</string>
|
|
67
|
+
|
|
68
|
+
<key>StandardErrorPath</key>
|
|
69
|
+
<string>${path.join(LAUNCHD_LOG_DIR, 'daemon.log')}</string>
|
|
70
|
+
|
|
71
|
+
<key>EnvironmentVariables</key>
|
|
72
|
+
<dict>
|
|
73
|
+
<key>NHA_DAEMON</key>
|
|
74
|
+
<string>1</string>
|
|
75
|
+
<key>HOME</key>
|
|
76
|
+
<string>${HOME}</string>
|
|
77
|
+
<key>PATH</key>
|
|
78
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${path.dirname(NODE_BIN)}</string>
|
|
79
|
+
</dict>
|
|
80
|
+
|
|
81
|
+
<key>ProcessType</key>
|
|
82
|
+
<string>Background</string>
|
|
83
|
+
|
|
84
|
+
<key>LowPriorityIO</key>
|
|
85
|
+
<true/>
|
|
86
|
+
</dict>
|
|
87
|
+
</plist>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function launchdInstall() {
|
|
92
|
+
const agentsDir = path.join(HOME, 'Library', 'LaunchAgents');
|
|
93
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
94
|
+
fs.mkdirSync(LAUNCHD_LOG_DIR, { recursive: true });
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(LAUNCHD_PLIST, generateLaunchdPlist(), { mode: 0o644 });
|
|
97
|
+
|
|
98
|
+
// Load the agent (starts it immediately)
|
|
99
|
+
const result = spawnSync('launchctl', ['load', '-w', LAUNCHD_PLIST], { encoding: 'utf-8' });
|
|
100
|
+
if (result.status !== 0) {
|
|
101
|
+
const stderr = (result.stderr || '').trim();
|
|
102
|
+
// Already loaded is not a real error
|
|
103
|
+
if (!stderr.includes('already loaded') && !stderr.includes('service already loaded')) {
|
|
104
|
+
throw new Error(`launchctl load failed: ${stderr}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function launchdUninstall() {
|
|
110
|
+
if (!fs.existsSync(LAUNCHD_PLIST)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Unload (stops the daemon)
|
|
115
|
+
spawnSync('launchctl', ['unload', '-w', LAUNCHD_PLIST], { encoding: 'utf-8' });
|
|
116
|
+
|
|
117
|
+
fs.rmSync(LAUNCHD_PLIST, { force: true });
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function launchdStatus() {
|
|
122
|
+
const installed = fs.existsSync(LAUNCHD_PLIST);
|
|
123
|
+
if (!installed) {
|
|
124
|
+
return { installed: false, running: false, pid: null };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if service is loaded and running
|
|
128
|
+
const result = spawnSync('launchctl', ['list'], { encoding: 'utf-8' });
|
|
129
|
+
const lines = (result.stdout || '').split('\n');
|
|
130
|
+
const match = lines.find(l => l.includes(LAUNCHD_LABEL));
|
|
131
|
+
|
|
132
|
+
if (!match) {
|
|
133
|
+
return { installed: true, running: false, pid: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Format: PID Status Label
|
|
137
|
+
const parts = match.trim().split(/\s+/);
|
|
138
|
+
const pid = parts[0] === '-' ? null : parseInt(parts[0], 10);
|
|
139
|
+
const running = pid !== null && !isNaN(pid);
|
|
140
|
+
|
|
141
|
+
return { installed: true, running, pid };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── systemd (Linux) ──────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function generateSystemdUnit() {
|
|
147
|
+
return `[Unit]
|
|
148
|
+
Description=NHA PAO Background Daemon
|
|
149
|
+
Documentation=https://nothumanallowed.com/docs/cli
|
|
150
|
+
After=network-online.target
|
|
151
|
+
Wants=network-online.target
|
|
152
|
+
|
|
153
|
+
[Service]
|
|
154
|
+
Type=simple
|
|
155
|
+
ExecStart=${NODE_BIN} ${DAEMON_SCRIPT_ABS} --daemon-loop
|
|
156
|
+
Environment=NHA_DAEMON=1
|
|
157
|
+
Environment=HOME=${HOME}
|
|
158
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${path.dirname(NODE_BIN)}
|
|
159
|
+
|
|
160
|
+
Restart=on-failure
|
|
161
|
+
RestartSec=10
|
|
162
|
+
StartLimitIntervalSec=300
|
|
163
|
+
StartLimitBurst=5
|
|
164
|
+
|
|
165
|
+
StandardOutput=append:${path.join(LAUNCHD_LOG_DIR, 'daemon.log')}
|
|
166
|
+
StandardError=append:${path.join(LAUNCHD_LOG_DIR, 'daemon.log')}
|
|
167
|
+
|
|
168
|
+
# Security hardening
|
|
169
|
+
NoNewPrivileges=true
|
|
170
|
+
ProtectSystem=strict
|
|
171
|
+
ProtectHome=read-only
|
|
172
|
+
ReadWritePaths=${NHA_DIR}
|
|
173
|
+
|
|
174
|
+
[Install]
|
|
175
|
+
WantedBy=default.target
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function systemdInstall() {
|
|
180
|
+
fs.mkdirSync(SYSTEMD_DIR, { recursive: true });
|
|
181
|
+
fs.mkdirSync(LAUNCHD_LOG_DIR, { recursive: true });
|
|
182
|
+
|
|
183
|
+
fs.writeFileSync(SYSTEMD_FILE, generateSystemdUnit(), { mode: 0o644 });
|
|
184
|
+
|
|
185
|
+
// Reload systemd user daemon to pick up new unit
|
|
186
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
187
|
+
|
|
188
|
+
// Enable (auto-start on login) and start immediately
|
|
189
|
+
const enableResult = spawnSync('systemctl', ['--user', 'enable', '--now', SYSTEMD_UNIT], {
|
|
190
|
+
encoding: 'utf-8',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (enableResult.status !== 0) {
|
|
194
|
+
const stderr = (enableResult.stderr || '').trim();
|
|
195
|
+
throw new Error(`systemctl enable failed: ${stderr}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Enable lingering so the user service runs even when not logged in (optional, best-effort)
|
|
199
|
+
spawnSync('loginctl', ['enable-linger', os.userInfo().username], { encoding: 'utf-8' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function systemdUninstall() {
|
|
203
|
+
if (!fs.existsSync(SYSTEMD_FILE)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Stop and disable
|
|
208
|
+
spawnSync('systemctl', ['--user', 'disable', '--now', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
209
|
+
|
|
210
|
+
fs.rmSync(SYSTEMD_FILE, { force: true });
|
|
211
|
+
|
|
212
|
+
// Reload to clean up
|
|
213
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function systemdStatus() {
|
|
219
|
+
const installed = fs.existsSync(SYSTEMD_FILE);
|
|
220
|
+
if (!installed) {
|
|
221
|
+
return { installed: false, running: false, pid: null };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = spawnSync('systemctl', ['--user', 'is-active', SYSTEMD_UNIT], {
|
|
225
|
+
encoding: 'utf-8',
|
|
226
|
+
});
|
|
227
|
+
const active = (result.stdout || '').trim() === 'active';
|
|
228
|
+
|
|
229
|
+
let pid = null;
|
|
230
|
+
if (active) {
|
|
231
|
+
const showResult = spawnSync('systemctl', ['--user', 'show', SYSTEMD_UNIT, '--property=MainPID'], {
|
|
232
|
+
encoding: 'utf-8',
|
|
233
|
+
});
|
|
234
|
+
const pidMatch = (showResult.stdout || '').match(/MainPID=(\d+)/);
|
|
235
|
+
if (pidMatch) pid = parseInt(pidMatch[1], 10);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { installed: true, running: active, pid };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Platform Dispatcher ──────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
function getPlatformAdapter() {
|
|
244
|
+
if (PLATFORM === 'darwin') {
|
|
245
|
+
return { install: launchdInstall, uninstall: launchdUninstall, status: launchdStatus, name: 'launchd' };
|
|
246
|
+
}
|
|
247
|
+
if (PLATFORM === 'linux') {
|
|
248
|
+
return { install: systemdInstall, uninstall: systemdUninstall, status: systemdStatus, name: 'systemd' };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Command Handler ──────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
export async function cmdAutostart(args) {
|
|
256
|
+
const sub = args[0] || 'status';
|
|
257
|
+
const adapter = getPlatformAdapter();
|
|
258
|
+
|
|
259
|
+
if (!adapter) {
|
|
260
|
+
fail(`Autostart is not supported on ${PLATFORM}`);
|
|
261
|
+
info('Supported platforms: macOS (launchd), Linux (systemd)');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (sub) {
|
|
266
|
+
case 'enable': {
|
|
267
|
+
// Verify the daemon script exists
|
|
268
|
+
if (!fs.existsSync(DAEMON_SCRIPT_ABS)) {
|
|
269
|
+
fail(`Daemon script not found at: ${DAEMON_SCRIPT_ABS}`);
|
|
270
|
+
info('Run "nha update" to re-download core files.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const currentStatus = adapter.status();
|
|
275
|
+
if (currentStatus.installed && currentStatus.running) {
|
|
276
|
+
warn('Autostart is already enabled and running.');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
adapter.install();
|
|
282
|
+
const newStatus = adapter.status();
|
|
283
|
+
ok(`Autostart enabled via ${adapter.name}`);
|
|
284
|
+
if (newStatus.running) {
|
|
285
|
+
ok(`Daemon started (PID ${newStatus.pid})`);
|
|
286
|
+
} else {
|
|
287
|
+
info('Daemon will start on next login.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (adapter.name === 'launchd') {
|
|
291
|
+
info(`Plist: ${LAUNCHD_PLIST}`);
|
|
292
|
+
} else {
|
|
293
|
+
info(`Unit: ${SYSTEMD_FILE}`);
|
|
294
|
+
}
|
|
295
|
+
info('The daemon auto-restarts on crash (10s cooldown).');
|
|
296
|
+
} catch (err) {
|
|
297
|
+
fail(`Failed to enable autostart: ${err.message}`);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'disable': {
|
|
303
|
+
try {
|
|
304
|
+
const removed = adapter.uninstall();
|
|
305
|
+
if (removed) {
|
|
306
|
+
ok('Autostart disabled. Daemon stopped and service removed.');
|
|
307
|
+
} else {
|
|
308
|
+
warn('Autostart was not enabled.');
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
fail(`Failed to disable autostart: ${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'status': {
|
|
317
|
+
const status = adapter.status();
|
|
318
|
+
console.log(`\n ${BOLD}Autostart Status${NC} ${D}(${adapter.name})${NC}\n`);
|
|
319
|
+
console.log(` Installed: ${status.installed ? G + 'yes' + NC : D + 'no' + NC}`);
|
|
320
|
+
console.log(` Running: ${status.running ? G + 'yes' + NC + (status.pid ? ` (PID ${status.pid})` : '') : R + 'no' + NC}`);
|
|
321
|
+
|
|
322
|
+
if (adapter.name === 'launchd') {
|
|
323
|
+
console.log(` Plist: ${D}${LAUNCHD_PLIST}${NC}`);
|
|
324
|
+
} else {
|
|
325
|
+
console.log(` Unit: ${D}${SYSTEMD_FILE}${NC}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(` Daemon: ${D}${DAEMON_SCRIPT_ABS}${NC}`);
|
|
329
|
+
console.log(` Node: ${D}${NODE_BIN}${NC}`);
|
|
330
|
+
console.log('');
|
|
331
|
+
|
|
332
|
+
if (!status.installed) {
|
|
333
|
+
info('Enable with: nha autostart enable');
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
fail(`Unknown: nha autostart ${sub}`);
|
|
340
|
+
info('Commands: enable, disable, status');
|
|
341
|
+
}
|
|
342
|
+
}
|