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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "4.1.0",
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.",
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\n`);
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
+ }