alvin-bot 4.12.4 → 4.13.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/CHANGELOG.md +121 -0
- package/dist/handlers/message.js +9 -0
- package/dist/paths.js +8 -0
- package/dist/providers/claude-sdk-provider.js +25 -5
- package/dist/services/alvin-dispatch.js +125 -0
- package/dist/services/alvin-mcp-tools.js +103 -0
- package/dist/services/async-agent-parser.js +50 -0
- package/dist/services/personality.js +36 -10
- package/dist/services/process-manager.js +291 -0
- package/dist/web/doctor-api.js +59 -67
- package/dist/web/setup-api.js +52 -0
- package/package.json +1 -1
- package/test/alvin-dispatch.test.ts +220 -0
- package/test/async-agent-parser-streamjson.test.ts +273 -0
- package/test/process-manager.test.ts +186 -0
- package/test/slack-test-connection.test.ts +176 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v4.13.1 — Process manager abstraction for the Maintenance Web UI.
|
|
3
|
+
*
|
|
4
|
+
* History: the bot was originally PM2-managed. Since v4.8 the macOS
|
|
5
|
+
* install uses launchd (`com.alvinbot.app.plist`). The WebUI
|
|
6
|
+
* Maintenance section kept calling `pm2 jlist`/`pm2 restart`/...
|
|
7
|
+
* which returned "PM2 not available" for launchd users — all status,
|
|
8
|
+
* stop, start, and logs buttons were broken.
|
|
9
|
+
*
|
|
10
|
+
* This module auto-detects the active manager per request and
|
|
11
|
+
* routes commands accordingly:
|
|
12
|
+
*
|
|
13
|
+
* - launchd (macOS) — via `launchctl print` / `bootout` / `bootstrap`
|
|
14
|
+
* - pm2 (VPS / Linux) — via `pm2 jlist` / `pm2 stop` / `pm2 start`
|
|
15
|
+
* - standalone — no supervisor; only `scheduleGracefulRestart` works
|
|
16
|
+
*
|
|
17
|
+
* Restart is NOT on this interface — it always uses
|
|
18
|
+
* `scheduleGracefulRestart` (Grammy-safe) and relies on whichever
|
|
19
|
+
* supervisor is present to bring the process back. For "standalone",
|
|
20
|
+
* a restart effectively kills the process and the user has to run it
|
|
21
|
+
* again manually (we warn in the UI).
|
|
22
|
+
*/
|
|
23
|
+
import { execSync } from "node:child_process";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
const LAUNCHD_LABEL = "com.alvinbot.app";
|
|
27
|
+
const LAUNCHD_PLIST = resolve(os.homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
28
|
+
const PM2_NAME = "alvin-bot";
|
|
29
|
+
// ── Detection ───────────────────────────────────────────────────
|
|
30
|
+
export function detectProcessManager(opts = {}) {
|
|
31
|
+
const platform = opts.platform ?? process.platform;
|
|
32
|
+
const uid = opts.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
|
|
33
|
+
// Only try launchd on macOS
|
|
34
|
+
if (platform === "darwin") {
|
|
35
|
+
try {
|
|
36
|
+
const out = execSync(`launchctl print gui/${uid}/${LAUNCHD_LABEL}`, { encoding: "utf-8", timeout: 3000, stdio: "pipe" });
|
|
37
|
+
if (out && out.length > 0) {
|
|
38
|
+
return createLaunchdManager(uid);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Not loaded in launchd — fall through
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// PM2 fallback (Linux VPS, or Mac installs that stayed on PM2)
|
|
46
|
+
try {
|
|
47
|
+
const out = execSync("pm2 jlist", {
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
timeout: 3000,
|
|
50
|
+
stdio: "pipe",
|
|
51
|
+
});
|
|
52
|
+
const parsed = JSON.parse(out);
|
|
53
|
+
if (Array.isArray(parsed) &&
|
|
54
|
+
parsed.some((p) => p?.name === PM2_NAME)) {
|
|
55
|
+
return createPm2Manager();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// pm2 not installed or didn't report our process
|
|
60
|
+
}
|
|
61
|
+
return createStandaloneManager();
|
|
62
|
+
}
|
|
63
|
+
function parseLaunchdPrint(text) {
|
|
64
|
+
const out = {};
|
|
65
|
+
// state = running
|
|
66
|
+
const stateMatch = text.match(/\bstate\s*=\s*(\S+)/);
|
|
67
|
+
if (stateMatch)
|
|
68
|
+
out.state = stateMatch[1];
|
|
69
|
+
// pid = 12345
|
|
70
|
+
const pidMatch = text.match(/\bpid\s*=\s*(\d+)/);
|
|
71
|
+
if (pidMatch)
|
|
72
|
+
out.pid = Number(pidMatch[1]);
|
|
73
|
+
// program = /path/to/node
|
|
74
|
+
const programMatch = text.match(/\bprogram\s*=\s*(\S+)/);
|
|
75
|
+
if (programMatch)
|
|
76
|
+
out.program = programMatch[1];
|
|
77
|
+
// working directory = /path
|
|
78
|
+
const cwdMatch = text.match(/\bworking directory\s*=\s*(\S+)/);
|
|
79
|
+
if (cwdMatch)
|
|
80
|
+
out.cwd = cwdMatch[1];
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
export function createLaunchdManager(uid) {
|
|
84
|
+
const service = `gui/${uid}/${LAUNCHD_LABEL}`;
|
|
85
|
+
return {
|
|
86
|
+
kind: "launchd",
|
|
87
|
+
async getStatus() {
|
|
88
|
+
try {
|
|
89
|
+
const out = execSync(`launchctl print ${service}`, {
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 3000,
|
|
92
|
+
stdio: "pipe",
|
|
93
|
+
});
|
|
94
|
+
const parsed = parseLaunchdPrint(out);
|
|
95
|
+
const pid = parsed.pid;
|
|
96
|
+
// Enrich with ps info if we have a PID
|
|
97
|
+
let memory;
|
|
98
|
+
let cpu;
|
|
99
|
+
let uptime;
|
|
100
|
+
if (pid) {
|
|
101
|
+
try {
|
|
102
|
+
// ps output: %cpu %mem rss etime
|
|
103
|
+
const psOut = execSync(`ps -p ${pid} -o %cpu=,%mem=,rss=,etime=`, { encoding: "utf-8", timeout: 2000, stdio: "pipe" }).trim();
|
|
104
|
+
const [cpuStr, , rssStr, etime] = psOut.split(/\s+/);
|
|
105
|
+
cpu = parseFloat(cpuStr) || 0;
|
|
106
|
+
memory = (parseInt(rssStr, 10) || 0) * 1024; // rss is kB
|
|
107
|
+
uptime = parseEtimeToMs(etime);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* ps may fail if pid vanished — ignore */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
kind: "launchd",
|
|
115
|
+
status: parsed.state === "running" ? "running" : parsed.state || "unknown",
|
|
116
|
+
pid,
|
|
117
|
+
uptime,
|
|
118
|
+
memory,
|
|
119
|
+
cpu,
|
|
120
|
+
execPath: parsed.program,
|
|
121
|
+
cwd: parsed.cwd,
|
|
122
|
+
nodeVersion: process.version,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { kind: "launchd", status: "not-loaded" };
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async stop() {
|
|
130
|
+
// bootout removes the service from the domain, which stops it
|
|
131
|
+
// and disables KeepAlive until bootstrap is run again.
|
|
132
|
+
execSync(`launchctl bootout ${service}`, {
|
|
133
|
+
encoding: "utf-8",
|
|
134
|
+
timeout: 5000,
|
|
135
|
+
stdio: "pipe",
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
async start() {
|
|
139
|
+
// bootstrap re-registers the plist with the domain.
|
|
140
|
+
execSync(`launchctl bootstrap gui/${uid} ${JSON.stringify(LAUNCHD_PLIST).slice(1, -1)}`, { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
141
|
+
},
|
|
142
|
+
async getLogs(lines = 30) {
|
|
143
|
+
// launchd redirects stdout/stderr to files — just tail them.
|
|
144
|
+
const logDir = resolve(process.env.ALVIN_DATA_DIR || resolve(os.homedir(), ".alvin-bot"), "logs");
|
|
145
|
+
const outLog = resolve(logDir, "alvin-bot.out.log");
|
|
146
|
+
const errLog = resolve(logDir, "alvin-bot.err.log");
|
|
147
|
+
try {
|
|
148
|
+
return execSync(`tail -n ${lines} ${outLog} ${errLog} 2>/dev/null`, {
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
timeout: 3000,
|
|
151
|
+
stdio: "pipe",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return "No logs available.";
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseEtimeToMs(etime) {
|
|
161
|
+
// ps etime format: "MM:SS", "HH:MM:SS", "D-HH:MM:SS"
|
|
162
|
+
if (!etime)
|
|
163
|
+
return undefined;
|
|
164
|
+
const parts = etime.split("-");
|
|
165
|
+
let days = 0;
|
|
166
|
+
let hms;
|
|
167
|
+
if (parts.length === 2) {
|
|
168
|
+
days = parseInt(parts[0], 10) || 0;
|
|
169
|
+
hms = parts[1];
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
hms = parts[0];
|
|
173
|
+
}
|
|
174
|
+
const bits = hms.split(":").map((x) => parseInt(x, 10) || 0);
|
|
175
|
+
let h = 0, m = 0, s = 0;
|
|
176
|
+
if (bits.length === 3)
|
|
177
|
+
[h, m, s] = bits;
|
|
178
|
+
else if (bits.length === 2)
|
|
179
|
+
[m, s] = bits;
|
|
180
|
+
else
|
|
181
|
+
return undefined;
|
|
182
|
+
return (((days * 24 + h) * 60 + m) * 60 + s) * 1000;
|
|
183
|
+
}
|
|
184
|
+
export function createPm2Manager() {
|
|
185
|
+
return {
|
|
186
|
+
kind: "pm2",
|
|
187
|
+
async getStatus() {
|
|
188
|
+
try {
|
|
189
|
+
const out = execSync("pm2 jlist", {
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
timeout: 3000,
|
|
192
|
+
stdio: "pipe",
|
|
193
|
+
});
|
|
194
|
+
const procs = JSON.parse(out);
|
|
195
|
+
const me = procs.find((p) => p.name === PM2_NAME);
|
|
196
|
+
if (!me) {
|
|
197
|
+
return { kind: "pm2", status: "unknown" };
|
|
198
|
+
}
|
|
199
|
+
const env = me.pm2_env ?? {};
|
|
200
|
+
return {
|
|
201
|
+
kind: "pm2",
|
|
202
|
+
status: env.status || "unknown",
|
|
203
|
+
pid: me.pid,
|
|
204
|
+
uptime: env.pm_uptime ? Date.now() - env.pm_uptime : undefined,
|
|
205
|
+
memory: me.monit?.memory,
|
|
206
|
+
cpu: me.monit?.cpu,
|
|
207
|
+
restarts: env.restart_time ?? 0,
|
|
208
|
+
version: env.version,
|
|
209
|
+
nodeVersion: env.node_version || process.version,
|
|
210
|
+
execPath: env.pm_exec_path,
|
|
211
|
+
cwd: env.pm_cwd,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { kind: "pm2", status: "unknown" };
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async stop() {
|
|
219
|
+
execSync(`pm2 stop ${PM2_NAME}`, {
|
|
220
|
+
encoding: "utf-8",
|
|
221
|
+
timeout: 10_000,
|
|
222
|
+
stdio: "pipe",
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
async start() {
|
|
226
|
+
execSync(`pm2 start ${PM2_NAME}`, {
|
|
227
|
+
encoding: "utf-8",
|
|
228
|
+
timeout: 10_000,
|
|
229
|
+
stdio: "pipe",
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
async getLogs(lines = 30) {
|
|
233
|
+
try {
|
|
234
|
+
const raw = execSync(`pm2 logs ${PM2_NAME} --nostream --lines ${lines} 2>&1`, {
|
|
235
|
+
encoding: "utf-8",
|
|
236
|
+
timeout: 5000,
|
|
237
|
+
stdio: "pipe",
|
|
238
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
239
|
+
});
|
|
240
|
+
// eslint-disable-next-line no-control-regex
|
|
241
|
+
return raw.replace(/\x1b\[[0-9;]*m/g, "");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return "No logs available.";
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ── standalone ──────────────────────────────────────────────────
|
|
250
|
+
export function createStandaloneManager() {
|
|
251
|
+
return {
|
|
252
|
+
kind: "standalone",
|
|
253
|
+
async getStatus() {
|
|
254
|
+
return {
|
|
255
|
+
kind: "standalone",
|
|
256
|
+
status: "running",
|
|
257
|
+
pid: process.pid,
|
|
258
|
+
uptime: process.uptime() * 1000,
|
|
259
|
+
memory: process.memoryUsage().rss,
|
|
260
|
+
nodeVersion: process.version,
|
|
261
|
+
execPath: process.execPath,
|
|
262
|
+
cwd: process.cwd(),
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
async stop() {
|
|
266
|
+
// No supervisor — just exit. User must restart manually.
|
|
267
|
+
setTimeout(() => process.exit(0), 300);
|
|
268
|
+
},
|
|
269
|
+
async start() {
|
|
270
|
+
// Cannot start ourselves if we're already running (nonsensical).
|
|
271
|
+
// Callers should not hit this path when status is "running".
|
|
272
|
+
throw new Error("standalone: cannot 'start' — no supervisor. Run the bot manually.");
|
|
273
|
+
},
|
|
274
|
+
async getLogs(lines = 30) {
|
|
275
|
+
// Standalone mode may or may not redirect stdout. Try the
|
|
276
|
+
// default ~/.alvin-bot/logs path first.
|
|
277
|
+
const logDir = resolve(process.env.ALVIN_DATA_DIR || resolve(os.homedir(), ".alvin-bot"), "logs");
|
|
278
|
+
const outLog = resolve(logDir, "alvin-bot.out.log");
|
|
279
|
+
try {
|
|
280
|
+
return execSync(`tail -n ${lines} ${outLog} 2>/dev/null`, {
|
|
281
|
+
encoding: "utf-8",
|
|
282
|
+
timeout: 3000,
|
|
283
|
+
stdio: "pipe",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return "No logs available (standalone mode — stdout not captured).";
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
package/dist/web/doctor-api.js
CHANGED
|
@@ -491,42 +491,42 @@ export async function handleDoctorAPI(req, res, urlPath, body) {
|
|
|
491
491
|
scheduleGracefulRestart(500);
|
|
492
492
|
return true;
|
|
493
493
|
}
|
|
494
|
-
// ──
|
|
495
|
-
//
|
|
494
|
+
// ── Process Control (v4.13.1: launchd/pm2/standalone auto-detect) ──
|
|
495
|
+
//
|
|
496
|
+
// Routes kept under `/api/pm2/*` for UI compat — the UI still calls
|
|
497
|
+
// those paths. Under the hood we now use the process-manager
|
|
498
|
+
// abstraction which auto-detects launchd (macOS native installs)
|
|
499
|
+
// or pm2 (VPS / legacy Mac installs) or standalone (neither).
|
|
500
|
+
// GET /api/pm2/status — Get process info via detected manager
|
|
496
501
|
if (urlPath === "/api/pm2/status") {
|
|
497
502
|
try {
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
const botProcess = processes.find((p) => p.name === "alvin-bot" ||
|
|
502
|
-
p.pm2_env?.pm_exec_path?.includes("alvin-bot")) || processes[0]; // fallback to first process
|
|
503
|
-
if (!botProcess) {
|
|
504
|
-
res.end(JSON.stringify({ error: "No PM2 process found" }));
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
507
|
-
const env = botProcess.pm2_env || {};
|
|
503
|
+
const { detectProcessManager } = await import("../services/process-manager.js");
|
|
504
|
+
const pm = detectProcessManager();
|
|
505
|
+
const status = await pm.getStatus();
|
|
508
506
|
res.end(JSON.stringify({
|
|
509
507
|
process: {
|
|
510
|
-
name:
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
508
|
+
name: "alvin-bot",
|
|
509
|
+
kind: status.kind,
|
|
510
|
+
pid: status.pid ?? 0,
|
|
511
|
+
status: status.status,
|
|
512
|
+
uptime: status.uptime ?? 0,
|
|
513
|
+
memory: status.memory ?? 0,
|
|
514
|
+
cpu: status.cpu ?? 0,
|
|
515
|
+
restarts: status.restarts ?? 0,
|
|
516
|
+
version: status.version || "?",
|
|
517
|
+
nodeVersion: status.nodeVersion || process.version,
|
|
518
|
+
execPath: status.execPath || "?",
|
|
519
|
+
cwd: status.cwd || "?",
|
|
521
520
|
},
|
|
522
521
|
}));
|
|
523
522
|
}
|
|
524
523
|
catch (err) {
|
|
525
|
-
|
|
524
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
525
|
+
res.end(JSON.stringify({ error: `Process manager detection failed: ${msg}` }));
|
|
526
526
|
}
|
|
527
527
|
return true;
|
|
528
528
|
}
|
|
529
|
-
// POST /api/pm2/action — Execute
|
|
529
|
+
// POST /api/pm2/action — Execute action via detected manager
|
|
530
530
|
if (urlPath === "/api/pm2/action" && req.method === "POST") {
|
|
531
531
|
try {
|
|
532
532
|
const { action } = JSON.parse(body);
|
|
@@ -536,41 +536,44 @@ export async function handleDoctorAPI(req, res, urlPath, body) {
|
|
|
536
536
|
res.end(JSON.stringify({ ok: false, error: `Invalid action: ${action}` }));
|
|
537
537
|
return true;
|
|
538
538
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
try {
|
|
542
|
-
const jlist = execSync("pm2 jlist", { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
|
|
543
|
-
const procs = JSON.parse(jlist);
|
|
544
|
-
const found = procs.find((p) => p.name === "alvin-bot" || p.name === "alvin-bot");
|
|
545
|
-
if (found)
|
|
546
|
-
processName = found.name;
|
|
547
|
-
}
|
|
548
|
-
catch { /* use default */ }
|
|
539
|
+
const { detectProcessManager } = await import("../services/process-manager.js");
|
|
540
|
+
const pm = detectProcessManager();
|
|
549
541
|
if (action === "flush") {
|
|
550
|
-
|
|
542
|
+
// Truncate our own log files directly — works on both launchd
|
|
543
|
+
// and standalone. PM2's flush is also just truncation.
|
|
544
|
+
const logDir = resolve(DATA_DIR, "logs");
|
|
545
|
+
for (const f of ["alvin-bot.out.log", "alvin-bot.err.log"]) {
|
|
546
|
+
try {
|
|
547
|
+
fs.truncateSync(resolve(logDir, f), 0);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
/* file may not exist — ignore */
|
|
551
|
+
}
|
|
552
|
+
}
|
|
551
553
|
res.end(JSON.stringify({ ok: true, message: "Logs flushed" }));
|
|
552
554
|
return true;
|
|
553
555
|
}
|
|
554
556
|
if (action === "stop") {
|
|
555
|
-
// Stop is special —
|
|
556
|
-
res.end(JSON.stringify({ ok: true, message:
|
|
557
|
+
// Stop is special — can't respond after we've killed ourselves.
|
|
558
|
+
res.end(JSON.stringify({ ok: true, message: `Bot is stopping (${pm.kind})...` }));
|
|
557
559
|
setTimeout(() => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
catch { /* process might already be dead */ }
|
|
560
|
+
pm.stop().catch(() => {
|
|
561
|
+
/* process might already be dead */
|
|
562
|
+
});
|
|
562
563
|
}, 300);
|
|
563
564
|
return true;
|
|
564
565
|
}
|
|
565
566
|
if (action === "start") {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
res.end(JSON.stringify({ ok: true, message: "Bot started" }));
|
|
567
|
+
await pm.start();
|
|
568
|
+
res.end(JSON.stringify({ ok: true, message: `Bot started via ${pm.kind}` }));
|
|
569
569
|
return true;
|
|
570
570
|
}
|
|
571
571
|
if (action === "restart" || action === "reload") {
|
|
572
572
|
const { scheduleGracefulRestart } = await import("../services/restart.js");
|
|
573
|
-
res.end(JSON.stringify({
|
|
573
|
+
res.end(JSON.stringify({
|
|
574
|
+
ok: true,
|
|
575
|
+
message: `Bot is ${action === "restart" ? "restarting" : "reloading"} (${pm.kind})...`,
|
|
576
|
+
}));
|
|
574
577
|
scheduleGracefulRestart(500);
|
|
575
578
|
return true;
|
|
576
579
|
}
|
|
@@ -580,31 +583,20 @@ export async function handleDoctorAPI(req, res, urlPath, body) {
|
|
|
580
583
|
}
|
|
581
584
|
return true;
|
|
582
585
|
}
|
|
583
|
-
// GET /api/pm2/logs — Get recent
|
|
586
|
+
// GET /api/pm2/logs — Get recent logs via detected manager
|
|
584
587
|
if (urlPath === "/api/pm2/logs") {
|
|
585
588
|
try {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const procs = JSON.parse(jlist);
|
|
591
|
-
const found = procs.find((p) => p.name === "alvin-bot" || p.name === "alvin-bot");
|
|
592
|
-
if (found)
|
|
593
|
-
processName = found.name;
|
|
594
|
-
}
|
|
595
|
-
catch { /* use default */ }
|
|
596
|
-
let logs = execSync(`pm2 logs ${processName} --nostream --lines 30 2>&1`, {
|
|
597
|
-
encoding: "utf-8",
|
|
598
|
-
timeout: 5000,
|
|
599
|
-
stdio: "pipe",
|
|
600
|
-
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
601
|
-
});
|
|
602
|
-
// Strip ANSI escape codes
|
|
603
|
-
logs = logs.replace(/\x1b\[[0-9;]*m/g, "");
|
|
604
|
-
res.end(JSON.stringify({ logs }));
|
|
589
|
+
const { detectProcessManager } = await import("../services/process-manager.js");
|
|
590
|
+
const pm = detectProcessManager();
|
|
591
|
+
const logs = await pm.getLogs(30);
|
|
592
|
+
res.end(JSON.stringify({ logs, kind: pm.kind }));
|
|
605
593
|
}
|
|
606
594
|
catch (err) {
|
|
607
|
-
res.end(JSON.stringify({
|
|
595
|
+
res.end(JSON.stringify({
|
|
596
|
+
error: "Logs not available",
|
|
597
|
+
logs: "",
|
|
598
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
599
|
+
}));
|
|
608
600
|
}
|
|
609
601
|
return true;
|
|
610
602
|
}
|
package/dist/web/setup-api.js
CHANGED
|
@@ -874,6 +874,58 @@ export async function handleSetupAPI(req, res, urlPath, body) {
|
|
|
874
874
|
}
|
|
875
875
|
return true;
|
|
876
876
|
}
|
|
877
|
+
if (platformId === "slack") {
|
|
878
|
+
// v4.13.1 — Validate Slack config via auth.test (Bot Token) +
|
|
879
|
+
// format check on App Token (xapp-). We can't actually "ping"
|
|
880
|
+
// Socket Mode without opening a WebSocket, so we rely on the
|
|
881
|
+
// App Token prefix as the cheapest sanity check.
|
|
882
|
+
const botToken = process.env.SLACK_BOT_TOKEN;
|
|
883
|
+
const appToken = process.env.SLACK_APP_TOKEN;
|
|
884
|
+
if (!botToken) {
|
|
885
|
+
res.end(JSON.stringify({ ok: false, error: "SLACK_BOT_TOKEN not set" }));
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const apiRes = await fetch("https://slack.com/api/auth.test", {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: {
|
|
892
|
+
"Authorization": `Bearer ${botToken}`,
|
|
893
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
const data = await apiRes.json();
|
|
897
|
+
if (!data.ok) {
|
|
898
|
+
res.end(JSON.stringify({
|
|
899
|
+
ok: false,
|
|
900
|
+
error: `Slack rejected Bot Token: ${data.error || "unknown error"}`,
|
|
901
|
+
}));
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
// Bot Token valid. Now check App Token format — Socket Mode
|
|
905
|
+
// requires it and the xapp- prefix is the quickest sanity check.
|
|
906
|
+
let warning = "";
|
|
907
|
+
if (!appToken) {
|
|
908
|
+
warning = " ⚠️ SLACK_APP_TOKEN not set — Socket Mode will fail.";
|
|
909
|
+
}
|
|
910
|
+
else if (!appToken.startsWith("xapp-")) {
|
|
911
|
+
warning =
|
|
912
|
+
" ⚠️ SLACK_APP_TOKEN has wrong prefix (expected xapp-) — Socket Mode will fail.";
|
|
913
|
+
}
|
|
914
|
+
res.end(JSON.stringify({
|
|
915
|
+
ok: true,
|
|
916
|
+
info: `@${data.user} on ${data.team} (team_id: ${data.team_id}, bot_id: ${data.bot_id})` +
|
|
917
|
+
warning,
|
|
918
|
+
}));
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
922
|
+
res.end(JSON.stringify({
|
|
923
|
+
ok: false,
|
|
924
|
+
error: `Failed to reach slack.com/api/auth.test: ${msg}`,
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
877
929
|
res.end(JSON.stringify({ ok: false, error: "Unknown platform" }));
|
|
878
930
|
}
|
|
879
931
|
catch (err) {
|