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
package/bin/cli.js ADDED
@@ -0,0 +1,1088 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Alvin Bot CLI — Setup, manage, and chat with your AI agent.
5
+ *
6
+ * Usage:
7
+ * alvin-bot setup — Interactive setup wizard
8
+ * alvin-bot tui — Terminal chat UI
9
+ * alvin-bot doctor — Check configuration
10
+ * alvin-bot audit — Security health check
11
+ * alvin-bot update — Pull latest & rebuild
12
+ * alvin-bot start — Start the bot
13
+ * alvin-bot stop — Stop the bot
14
+ *
15
+ * Flags:
16
+ * --lang en|de — Language (default: en, auto-detects from LANG env)
17
+ */
18
+
19
+ import { createInterface } from "readline";
20
+ import { existsSync, writeFileSync, readFileSync, mkdirSync, copyFileSync, readdirSync } from "fs";
21
+ import { resolve, join } from "path";
22
+ import { homedir } from "os";
23
+ import { execSync } from "child_process";
24
+ import { initI18n, t, getLocale } from "../dist/i18n.js";
25
+
26
+ // Data directory — same logic as src/paths.ts
27
+ const DATA_DIR = process.env.ALVIN_DATA_DIR || join(homedir(), ".alvin-bot");
28
+
29
+ // Init i18n early
30
+ initI18n();
31
+
32
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
33
+ const ask = (q) => new Promise((r) => rl.question(q, r));
34
+
35
+ const LOGO = `
36
+ ╔══════════════════════════════════════╗
37
+ ║ 🤖 Alvin Bot — Setup Wizard v3.0 ║
38
+ ║ Your Personal AI Agent ║
39
+ ╚══════════════════════════════════════╝
40
+ `;
41
+
42
+ // ── Provider Definitions ────────────────────────────────────────────────────
43
+
44
+ const PROVIDERS = [
45
+ {
46
+ key: "groq",
47
+ name: "Groq (Llama 3.3 70B)",
48
+ desc: () => t("provider.groq.desc"),
49
+ free: true,
50
+ envKey: "GROQ_API_KEY",
51
+ signup: "https://console.groq.com",
52
+ model: "llama-3.3-70b-versatile",
53
+ needsCLI: false,
54
+ },
55
+ {
56
+ key: "nvidia-kimi-k2.5",
57
+ name: "NVIDIA NIM (Kimi K2.5 — Best Tool Use)",
58
+ desc: () => t("provider.nvidia.desc"),
59
+ free: true,
60
+ envKey: "NVIDIA_API_KEY",
61
+ signup: "https://build.nvidia.com",
62
+ model: "moonshotai/kimi-k2.5",
63
+ needsCLI: false,
64
+ fallbackModel: "meta/llama-3.3-70b-instruct",
65
+ },
66
+ {
67
+ key: "gemini-2.5-flash",
68
+ name: "Google Gemini (2.5 Flash)",
69
+ desc: () => t("provider.gemini.desc"),
70
+ free: true,
71
+ envKey: "GOOGLE_API_KEY",
72
+ signup: "https://aistudio.google.com",
73
+ model: "gemini-2.5-flash",
74
+ needsCLI: false,
75
+ },
76
+ {
77
+ key: "openai",
78
+ name: "OpenAI (GPT-4o)",
79
+ desc: () => t("provider.openai.desc"),
80
+ free: false,
81
+ envKey: "OPENAI_API_KEY",
82
+ signup: "https://platform.openai.com",
83
+ model: "gpt-4o",
84
+ needsCLI: false,
85
+ },
86
+ {
87
+ key: "openrouter",
88
+ name: "OpenRouter (100+ Models)",
89
+ desc: () => t("provider.openrouter.desc"),
90
+ free: false,
91
+ envKey: "OPENROUTER_API_KEY",
92
+ signup: "https://openrouter.ai",
93
+ model: "anthropic/claude-sonnet-4",
94
+ needsCLI: false,
95
+ },
96
+ {
97
+ key: "claude-sdk",
98
+ name: "Claude Agent SDK (Premium)",
99
+ desc: () => t("provider.claude.desc"),
100
+ free: false,
101
+ envKey: null,
102
+ signup: "https://claude.ai",
103
+ model: "claude-sonnet-4-20250514",
104
+ needsCLI: true,
105
+ },
106
+ ];
107
+
108
+ // ── Provider Validation ────────────────────────────────────────────────────
109
+
110
+ /**
111
+ * Validate a provider's API key or auth by making a lightweight API call.
112
+ * Returns { ok: true, detail: "..." } or { ok: false, error: "..." }.
113
+ */
114
+ async function validateProviderKey(providerKey, apiKey) {
115
+ const timeout = 10_000;
116
+
117
+ try {
118
+ switch (providerKey) {
119
+ case "groq": {
120
+ const res = await fetch("https://api.groq.com/openai/v1/models", {
121
+ headers: { Authorization: `Bearer ${apiKey}` },
122
+ signal: AbortSignal.timeout(timeout),
123
+ });
124
+ if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
125
+ return { ok: true, detail: "Groq API key valid" };
126
+ }
127
+
128
+ case "nvidia-llama-3.3-70b":
129
+ case "nvidia-kimi-k2.5": {
130
+ const res = await fetch("https://integrate.api.nvidia.com/v1/models", {
131
+ headers: { Authorization: `Bearer ${apiKey}` },
132
+ signal: AbortSignal.timeout(timeout),
133
+ });
134
+ if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
135
+ return { ok: true, detail: "NVIDIA API key valid" };
136
+ }
137
+
138
+ case "gemini-2.5-flash": {
139
+ const res = await fetch(
140
+ `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
141
+ { signal: AbortSignal.timeout(timeout) }
142
+ );
143
+ if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
144
+ return { ok: true, detail: "Google API key valid" };
145
+ }
146
+
147
+ case "openai":
148
+ case "gpt-4o": {
149
+ const res = await fetch("https://api.openai.com/v1/models", {
150
+ headers: { Authorization: `Bearer ${apiKey}` },
151
+ signal: AbortSignal.timeout(timeout),
152
+ });
153
+ if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
154
+ return { ok: true, detail: "OpenAI API key valid" };
155
+ }
156
+
157
+ case "openrouter": {
158
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
159
+ headers: { Authorization: `Bearer ${apiKey}` },
160
+ signal: AbortSignal.timeout(timeout),
161
+ });
162
+ if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
163
+ return { ok: true, detail: "OpenRouter API key valid" };
164
+ }
165
+
166
+ case "claude-sdk": {
167
+ // Find claude binary — check PATH and common locations
168
+ let claudeBin = null;
169
+ try {
170
+ execSync("claude --version", { stdio: "pipe", timeout: 5000 });
171
+ claudeBin = "claude";
172
+ } catch {
173
+ // Not in PATH — try common native install locations
174
+ const candidates = [
175
+ join(homedir(), ".local", "bin", "claude"),
176
+ "/usr/local/bin/claude",
177
+ ];
178
+ for (const p of candidates) {
179
+ if (existsSync(p)) { claudeBin = p; break; }
180
+ }
181
+ }
182
+ if (!claudeBin) {
183
+ return { ok: false, error: "Claude CLI not installed" };
184
+ }
185
+ try {
186
+ // Use `auth status` instead of `-p "ping"` — faster and doesn't require a full query
187
+ const authJson = execSync(`${claudeBin} auth status`, {
188
+ stdio: "pipe", timeout: 10000, encoding: "utf-8",
189
+ });
190
+ const authData = JSON.parse(authJson);
191
+ if (authData.loggedIn) {
192
+ return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
193
+ }
194
+ return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
195
+ } catch (err) {
196
+ const msg = err.stdout?.toString() || err.stderr?.toString() || err.message || "";
197
+ // Try parsing JSON from stdout (auth status exits with code 1 when not logged in)
198
+ try {
199
+ const authData = JSON.parse(msg);
200
+ if (authData.loggedIn) {
201
+ return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
202
+ }
203
+ } catch {}
204
+ return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
205
+ }
206
+ }
207
+
208
+ default:
209
+ return { ok: true, detail: "No validation available for this provider" };
210
+ }
211
+ } catch (err) {
212
+ if (err.name === "TimeoutError" || err.code === "ABORT_ERR") {
213
+ return { ok: false, error: "Connection timed out — check your internet" };
214
+ }
215
+ return { ok: false, error: err.message || "Unknown error" };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Validate a Telegram bot token by calling getMe.
221
+ * Returns { ok: true, botName: "@username" } or { ok: false, error: "reason" }.
222
+ */
223
+ async function validateTelegramToken(token) {
224
+ if (!token) return { ok: false, error: "No token provided" };
225
+ try {
226
+ const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
227
+ signal: AbortSignal.timeout(5000),
228
+ });
229
+ const data = await res.json();
230
+ if (data.ok) {
231
+ return { ok: true, botName: `@${data.result.username}` };
232
+ }
233
+ return { ok: false, error: data.description || "Invalid token" };
234
+ } catch (err) {
235
+ return { ok: false, error: err.message || "Connection failed" };
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Run post-setup validation: provider, Telegram, port.
241
+ */
242
+ async function runPostSetupValidation(providerKey, apiKey, botToken, webPort) {
243
+ console.log(`\n━━━ Validating Setup ━━━\n`);
244
+ let allGood = true;
245
+
246
+ // 1. Provider
247
+ if (providerKey === "claude-sdk" || apiKey) {
248
+ console.log(` Testing provider...`);
249
+ const pResult = await validateProviderKey(providerKey, apiKey);
250
+ if (pResult.ok) {
251
+ console.log(` ✅ Provider — ${pResult.detail}`);
252
+ } else {
253
+ console.log(` ❌ Provider — ${pResult.error}`);
254
+ allGood = false;
255
+ }
256
+ } else {
257
+ console.log(` ⚠️ Provider: No API key configured`);
258
+ allGood = false;
259
+ }
260
+
261
+ // 2. Telegram
262
+ if (botToken) {
263
+ console.log(` Testing Telegram...`);
264
+ const tResult = await validateTelegramToken(botToken);
265
+ if (tResult.ok) {
266
+ console.log(` ✅ Telegram: ${tResult.botName}`);
267
+ } else {
268
+ console.log(` ❌ Telegram: ${tResult.error}`);
269
+ allGood = false;
270
+ }
271
+ } else {
272
+ console.log(` ℹ️ Telegram: Skipped (no token)`);
273
+ }
274
+
275
+ // 3. Web UI port
276
+ const port = webPort || 3100;
277
+ try {
278
+ const net = await import("net");
279
+ await new Promise((resolve, reject) => {
280
+ const srv = net.createServer();
281
+ srv.once("error", () => { srv.close(); reject(); });
282
+ srv.once("listening", () => { srv.close(); resolve(); });
283
+ srv.listen(port);
284
+ });
285
+ console.log(` ✅ Web UI: Port ${port} available`);
286
+ } catch {
287
+ console.log(` ⚠️ Web UI: Port ${port} in use (another instance running?)`);
288
+ }
289
+
290
+ console.log("");
291
+
292
+ if (!allGood) {
293
+ console.log(` Some checks failed. Run 'alvin-bot doctor' after fixing to verify.\n`);
294
+ } else {
295
+ console.log(` All checks passed! ✅\n`);
296
+ }
297
+
298
+ return allGood;
299
+ }
300
+
301
+ // ── Setup Wizard ────────────────────────────────────────────────────────────
302
+
303
+ async function setup() {
304
+ console.log(LOGO);
305
+
306
+ // ── Prerequisites
307
+ console.log(t("setup.checkingPrereqs"));
308
+
309
+ let hasNode = false;
310
+ try {
311
+ const nodeVersion = execSync("node --version", { encoding: "utf-8" }).trim();
312
+ const major = parseInt(nodeVersion.slice(1));
313
+ hasNode = major >= 18;
314
+ console.log(` ${hasNode ? "✅" : "❌"} Node.js ${nodeVersion}${major < 18 ? ` (${t("setup.needVersion")})` : ""}`);
315
+ } catch {
316
+ console.log(` ❌ ${t("setup.nodeNotFound")}`);
317
+ }
318
+
319
+ if (!hasNode) {
320
+ console.log(`\n❌ ${t("setup.nodeRequired")}`);
321
+ rl.close();
322
+ return;
323
+ }
324
+
325
+ // ── Step 1: Telegram Bot
326
+ console.log(`\n━━━ ${t("setup.step1")} ━━━`);
327
+ console.log(t("setup.step1.intro"));
328
+ console.log(` (Press Enter to skip — WebUI-only mode)\n`);
329
+ let botToken = (await ask(t("setup.botToken"))).trim();
330
+
331
+ if (!botToken) {
332
+ console.log(` ℹ️ Skipping Telegram — bot will run in WebUI-only mode.`);
333
+ console.log(` You can add BOT_TOKEN to ~/.alvin-bot/.env later.\n`);
334
+ } else if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
335
+ console.log(`\n ⚠️ That doesn't look like a valid bot token.`);
336
+ console.log(` Expected format: 123456789:ABCdefGHI-jklMNO`);
337
+ console.log(` Get one from @BotFather on Telegram.\n`);
338
+ const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
339
+ if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
340
+ rl.close();
341
+ return;
342
+ }
343
+ }
344
+
345
+ // Validate token with Telegram API
346
+ if (botToken && /^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
347
+ console.log(` Validating...`);
348
+ const tgResult = await validateTelegramToken(botToken);
349
+ if (tgResult.ok) {
350
+ console.log(` ✅ Bot: ${tgResult.botName}\n`);
351
+ } else {
352
+ console.log(` ❌ ${tgResult.error}`);
353
+ console.log(` Check your token at @BotFather on Telegram.\n`);
354
+ const retryToken = (await ask(` Re-enter token (or Enter to skip): `)).trim();
355
+ if (retryToken) {
356
+ botToken = retryToken;
357
+ const retry = await validateTelegramToken(botToken);
358
+ if (retry.ok) console.log(` ✅ Bot: ${retry.botName}\n`);
359
+ else console.log(` ⚠️ Still invalid — continuing anyway.\n`);
360
+ }
361
+ }
362
+ }
363
+
364
+ // ── Step 2: User ID
365
+ let userId = "";
366
+ if (botToken) {
367
+ console.log(`\n━━━ ${t("setup.step2")} ━━━`);
368
+ console.log(t("setup.step2.intro"));
369
+ console.log(` 💡 Send /start to @userinfobot on Telegram to find your ID.`);
370
+ console.log(` (Press Enter to skip — you can add it later)\n`);
371
+ userId = (await ask(t("setup.userId"))).trim();
372
+
373
+ if (!userId) {
374
+ console.log(` ℹ️ Skipping — add ALLOWED_USERS to ~/.alvin-bot/.env later.\n`);
375
+ } else {
376
+ // Validate user ID is numeric
377
+ const userIds = userId.split(",").map(s => s.trim());
378
+ const invalidIds = userIds.filter(id => !/^\d+$/.test(id));
379
+ if (invalidIds.length > 0) {
380
+ console.log(`\n ⚠️ User IDs must be numbers, got: ${invalidIds.join(", ")}`);
381
+ console.log(` Send /start to @userinfobot on Telegram to get your numeric ID.\n`);
382
+ const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
383
+ if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
384
+ rl.close();
385
+ return;
386
+ }
387
+ }
388
+
389
+ // Warn if user ID matches bot token prefix (common mistake)
390
+ const botIdPrefix = botToken.split(":")[0];
391
+ const userIdList = userId.split(",").map(s => s.trim());
392
+ if (userIdList.includes(botIdPrefix)) {
393
+ console.log(`\n ⚠️ "${botIdPrefix}" looks like the bot's own ID, not yours!`);
394
+ console.log(` The bot token starts with the bot's ID. You need YOUR user ID.`);
395
+ console.log(` Send /start to @userinfobot on Telegram to get your ID.\n`);
396
+ const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
397
+ if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
398
+ userId = "";
399
+ console.log(` ℹ️ Cleared — add ALLOWED_USERS to ~/.alvin-bot/.env later.\n`);
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ // ── Step 3: AI Provider
406
+ console.log(`\n━━━ ${t("setup.step3")} ━━━`);
407
+ console.log(t("setup.step3.intro") + "\n");
408
+
409
+ for (let i = 0; i < PROVIDERS.length; i++) {
410
+ const p = PROVIDERS[i];
411
+ const badge = p.free ? "🆓" : "💰";
412
+ const premium = p.needsCLI ? " ⭐" : "";
413
+ console.log(` ${i + 1}. ${badge} ${p.name}${premium}`);
414
+ console.log(` ${p.desc()}`);
415
+ if (p.signup) console.log(` → ${p.signup}`);
416
+ console.log("");
417
+ }
418
+
419
+ const providerChoice = parseInt((await ask(t("setup.yourChoice"))).trim()) || 1;
420
+ let provider = PROVIDERS[Math.max(0, Math.min(providerChoice - 1, PROVIDERS.length - 1))];
421
+
422
+ console.log(`\n✅ ${t("setup.providerSelected")} ${provider.name}`);
423
+
424
+ // ── Validate Provider ────────────────────────────────────────────
425
+
426
+ // Claude SDK: show requirements upfront
427
+ if (provider.needsCLI) {
428
+ console.log(`\n ⚠️ Claude SDK requires one of:`);
429
+ console.log(` • Claude Max/Team subscription ($20+/mo)`);
430
+ console.log(` • Anthropic API key with Agent SDK access\n`);
431
+
432
+ const yesChars = getLocale() === "de" ? ["j", "ja", "y", "yes", ""] : ["y", "yes", ""];
433
+ const proceed = (await ask(` Continue with Claude SDK? (Y/n): `)).trim().toLowerCase();
434
+ if (!yesChars.includes(proceed)) {
435
+ console.log(`\n Switching to provider selection...\n`);
436
+ provider = PROVIDERS[0];
437
+ console.log(` ✅ Switched to ${provider.name} (free)\n`);
438
+ } else {
439
+ // Check CLI installed
440
+ let cliInstalled = false;
441
+ try {
442
+ execSync("claude --version", { encoding: "utf-8", stdio: "pipe" });
443
+ cliInstalled = true;
444
+ console.log(` ✅ Claude CLI found`);
445
+ } catch {
446
+ console.log(` ⚠️ Claude CLI not found (native binary required).`);
447
+ console.log(`\n The Claude Agent SDK needs the native Claude Code binary.`);
448
+ console.log(` Install it with:\n`);
449
+ console.log(` curl -fsSL https://claude.ai/install.sh | sh\n`);
450
+ console.log(` (npm install @anthropic-ai/claude-code does NOT work for this)\n`);
451
+ const yc = getLocale() === "de" ? ["j", "ja"] : ["y", "yes"];
452
+ const doInstall = (await ask(` Already installed or want to try now? (y/n): `)).trim().toLowerCase();
453
+ if (yc.includes(doInstall)) {
454
+ console.log(`\n Installing Claude CLI (native)...`);
455
+ try {
456
+ execSync("curl -fsSL https://claude.ai/install.sh | sh", { stdio: "inherit", timeout: 120_000 });
457
+ // Add ~/.local/bin to PATH for this process (installer puts claude there)
458
+ const localBin = join(homedir(), ".local", "bin");
459
+ if (!process.env.PATH.includes(localBin)) {
460
+ process.env.PATH = `${localBin}:${process.env.PATH}`;
461
+ }
462
+ cliInstalled = true;
463
+ console.log(` ✅ Claude CLI installed\n`);
464
+ } catch {
465
+ console.log(` ❌ Installation failed. Try manually: curl -fsSL https://claude.ai/install.sh | sh`);
466
+ }
467
+ }
468
+ }
469
+
470
+ if (cliInstalled) {
471
+ console.log(`\n Checking Claude SDK authentication...`);
472
+ const authResult = await validateProviderKey("claude-sdk", null);
473
+ if (!authResult.ok) {
474
+ console.log(` ⚠️ ${authResult.error}`);
475
+ console.log(`\n Logging in to Claude...`);
476
+ console.log(` This will open your browser for authentication.\n`);
477
+ try {
478
+ // Find claude binary for auth login
479
+ let authBin = "claude";
480
+ const authLocalBin = join(homedir(), ".local", "bin", "claude");
481
+ if (existsSync(authLocalBin)) authBin = `"${authLocalBin}"`;
482
+ execSync(`${authBin} auth login --claudeai`, {
483
+ stdio: "inherit",
484
+ timeout: 120000,
485
+ });
486
+ } catch {
487
+ console.log(`\n ⚠️ Auto-login failed. Please run manually in another terminal:`);
488
+ console.log(` claude auth login\n`);
489
+ await ask(` Press Enter when you've logged in...`);
490
+ }
491
+
492
+ const recheck = await validateProviderKey("claude-sdk", null);
493
+ if (recheck.ok) {
494
+ console.log(`\n ✅ Claude SDK authenticated!\n`);
495
+ } else {
496
+ console.log(`\n ❌ Claude SDK still not working: ${recheck.error}`);
497
+ console.log(` Switching to a free provider.\n`);
498
+ provider = PROVIDERS[0];
499
+ console.log(` ✅ Switched to ${provider.name} (free)\n`);
500
+ }
501
+ } else {
502
+ console.log(` ✅ ${authResult.detail}\n`);
503
+ }
504
+ } else {
505
+ console.log(`\n Claude CLI not available. Switching to a free provider.\n`);
506
+ provider = PROVIDERS[0];
507
+ console.log(` ✅ Switched to ${provider.name} (free)\n`);
508
+ }
509
+ }
510
+ }
511
+
512
+ // Get and validate API key
513
+ let providerApiKey = "";
514
+ if (provider.envKey) {
515
+ console.log(`\n API key for ${provider.name}:`);
516
+ console.log(` Get one at: ${provider.signup}\n`);
517
+ providerApiKey = (await ask(` ${provider.envKey}: `)).trim();
518
+
519
+ if (providerApiKey) {
520
+ console.log(`\n Validating...`);
521
+ let keyResult = await validateProviderKey(provider.key, providerApiKey);
522
+ if (keyResult.ok) {
523
+ console.log(` ✅ ${keyResult.detail}\n`);
524
+ } else {
525
+ console.log(` ❌ ${keyResult.error}\n`);
526
+ let resolved = false;
527
+ for (let attempt = 0; attempt < 2 && !resolved; attempt++) {
528
+ const choice = (await ask(` 1. Enter new key 2. Switch provider 3. Skip\n Choice: `)).trim();
529
+ if (choice === "1") {
530
+ providerApiKey = (await ask(`\n ${provider.envKey}: `)).trim();
531
+ if (providerApiKey) {
532
+ console.log(` Validating...`);
533
+ keyResult = await validateProviderKey(provider.key, providerApiKey);
534
+ if (keyResult.ok) {
535
+ console.log(` ✅ ${keyResult.detail}\n`);
536
+ resolved = true;
537
+ } else {
538
+ console.log(` ❌ ${keyResult.error}\n`);
539
+ }
540
+ }
541
+ } else if (choice === "2") {
542
+ provider = PROVIDERS[0];
543
+ console.log(`\n Switched to ${provider.name} (free)`);
544
+ console.log(` Get a free key at: ${provider.signup}\n`);
545
+ providerApiKey = (await ask(` ${provider.envKey}: `)).trim();
546
+ if (providerApiKey) {
547
+ const gr = await validateProviderKey(provider.key, providerApiKey);
548
+ if (gr.ok) console.log(` ✅ ${gr.detail}\n`);
549
+ else console.log(` ⚠️ ${gr.error} — continuing anyway\n`);
550
+ }
551
+ resolved = true;
552
+ } else {
553
+ console.log(` ⚠️ Skipping — bot won't work until a valid key is configured.\n`);
554
+ providerApiKey = "";
555
+ resolved = true;
556
+ }
557
+ }
558
+ }
559
+ } else {
560
+ console.log(`\n ⚠️ No API key provided. Bot won't work until configured.`);
561
+ console.log(` Get one at: ${provider.signup}\n`);
562
+ }
563
+ }
564
+
565
+ // ── Step 4: Fallback & Extras
566
+ console.log(`\n━━━ ${t("setup.step4")} ━━━\n`);
567
+
568
+ let groqKey = "";
569
+ if (provider.key !== "groq") {
570
+ console.log(` ${t("setup.groqFallback")}\n`);
571
+ groqKey = (await ask(t("setup.groqKeyPrompt"))).trim();
572
+ if (!groqKey) {
573
+ console.log(` ℹ️ ${t("setup.noGroqKey")}\n`);
574
+ }
575
+ } else {
576
+ groqKey = providerApiKey;
577
+ }
578
+
579
+ console.log(` ${t("setup.extraKeys")}\n`);
580
+ const extraKeys = {};
581
+ if (provider.key !== "nvidia-llama-3.3-70b" && provider.key !== "nvidia-kimi-k2.5") {
582
+ const nk = (await ask(` ${t("setup.nvidiaKeyPrompt")}`)).trim();
583
+ if (nk) extraKeys["NVIDIA_API_KEY"] = nk;
584
+ }
585
+ if (provider.key !== "gemini-2.5-flash") {
586
+ const gk = (await ask(` ${t("setup.googleKeyPrompt")}`)).trim();
587
+ if (gk) extraKeys["GOOGLE_API_KEY"] = gk;
588
+ }
589
+ if (provider.key !== "openai" && provider.key !== "gpt-4o") {
590
+ const ok = (await ask(` ${t("setup.openaiKeyPrompt")}`)).trim();
591
+ if (ok) extraKeys["OPENAI_API_KEY"] = ok;
592
+ }
593
+
594
+ // Fallback order
595
+ console.log(`\n ${t("setup.fallbackOrder")}`);
596
+ const availableFallbacks = [];
597
+ if (groqKey && provider.key !== "groq") availableFallbacks.push("groq");
598
+ if (extraKeys["NVIDIA_API_KEY"]) availableFallbacks.push("nvidia-llama-3.3-70b");
599
+ // If NVIDIA is primary, add llama as fallback automatically
600
+ if (provider.key === "nvidia-kimi-k2.5" && !availableFallbacks.includes("nvidia-llama-3.3-70b")) {
601
+ availableFallbacks.push("nvidia-llama-3.3-70b");
602
+ }
603
+ if (extraKeys["GOOGLE_API_KEY"]) availableFallbacks.push("gemini-2.5-flash");
604
+ if (extraKeys["OPENAI_API_KEY"]) availableFallbacks.push("gpt-4o");
605
+
606
+ if (availableFallbacks.length > 0) {
607
+ console.log(` ${t("setup.defaultOrder")} ${availableFallbacks.join(" → ")}`);
608
+ const customOrder = (await ask(` ${t("setup.customOrder")}`)).trim();
609
+ if (customOrder) {
610
+ availableFallbacks.length = 0;
611
+ availableFallbacks.push(...customOrder.split(",").map(s => s.trim()).filter(Boolean));
612
+ }
613
+ } else {
614
+ console.log(` ${t("setup.noFallbacks")}`);
615
+ }
616
+
617
+ console.log("");
618
+ const webPassword = (await ask(t("setup.webPassword"))).trim();
619
+
620
+ // ── Step 5: Platforms
621
+ console.log(`\n━━━ ${t("setup.step5")} ━━━`);
622
+ console.log(`${t("setup.step5.intro")}\n`);
623
+ console.log(` 1. ${t("setup.platform.telegramOnly")}`);
624
+ console.log(` 2. ${t("setup.platform.whatsapp")}`);
625
+ console.log(` 3. ${t("setup.platform.later")}\n`);
626
+
627
+ const platformChoice = parseInt((await ask(t("setup.platformChoice"))).trim()) || 1;
628
+ const enableWhatsApp = platformChoice === 2;
629
+
630
+ // ── Write .env
631
+ console.log(`\n${t("setup.writingConfig")}`);
632
+
633
+ const envLines = [
634
+ "# === Telegram ===",
635
+ `BOT_TOKEN=${botToken || ""}`,
636
+ `ALLOWED_USERS=${userId || ""}`,
637
+ "",
638
+ "# === AI Provider ===",
639
+ `PRIMARY_PROVIDER=${provider.key}`,
640
+ ];
641
+
642
+ if (provider.envKey && providerApiKey) {
643
+ envLines.push(`${provider.envKey}=${providerApiKey}`);
644
+ }
645
+
646
+ if (groqKey && provider.key !== "groq") {
647
+ envLines.push(`GROQ_API_KEY=${groqKey}`);
648
+ }
649
+
650
+ for (const [envKey, value] of Object.entries(extraKeys)) {
651
+ envLines.push(`${envKey}=${value}`);
652
+ }
653
+
654
+ if (availableFallbacks.length > 0) {
655
+ envLines.push(`FALLBACK_PROVIDERS=${availableFallbacks.join(",")}`);
656
+ }
657
+
658
+ envLines.push("");
659
+ envLines.push("# === Agent ===");
660
+ envLines.push(`WORKING_DIR=${os.homedir()}`);
661
+ envLines.push("MAX_BUDGET_USD=5.0");
662
+
663
+ if (webPassword) {
664
+ envLines.push(`WEB_PASSWORD=${webPassword}`);
665
+ }
666
+
667
+ envLines.push("WEB_PORT=3100");
668
+
669
+ if (enableWhatsApp) {
670
+ envLines.push("");
671
+ envLines.push("# === WhatsApp ===");
672
+ envLines.push("WHATSAPP_ENABLED=true");
673
+ }
674
+
675
+ const envContent = envLines.join("\n") + "\n";
676
+
677
+ // Ensure DATA_DIR exists
678
+ if (!existsSync(DATA_DIR)) {
679
+ mkdirSync(DATA_DIR, { recursive: true });
680
+ }
681
+
682
+ // Write .env to ~/.alvin-bot/.env (works for both global npm install and local dev)
683
+ const envPath = resolve(DATA_DIR, ".env");
684
+
685
+ if (existsSync(envPath)) {
686
+ const backup = `${envPath}.backup-${Date.now()}`;
687
+ writeFileSync(backup, readFileSync(envPath));
688
+ console.log(` ${t("setup.backup")} ${backup}`);
689
+ }
690
+
691
+ writeFileSync(envPath, envContent);
692
+ console.log(` ✅ Config saved to ${envPath}`);
693
+
694
+ // Also write to cwd if we're in a dev/git environment (convenience)
695
+ const cwdEnvPath = resolve(process.cwd(), ".env");
696
+ const isDevMode = existsSync(resolve(process.cwd(), ".git"));
697
+ if (isDevMode && cwdEnvPath !== envPath) {
698
+ writeFileSync(cwdEnvPath, envContent);
699
+ console.log(` ✅ Dev copy saved to ${cwdEnvPath}`);
700
+ }
701
+
702
+ // Create ~/.alvin-bot/ data directory
703
+ const memoryDir = resolve(DATA_DIR, "memory");
704
+ if (!existsSync(memoryDir)) {
705
+ mkdirSync(memoryDir, { recursive: true });
706
+ }
707
+
708
+ // Create soul.md if not exists
709
+ const soulPath = resolve(DATA_DIR, "soul.md");
710
+ if (!existsSync(soulPath)) {
711
+ const soulExample = resolve(process.cwd(), "SOUL.example.md");
712
+ if (existsSync(soulExample)) {
713
+ copyFileSync(soulExample, soulPath);
714
+ console.log(" ✅ soul.md initialized from example");
715
+ } else {
716
+ writeFileSync(soulPath, t("soul.default"));
717
+ console.log(` ✅ ${t("setup.soulCreated")}`);
718
+ }
719
+ }
720
+
721
+ // Initialize memory/MEMORY.md if not exists
722
+ const memoryMdPath = resolve(DATA_DIR, "memory", "MEMORY.md");
723
+ if (!existsSync(memoryMdPath)) {
724
+ writeFileSync(memoryMdPath, "# Long-term Memory\n\n> This file is your agent's long-term memory. Add important context here.\n> It persists across sessions and is read at every startup.\n");
725
+ console.log(" ✅ memory/MEMORY.md created");
726
+ }
727
+
728
+ // Initialize custom-models.json if not exists
729
+ const customModelsPath = resolve(DATA_DIR, "custom-models.json");
730
+ if (!existsSync(customModelsPath)) {
731
+ writeFileSync(customModelsPath, "[]");
732
+ console.log(" ✅ custom-models.json initialized");
733
+ }
734
+
735
+ // Copy TOOLS.example.md → tools.md if not exists
736
+ const toolsMdPath = resolve(DATA_DIR, "tools.md");
737
+ const toolsMdExample = resolve(process.cwd(), "TOOLS.example.md");
738
+ if (!existsSync(toolsMdPath) && existsSync(toolsMdExample)) {
739
+ copyFileSync(toolsMdExample, toolsMdPath);
740
+ console.log(" ✅ Custom tools initialized from example (tools.md)");
741
+ }
742
+
743
+ // Copy CLAUDE.example.md → CLAUDE.md in BOT_ROOT if not exists
744
+ const claudePath = resolve(process.cwd(), "CLAUDE.md");
745
+ const claudeExample = resolve(process.cwd(), "CLAUDE.example.md");
746
+ if (!existsSync(claudePath) && existsSync(claudeExample)) {
747
+ copyFileSync(claudeExample, claudePath);
748
+ console.log(" ✅ CLAUDE.md initialized from example");
749
+ }
750
+
751
+ // ── Build (only for local/dev installs — global npm installs already have dist/)
752
+ const isGlobalInstall = !existsSync(resolve(process.cwd(), "tsconfig.json"));
753
+ if (!isGlobalInstall) {
754
+ console.log(`\n${t("setup.building")}`);
755
+ try {
756
+ execSync("npm run build", { stdio: "inherit" });
757
+ console.log(` ✅ ${t("setup.buildOk")}`);
758
+ } catch {
759
+ console.log(`\n ❌ ${t("setup.buildFailed")}`);
760
+ console.log(` The bot cannot start without a successful build.`);
761
+ console.log(` Try running 'npm run build' manually to see the error.\n`);
762
+ rl.close();
763
+ return;
764
+ }
765
+ }
766
+
767
+ // ── Post-Setup Validation ──────────────────────────────────────────────
768
+ await runPostSetupValidation(provider.key, providerApiKey, botToken, 3100);
769
+
770
+ // ── Summary
771
+ const providerInfo = "";
772
+
773
+ const startCmds = isGlobalInstall
774
+ ? ` alvin-bot start (start the bot)
775
+ alvin-bot doctor (check configuration)
776
+
777
+ # Keep running permanently:
778
+ npm install -g pm2
779
+ pm2 start "alvin-bot start" --name alvin-bot
780
+ pm2 save && pm2 startup`
781
+ : ` npm run dev (development, hot reload)
782
+ npm start (production)
783
+ pm2 start ecosystem.config.cjs (production, auto-restart)`;
784
+
785
+ console.log(`
786
+ ━━━ ${t("setup.done")} ━━━
787
+
788
+ 🤖 Provider: ${provider.name}
789
+ 💬 Telegram: @... (check @BotFather)
790
+ 🌐 Web UI: http://localhost:3100${webPassword ? ` (${t("setup.passwordProtected")})` : ""}
791
+ 📁 Config: ${envPath}
792
+ ${enableWhatsApp ? ` 📱 ${t("setup.scanQr")}\n` : ""}${providerInfo}
793
+ Start:
794
+ ${startCmds}
795
+
796
+ Bot commands:
797
+ /help — Show all commands
798
+ /model — Switch AI model
799
+ /effort — Set thinking depth
800
+ /imagine — Generate images
801
+ /web — Web search
802
+ /cron — Scheduled tasks
803
+
804
+ ${t("setup.haveFun")}
805
+ `);
806
+
807
+ rl.close();
808
+ }
809
+
810
+ // ── Doctor ──────────────────────────────────────────────────────────────────
811
+
812
+ async function doctor() {
813
+ console.log(`\n━━━ Alvin Bot Health Check ━━━\n`);
814
+
815
+ // ── System ──
816
+ console.log(" System:");
817
+ try {
818
+ const v = execSync("node --version", { encoding: "utf-8" }).trim();
819
+ const major = parseInt(v.slice(1));
820
+ console.log(` ${major >= 18 ? "✅" : "❌"} Node.js ${v}${major < 18 ? " (need ≥ 18)" : ""}`);
821
+ } catch {
822
+ console.log(" ❌ Node.js not found");
823
+ }
824
+
825
+ // Config file
826
+ const dataEnvPath = resolve(DATA_DIR, ".env");
827
+ const cwdEnvPath = resolve(process.cwd(), ".env");
828
+ const envPath = existsSync(dataEnvPath) ? dataEnvPath : existsSync(cwdEnvPath) ? cwdEnvPath : null;
829
+
830
+ if (envPath) {
831
+ console.log(` ✅ Config: ${envPath}`);
832
+ } else {
833
+ console.log(` ❌ No .env found`);
834
+ console.log(` Run: alvin-bot setup\n`);
835
+ return;
836
+ }
837
+
838
+ const env = readFileSync(envPath, "utf-8");
839
+ const getEnv = (key) => {
840
+ const m = env.match(new RegExp(`^${key}=(.+)$`, "m"));
841
+ return m?.[1]?.trim() || "";
842
+ };
843
+
844
+ // Build
845
+ const distPaths = [
846
+ resolve(process.cwd(), "dist/index.js"),
847
+ resolve(import.meta.dirname || ".", "../dist/index.js"),
848
+ ];
849
+ console.log(` ${distPaths.some(p => existsSync(p)) ? "✅" : "❌"} Build present`);
850
+
851
+ // ── Provider ──
852
+ console.log("\n Provider:");
853
+ const primary = getEnv("PRIMARY_PROVIDER");
854
+ if (primary) {
855
+ const apiKeyMap = {
856
+ groq: "GROQ_API_KEY",
857
+ "nvidia-llama-3.3-70b": "NVIDIA_API_KEY",
858
+ "nvidia-kimi-k2.5": "NVIDIA_API_KEY",
859
+ "gemini-2.5-flash": "GOOGLE_API_KEY",
860
+ openai: "OPENAI_API_KEY",
861
+ "gpt-4o": "OPENAI_API_KEY",
862
+ openrouter: "OPENROUTER_API_KEY",
863
+ };
864
+ const keyName = apiKeyMap[primary];
865
+ const key = keyName ? getEnv(keyName) : null;
866
+
867
+ console.log(` Validating ${primary}...`);
868
+ const result = await validateProviderKey(primary, key);
869
+ if (result.ok) {
870
+ console.log(` ✅ ${primary} — ${result.detail}`);
871
+ } else {
872
+ console.log(` ❌ ${primary} — ${result.error}`);
873
+ }
874
+
875
+ const fallbacks = getEnv("FALLBACK_PROVIDERS");
876
+ if (fallbacks) {
877
+ console.log(` ℹ️ Fallbacks: ${fallbacks}`);
878
+ } else {
879
+ console.log(` ⚠️ No fallback providers configured`);
880
+ }
881
+ } else {
882
+ console.log(` ❌ PRIMARY_PROVIDER not set`);
883
+ }
884
+
885
+ // ── Telegram ──
886
+ console.log("\n Telegram:");
887
+ const botToken = getEnv("BOT_TOKEN");
888
+ if (botToken) {
889
+ const tResult = await validateTelegramToken(botToken);
890
+ if (tResult.ok) {
891
+ console.log(` ✅ Bot: ${tResult.botName}`);
892
+ } else {
893
+ console.log(` ❌ ${tResult.error}`);
894
+ }
895
+ } else {
896
+ console.log(` ⚠️ BOT_TOKEN not configured (WebUI-only mode)`);
897
+ }
898
+
899
+ const users = getEnv("ALLOWED_USERS");
900
+ if (users) {
901
+ const ids = users.split(",").map(s => s.trim());
902
+ const invalid = ids.filter(id => !/^\d+$/.test(id));
903
+ if (invalid.length > 0) {
904
+ console.log(` ⚠️ ALLOWED_USERS has non-numeric: ${invalid.join(", ")}`);
905
+ } else {
906
+ console.log(` ✅ ALLOWED_USERS: ${ids.length} user${ids.length > 1 ? "s" : ""}`);
907
+ }
908
+ } else if (botToken) {
909
+ console.log(` ❌ ALLOWED_USERS not set (nobody can message the bot)`);
910
+ }
911
+
912
+ // ── Extras ──
913
+ console.log("\n Extras:");
914
+
915
+ if (existsSync(resolve(DATA_DIR, "soul.md")) || existsSync(resolve(process.cwd(), "SOUL.md"))) {
916
+ console.log(` ✅ Personality (soul.md)`);
917
+ } else {
918
+ console.log(` ⚠️ No soul.md (bot uses default personality)`);
919
+ }
920
+
921
+ const pluginsDir = resolve(process.cwd(), "plugins");
922
+ if (existsSync(pluginsDir)) {
923
+ try {
924
+ const plugins = readdirSync(pluginsDir).filter(d => {
925
+ try { return existsSync(resolve(pluginsDir, d, "index.js")); } catch { return false; }
926
+ });
927
+ if (plugins.length > 0) console.log(` ✅ Plugins: ${plugins.join(", ")}`);
928
+ } catch { /* ignore */ }
929
+ }
930
+
931
+ if (env.includes("WHATSAPP_ENABLED=true")) {
932
+ const chromePaths = [
933
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
934
+ "/usr/bin/google-chrome", "/usr/bin/chromium",
935
+ ];
936
+ const hasChrome = chromePaths.some(p => existsSync(p));
937
+ console.log(` ${hasChrome ? "✅" : "⚠️ "} WhatsApp (Chrome: ${hasChrome ? "found" : "not found"})`);
938
+ }
939
+
940
+ console.log("");
941
+ }
942
+
943
+ // ── Update ──────────────────────────────────────────────────────────────────
944
+
945
+ async function update() {
946
+ console.log(`${t("update.title")}\n`);
947
+
948
+ try {
949
+ const isGit = existsSync(resolve(process.cwd(), ".git"));
950
+
951
+ if (isGit) {
952
+ console.log(` ${t("update.pulling")}`);
953
+ execSync("git pull", { stdio: "inherit" });
954
+ console.log(`\n ${t("update.installing")}`);
955
+ execSync("npm install", { stdio: "inherit" });
956
+ console.log(`\n ${t("update.building")}`);
957
+ execSync("npm run build", { stdio: "inherit" });
958
+ console.log(`\n ✅ ${t("update.done")}`);
959
+ } else {
960
+ console.log(` ${t("update.npm")}`);
961
+ execSync("npm update alvin-bot", { stdio: "inherit" });
962
+ console.log(`\n ✅ ${t("update.done")}`);
963
+ }
964
+ } catch (err) {
965
+ console.error(`\n ❌ ${t("update.failed")} ${err.message}`);
966
+ }
967
+ }
968
+
969
+ // ── Version ─────────────────────────────────────────────────────────────────
970
+
971
+ async function version() {
972
+ try {
973
+ const pkg = JSON.parse(readFileSync(resolve(import.meta.dirname || ".", "../package.json"), "utf-8"));
974
+ console.log(`Alvin Bot v${pkg.version}`);
975
+ } catch {
976
+ console.log("Alvin Bot (version unknown)");
977
+ }
978
+ }
979
+
980
+ // ── CLI Router ──────────────────────────────────────────────────────────────
981
+
982
+ const cmd = process.argv[2];
983
+ switch (cmd) {
984
+ case "setup":
985
+ setup().catch(console.error);
986
+ break;
987
+ case "doctor":
988
+ doctor().catch(console.error);
989
+ break;
990
+ case "update":
991
+ update().catch(console.error);
992
+ break;
993
+ case "start": {
994
+ const fg = process.argv.includes("--foreground") || process.argv.includes("-f");
995
+ if (fg) {
996
+ import("../dist/index.js");
997
+ } else {
998
+ // Start via PM2 (background, survives terminal close, auto-restart on crash)
999
+ try {
1000
+ execSync("pm2 --version", { stdio: "pipe" });
1001
+ } catch {
1002
+ // PM2 not installed — install it
1003
+ console.log("Installing PM2 for background operation...");
1004
+ try {
1005
+ execSync("npm install -g pm2", { stdio: "inherit", timeout: 60000 });
1006
+ } catch {
1007
+ console.log("Could not install PM2. Starting in foreground instead.");
1008
+ console.log("Tip: Install PM2 manually (npm install -g pm2) to run in background.\n");
1009
+ await import("../dist/index.js");
1010
+ break;
1011
+ }
1012
+ }
1013
+ const cliPath = resolve(join(import.meta.dirname, "cli.js"));
1014
+ try {
1015
+ // Stop existing instance if running
1016
+ execSync("pm2 delete alvin-bot", { stdio: "pipe" });
1017
+ } catch { /* not running — fine */ }
1018
+ execSync(`pm2 start "${cliPath}" --name alvin-bot -- start --foreground`, {
1019
+ stdio: "inherit",
1020
+ timeout: 15000,
1021
+ });
1022
+ console.log("\n✅ Bot is running in the background.");
1023
+ console.log(" Logs: pm2 logs alvin-bot");
1024
+ console.log(" Stop: alvin-bot stop");
1025
+ console.log(" Restart: alvin-bot start\n");
1026
+ process.exit(0);
1027
+ }
1028
+ break;
1029
+ }
1030
+ case "stop": {
1031
+ try {
1032
+ execSync("pm2 stop alvin-bot", { stdio: "inherit", timeout: 10000 });
1033
+ } catch {
1034
+ console.log("Bot is not running via PM2. If running in foreground, use Ctrl+C.");
1035
+ }
1036
+ process.exit(0);
1037
+ }
1038
+ case "tui":
1039
+ case "chat":
1040
+ import("../dist/tui/index.js").then(m => m.startTUI()).catch(console.error);
1041
+ break;
1042
+ case "search": {
1043
+ const searchQuery = process.argv.slice(3).join(" ");
1044
+ if (!searchQuery) {
1045
+ console.log("Usage: alvin-bot search <query>");
1046
+ console.log('Example: alvin-bot search "cover letter"');
1047
+ process.exit(1);
1048
+ }
1049
+ const { searchSelf, formatSearchResults } = await import("../dist/services/self-search.js");
1050
+ const results = await searchSelf(searchQuery);
1051
+ console.log(formatSearchResults(results));
1052
+ process.exit(0);
1053
+ }
1054
+ case "audit": {
1055
+ const { runAudit, formatAuditReport } = await import("../dist/services/security-audit.js");
1056
+ const checks = runAudit();
1057
+ console.log(formatAuditReport(checks));
1058
+ process.exit(checks.some(c => c.status === "FAIL") ? 1 : 0);
1059
+ break;
1060
+ }
1061
+ case "version":
1062
+ case "--version":
1063
+ case "-v":
1064
+ version();
1065
+ break;
1066
+ default:
1067
+ console.log(`
1068
+ ${t("cli.title")}
1069
+
1070
+ ${t("cli.commands")}
1071
+ setup ${t("cli.setup")}
1072
+ tui ${t("cli.tui")}
1073
+ chat ${t("cli.chatAlias")}
1074
+ doctor ${t("cli.doctorDesc")}
1075
+ audit Security health check (permissions, secrets, config)
1076
+ search Search your assets, memories, and skills
1077
+ update ${t("cli.updateDesc")}
1078
+ start ${t("cli.startDesc")} (background via PM2)
1079
+ start -f Start in foreground (for debugging)
1080
+ stop Stop the bot
1081
+ version ${t("cli.versionDesc")}
1082
+
1083
+ ${t("cli.example")}
1084
+ alvin-bot setup
1085
+ alvin-bot tui
1086
+ alvin-bot tui --lang de
1087
+ `);
1088
+ }