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/dist/index.js ADDED
@@ -0,0 +1,442 @@
1
+ // ── Bootstrap: ensure ~/.alvin-bot/ exists + migrate legacy data ────
2
+ import { ensureDataDirs, seedDefaults } from "./init-data-dir.js";
3
+ import { hasLegacyData, migrateFromLegacy } from "./migrate.js";
4
+ // 1. Create directory structure (no files yet)
5
+ ensureDataDirs();
6
+ // 2. Migrate legacy data BEFORE seeding defaults (so real data wins over templates)
7
+ if (hasLegacyData()) {
8
+ console.log("📦 Legacy data detected in repo — migrating to ~/.alvin-bot/ ...");
9
+ const result = migrateFromLegacy();
10
+ if (result.copied.length > 0) {
11
+ console.log(` Copied: ${result.copied.join(", ")}`);
12
+ }
13
+ console.log(" Migration done. Old files left in place (clean up manually).");
14
+ }
15
+ // 3. Seed defaults for any files that don't exist yet (fresh install)
16
+ seedDefaults();
17
+ // ── Normal imports (safe now — DATA_DIR is ready) ──────────────────
18
+ import { Bot, InlineKeyboard } from "grammy";
19
+ import { config } from "./config.js";
20
+ // ── Pre-flight config validation (warnings, not fatal) ──────────────
21
+ const hasTelegram = !!config.botToken;
22
+ let hasProvider = true;
23
+ if (!hasTelegram) {
24
+ console.warn("⚠️ BOT_TOKEN not set — Telegram disabled. WebUI + Cron still active.");
25
+ console.warn(" Run 'alvin-bot setup' or set BOT_TOKEN in ~/.alvin-bot/.env");
26
+ }
27
+ if (config.allowedUsers.length === 0 && hasTelegram) {
28
+ console.warn("⚠️ ALLOWED_USERS not set — nobody can message the Telegram bot yet.");
29
+ console.warn(" Send /start to @userinfobot on Telegram to find your ID.");
30
+ }
31
+ // Check if the chosen provider has a corresponding API key
32
+ const providerKeyMap = {
33
+ groq: "GROQ_API_KEY",
34
+ "nvidia-llama-3.3-70b": "NVIDIA_API_KEY",
35
+ "nvidia-kimi-k2.5": "NVIDIA_API_KEY",
36
+ "gemini-2.5-flash": "GOOGLE_API_KEY",
37
+ openai: "OPENAI_API_KEY",
38
+ "gpt-4o": "OPENAI_API_KEY",
39
+ openrouter: "OPENROUTER_API_KEY",
40
+ };
41
+ const requiredKey = providerKeyMap[config.primaryProvider];
42
+ if (requiredKey) {
43
+ const keyName = requiredKey.replace("_API_KEY", "").toLowerCase();
44
+ if (!config.apiKeys[keyName]) {
45
+ hasProvider = false;
46
+ console.warn(`⚠️ ${requiredKey} is missing — AI chat won't work until configured.`);
47
+ console.warn(` Your provider "${config.primaryProvider}" needs this key.`);
48
+ console.warn(` Run 'alvin-bot setup' or edit ~/.alvin-bot/.env`);
49
+ }
50
+ }
51
+ import { authMiddleware, addApprovedUser, removePendingPairing } from "./middleware/auth.js";
52
+ import { registerCommands } from "./handlers/commands.js";
53
+ import { handleMessage } from "./handlers/message.js";
54
+ import { handlePhoto } from "./handlers/photo.js";
55
+ import { handleVoice } from "./handlers/voice.js";
56
+ import { handleDocument } from "./handlers/document.js";
57
+ import { handleVideo } from "./handlers/video.js";
58
+ import { initEngine } from "./engine.js";
59
+ import { loadPlugins, registerPluginCommands, unloadPlugins } from "./services/plugins.js";
60
+ import { initMCP, disconnectMCP, hasMCPConfig } from "./services/mcp.js";
61
+ import { startWebServer } from "./web/server.js";
62
+ import { startScheduler, stopScheduler, setNotifyCallback } from "./services/cron.js";
63
+ import { processQueue, cleanupQueue, setSenders, enqueue } from "./services/delivery-queue.js";
64
+ import { discoverTools } from "./services/tool-discovery.js";
65
+ import { startHeartbeat } from "./services/heartbeat.js";
66
+ import { initEmbeddings } from "./services/embeddings.js";
67
+ import { loadSkills } from "./services/skills.js";
68
+ import { loadHooks } from "./services/hooks.js";
69
+ import { registerShutdownHandler } from "./services/restart.js";
70
+ import { cancelAllSubAgents } from "./services/subagents.js";
71
+ import { scanAssets } from "./services/asset-index.js";
72
+ // Scan asset directory and generate INDEX.json + INDEX.md
73
+ const assetScanResult = scanAssets();
74
+ if (assetScanResult.assets.length > 0) {
75
+ console.log(`📂 Assets: ${assetScanResult.assets.length} files indexed`);
76
+ }
77
+ // Discover available system tools (cached for prompt injection)
78
+ discoverTools();
79
+ // Load skill files
80
+ loadSkills();
81
+ // Load user-defined lifecycle hooks from ~/.alvin-bot/hooks/
82
+ const hookCount = loadHooks();
83
+ if (hookCount > 0)
84
+ console.log(`Hooks: ${hookCount} loaded`);
85
+ // Initialize multi-model engine (skip if no provider key)
86
+ let registry = null;
87
+ if (hasProvider) {
88
+ registry = initEngine();
89
+ console.log(`Engine initialized. Primary: ${registry.getActiveKey()}`);
90
+ }
91
+ else {
92
+ console.warn("⚠️ Engine not initialized — no AI provider configured.");
93
+ }
94
+ // Load plugins
95
+ const pluginResult = await loadPlugins();
96
+ if (pluginResult.loaded.length > 0) {
97
+ console.log(`Plugins loaded: ${pluginResult.loaded.join(", ")}`);
98
+ }
99
+ if (pluginResult.errors.length > 0) {
100
+ for (const err of pluginResult.errors) {
101
+ console.error(`Plugin error (${err.name}): ${err.error}`);
102
+ }
103
+ }
104
+ // Initialize MCP servers (if configured)
105
+ if (hasMCPConfig()) {
106
+ const mcpResult = await initMCP();
107
+ if (mcpResult.connected.length > 0) {
108
+ console.log(`MCP servers: ${mcpResult.connected.join(", ")}`);
109
+ }
110
+ if (mcpResult.errors.length > 0) {
111
+ for (const err of mcpResult.errors) {
112
+ console.error(`MCP error (${err.name}): ${err.error}`);
113
+ }
114
+ }
115
+ }
116
+ // Telegram bot instance (null if no BOT_TOKEN)
117
+ let bot = null;
118
+ if (hasTelegram) {
119
+ bot = new Bot(config.botToken);
120
+ // Auth middleware — alle Messages durchlaufen das
121
+ bot.use(authMiddleware);
122
+ // Commands registrieren
123
+ registerCommands(bot);
124
+ registerPluginCommands(bot);
125
+ // ── WhatsApp Approval Callbacks ──────────────────────────────────────────────
126
+ bot.callbackQuery(/^wa:approve:(.+)$/, async (ctx) => {
127
+ const approvalId = ctx.match[1];
128
+ const { removePendingApproval, getWhatsAppAdapter } = await import("./platforms/whatsapp.js");
129
+ const pending = removePendingApproval(approvalId);
130
+ if (!pending) {
131
+ await ctx.answerCallbackQuery("⏰ Anfrage abgelaufen");
132
+ await ctx.editMessageText(ctx.msg?.text + "\n\n⏰ _Abgelaufen_", { parse_mode: "Markdown" }).catch(() => { });
133
+ return;
134
+ }
135
+ await ctx.answerCallbackQuery("✅ Approved");
136
+ await ctx.editMessageText(ctx.msg?.text + `\n\n✅ Approved`, { parse_mode: "HTML" }).catch(() => { });
137
+ // Process the message through the platform handler
138
+ const adapter = getWhatsAppAdapter();
139
+ if (adapter) {
140
+ adapter.processApprovedMessage(pending.incoming).catch(err => console.error("WhatsApp approved message processing error:", err));
141
+ }
142
+ });
143
+ bot.callbackQuery(/^wa:deny:(.+)$/, async (ctx) => {
144
+ const approvalId = ctx.match[1];
145
+ const { removePendingApproval } = await import("./platforms/whatsapp.js");
146
+ const pending = removePendingApproval(approvalId);
147
+ await ctx.answerCallbackQuery("❌ Abgelehnt");
148
+ await ctx.editMessageText((ctx.msg?.text || "") + `\n\n❌ Abgelehnt`, { parse_mode: "HTML" }).catch(() => { });
149
+ // Clean up temp media files
150
+ if (pending?.incoming.media?.path) {
151
+ const fs = await import("fs");
152
+ fs.unlink(pending.incoming.media.path, () => { });
153
+ }
154
+ });
155
+ // ── DM Pairing Approval Callbacks ───────────────────────────────────────────
156
+ bot.callbackQuery(/^pair:(approve|deny):(\d+)$/, async (ctx) => {
157
+ const action = ctx.match[1];
158
+ const code = ctx.match[2];
159
+ const pairing = removePendingPairing(code);
160
+ if (!pairing) {
161
+ await ctx.answerCallbackQuery("⏰ Request expired or already handled");
162
+ await ctx.editMessageText((ctx.msg?.text || "") + "\n\n⏰ _Expired_", { parse_mode: "Markdown" }).catch(() => { });
163
+ return;
164
+ }
165
+ if (action === "approve") {
166
+ addApprovedUser(pairing.userId);
167
+ await ctx.answerCallbackQuery("✅ User approved");
168
+ const userTag = pairing.username ? `@${pairing.username}` : `ID ${pairing.userId}`;
169
+ await ctx.editMessageText((ctx.msg?.text || "") + `\n\n✅ Approved — ${userTag} can now chat with the bot.`, { parse_mode: "Markdown" }).catch(() => { });
170
+ // Notify the user they've been approved
171
+ try {
172
+ await ctx.api.sendMessage(pairing.userId, "✅ You've been approved! You can now chat with the bot.");
173
+ }
174
+ catch { /* user may have blocked the bot */ }
175
+ }
176
+ else {
177
+ await ctx.answerCallbackQuery("❌ User denied");
178
+ const userTag = pairing.username ? `@${pairing.username}` : `ID ${pairing.userId}`;
179
+ await ctx.editMessageText((ctx.msg?.text || "") + `\n\n❌ Denied — ${userTag} will not be able to chat.`, { parse_mode: "Markdown" }).catch(() => { });
180
+ // Notify the user they've been denied
181
+ try {
182
+ await ctx.api.sendMessage(pairing.userId, "❌ Your access request was denied by the admin.");
183
+ }
184
+ catch { /* user may have blocked the bot */ }
185
+ }
186
+ });
187
+ // Content handlers (Reihenfolge wichtig: spezifisch vor allgemein)
188
+ bot.on("message:voice", handleVoice);
189
+ bot.on("message:video", handleVideo);
190
+ bot.on("message:video_note", handleVideo);
191
+ bot.on("message:photo", handlePhoto);
192
+ bot.on("message:document", handleDocument);
193
+ bot.on("message:text", handleMessage);
194
+ // Error handling — log but don't crash
195
+ bot.catch((err) => {
196
+ const ctx = err.ctx;
197
+ const e = err.error;
198
+ console.error(`Error handling update ${ctx?.update?.update_id}:`, e);
199
+ // Try to notify the user
200
+ if (ctx?.chat?.id) {
201
+ ctx.reply("⚠️ An internal error occurred. Please try again.").catch(() => { });
202
+ }
203
+ });
204
+ }
205
+ // Delivery queue intervals (started later, cleared on shutdown)
206
+ let queueInterval = null;
207
+ let queueCleanupInterval = null;
208
+ // Graceful shutdown
209
+ let isShuttingDown = false;
210
+ const shutdown = async () => {
211
+ if (isShuttingDown)
212
+ return;
213
+ isShuttingDown = true;
214
+ console.log("Graceful shutdown initiated...");
215
+ cancelAllSubAgents();
216
+ stopScheduler();
217
+ if (queueInterval)
218
+ clearInterval(queueInterval);
219
+ if (queueCleanupInterval)
220
+ clearInterval(queueCleanupInterval);
221
+ if (bot)
222
+ bot.stop();
223
+ await unloadPlugins().catch(() => { });
224
+ await disconnectMCP().catch(() => { });
225
+ console.log("Goodbye! 👋");
226
+ process.exit(0);
227
+ };
228
+ // Register for graceful self-restart (used by tool-executor when AI triggers restart)
229
+ registerShutdownHandler(shutdown);
230
+ process.on("SIGINT", shutdown);
231
+ process.on("SIGTERM", shutdown);
232
+ process.on("uncaughtException", (err) => {
233
+ console.error("Uncaught exception:", err);
234
+ // Don't exit on uncaught exceptions — try to keep running
235
+ });
236
+ process.on("unhandledRejection", (reason) => {
237
+ console.error("Unhandled rejection:", reason);
238
+ });
239
+ // Start optional platform adapters via Platform Manager
240
+ async function startOptionalPlatforms() {
241
+ const { handlePlatformMessage } = await import("./handlers/platform-message.js");
242
+ const { autoLoadPlatforms, startAllAdapters, getAllAdapters } = await import("./platforms/index.js");
243
+ const loaded = await autoLoadPlatforms();
244
+ if (loaded.length > 0) {
245
+ await startAllAdapters(async (msg) => {
246
+ const adapter = getAllAdapters().find(a => a.platform === msg.platform);
247
+ if (adapter)
248
+ await handlePlatformMessage(msg, adapter);
249
+ });
250
+ const icons = { whatsapp: "📱", discord: "🎮", signal: "🔒" };
251
+ for (const p of loaded) {
252
+ console.log(`${icons[p] || "📡"} ${p.charAt(0).toUpperCase() + p.slice(1)} platform started`);
253
+ }
254
+ // Wire WhatsApp approval flow — routes to best available channel
255
+ if (loaded.includes("whatsapp") && bot) {
256
+ const { setApprovalRequestFn, setApprovalChannel, getWhatsAppAdapter } = await import("./platforms/whatsapp.js");
257
+ const telegramBot = bot; // capture for closure
258
+ setApprovalRequestFn(async (pending) => {
259
+ const mediaTag = pending.mediaType ? ` [${pending.mediaType}]` : "";
260
+ // ── Strategy: Try Telegram first → fallback to WhatsApp DM → Discord → Signal
261
+ let sent = false;
262
+ // 1. Telegram (preferred — has inline keyboards)
263
+ if (!sent && config.botToken && config.allowedUsers.length > 0) {
264
+ try {
265
+ const ownerChatId = config.allowedUsers[0];
266
+ const msgText = `💬 <b>WhatsApp Approval</b>\n\n` +
267
+ `<b>Gruppe:</b> ${pending.groupName}\n` +
268
+ `<b>Von:</b> ${pending.senderName} (+${pending.senderNumber})\n` +
269
+ `<b>Message:</b>${mediaTag}\n` +
270
+ `<blockquote>${pending.preview || "(no text)"}</blockquote>`;
271
+ const keyboard = new InlineKeyboard()
272
+ .text("✅ Approve", `wa:approve:${pending.id}`)
273
+ .text("❌ Ablehnen", `wa:deny:${pending.id}`);
274
+ await telegramBot.api.sendMessage(ownerChatId, msgText, {
275
+ parse_mode: "HTML",
276
+ reply_markup: keyboard,
277
+ });
278
+ setApprovalChannel("telegram");
279
+ sent = true;
280
+ }
281
+ catch (err) {
282
+ console.warn("Approval via Telegram failed, trying fallback:", err instanceof Error ? err.message : err);
283
+ }
284
+ }
285
+ // 2. WhatsApp DM (self-chat) — text-based approval
286
+ if (!sent) {
287
+ try {
288
+ const adapter = getWhatsAppAdapter();
289
+ const ownerWaId = adapter?.getOwnerChatId();
290
+ if (adapter && ownerWaId) {
291
+ const plainText = `🔐 *WhatsApp Approval*\n\n` +
292
+ `*Gruppe:* ${pending.groupName}\n` +
293
+ `*Von:* ${pending.senderName} (+${pending.senderNumber})\n` +
294
+ `*Message:*${mediaTag}\n` +
295
+ `> ${pending.preview || "(no text)"}\n\n` +
296
+ `Antworte *ok* oder *nein*`;
297
+ await adapter.sendText(ownerWaId, plainText);
298
+ setApprovalChannel("whatsapp");
299
+ sent = true;
300
+ }
301
+ }
302
+ catch (err) {
303
+ console.warn("Approval via WhatsApp DM failed, trying fallback:", err instanceof Error ? err.message : err);
304
+ }
305
+ }
306
+ // 3. Discord DM
307
+ if (!sent) {
308
+ try {
309
+ const { getAdapter } = await import("./platforms/index.js");
310
+ const discord = getAdapter("discord");
311
+ if (discord) {
312
+ await discord.sendText("owner", `🔐 WhatsApp Approval\n\nGroup: ${pending.groupName}\nFrom: ${pending.senderName} (+${pending.senderNumber})\nMessage:${mediaTag}\n> ${pending.preview || "(no text)"}\n\nReact with ✅ or ❌`);
313
+ setApprovalChannel("discord");
314
+ sent = true;
315
+ }
316
+ }
317
+ catch { /* Discord not available */ }
318
+ }
319
+ // 4. Signal
320
+ if (!sent) {
321
+ try {
322
+ const { getAdapter } = await import("./platforms/index.js");
323
+ const signal = getAdapter("signal");
324
+ if (signal) {
325
+ await signal.sendText("owner", `🔐 WhatsApp Approval\n\nGroup: ${pending.groupName}\nFrom: ${pending.senderName}\nMessage: ${pending.preview || "(no text)"}\n\nReply ok or no`);
326
+ setApprovalChannel("signal");
327
+ sent = true;
328
+ }
329
+ }
330
+ catch { /* Signal not available */ }
331
+ }
332
+ if (!sent) {
333
+ console.error("❌ No channel available for WhatsApp approval! Auto-denying.");
334
+ }
335
+ });
336
+ }
337
+ }
338
+ }
339
+ startOptionalPlatforms().catch(err => console.error("Platform startup error:", err));
340
+ // Start Web UI (ALWAYS — regardless of Telegram/AI config)
341
+ const webServer = startWebServer();
342
+ // Start Cron Scheduler — route notifications through delivery queue for reliability
343
+ setNotifyCallback(async (target, text) => {
344
+ if (target.platform === "web") {
345
+ // Web notifications are handled by the WebSocket clients polling cron status
346
+ return;
347
+ }
348
+ enqueue(target.platform, String(target.chatId), text);
349
+ });
350
+ startScheduler();
351
+ // Wire delivery queue senders
352
+ setSenders({
353
+ telegram: async (chatId, content) => {
354
+ if (!bot)
355
+ throw new Error("Telegram bot not initialized");
356
+ await bot.api.sendMessage(Number(chatId), content, { parse_mode: "Markdown" }).catch(() => bot.api.sendMessage(Number(chatId), content));
357
+ },
358
+ whatsapp: async (chatId, content) => {
359
+ const { getAdapter } = await import("./platforms/index.js");
360
+ const adapter = getAdapter("whatsapp");
361
+ if (adapter) {
362
+ await adapter.sendText(chatId, content);
363
+ }
364
+ else {
365
+ throw new Error("WhatsApp adapter not loaded");
366
+ }
367
+ },
368
+ discord: async (chatId, content) => {
369
+ const { getAdapter } = await import("./platforms/index.js");
370
+ const adapter = getAdapter("discord");
371
+ if (adapter) {
372
+ await adapter.sendText(chatId, content);
373
+ }
374
+ else {
375
+ throw new Error("Discord adapter not loaded");
376
+ }
377
+ },
378
+ slack: async (chatId, content) => {
379
+ const { getAdapter } = await import("./platforms/index.js");
380
+ const adapter = getAdapter("slack");
381
+ if (adapter) {
382
+ await adapter.sendText(chatId, content);
383
+ }
384
+ else {
385
+ throw new Error("Slack adapter not loaded");
386
+ }
387
+ },
388
+ signal: async (chatId, content) => {
389
+ const { getAdapter } = await import("./platforms/index.js");
390
+ const adapter = getAdapter("signal");
391
+ if (adapter) {
392
+ await adapter.sendText(chatId, content);
393
+ }
394
+ else {
395
+ throw new Error("Signal adapter not loaded");
396
+ }
397
+ },
398
+ });
399
+ // Start delivery queue processor (30s interval)
400
+ queueInterval = setInterval(async () => {
401
+ try {
402
+ await processQueue();
403
+ }
404
+ catch (err) {
405
+ console.error("Delivery queue error:", err);
406
+ }
407
+ }, 30000);
408
+ // Cleanup old entries every hour
409
+ queueCleanupInterval = setInterval(() => {
410
+ try {
411
+ cleanupQueue();
412
+ }
413
+ catch (err) {
414
+ console.error("Queue cleanup error:", err);
415
+ }
416
+ }, 3600000);
417
+ // Start Telegram polling (if configured)
418
+ import { setTelegramConnected } from "./platforms/telegram.js";
419
+ if (bot) {
420
+ await bot.start({
421
+ drop_pending_updates: true,
422
+ onStart: () => {
423
+ const me = bot.botInfo;
424
+ setTelegramConnected(me.first_name, me.username);
425
+ console.log(`🤖 Alvin Bot started (@${me.username})`);
426
+ console.log(` Provider: ${registry?.getActiveKey() || "none"}`);
427
+ console.log(` Users: ${config.allowedUsers.length} authorized`);
428
+ // Start heartbeat monitor
429
+ startHeartbeat();
430
+ // Index memory vectors in background (non-blocking)
431
+ initEmbeddings().catch(() => { });
432
+ },
433
+ });
434
+ }
435
+ else {
436
+ console.log(`🤖 Alvin Bot started (WebUI-only mode)`);
437
+ console.log(` Provider: ${registry?.getActiveKey() || "none"}`);
438
+ console.log(` WebUI: http://localhost:${process.env.WEB_PORT || 3100}`);
439
+ // Start heartbeat monitor even without Telegram
440
+ startHeartbeat();
441
+ initEmbeddings().catch(() => { });
442
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Data Directory Bootstrap — Ensures ~/.alvin-bot/ exists with all required structure.
3
+ *
4
+ * Called as the very first thing at bot startup, before any service imports.
5
+ * Idempotent — safe to call multiple times.
6
+ */
7
+ import fs from "fs";
8
+ import { DATA_DIR, MEMORY_DIR, USERS_DIR, RUNTIME_DIR, WHATSAPP_AUTH, BACKUP_DIR, SOUL_FILE, TOOLS_MD, TOOLS_JSON, CRON_FILE, MCP_CONFIG, FALLBACK_FILE, CUSTOM_MODELS, WA_GROUPS, SOUL_EXAMPLE, TOOLS_EXAMPLE_MD, TOOLS_EXAMPLE_JSON, WA_MEDIA_DIR, DELIVERY_QUEUE_FILE, AGENTS_FILE, HOOKS_DIR, USER_SKILLS_DIR, APPROVED_USERS_FILE } from "./paths.js";
9
+ /**
10
+ * Create the directory structure only (no file seeding).
11
+ * Must run BEFORE migration so directories exist for copying.
12
+ */
13
+ export function ensureDataDirs() {
14
+ const dirs = [
15
+ DATA_DIR,
16
+ MEMORY_DIR,
17
+ USERS_DIR,
18
+ RUNTIME_DIR,
19
+ WHATSAPP_AUTH,
20
+ WA_MEDIA_DIR,
21
+ BACKUP_DIR,
22
+ HOOKS_DIR,
23
+ USER_SKILLS_DIR,
24
+ ];
25
+ for (const dir of dirs) {
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Seed default files for a fresh install (only if they don't exist yet).
33
+ * Must run AFTER migration so legacy data takes priority over templates.
34
+ */
35
+ export function seedDefaults() {
36
+ // SOUL.md — copy from example template if available
37
+ if (!fs.existsSync(SOUL_FILE)) {
38
+ if (fs.existsSync(SOUL_EXAMPLE)) {
39
+ fs.copyFileSync(SOUL_EXAMPLE, SOUL_FILE);
40
+ }
41
+ else {
42
+ fs.writeFileSync(SOUL_FILE, "# Bot Personality\n\nYou are a direct, lightly sarcastic, and genuinely helpful AI assistant.\nYou have opinions, you verify your work, and you don't pad answers with filler.\nMirror the user's language naturally.\n");
43
+ }
44
+ }
45
+ // TOOLS.md — copy from example template if available
46
+ if (!fs.existsSync(TOOLS_MD)) {
47
+ if (fs.existsSync(TOOLS_EXAMPLE_MD)) {
48
+ fs.copyFileSync(TOOLS_EXAMPLE_MD, TOOLS_MD);
49
+ }
50
+ }
51
+ // tools.json (legacy) — copy from example if available
52
+ if (!fs.existsSync(TOOLS_JSON)) {
53
+ if (fs.existsSync(TOOLS_EXAMPLE_JSON)) {
54
+ fs.copyFileSync(TOOLS_EXAMPLE_JSON, TOOLS_JSON);
55
+ }
56
+ }
57
+ // Empty JSON defaults
58
+ const jsonDefaults = [
59
+ [CRON_FILE, "[]"],
60
+ [DELIVERY_QUEUE_FILE, "[]"],
61
+ [CUSTOM_MODELS, "[]"],
62
+ [APPROVED_USERS_FILE, "[]"],
63
+ [WA_GROUPS, '{"groups":[]}'],
64
+ [FALLBACK_FILE, ""], // Empty = use env defaults
65
+ [MCP_CONFIG, ""], // Empty = no MCP servers
66
+ ];
67
+ for (const [file, defaultContent] of jsonDefaults) {
68
+ if (!fs.existsSync(file) && defaultContent) {
69
+ fs.writeFileSync(file, defaultContent);
70
+ }
71
+ }
72
+ // MEMORY.md — seed with empty template
73
+ const memoryFile = `${MEMORY_DIR}/MEMORY.md`;
74
+ if (!fs.existsSync(memoryFile)) {
75
+ fs.writeFileSync(memoryFile, "# Long-term Memory\n\n> This file is your agent's long-term memory. Add important context here.\n");
76
+ }
77
+ // AGENTS.md — seed with default standing orders template
78
+ if (!fs.existsSync(AGENTS_FILE)) {
79
+ fs.writeFileSync(AGENTS_FILE, "# Standing Orders\n\n> Permanent instructions that apply to every session.\n> Edit this file to add rules, workflows, or recurring tasks.\n");
80
+ }
81
+ }