alvin-bot 4.4.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.
Files changed (136) hide show
  1. package/.env.example +43 -0
  2. package/BACKLOG.md +223 -0
  3. package/CHANGELOG.md +63 -0
  4. package/CLAUDE.example.md +152 -0
  5. package/CODE_OF_CONDUCT.md +52 -0
  6. package/CONTRIBUTING.md +72 -0
  7. package/LICENSE +21 -0
  8. package/README.md +529 -0
  9. package/SECURITY.md +38 -0
  10. package/SOUL.example.md +60 -0
  11. package/TOOLS.example.md +42 -0
  12. package/alvin-bot.config.example.json +24 -0
  13. package/bin/cli.js +1088 -0
  14. package/dist/.metadata_never_index +0 -0
  15. package/dist/claude.js +102 -0
  16. package/dist/config.js +65 -0
  17. package/dist/engine.js +90 -0
  18. package/dist/find-claude-binary.js +98 -0
  19. package/dist/handlers/commands.js +1489 -0
  20. package/dist/handlers/document.js +187 -0
  21. package/dist/handlers/message.js +200 -0
  22. package/dist/handlers/photo.js +154 -0
  23. package/dist/handlers/platform-message.js +275 -0
  24. package/dist/handlers/video.js +237 -0
  25. package/dist/handlers/voice.js +148 -0
  26. package/dist/i18n.js +299 -0
  27. package/dist/index.js +442 -0
  28. package/dist/init-data-dir.js +81 -0
  29. package/dist/middleware/auth.js +215 -0
  30. package/dist/migrate.js +139 -0
  31. package/dist/paths.js +87 -0
  32. package/dist/platforms/discord.js +161 -0
  33. package/dist/platforms/index.js +130 -0
  34. package/dist/platforms/signal.js +205 -0
  35. package/dist/platforms/slack.js +318 -0
  36. package/dist/platforms/telegram.js +111 -0
  37. package/dist/platforms/types.js +8 -0
  38. package/dist/platforms/whatsapp.js +648 -0
  39. package/dist/providers/claude-sdk-provider.js +173 -0
  40. package/dist/providers/codex-cli-provider.js +121 -0
  41. package/dist/providers/index.js +7 -0
  42. package/dist/providers/openai-compatible.js +388 -0
  43. package/dist/providers/registry.js +209 -0
  44. package/dist/providers/tool-executor.js +450 -0
  45. package/dist/providers/types.js +205 -0
  46. package/dist/services/access.js +144 -0
  47. package/dist/services/asset-index.js +230 -0
  48. package/dist/services/browser-manager.js +161 -0
  49. package/dist/services/browser.js +121 -0
  50. package/dist/services/compaction.js +129 -0
  51. package/dist/services/cron.js +462 -0
  52. package/dist/services/custom-tools.js +317 -0
  53. package/dist/services/delivery-queue.js +154 -0
  54. package/dist/services/elevenlabs.js +58 -0
  55. package/dist/services/embeddings.js +386 -0
  56. package/dist/services/exec-guard.js +46 -0
  57. package/dist/services/fallback-order.js +151 -0
  58. package/dist/services/heartbeat.js +192 -0
  59. package/dist/services/hooks.js +44 -0
  60. package/dist/services/imagegen.js +72 -0
  61. package/dist/services/language-detect.js +144 -0
  62. package/dist/services/markdown.js +63 -0
  63. package/dist/services/mcp.js +252 -0
  64. package/dist/services/memory.js +133 -0
  65. package/dist/services/personality.js +227 -0
  66. package/dist/services/plugins.js +171 -0
  67. package/dist/services/reminders.js +97 -0
  68. package/dist/services/restart.js +48 -0
  69. package/dist/services/security-audit.js +66 -0
  70. package/dist/services/self-search.js +129 -0
  71. package/dist/services/session.js +93 -0
  72. package/dist/services/skills.js +287 -0
  73. package/dist/services/standing-orders.js +29 -0
  74. package/dist/services/subagents.js +142 -0
  75. package/dist/services/sudo.js +243 -0
  76. package/dist/services/telegram.js +113 -0
  77. package/dist/services/tool-discovery.js +214 -0
  78. package/dist/services/usage-tracker.js +137 -0
  79. package/dist/services/users.js +199 -0
  80. package/dist/services/voice.js +95 -0
  81. package/dist/tui/index.js +507 -0
  82. package/dist/web/canvas.js +30 -0
  83. package/dist/web/doctor-api.js +606 -0
  84. package/dist/web/openai-compat.js +252 -0
  85. package/dist/web/server.js +1351 -0
  86. package/dist/web/setup-api.js +1078 -0
  87. package/docs/mcp.example.json +16 -0
  88. package/docs/screenshots/00-Login.png +0 -0
  89. package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
  90. package/docs/screenshots/02-Chat.png +0 -0
  91. package/docs/screenshots/03-Dashboard-Overview.png +0 -0
  92. package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
  93. package/docs/screenshots/05-Personality-Editor.png +0 -0
  94. package/docs/screenshots/06-Memory-Manager.png +0 -0
  95. package/docs/screenshots/07-Active-Sessions.png +0 -0
  96. package/docs/screenshots/08-File-Browser.png +0 -0
  97. package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
  98. package/docs/screenshots/10-Custom-Tools.png +0 -0
  99. package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
  100. package/docs/screenshots/12-Messaging-Platforms.png +0 -0
  101. package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
  102. package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
  103. package/docs/screenshots/13-User-Management.png +0 -0
  104. package/docs/screenshots/14-Web-Terminal.png +0 -0
  105. package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
  106. package/docs/screenshots/16-Settings-and-Env.png +0 -0
  107. package/docs/screenshots/TG-commands.png +0 -0
  108. package/docs/screenshots/TG.png +0 -0
  109. package/docs/screenshots/_Mac-Installer.png +0 -0
  110. package/docs/tools.example.json +33 -0
  111. package/install.sh +165 -0
  112. package/package.json +190 -0
  113. package/plugins/calendar/index.js +270 -0
  114. package/plugins/email/index.js +231 -0
  115. package/plugins/finance/index.js +254 -0
  116. package/plugins/notes/index.js +227 -0
  117. package/plugins/smarthome/index.js +230 -0
  118. package/plugins/weather/index.js +122 -0
  119. package/skills/apple-notes/SKILL.md +31 -0
  120. package/skills/browse/SKILL.md +136 -0
  121. package/skills/code-project/SKILL.md +43 -0
  122. package/skills/data-analysis/SKILL.md +39 -0
  123. package/skills/document-creation/SKILL.md +48 -0
  124. package/skills/email-summary/SKILL.md +46 -0
  125. package/skills/github/SKILL.md +42 -0
  126. package/skills/summarize/SKILL.md +28 -0
  127. package/skills/system-admin/SKILL.md +39 -0
  128. package/skills/weather/SKILL.md +34 -0
  129. package/skills/web-research/SKILL.md +35 -0
  130. package/web/public/canvas.html +52 -0
  131. package/web/public/css/style.css +555 -0
  132. package/web/public/index.html +189 -0
  133. package/web/public/js/app.js +3102 -0
  134. package/web/public/js/i18n.js +1048 -0
  135. package/web/public/js/icons.js +104 -0
  136. package/web/public/login.html +48 -0
