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.
- package/.env.example +43 -0
- package/BACKLOG.md +223 -0
- package/CHANGELOG.md +63 -0
- package/CLAUDE.example.md +152 -0
- package/CODE_OF_CONDUCT.md +52 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/SECURITY.md +38 -0
- package/SOUL.example.md +60 -0
- package/TOOLS.example.md +42 -0
- package/alvin-bot.config.example.json +24 -0
- package/bin/cli.js +1088 -0
- package/dist/.metadata_never_index +0 -0
- package/dist/claude.js +102 -0
- package/dist/config.js +65 -0
- package/dist/engine.js +90 -0
- package/dist/find-claude-binary.js +98 -0
- package/dist/handlers/commands.js +1489 -0
- package/dist/handlers/document.js +187 -0
- package/dist/handlers/message.js +200 -0
- package/dist/handlers/photo.js +154 -0
- package/dist/handlers/platform-message.js +275 -0
- package/dist/handlers/video.js +237 -0
- package/dist/handlers/voice.js +148 -0
- package/dist/i18n.js +299 -0
- package/dist/index.js +442 -0
- package/dist/init-data-dir.js +81 -0
- package/dist/middleware/auth.js +215 -0
- package/dist/migrate.js +139 -0
- package/dist/paths.js +87 -0
- package/dist/platforms/discord.js +161 -0
- package/dist/platforms/index.js +130 -0
- package/dist/platforms/signal.js +205 -0
- package/dist/platforms/slack.js +318 -0
- package/dist/platforms/telegram.js +111 -0
- package/dist/platforms/types.js +8 -0
- package/dist/platforms/whatsapp.js +648 -0
- package/dist/providers/claude-sdk-provider.js +173 -0
- package/dist/providers/codex-cli-provider.js +121 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai-compatible.js +388 -0
- package/dist/providers/registry.js +209 -0
- package/dist/providers/tool-executor.js +450 -0
- package/dist/providers/types.js +205 -0
- package/dist/services/access.js +144 -0
- package/dist/services/asset-index.js +230 -0
- package/dist/services/browser-manager.js +161 -0
- package/dist/services/browser.js +121 -0
- package/dist/services/compaction.js +129 -0
- package/dist/services/cron.js +462 -0
- package/dist/services/custom-tools.js +317 -0
- package/dist/services/delivery-queue.js +154 -0
- package/dist/services/elevenlabs.js +58 -0
- package/dist/services/embeddings.js +386 -0
- package/dist/services/exec-guard.js +46 -0
- package/dist/services/fallback-order.js +151 -0
- package/dist/services/heartbeat.js +192 -0
- package/dist/services/hooks.js +44 -0
- package/dist/services/imagegen.js +72 -0
- package/dist/services/language-detect.js +144 -0
- package/dist/services/markdown.js +63 -0
- package/dist/services/mcp.js +252 -0
- package/dist/services/memory.js +133 -0
- package/dist/services/personality.js +227 -0
- package/dist/services/plugins.js +171 -0
- package/dist/services/reminders.js +97 -0
- package/dist/services/restart.js +48 -0
- package/dist/services/security-audit.js +66 -0
- package/dist/services/self-search.js +129 -0
- package/dist/services/session.js +93 -0
- package/dist/services/skills.js +287 -0
- package/dist/services/standing-orders.js +29 -0
- package/dist/services/subagents.js +142 -0
- package/dist/services/sudo.js +243 -0
- package/dist/services/telegram.js +113 -0
- package/dist/services/tool-discovery.js +214 -0
- package/dist/services/usage-tracker.js +137 -0
- package/dist/services/users.js +199 -0
- package/dist/services/voice.js +95 -0
- package/dist/tui/index.js +507 -0
- package/dist/web/canvas.js +30 -0
- package/dist/web/doctor-api.js +606 -0
- package/dist/web/openai-compat.js +252 -0
- package/dist/web/server.js +1351 -0
- package/dist/web/setup-api.js +1078 -0
- package/docs/mcp.example.json +16 -0
- package/docs/screenshots/00-Login.png +0 -0
- package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
- package/docs/screenshots/02-Chat.png +0 -0
- package/docs/screenshots/03-Dashboard-Overview.png +0 -0
- package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
- package/docs/screenshots/05-Personality-Editor.png +0 -0
- package/docs/screenshots/06-Memory-Manager.png +0 -0
- package/docs/screenshots/07-Active-Sessions.png +0 -0
- package/docs/screenshots/08-File-Browser.png +0 -0
- package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
- package/docs/screenshots/10-Custom-Tools.png +0 -0
- package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
- package/docs/screenshots/12-Messaging-Platforms.png +0 -0
- package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
- package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
- package/docs/screenshots/13-User-Management.png +0 -0
- package/docs/screenshots/14-Web-Terminal.png +0 -0
- package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
- package/docs/screenshots/16-Settings-and-Env.png +0 -0
- package/docs/screenshots/TG-commands.png +0 -0
- package/docs/screenshots/TG.png +0 -0
- package/docs/screenshots/_Mac-Installer.png +0 -0
- package/docs/tools.example.json +33 -0
- package/install.sh +165 -0
- package/package.json +190 -0
- package/plugins/calendar/index.js +270 -0
- package/plugins/email/index.js +231 -0
- package/plugins/finance/index.js +254 -0
- package/plugins/notes/index.js +227 -0
- package/plugins/smarthome/index.js +230 -0
- package/plugins/weather/index.js +122 -0
- package/skills/apple-notes/SKILL.md +31 -0
- package/skills/browse/SKILL.md +136 -0
- package/skills/code-project/SKILL.md +43 -0
- package/skills/data-analysis/SKILL.md +39 -0
- package/skills/document-creation/SKILL.md +48 -0
- package/skills/email-summary/SKILL.md +46 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/summarize/SKILL.md +28 -0
- package/skills/system-admin/SKILL.md +39 -0
- package/skills/weather/SKILL.md +34 -0
- package/skills/web-research/SKILL.md +35 -0
- package/web/public/canvas.html +52 -0
- package/web/public/css/style.css +555 -0
- package/web/public/index.html +189 -0
- package/web/public/js/app.js +3102 -0
- package/web/public/js/i18n.js +1048 -0
- package/web/public/js/icons.js +104 -0
- 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
|
+
}
|