nothumanallowed 5.0.0 → 6.0.1
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 +111 -3
- package/src/commands/autostart.mjs +342 -0
- package/src/commands/chat.mjs +12 -2
- package/src/commands/ops.mjs +37 -0
- package/src/commands/ui.mjs +22 -5
- package/src/config.mjs +38 -0
- package/src/constants.mjs +8 -1
- package/src/services/llm.mjs +22 -1
- package/src/services/memory.mjs +627 -0
- package/src/services/message-responder.mjs +778 -0
- package/src/services/ops-daemon.mjs +463 -9
- package/src/services/tool-executor.mjs +392 -0
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.1",
|
|
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,6 +12,7 @@ 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';
|
|
@@ -60,6 +61,12 @@ export async function main(argv) {
|
|
|
60
61
|
case 'ops':
|
|
61
62
|
return cmdOps(args);
|
|
62
63
|
|
|
64
|
+
case 'autostart':
|
|
65
|
+
return cmdAutostart(args);
|
|
66
|
+
|
|
67
|
+
case 'responder':
|
|
68
|
+
return cmdResponder(args);
|
|
69
|
+
|
|
63
70
|
case 'chat':
|
|
64
71
|
return cmdChat(args);
|
|
65
72
|
|
|
@@ -131,6 +138,73 @@ export async function main(argv) {
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
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');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
134
208
|
// ── nha run ────────────────────────────────────────────────────────────────
|
|
135
209
|
async function cmdRun(args) {
|
|
136
210
|
if (args.length === 0) {
|
|
@@ -252,6 +326,8 @@ function cmdConfig(args) {
|
|
|
252
326
|
info(' verbose, immersive, deliberation, rounds, convergence, tribunal, knowledge');
|
|
253
327
|
info(' google-client-id, google-client-secret');
|
|
254
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');
|
|
255
331
|
return;
|
|
256
332
|
}
|
|
257
333
|
const success = setConfigValue(key, value);
|
|
@@ -264,9 +340,8 @@ function cmdConfig(args) {
|
|
|
264
340
|
}
|
|
265
341
|
|
|
266
342
|
if (sub === 'reset') {
|
|
267
|
-
const { saveConfig } = loadConfig; // re-import
|
|
268
343
|
fs.rmSync(path.join(NHA_DIR, 'config.json'), { force: true });
|
|
269
|
-
ok('Config reset to defaults');
|
|
344
|
+
ok('Config reset to defaults. Run any command to regenerate.');
|
|
270
345
|
return;
|
|
271
346
|
}
|
|
272
347
|
|
|
@@ -322,6 +397,23 @@ function cmdConfig(args) {
|
|
|
322
397
|
if (config.voice.language) console.log(` Language: ${D}${config.voice.language}${NC}`);
|
|
323
398
|
}
|
|
324
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
|
+
|
|
325
417
|
console.log('');
|
|
326
418
|
}
|
|
327
419
|
|
|
@@ -431,7 +523,23 @@ function cmdHelp() {
|
|
|
431
523
|
console.log(` tasks week Week overview`);
|
|
432
524
|
console.log(` ops start Start background daemon (auto-alerts + WebSocket)`);
|
|
433
525
|
console.log(` ops stop Stop daemon`);
|
|
434
|
-
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`);
|
|
435
543
|
|
|
436
544
|
console.log(` ${C}Google Integration${NC}`);
|
|
437
545
|
console.log(` google auth Connect Gmail + Calendar`);
|
|
@@ -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
|
+
}
|
package/src/commands/chat.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
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) ───────────
|
|
@@ -493,8 +494,9 @@ async function handleSlashCommand(input, config, history) {
|
|
|
493
494
|
|
|
494
495
|
if (trimmed === '/clear') {
|
|
495
496
|
history.length = 0;
|
|
497
|
+
try { saveChatHistory([]); } catch { /* non-critical */ }
|
|
496
498
|
console.clear();
|
|
497
|
-
console.log(` ${G}Conversation cleared.${NC}`);
|
|
499
|
+
console.log(` ${G}Conversation cleared (memory preserved, chat history reset).${NC}`);
|
|
498
500
|
return true;
|
|
499
501
|
}
|
|
500
502
|
|
|
@@ -582,7 +584,11 @@ export async function cmdChat(args) {
|
|
|
582
584
|
terminal: true,
|
|
583
585
|
});
|
|
584
586
|
|
|
585
|
-
|
|
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
|
+
}
|
|
586
592
|
const systemPrompt = buildSystemPrompt(initialContext);
|
|
587
593
|
|
|
588
594
|
// ── Graceful exit ───────────────────────────────────────────────────────
|
|
@@ -695,6 +701,10 @@ export async function cmdChat(args) {
|
|
|
695
701
|
history.shift();
|
|
696
702
|
history.shift();
|
|
697
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 */ }
|
|
698
708
|
} catch (err) {
|
|
699
709
|
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
700
710
|
console.log(`\n ${R}LLM error: ${err.message}${NC}\n`);
|
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
|
}
|