@@ -0,0 +1,606 @@
1
+ /**
2
+ * Doctor & Backup API — Self-healing, diagnostics, and backup/restore.
3
+ *
4
+ * Features:
5
+ * - Health check (diagnose config issues)
6
+ * - Auto-repair (fix common problems)
7
+ * - Backup (snapshot all config files)
8
+ * - Restore from backup
9
+ * - Bot restart
10
+ */
11
+ import fs from "fs";
12
+ import { resolve, dirname } from "path";
13
+ import { execSync } from "child_process";
14
+ import { BOT_ROOT, ENV_FILE, BACKUP_DIR, DATA_DIR, MEMORY_DIR, MEMORY_FILE, SOUL_FILE, SOUL_EXAMPLE, TOOLS_MD, TOOLS_JSON, CUSTOM_MODELS, CRON_FILE, MCP_CONFIG } from "../paths.js";
15
+ // Files to include in backups (absolute paths)
16
+ const BACKUP_FILES = [
17
+ { src: ENV_FILE, label: ".env" },
18
+ { src: SOUL_FILE, label: "soul.md" },
19
+ { src: resolve(BOT_ROOT, "CLAUDE.md"), label: "CLAUDE.md" },
20
+ { src: TOOLS_MD, label: "tools.md" },
21
+ { src: CUSTOM_MODELS, label: "custom-models.json" },
22
+ { src: CRON_FILE, label: "cron-jobs.json" },
23
+ { src: MCP_CONFIG, label: "mcp.json" },
24
+ { src: MEMORY_FILE, label: "MEMORY.md" },
25
+ ];
26
+ function runHealthCheck() {
27
+ const issues = [];
28
+ // 1. Check .env exists
29
+ if (!fs.existsSync(ENV_FILE)) {
30
+ issues.push({
31
+ severity: "error",
32
+ category: "Config",
33
+ message: ".env file missing",
34
+ fix: "Create a default .env from .env.example",
35
+ fixAction: "create-env",
36
+ });
37
+ }
38
+ else {
39
+ // Parse .env
40
+ const envContent = fs.readFileSync(ENV_FILE, "utf-8");
41
+ // Check BOT_TOKEN
42
+ if (!envContent.includes("BOT_TOKEN=") || envContent.match(/BOT_TOKEN=\s*$/m)) {
43
+ issues.push({
44
+ severity: "error",
45
+ category: "Telegram",
46
+ message: "BOT_TOKEN not set — Telegram bot cannot start",
47
+ });
48
+ }
49
+ // Check ALLOWED_USERS
50
+ if (!envContent.includes("ALLOWED_USERS=") || envContent.match(/ALLOWED_USERS=\s*$/m)) {
51
+ issues.push({
52
+ severity: "warning",
53
+ category: "Security",
54
+ message: "ALLOWED_USERS not set — anyone can use the bot",
55
+ });
56
+ }
57
+ // Check for syntax errors in .env
58
+ const lines = envContent.split("\n");
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i].trim();
61
+ if (!line || line.startsWith("#"))
62
+ continue;
63
+ if (!line.includes("=")) {
64
+ issues.push({
65
+ severity: "error",
66
+ category: "Config",
67
+ message: `.env line ${i + 1}: Invalid format "${line.slice(0, 40)}..."`,
68
+ fix: `Remove or fix the line`,
69
+ fixAction: `fix-env-line:${i}`,
70
+ });
71
+ }
72
+ }
73
+ // Check for common issues
74
+ if (envContent.includes('""') || envContent.match(/="?\s*$/m)) {
75
+ issues.push({
76
+ severity: "warning",
77
+ category: "Config",
78
+ message: "Empty values found in .env — some features may not work",
79
+ });
80
+ }
81
+ }
82
+ // 2. Check data directory
83
+ if (!fs.existsSync(DATA_DIR)) {
84
+ issues.push({
85
+ severity: "error",
86
+ category: "Files",
87
+ message: "Data directory missing (~/.alvin-bot/)",
88
+ fix: "Create data directory",
89
+ fixAction: "create-docs",
90
+ });
91
+ }
92
+ // 3. Check TOOLS.md validity (legacy tools.json as fallback)
93
+ if (fs.existsSync(TOOLS_MD)) {
94
+ // Validate TOOLS.md has at least one ## heading (tool definition)
95
+ const content = fs.readFileSync(TOOLS_MD, "utf-8");
96
+ if (!content.includes("## ")) {
97
+ issues.push({
98
+ severity: "warning",
99
+ category: "Tools",
100
+ message: "TOOLS.md contains no tool definitions (## headings missing)",
101
+ fix: "Recreate TOOLS.md from TOOLS.example.md",
102
+ fixAction: "fix-tools-json",
103
+ });
104
+ }
105
+ }
106
+ else if (fs.existsSync(TOOLS_JSON)) {
107
+ try {
108
+ JSON.parse(fs.readFileSync(TOOLS_JSON, "utf-8"));
109
+ }
110
+ catch {
111
+ issues.push({
112
+ severity: "error",
113
+ category: "Tools",
114
+ message: "tools.json is not valid JSON",
115
+ fix: "Auto-repair JSON errors or reset to backup",
116
+ fixAction: "fix-tools-json",
117
+ });
118
+ }
119
+ }
120
+ else {
121
+ issues.push({
122
+ severity: "info",
123
+ category: "Tools",
124
+ message: "No custom tools configured (tools.md missing)",
125
+ fix: "Create tools.md from example",
126
+ fixAction: "fix-tools-json",
127
+ });
128
+ }
129
+ // 4. Check custom-models.json validity
130
+ if (fs.existsSync(CUSTOM_MODELS)) {
131
+ try {
132
+ JSON.parse(fs.readFileSync(CUSTOM_MODELS, "utf-8"));
133
+ }
134
+ catch {
135
+ issues.push({
136
+ severity: "error",
137
+ category: "Models",
138
+ message: "custom-models.json is not valid JSON",
139
+ fix: "Reset to empty array",
140
+ fixAction: "fix-custom-models",
141
+ });
142
+ }
143
+ }
144
+ // 5. Check cron-jobs.json
145
+ if (fs.existsSync(CRON_FILE)) {
146
+ try {
147
+ JSON.parse(fs.readFileSync(CRON_FILE, "utf-8"));
148
+ }
149
+ catch {
150
+ issues.push({
151
+ severity: "error",
152
+ category: "Cron",
153
+ message: "cron-jobs.json is not valid JSON",
154
+ fix: "Reset to empty array",
155
+ fixAction: "fix-cron-json",
156
+ });
157
+ }
158
+ }
159
+ // 6. Check soul.md exists
160
+ if (!fs.existsSync(SOUL_FILE)) {
161
+ issues.push({
162
+ severity: "warning",
163
+ category: "Personality",
164
+ message: "soul.md missing — bot has no personality",
165
+ fix: "Create default soul.md",
166
+ fixAction: "create-soul",
167
+ });
168
+ }
169
+ // 7. Check Node.js version
170
+ try {
171
+ const nodeVersion = process.version;
172
+ const major = parseInt(nodeVersion.slice(1));
173
+ if (major < 20) {
174
+ issues.push({
175
+ severity: "warning",
176
+ category: "System",
177
+ message: `Node.js ${nodeVersion} — v20+ recommended`,
178
+ });
179
+ }
180
+ }
181
+ catch { /* ignore */ }
182
+ // 8. Check disk space (basic)
183
+ try {
184
+ const dfOutput = execSync("df -h . | tail -1", { cwd: BOT_ROOT, stdio: "pipe", timeout: 5000 }).toString();
185
+ const parts = dfOutput.trim().split(/\s+/);
186
+ const usagePercent = parseInt(parts[4]);
187
+ if (usagePercent > 90) {
188
+ issues.push({
189
+ severity: "warning",
190
+ category: "System",
191
+ message: `Disk ${usagePercent}% full`,
192
+ });
193
+ }
194
+ }
195
+ catch { /* ignore */ }
196
+ // 9. Check PM2
197
+ try {
198
+ execSync("pm2 jlist", { stdio: "pipe", timeout: 5000 });
199
+ }
200
+ catch {
201
+ issues.push({
202
+ severity: "info",
203
+ category: "System",
204
+ message: "PM2 not found — recommended for process management",
205
+ });
206
+ }
207
+ // Good news if no issues
208
+ if (issues.length === 0) {
209
+ issues.push({
210
+ severity: "info",
211
+ category: "Status",
212
+ message: "All good! No issues found.",
213
+ });
214
+ }
215
+ return issues;
216
+ }
217
+ // ── Auto-Repair ─────────────────────────────────────────
218
+ function autoRepair(action) {
219
+ try {
220
+ switch (action) {
221
+ case "create-env": {
222
+ const exampleFile = resolve(BOT_ROOT, ".env.example");
223
+ if (fs.existsSync(exampleFile)) {
224
+ fs.copyFileSync(exampleFile, ENV_FILE);
225
+ return { ok: true, message: ".env created from .env.example" };
226
+ }
227
+ fs.writeFileSync(ENV_FILE, "BOT_TOKEN=\nALLOWED_USERS=\nPRIMARY_PROVIDER=claude-sdk\n");
228
+ return { ok: true, message: "Default .env created (BOT_TOKEN still needs to be set)" };
229
+ }
230
+ case "create-docs": {
231
+ fs.mkdirSync(DATA_DIR, { recursive: true });
232
+ fs.mkdirSync(MEMORY_DIR, { recursive: true });
233
+ return { ok: true, message: "Data directory created" };
234
+ }
235
+ case "fix-tools-json": {
236
+ // Reset to empty — prefer creating tools.md
237
+ if (!fs.existsSync(TOOLS_MD)) {
238
+ fs.mkdirSync(dirname(TOOLS_MD), { recursive: true });
239
+ fs.writeFileSync(TOOLS_MD, "# Custom Tools\n\n> Define your own tools here. Each `##` heading creates a new tool.\n");
240
+ return { ok: true, message: "tools.md created with empty toolset" };
241
+ }
242
+ fs.mkdirSync(dirname(TOOLS_JSON), { recursive: true });
243
+ fs.writeFileSync(TOOLS_JSON, JSON.stringify({ tools: [] }, null, 2));
244
+ return { ok: true, message: "tools.json reset to empty toolset" };
245
+ }
246
+ case "fix-custom-models": {
247
+ fs.mkdirSync(dirname(CUSTOM_MODELS), { recursive: true });
248
+ fs.writeFileSync(CUSTOM_MODELS, "[]");
249
+ return { ok: true, message: "custom-models.json reset" };
250
+ }
251
+ case "fix-cron-json": {
252
+ fs.mkdirSync(dirname(CRON_FILE), { recursive: true });
253
+ fs.writeFileSync(CRON_FILE, "[]");
254
+ return { ok: true, message: "cron-jobs.json reset" };
255
+ }
256
+ case "create-soul": {
257
+ fs.mkdirSync(dirname(SOUL_FILE), { recursive: true });
258
+ // Try to copy from example, otherwise create default
259
+ if (fs.existsSync(SOUL_EXAMPLE)) {
260
+ fs.copyFileSync(SOUL_EXAMPLE, SOUL_FILE);
261
+ }
262
+ else {
263
+ fs.writeFileSync(SOUL_FILE, "# Alvin Bot — Personality\n\n" +
264
+ "You are a helpful, direct, and competent AI assistant.\n" +
265
+ "Reply clearly and precisely. Have opinions. Be genuinely helpful.\n");
266
+ }
267
+ return { ok: true, message: "Default soul.md created" };
268
+ }
269
+ default: {
270
+ if (action.startsWith("fix-env-line:")) {
271
+ const lineIdx = parseInt(action.split(":")[1]);
272
+ const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
273
+ if (lineIdx >= 0 && lineIdx < lines.length) {
274
+ lines[lineIdx] = "# " + lines[lineIdx]; // Comment out broken line
275
+ fs.writeFileSync(ENV_FILE, lines.join("\n"));
276
+ return { ok: true, message: `Line ${lineIdx + 1} commented out` };
277
+ }
278
+ }
279
+ return { ok: false, message: `Unknown action: ${action}` };
280
+ }
281
+ }
282
+ }
283
+ catch (err) {
284
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
285
+ }
286
+ }
287
+ // ── Backup ──────────────────────────────────────────────
288
+ function createBackup(name) {
289
+ const id = name || `backup-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
290
+ const backupPath = resolve(BACKUP_DIR, id);
291
+ fs.mkdirSync(backupPath, { recursive: true });
292
+ const backedUp = [];
293
+ for (const { src, label } of BACKUP_FILES) {
294
+ if (fs.existsSync(src)) {
295
+ const dest = resolve(backupPath, label);
296
+ fs.mkdirSync(dirname(dest), { recursive: true });
297
+ fs.copyFileSync(src, dest);
298
+ backedUp.push(label);
299
+ }
300
+ }
301
+ // Also backup the memory directory
302
+ if (fs.existsSync(MEMORY_DIR)) {
303
+ const memBackup = resolve(backupPath, "memory");
304
+ fs.mkdirSync(memBackup, { recursive: true });
305
+ for (const f of fs.readdirSync(MEMORY_DIR)) {
306
+ if (f.endsWith(".md")) {
307
+ fs.copyFileSync(resolve(MEMORY_DIR, f), resolve(memBackup, f));
308
+ backedUp.push(`memory/${f}`);
309
+ }
310
+ }
311
+ }
312
+ return { ok: true, id, files: backedUp, path: backupPath };
313
+ }
314
+ function listBackups() {
315
+ if (!fs.existsSync(BACKUP_DIR))
316
+ return [];
317
+ return fs.readdirSync(BACKUP_DIR)
318
+ .filter(d => {
319
+ const p = resolve(BACKUP_DIR, d);
320
+ return fs.statSync(p).isDirectory();
321
+ })
322
+ .map(d => {
323
+ const p = resolve(BACKUP_DIR, d);
324
+ const stat = fs.statSync(p);
325
+ let fileCount = 0;
326
+ let totalSize = 0;
327
+ function countFiles(dir) {
328
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
329
+ if (f.isDirectory())
330
+ countFiles(resolve(dir, f.name));
331
+ else {
332
+ fileCount++;
333
+ totalSize += fs.statSync(resolve(dir, f.name)).size;
334
+ }
335
+ }
336
+ }
337
+ countFiles(p);
338
+ return { id: d, createdAt: stat.mtimeMs, fileCount, size: totalSize };
339
+ })
340
+ .sort((a, b) => b.createdAt - a.createdAt);
341
+ }
342
+ function restoreBackup(id, files) {
343
+ const backupPath = resolve(BACKUP_DIR, id);
344
+ if (!backupPath.startsWith(BACKUP_DIR) || !fs.existsSync(backupPath)) {
345
+ return { ok: false, restored: [], errors: ["Backup not found"] };
346
+ }
347
+ const restored = [];
348
+ const errors = [];
349
+ // Build label→dest mapping from BACKUP_FILES
350
+ const labelToSrc = new Map(BACKUP_FILES.map(bf => [bf.label, bf.src]));
351
+ const filesToRestore = files || BACKUP_FILES.map(bf => bf.label);
352
+ for (const label of filesToRestore) {
353
+ const src = resolve(backupPath, label);
354
+ const dest = labelToSrc.get(label) || resolve(DATA_DIR, label);
355
+ if (fs.existsSync(src)) {
356
+ try {
357
+ fs.mkdirSync(dirname(dest), { recursive: true });
358
+ fs.copyFileSync(src, dest);
359
+ restored.push(label);
360
+ }
361
+ catch (err) {
362
+ errors.push(`${label}: ${err instanceof Error ? err.message : String(err)}`);
363
+ }
364
+ }
365
+ }
366
+ return { ok: errors.length === 0, restored, errors };
367
+ }
368
+ function getBackupFiles(id) {
369
+ const backupPath = resolve(BACKUP_DIR, id);
370
+ if (!backupPath.startsWith(BACKUP_DIR) || !fs.existsSync(backupPath))
371
+ return [];
372
+ const files = [];
373
+ function walk(dir, prefix) {
374
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
375
+ const rel = prefix ? `${prefix}/${f.name}` : f.name;
376
+ if (f.isDirectory())
377
+ walk(resolve(dir, f.name), rel);
378
+ else
379
+ files.push(rel);
380
+ }
381
+ }
382
+ walk(backupPath, "");
383
+ return files;
384
+ }
385
+ function deleteBackup(id) {
386
+ const backupPath = resolve(BACKUP_DIR, id);
387
+ if (!backupPath.startsWith(BACKUP_DIR) || !fs.existsSync(backupPath))
388
+ return false;
389
+ fs.rmSync(backupPath, { recursive: true });
390
+ return true;
391
+ }
392
+ // ── API Handler ─────────────────────────────────────────
393
+ export async function handleDoctorAPI(req, res, urlPath, body) {
394
+ res.setHeader("Content-Type", "application/json");
395
+ // GET /api/doctor — run health check
396
+ if (urlPath === "/api/doctor") {
397
+ const issues = runHealthCheck();
398
+ const errorCount = issues.filter(i => i.severity === "error").length;
399
+ const warnCount = issues.filter(i => i.severity === "warning").length;
400
+ res.end(JSON.stringify({ issues, errorCount, warnCount, healthy: errorCount === 0 }));
401
+ return true;
402
+ }
403
+ // POST /api/doctor/repair — auto-repair an issue
404
+ if (urlPath === "/api/doctor/repair" && req.method === "POST") {
405
+ try {
406
+ const { action } = JSON.parse(body);
407
+ const result = autoRepair(action);
408
+ res.end(JSON.stringify(result));
409
+ }
410
+ catch {
411
+ res.statusCode = 400;
412
+ res.end(JSON.stringify({ error: "Invalid request" }));
413
+ }
414
+ return true;
415
+ }
416
+ // POST /api/doctor/repair-all — fix all auto-fixable issues
417
+ if (urlPath === "/api/doctor/repair-all" && req.method === "POST") {
418
+ const issues = runHealthCheck();
419
+ const results = [];
420
+ for (const issue of issues) {
421
+ if (issue.fixAction) {
422
+ const result = autoRepair(issue.fixAction);
423
+ results.push({ action: issue.fixAction, ...result });
424
+ }
425
+ }
426
+ res.end(JSON.stringify({ results }));
427
+ return true;
428
+ }
429
+ // GET /api/backups — list backups
430
+ if (urlPath === "/api/backups") {
431
+ const backups = listBackups();
432
+ res.end(JSON.stringify({ backups }));
433
+ return true;
434
+ }
435
+ // POST /api/backups/create — create a backup
436
+ if (urlPath === "/api/backups/create" && req.method === "POST") {
437
+ try {
438
+ const { name } = JSON.parse(body || "{}");
439
+ const result = createBackup(name);
440
+ res.end(JSON.stringify(result));
441
+ }
442
+ catch (err) {
443
+ const error = err instanceof Error ? err.message : String(err);
444
+ res.end(JSON.stringify({ ok: false, error }));
445
+ }
446
+ return true;
447
+ }
448
+ // POST /api/backups/restore — restore from a backup
449
+ if (urlPath === "/api/backups/restore" && req.method === "POST") {
450
+ try {
451
+ const { id, files } = JSON.parse(body);
452
+ const result = restoreBackup(id, files);
453
+ res.end(JSON.stringify(result));
454
+ }
455
+ catch {
456
+ res.statusCode = 400;
457
+ res.end(JSON.stringify({ error: "Invalid request" }));
458
+ }
459
+ return true;
460
+ }
461
+ // GET /api/backups/:id/files — list files in a backup
462
+ if (urlPath.match(/^\/api\/backups\/[^/]+\/files$/)) {
463
+ const id = urlPath.split("/")[3];
464
+ const files = getBackupFiles(id);
465
+ res.end(JSON.stringify({ id, files }));
466
+ return true;
467
+ }
468
+ // POST /api/backups/delete — delete a backup
469
+ if (urlPath === "/api/backups/delete" && req.method === "POST") {
470
+ try {
471
+ const { id } = JSON.parse(body);
472
+ const ok = deleteBackup(id);
473
+ res.end(JSON.stringify({ ok }));
474
+ }
475
+ catch {
476
+ res.statusCode = 400;
477
+ res.end(JSON.stringify({ error: "Invalid request" }));
478
+ }
479
+ return true;
480
+ }
481
+ // POST /api/restart — restart the bot (legacy)
482
+ if (urlPath === "/api/bot/restart" && req.method === "POST") {
483
+ const { scheduleGracefulRestart } = await import("../services/restart.js");
484
+ res.end(JSON.stringify({ ok: true, note: "Bot is restarting..." }));
485
+ scheduleGracefulRestart(500);
486
+ return true;
487
+ }
488
+ // ── PM2 Process Control ────────────────────────────────
489
+ // GET /api/pm2/status — Get PM2 process info
490
+ if (urlPath === "/api/pm2/status") {
491
+ try {
492
+ const output = execSync("pm2 jlist", { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
493
+ const processes = JSON.parse(output);
494
+ // Find our process (by name or script)
495
+ const botProcess = processes.find((p) => p.name === "alvin-bot" ||
496
+ p.pm2_env?.pm_exec_path?.includes("alvin-bot")) || processes[0]; // fallback to first process
497
+ if (!botProcess) {
498
+ res.end(JSON.stringify({ error: "No PM2 process found" }));
499
+ return true;
500
+ }
501
+ const env = botProcess.pm2_env || {};
502
+ res.end(JSON.stringify({
503
+ process: {
504
+ name: botProcess.name,
505
+ pid: botProcess.pid,
506
+ status: env.status || "unknown",
507
+ uptime: env.pm_uptime ? Date.now() - env.pm_uptime : 0,
508
+ memory: botProcess.monit?.memory || 0,
509
+ cpu: botProcess.monit?.cpu || 0,
510
+ restarts: env.restart_time || 0,
511
+ version: env.version || "?",
512
+ nodeVersion: env.node_version || process.version,
513
+ execPath: env.pm_exec_path || "?",
514
+ cwd: env.pm_cwd || "?",
515
+ },
516
+ }));
517
+ }
518
+ catch (err) {
519
+ res.end(JSON.stringify({ error: "PM2 not available" }));
520
+ }
521
+ return true;
522
+ }
523
+ // POST /api/pm2/action — Execute PM2 action (restart, stop, start, reload, flush)
524
+ if (urlPath === "/api/pm2/action" && req.method === "POST") {
525
+ try {
526
+ const { action } = JSON.parse(body);
527
+ const allowed = ["restart", "stop", "start", "reload", "flush"];
528
+ if (!allowed.includes(action)) {
529
+ res.statusCode = 400;
530
+ res.end(JSON.stringify({ ok: false, error: `Invalid action: ${action}` }));
531
+ return true;
532
+ }
533
+ // Find our process name
534
+ let processName = "alvin-bot";
535
+ try {
536
+ const jlist = execSync("pm2 jlist", { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
537
+ const procs = JSON.parse(jlist);
538
+ const found = procs.find((p) => p.name === "alvin-bot" || p.name === "alvin-bot");
539
+ if (found)
540
+ processName = found.name;
541
+ }
542
+ catch { /* use default */ }
543
+ if (action === "flush") {
544
+ execSync(`pm2 flush ${processName}`, { encoding: "utf-8", timeout: 10000, stdio: "pipe" });
545
+ res.end(JSON.stringify({ ok: true, message: "Logs flushed" }));
546
+ return true;
547
+ }
548
+ if (action === "stop") {
549
+ // Stop is special — we can't respond after stopping ourselves
550
+ res.end(JSON.stringify({ ok: true, message: "Bot is stopping..." }));
551
+ setTimeout(() => {
552
+ try {
553
+ execSync(`pm2 stop ${processName}`, { timeout: 10000, stdio: "pipe" });
554
+ }
555
+ catch { /* process might already be dead */ }
556
+ }, 300);
557
+ return true;
558
+ }
559
+ if (action === "start") {
560
+ // Start the process if stopped
561
+ execSync(`pm2 start ${processName}`, { encoding: "utf-8", timeout: 10000, stdio: "pipe" });
562
+ res.end(JSON.stringify({ ok: true, message: "Bot started" }));
563
+ return true;
564
+ }
565
+ if (action === "restart" || action === "reload") {
566
+ const { scheduleGracefulRestart } = await import("../services/restart.js");
567
+ res.end(JSON.stringify({ ok: true, message: `Bot is ${action === "restart" ? "restarting" : "reloading"}...` }));
568
+ scheduleGracefulRestart(500);
569
+ return true;
570
+ }
571
+ }
572
+ catch (err) {
573
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
574
+ }
575
+ return true;
576
+ }
577
+ // GET /api/pm2/logs — Get recent PM2 logs
578
+ if (urlPath === "/api/pm2/logs") {
579
+ try {
580
+ // Find process name
581
+ let processName = "alvin-bot";
582
+ try {
583
+ const jlist = execSync("pm2 jlist", { encoding: "utf-8", timeout: 5000, stdio: "pipe" });
584
+ const procs = JSON.parse(jlist);
585
+ const found = procs.find((p) => p.name === "alvin-bot" || p.name === "alvin-bot");
586
+ if (found)
587
+ processName = found.name;
588
+ }
589
+ catch { /* use default */ }
590
+ let logs = execSync(`pm2 logs ${processName} --nostream --lines 30 2>&1`, {
591
+ encoding: "utf-8",
592
+ timeout: 5000,
593
+ stdio: "pipe",
594
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
595
+ });
596
+ // Strip ANSI escape codes
597
+ logs = logs.replace(/\x1b\[[0-9;]*m/g, "");
598
+ res.end(JSON.stringify({ logs }));
599
+ }
600
+ catch (err) {
601
+ res.end(JSON.stringify({ error: "Logs not available", logs: "" }));
602
+ }
603
+ return true;
604
+ }
605
+ return false;
606
+ }