alvin-bot 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.env.example +43 -0
  2. package/BACKLOG.md +223 -0
  3. package/CHANGELOG.md +63 -0
  4. package/CLAUDE.example.md +152 -0
  5. package/CODE_OF_CONDUCT.md +52 -0
  6. package/CONTRIBUTING.md +72 -0
  7. package/LICENSE +21 -0
  8. package/README.md +529 -0
  9. package/SECURITY.md +38 -0
  10. package/SOUL.example.md +60 -0
  11. package/TOOLS.example.md +42 -0
  12. package/alvin-bot.config.example.json +24 -0
  13. package/bin/cli.js +1088 -0
  14. package/dist/.metadata_never_index +0 -0
  15. package/dist/claude.js +102 -0
  16. package/dist/config.js +65 -0
  17. package/dist/engine.js +90 -0
  18. package/dist/find-claude-binary.js +98 -0
  19. package/dist/handlers/commands.js +1489 -0
  20. package/dist/handlers/document.js +187 -0
  21. package/dist/handlers/message.js +200 -0
  22. package/dist/handlers/photo.js +154 -0
  23. package/dist/handlers/platform-message.js +275 -0
  24. package/dist/handlers/video.js +237 -0
  25. package/dist/handlers/voice.js +148 -0
  26. package/dist/i18n.js +299 -0
  27. package/dist/index.js +442 -0
  28. package/dist/init-data-dir.js +81 -0
  29. package/dist/middleware/auth.js +215 -0
  30. package/dist/migrate.js +139 -0
  31. package/dist/paths.js +87 -0
  32. package/dist/platforms/discord.js +161 -0
  33. package/dist/platforms/index.js +130 -0
  34. package/dist/platforms/signal.js +205 -0
  35. package/dist/platforms/slack.js +318 -0
  36. package/dist/platforms/telegram.js +111 -0
  37. package/dist/platforms/types.js +8 -0
  38. package/dist/platforms/whatsapp.js +648 -0
  39. package/dist/providers/claude-sdk-provider.js +173 -0
  40. package/dist/providers/codex-cli-provider.js +121 -0
  41. package/dist/providers/index.js +7 -0
  42. package/dist/providers/openai-compatible.js +388 -0
  43. package/dist/providers/registry.js +209 -0
  44. package/dist/providers/tool-executor.js +450 -0
  45. package/dist/providers/types.js +205 -0
  46. package/dist/services/access.js +144 -0
  47. package/dist/services/asset-index.js +230 -0
  48. package/dist/services/browser-manager.js +161 -0
  49. package/dist/services/browser.js +121 -0
  50. package/dist/services/compaction.js +129 -0
  51. package/dist/services/cron.js +462 -0
  52. package/dist/services/custom-tools.js +317 -0
  53. package/dist/services/delivery-queue.js +154 -0
  54. package/dist/services/elevenlabs.js +58 -0
  55. package/dist/services/embeddings.js +386 -0
  56. package/dist/services/exec-guard.js +46 -0
  57. package/dist/services/fallback-order.js +151 -0
  58. package/dist/services/heartbeat.js +192 -0
  59. package/dist/services/hooks.js +44 -0
  60. package/dist/services/imagegen.js +72 -0
  61. package/dist/services/language-detect.js +144 -0
  62. package/dist/services/markdown.js +63 -0
  63. package/dist/services/mcp.js +252 -0
  64. package/dist/services/memory.js +133 -0
  65. package/dist/services/personality.js +227 -0
  66. package/dist/services/plugins.js +171 -0
  67. package/dist/services/reminders.js +97 -0
  68. package/dist/services/restart.js +48 -0
  69. package/dist/services/security-audit.js +66 -0
  70. package/dist/services/self-search.js +129 -0
  71. package/dist/services/session.js +93 -0
  72. package/dist/services/skills.js +287 -0
  73. package/dist/services/standing-orders.js +29 -0
  74. package/dist/services/subagents.js +142 -0
  75. package/dist/services/sudo.js +243 -0
  76. package/dist/services/telegram.js +113 -0
  77. package/dist/services/tool-discovery.js +214 -0
  78. package/dist/services/usage-tracker.js +137 -0
  79. package/dist/services/users.js +199 -0
  80. package/dist/services/voice.js +95 -0
  81. package/dist/tui/index.js +507 -0
  82. package/dist/web/canvas.js +30 -0
  83. package/dist/web/doctor-api.js +606 -0
  84. package/dist/web/openai-compat.js +252 -0
  85. package/dist/web/server.js +1351 -0
  86. package/dist/web/setup-api.js +1078 -0
  87. package/docs/mcp.example.json +16 -0
  88. package/docs/screenshots/00-Login.png +0 -0
  89. package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
  90. package/docs/screenshots/02-Chat.png +0 -0
  91. package/docs/screenshots/03-Dashboard-Overview.png +0 -0
  92. package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
  93. package/docs/screenshots/05-Personality-Editor.png +0 -0
  94. package/docs/screenshots/06-Memory-Manager.png +0 -0
  95. package/docs/screenshots/07-Active-Sessions.png +0 -0
  96. package/docs/screenshots/08-File-Browser.png +0 -0
  97. package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
  98. package/docs/screenshots/10-Custom-Tools.png +0 -0
  99. package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
  100. package/docs/screenshots/12-Messaging-Platforms.png +0 -0
  101. package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
  102. package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
  103. package/docs/screenshots/13-User-Management.png +0 -0
  104. package/docs/screenshots/14-Web-Terminal.png +0 -0
  105. package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
  106. package/docs/screenshots/16-Settings-and-Env.png +0 -0
  107. package/docs/screenshots/TG-commands.png +0 -0
  108. package/docs/screenshots/TG.png +0 -0
  109. package/docs/screenshots/_Mac-Installer.png +0 -0
  110. package/docs/tools.example.json +33 -0
  111. package/install.sh +165 -0
  112. package/package.json +190 -0
  113. package/plugins/calendar/index.js +270 -0
  114. package/plugins/email/index.js +231 -0
  115. package/plugins/finance/index.js +254 -0
  116. package/plugins/notes/index.js +227 -0
  117. package/plugins/smarthome/index.js +230 -0
  118. package/plugins/weather/index.js +122 -0
  119. package/skills/apple-notes/SKILL.md +31 -0
  120. package/skills/browse/SKILL.md +136 -0
  121. package/skills/code-project/SKILL.md +43 -0
  122. package/skills/data-analysis/SKILL.md +39 -0
  123. package/skills/document-creation/SKILL.md +48 -0
  124. package/skills/email-summary/SKILL.md +46 -0
  125. package/skills/github/SKILL.md +42 -0
  126. package/skills/summarize/SKILL.md +28 -0
  127. package/skills/system-admin/SKILL.md +39 -0
  128. package/skills/weather/SKILL.md +34 -0
  129. package/skills/web-research/SKILL.md +35 -0
  130. package/web/public/canvas.html +52 -0
  131. package/web/public/css/style.css +555 -0
  132. package/web/public/index.html +189 -0
  133. package/web/public/js/app.js +3102 -0
  134. package/web/public/js/i18n.js +1048 -0
  135. package/web/public/js/icons.js +104 -0
  136. package/web/public/login.html +48 -0
@@ -0,0 +1,215 @@
1
+ import fs from "fs";
2
+ import { InlineKeyboard } from "grammy";
3
+ import { config } from "../config.js";
4
+ import { APPROVED_USERS_FILE } from "../paths.js";
5
+ import { getGroupStatus, registerGroup, trackGroupMessage, } from "../services/access.js";
6
+ /**
7
+ * Auth + Group Chat + Access Control middleware.
8
+ *
9
+ * Security model:
10
+ * - DMs: controlled by AUTH_MODE env var
11
+ * - "allowlist" (default): only ALLOWED_USERS can interact
12
+ * - "pairing": unknown users get a 6-digit code, admin must approve
13
+ * - "open": all DMs allowed
14
+ * - Groups: must be approved by admin + only respond to @mentions/replies
15
+ * - New groups: sends approval request to admin, stays silent until approved
16
+ * - Blocked groups: completely ignored
17
+ * - Forwarded messages: can be disabled globally
18
+ */
19
+ // ── Approved Users (persistent, for pairing mode) ──────────────────
20
+ function loadApprovedUsers() {
21
+ try {
22
+ const raw = fs.readFileSync(APPROVED_USERS_FILE, "utf-8");
23
+ const parsed = JSON.parse(raw);
24
+ return Array.isArray(parsed) ? parsed.map(Number).filter(Boolean) : [];
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ function saveApprovedUsers(ids) {
31
+ fs.writeFileSync(APPROVED_USERS_FILE, JSON.stringify(ids, null, 2));
32
+ }
33
+ export function addApprovedUser(userId) {
34
+ const current = loadApprovedUsers();
35
+ if (!current.includes(userId)) {
36
+ current.push(userId);
37
+ saveApprovedUsers(current);
38
+ }
39
+ }
40
+ export function isApprovedUser(userId) {
41
+ return loadApprovedUsers().includes(userId);
42
+ }
43
+ const MAX_PENDING = 3;
44
+ const pendingPairings = new Map(); // code → pairing
45
+ function generateCode() {
46
+ return String(Math.floor(100000 + Math.random() * 900000));
47
+ }
48
+ function cleanExpired() {
49
+ const now = Date.now();
50
+ for (const [code, pairing] of pendingPairings.entries()) {
51
+ if (pairing.expiresAt <= now) {
52
+ pendingPairings.delete(code);
53
+ }
54
+ }
55
+ }
56
+ /** Get a pending pairing by code. Returns undefined if not found or expired. */
57
+ export function getPendingPairing(code) {
58
+ const pairing = pendingPairings.get(code);
59
+ if (!pairing)
60
+ return undefined;
61
+ if (pairing.expiresAt <= Date.now()) {
62
+ pendingPairings.delete(code);
63
+ return undefined;
64
+ }
65
+ return pairing;
66
+ }
67
+ /** Remove a pending pairing by code. */
68
+ export function removePendingPairing(code) {
69
+ const pairing = pendingPairings.get(code);
70
+ pendingPairings.delete(code);
71
+ return pairing;
72
+ }
73
+ // ── Middleware ───────────────────────────────────────────────────────
74
+ export async function authMiddleware(ctx, next) {
75
+ const userId = ctx.from?.id;
76
+ const chatType = ctx.chat?.type;
77
+ const isGroup = chatType === "group" || chatType === "supergroup";
78
+ // ── DM Auth ─────────────────────────────────────
79
+ if (chatType === "private") {
80
+ // "open" mode: allow everyone
81
+ if (config.authMode === "open") {
82
+ await next();
83
+ return;
84
+ }
85
+ // Always allow configured users
86
+ if (userId && config.allowedUsers.includes(userId)) {
87
+ await next();
88
+ return;
89
+ }
90
+ // "pairing" mode: unknown users go through code-based approval
91
+ if (config.authMode === "pairing" && userId) {
92
+ // Already approved via pairing?
93
+ if (isApprovedUser(userId)) {
94
+ await next();
95
+ return;
96
+ }
97
+ // Check if user already has a pending pairing (avoid duplicate codes)
98
+ cleanExpired();
99
+ const existingEntry = [...pendingPairings.values()].find(p => p.userId === userId);
100
+ if (existingEntry) {
101
+ await ctx.reply(`Your approval request is still pending.\n\nYour code: \`${existingEntry.code}\`\n\nAsk the bot admin to approve it.`, { parse_mode: "Markdown" });
102
+ return;
103
+ }
104
+ // Enforce max pending limit
105
+ if (pendingPairings.size >= MAX_PENDING) {
106
+ await ctx.reply("The approval queue is currently full. Please try again later.");
107
+ return;
108
+ }
109
+ // Generate pairing code
110
+ const code = generateCode();
111
+ const pairing = {
112
+ userId,
113
+ username: ctx.from?.username,
114
+ code,
115
+ expiresAt: Date.now() + 3_600_000, // 1 hour
116
+ };
117
+ pendingPairings.set(code, pairing);
118
+ // Tell user their code
119
+ await ctx.reply(`Hi! I need admin approval before we can chat.\n\nSend this code to the bot admin: \`${code}\``, { parse_mode: "Markdown" });
120
+ // Notify admin with approve/deny inline keyboard
121
+ const adminId = config.allowedUsers[0];
122
+ if (adminId) {
123
+ const keyboard = new InlineKeyboard()
124
+ .text("✅ Approve", `pair:approve:${code}`)
125
+ .text("❌ Deny", `pair:deny:${code}`);
126
+ const userTag = pairing.username ? `@${pairing.username}` : `ID ${userId}`;
127
+ try {
128
+ await ctx.api.sendMessage(adminId, `🔔 *New DM Pairing Request*\n\n` +
129
+ `*User:* ${userTag}\n` +
130
+ `*User ID:* \`${userId}\`\n` +
131
+ `*Code:* \`${code}\`\n\n` +
132
+ `Approve this user to chat with the bot?`, { parse_mode: "Markdown", reply_markup: keyboard });
133
+ }
134
+ catch (err) {
135
+ console.error("Failed to send pairing approval request:", err);
136
+ }
137
+ }
138
+ return;
139
+ }
140
+ // Default "allowlist" mode (or pairing mode but no userId)
141
+ console.log(`Unauthorized DM attempt from user ID: ${userId || "unknown"} (username: ${ctx.from?.username || "none"})`);
142
+ await ctx.reply(`Hi! I'm not set up to chat with you yet.\n\nAsk my admin to add your user ID: ${userId || "unknown"}`);
143
+ return;
144
+ }
145
+ // ── Group Access Control ────────────────────────
146
+ if (isGroup) {
147
+ const chatId = ctx.chat.id;
148
+ const chatTitle = ctx.chat && "title" in ctx.chat ? ctx.chat.title || "Unknown" : "Unknown";
149
+ // Check group approval status
150
+ const status = getGroupStatus(chatId);
151
+ if (status === "blocked") {
152
+ return; // Completely ignore blocked groups
153
+ }
154
+ if (status === "new") {
155
+ // Register and request approval from admin
156
+ registerGroup(chatId, chatTitle, userId);
157
+ // Notify the first allowed user (admin) about the new group
158
+ const adminId = config.allowedUsers[0];
159
+ if (adminId) {
160
+ const keyboard = new InlineKeyboard()
161
+ .text("✅ Approve", `access:approve:${chatId}`)
162
+ .text("❌ Block", `access:block:${chatId}`);
163
+ try {
164
+ await ctx.api.sendMessage(adminId, `🔔 *New group request*\n\n` +
165
+ `*Gruppe:* ${chatTitle}\n` +
166
+ `*Chat-ID:* \`${chatId}\`\n` +
167
+ `*Added by:* ${userId}\n\n` +
168
+ `Soll Alvin Bot in dieser Gruppe antworten?`, { parse_mode: "Markdown", reply_markup: keyboard });
169
+ }
170
+ catch (err) {
171
+ console.error("Failed to send group approval request:", err);
172
+ }
173
+ }
174
+ return; // Don't respond until approved
175
+ }
176
+ if (status === "pending") {
177
+ return; // Still waiting for approval
178
+ }
179
+ // status === "approved" — continue with group logic
180
+ // Only allowed users can trigger the bot in groups
181
+ if (!userId || !config.allowedUsers.includes(userId)) {
182
+ return; // Silently ignore unauthorized users
183
+ }
184
+ trackGroupMessage(chatId);
185
+ const message = ctx.message;
186
+ if (!message) {
187
+ await next(); // callback queries
188
+ return;
189
+ }
190
+ // Commands always go through
191
+ if (message.text?.startsWith("/")) {
192
+ await next();
193
+ return;
194
+ }
195
+ // Check if bot is mentioned
196
+ const botUsername = ctx.me?.username?.toLowerCase();
197
+ const text = message.text || message.caption || "";
198
+ if (botUsername && text.toLowerCase().includes(`@${botUsername}`)) {
199
+ if (message.text) {
200
+ message.text = message.text.replace(new RegExp(`@${botUsername}`, "gi"), "").trim();
201
+ }
202
+ await next();
203
+ return;
204
+ }
205
+ // Check if replying to a bot message
206
+ if (message.reply_to_message?.from?.id === ctx.me?.id) {
207
+ await next();
208
+ return;
209
+ }
210
+ // Otherwise: ignore in groups
211
+ return;
212
+ }
213
+ // ── Callback queries (inline keyboards) ─────────
214
+ await next();
215
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Legacy Data Migration — Copies data from old in-repo locations to ~/.alvin-bot/.
3
+ *
4
+ * Old layout (in BOT_ROOT):
5
+ * docs/MEMORY.md, docs/memory/, docs/users/, docs/tools.json, docs/cron-jobs.json,
6
+ * docs/mcp.json, docs/fallback-order.json, docs/custom-models.json, docs/whatsapp-groups.json
7
+ * data/access.json, data/whatsapp-auth/, data/wa-media/, data/.sudo-*
8
+ * SOUL.md, TOOLS.md
9
+ * backups/
10
+ *
11
+ * New layout (in DATA_DIR = ~/.alvin-bot/):
12
+ * memory/MEMORY.md, memory/*.md, memory/.embeddings.json
13
+ * users/
14
+ * data/access.json, data/whatsapp-auth/, data/wa-media/, data/.sudo-*
15
+ * soul.md, tools.md, tools.json
16
+ * cron-jobs.json, mcp.json, fallback-order.json, custom-models.json, whatsapp-groups.json
17
+ * backups/
18
+ *
19
+ * Does NOT delete source files — the user can clean up manually.
20
+ */
21
+ import fs from "fs";
22
+ import { resolve } from "path";
23
+ import { BOT_ROOT, MEMORY_DIR, USERS_DIR, BACKUP_DIR, SOUL_FILE, TOOLS_MD, TOOLS_JSON, CRON_FILE, MCP_CONFIG, FALLBACK_FILE, CUSTOM_MODELS, WA_GROUPS, WHATSAPP_AUTH, WA_MEDIA_DIR, ACCESS_FILE, SUDO_ENC_FILE, SUDO_KEY_FILE, MEMORY_FILE, EMBEDDINGS_IDX } from "./paths.js";
24
+ /**
25
+ * Check if legacy data exists in the old locations.
26
+ */
27
+ export function hasLegacyData() {
28
+ const legacyIndicators = [
29
+ resolve(BOT_ROOT, "docs", "MEMORY.md"),
30
+ resolve(BOT_ROOT, "docs", "memory"),
31
+ resolve(BOT_ROOT, "docs", "users"),
32
+ resolve(BOT_ROOT, "data", "access.json"),
33
+ resolve(BOT_ROOT, "SOUL.md"),
34
+ ];
35
+ return legacyIndicators.some(p => fs.existsSync(p));
36
+ }
37
+ /**
38
+ * Copy a file if source exists and destination doesn't.
39
+ */
40
+ function copyIfNew(src, dest) {
41
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
42
+ const destDir = resolve(dest, "..");
43
+ if (!fs.existsSync(destDir))
44
+ fs.mkdirSync(destDir, { recursive: true });
45
+ fs.copyFileSync(src, dest);
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Recursively copy a directory if source exists and destination doesn't have the files.
52
+ */
53
+ function copyDirIfNew(src, dest) {
54
+ if (!fs.existsSync(src))
55
+ return 0;
56
+ if (!fs.existsSync(dest))
57
+ fs.mkdirSync(dest, { recursive: true });
58
+ let count = 0;
59
+ const entries = fs.readdirSync(src, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const srcPath = resolve(src, entry.name);
62
+ const destPath = resolve(dest, entry.name);
63
+ if (entry.isDirectory()) {
64
+ count += copyDirIfNew(srcPath, destPath);
65
+ }
66
+ else if (!fs.existsSync(destPath)) {
67
+ try {
68
+ fs.copyFileSync(srcPath, destPath);
69
+ count++;
70
+ }
71
+ catch {
72
+ // Source may have vanished between readdir and copy (e.g. WhatsApp session files)
73
+ }
74
+ }
75
+ }
76
+ return count;
77
+ }
78
+ /**
79
+ * Migrate all legacy data to the new DATA_DIR.
80
+ * Returns a summary of what was copied.
81
+ */
82
+ export function migrateFromLegacy() {
83
+ const copied = [];
84
+ const skipped = [];
85
+ function track(label, result) {
86
+ if (result)
87
+ copied.push(label);
88
+ else
89
+ skipped.push(label);
90
+ }
91
+ // ── Single files ─────────────────────────────────────────
92
+ // SOUL.md → soul.md
93
+ track("SOUL.md → soul.md", copyIfNew(resolve(BOT_ROOT, "SOUL.md"), SOUL_FILE));
94
+ // TOOLS.md → tools.md
95
+ track("TOOLS.md → tools.md", copyIfNew(resolve(BOT_ROOT, "TOOLS.md"), TOOLS_MD));
96
+ // docs/tools.json → tools.json
97
+ track("docs/tools.json", copyIfNew(resolve(BOT_ROOT, "docs", "tools.json"), TOOLS_JSON));
98
+ // docs/MEMORY.md → memory/MEMORY.md
99
+ track("docs/MEMORY.md", copyIfNew(resolve(BOT_ROOT, "docs", "MEMORY.md"), MEMORY_FILE));
100
+ // docs/memory/.embeddings.json → memory/.embeddings.json
101
+ track(".embeddings.json", copyIfNew(resolve(BOT_ROOT, "docs", "memory", ".embeddings.json"), EMBEDDINGS_IDX));
102
+ // docs/cron-jobs.json → cron-jobs.json
103
+ track("cron-jobs.json", copyIfNew(resolve(BOT_ROOT, "docs", "cron-jobs.json"), CRON_FILE));
104
+ // docs/mcp.json → mcp.json
105
+ track("mcp.json", copyIfNew(resolve(BOT_ROOT, "docs", "mcp.json"), MCP_CONFIG));
106
+ // docs/fallback-order.json → fallback-order.json
107
+ track("fallback-order.json", copyIfNew(resolve(BOT_ROOT, "docs", "fallback-order.json"), FALLBACK_FILE));
108
+ // docs/custom-models.json → custom-models.json
109
+ track("custom-models.json", copyIfNew(resolve(BOT_ROOT, "docs", "custom-models.json"), CUSTOM_MODELS));
110
+ // docs/whatsapp-groups.json → whatsapp-groups.json
111
+ track("whatsapp-groups.json", copyIfNew(resolve(BOT_ROOT, "docs", "whatsapp-groups.json"), WA_GROUPS));
112
+ // data/access.json → data/access.json
113
+ track("data/access.json", copyIfNew(resolve(BOT_ROOT, "data", "access.json"), ACCESS_FILE));
114
+ // data/.sudo-enc → data/.sudo-enc
115
+ track("data/.sudo-enc", copyIfNew(resolve(BOT_ROOT, "data", ".sudo-enc"), SUDO_ENC_FILE));
116
+ track("data/.sudo-key", copyIfNew(resolve(BOT_ROOT, "data", ".sudo-key"), SUDO_KEY_FILE));
117
+ // ── Directories ──────────────────────────────────────────
118
+ // docs/memory/*.md → memory/*.md
119
+ const memCount = copyDirIfNew(resolve(BOT_ROOT, "docs", "memory"), MEMORY_DIR);
120
+ if (memCount > 0)
121
+ copied.push(`memory/ (${memCount} files)`);
122
+ // docs/users/ → users/
123
+ const usersCount = copyDirIfNew(resolve(BOT_ROOT, "docs", "users"), USERS_DIR);
124
+ if (usersCount > 0)
125
+ copied.push(`users/ (${usersCount} files)`);
126
+ // data/whatsapp-auth/ → data/whatsapp-auth/
127
+ const waAuthCount = copyDirIfNew(resolve(BOT_ROOT, "data", "whatsapp-auth"), WHATSAPP_AUTH);
128
+ if (waAuthCount > 0)
129
+ copied.push(`whatsapp-auth/ (${waAuthCount} files)`);
130
+ // data/wa-media/ → data/wa-media/
131
+ const waMediaCount = copyDirIfNew(resolve(BOT_ROOT, "data", "wa-media"), WA_MEDIA_DIR);
132
+ if (waMediaCount > 0)
133
+ copied.push(`wa-media/ (${waMediaCount} files)`);
134
+ // backups/ → backups/
135
+ const backupCount = copyDirIfNew(resolve(BOT_ROOT, "backups"), BACKUP_DIR);
136
+ if (backupCount > 0)
137
+ copied.push(`backups/ (${backupCount} files)`);
138
+ return { copied: copied.filter(c => !c.includes("false")), skipped };
139
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Centralized Path Registry — Single source of truth for all file paths.
3
+ *
4
+ * BOT_ROOT = Code directory (where src/, dist/, plugins/, etc. live)
5
+ * DATA_DIR = User data directory (~/.alvin-bot by default, override with ALVIN_DATA_DIR)
6
+ *
7
+ * All personal/runtime data lives in DATA_DIR (outside the repo).
8
+ * All code/templates/plugins live in BOT_ROOT (inside the repo).
9
+ */
10
+ import { resolve, dirname } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import os from "os";
13
+ // ── Code Directory (repo root) ─────────────────────────────────────
14
+ export const BOT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
15
+ // ── Data Directory (~/.alvin-bot) ──────────────────────────────────
16
+ export const DATA_DIR = resolve(process.env.ALVIN_DATA_DIR || resolve(os.homedir(), ".alvin-bot"));
17
+ // ── Code paths (BOT_ROOT) ──────────────────────────────────────────
18
+ /** web/public/ — Static assets for Web UI */
19
+ export const PUBLIC_DIR = resolve(BOT_ROOT, "web", "public");
20
+ /** plugins/ — Plugin directory */
21
+ export const PLUGINS_DIR = resolve(BOT_ROOT, "plugins");
22
+ /** skills/ — Skill definitions */
23
+ export const SKILLS_DIR = resolve(BOT_ROOT, "skills");
24
+ /** User skills directory (custom, outside repo) */
25
+ export const USER_SKILLS_DIR = resolve(DATA_DIR, "skills");
26
+ /** .env — Environment config (stays in BOT_ROOT for dev, or DATA_DIR for packaged) */
27
+ export const ENV_FILE = resolve(BOT_ROOT, ".env");
28
+ /** Example/template files (always in repo) */
29
+ export const SOUL_EXAMPLE = resolve(BOT_ROOT, "SOUL.example.md");
30
+ export const TOOLS_EXAMPLE_MD = resolve(BOT_ROOT, "TOOLS.example.md");
31
+ export const TOOLS_EXAMPLE_JSON = resolve(BOT_ROOT, "docs", "tools.example.json");
32
+ // ── Data paths (DATA_DIR = ~/.alvin-bot) ───────────────────────────
33
+ /** memory/ — Daily logs and embeddings */
34
+ export const MEMORY_DIR = resolve(DATA_DIR, "memory");
35
+ /** memory/MEMORY.md — Long-term curated memory */
36
+ export const MEMORY_FILE = resolve(DATA_DIR, "memory", "MEMORY.md");
37
+ /** memory/.embeddings.json — Vector index */
38
+ export const EMBEDDINGS_IDX = resolve(DATA_DIR, "memory", ".embeddings.json");
39
+ /** users/ — User profiles and per-user memory */
40
+ export const USERS_DIR = resolve(DATA_DIR, "users");
41
+ /** data/ — Runtime control data */
42
+ export const RUNTIME_DIR = resolve(DATA_DIR, "data");
43
+ /** data/access.json — Group approval status */
44
+ export const ACCESS_FILE = resolve(DATA_DIR, "data", "access.json");
45
+ /** data/approved-users.json — DM-pairing approved user IDs */
46
+ export const APPROVED_USERS_FILE = resolve(DATA_DIR, "data", "approved-users.json");
47
+ /** data/whatsapp-auth/ — WhatsApp session persistence */
48
+ export const WHATSAPP_AUTH = resolve(DATA_DIR, "data", "whatsapp-auth");
49
+ /** data/wa-media/ — WhatsApp temp media */
50
+ export const WA_MEDIA_DIR = resolve(DATA_DIR, "data", "wa-media");
51
+ /** data/.sudo-enc / .sudo-key — Encrypted sudo password */
52
+ export const SUDO_ENC_FILE = resolve(DATA_DIR, "data", ".sudo-enc");
53
+ export const SUDO_KEY_FILE = resolve(DATA_DIR, "data", ".sudo-key");
54
+ /** backups/ — Config snapshots */
55
+ export const BACKUP_DIR = resolve(DATA_DIR, "backups");
56
+ /** soul.md — Bot personality */
57
+ export const SOUL_FILE = resolve(DATA_DIR, "soul.md");
58
+ /** tools.md — Custom tool definitions (Markdown) */
59
+ export const TOOLS_MD = resolve(DATA_DIR, "tools.md");
60
+ /** tools.json — Custom tool definitions (legacy JSON) */
61
+ export const TOOLS_JSON = resolve(DATA_DIR, "tools.json");
62
+ /** cron-jobs.json — Scheduled tasks */
63
+ export const CRON_FILE = resolve(DATA_DIR, "cron-jobs.json");
64
+ /** mcp.json — MCP server config */
65
+ export const MCP_CONFIG = resolve(DATA_DIR, "mcp.json");
66
+ /** fallback-order.json — Provider fallback chain */
67
+ export const FALLBACK_FILE = resolve(DATA_DIR, "fallback-order.json");
68
+ /** custom-models.json — Custom LLM endpoints */
69
+ export const CUSTOM_MODELS = resolve(DATA_DIR, "custom-models.json");
70
+ /** whatsapp-groups.json — WhatsApp group tracking */
71
+ export const WA_GROUPS = resolve(DATA_DIR, "whatsapp-groups.json");
72
+ /** delivery-queue.json — Reliable message delivery queue */
73
+ export const DELIVERY_QUEUE_FILE = resolve(DATA_DIR, "delivery-queue.json");
74
+ /** AGENTS.md — Standing orders (permanent instructions for every session) */
75
+ export const AGENTS_FILE = resolve(DATA_DIR, "AGENTS.md");
76
+ /** hooks/ — User-defined lifecycle event handlers */
77
+ export const HOOKS_DIR = resolve(DATA_DIR, "hooks");
78
+ /** scripts/browse-server.cjs — HTTP gateway for persistent browser sessions */
79
+ export const BROWSE_SERVER_SCRIPT = resolve(BOT_ROOT, "scripts", "browse-server.cjs");
80
+ /** data/exec-allowlist.json — User-defined exec allowlist */
81
+ export const EXEC_ALLOWLIST_FILE = resolve(DATA_DIR, "exec-allowlist.json");
82
+ /** assets/ — User asset files (CVs, cover letters, legal docs, photos) */
83
+ export const ASSETS_DIR = resolve(DATA_DIR, "assets");
84
+ /** assets/INDEX.json — Machine-readable asset registry */
85
+ export const ASSETS_INDEX_JSON = resolve(DATA_DIR, "assets", "INDEX.json");
86
+ /** assets/INDEX.md — Human-readable asset summary (injected into prompts) */
87
+ export const ASSETS_INDEX_MD = resolve(DATA_DIR, "assets", "INDEX.md");
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Discord Platform Adapter
3
+ *
4
+ * Uses discord.js to connect to Discord.
5
+ * Optional dependency — only loaded if DISCORD_TOKEN is set.
6
+ *
7
+ * Setup:
8
+ * 1. Create a bot at https://discord.com/developers/applications
9
+ * 2. Enable Message Content Intent
10
+ * 3. Set DISCORD_TOKEN in .env
11
+ * 4. Invite bot to server with messages.read + messages.write permissions
12
+ */
13
+ let _discordState = {
14
+ status: "disconnected",
15
+ botName: null,
16
+ botTag: null,
17
+ guildCount: 0,
18
+ connectedAt: null,
19
+ error: null,
20
+ };
21
+ export function getDiscordState() {
22
+ return { ..._discordState };
23
+ }
24
+ export class DiscordAdapter {
25
+ platform = "discord";
26
+ handler = null;
27
+ client = null; // discord.js Client (dynamic import)
28
+ token;
29
+ constructor(token) {
30
+ this.token = token;
31
+ }
32
+ async start() {
33
+ try {
34
+ // Dynamic import — discord.js is optional
35
+ // @ts-ignore — discord.js is an optional dependency
36
+ const { Client, GatewayIntentBits } = await import("discord.js");
37
+ this.client = new Client({
38
+ intents: [
39
+ GatewayIntentBits.Guilds,
40
+ GatewayIntentBits.GuildMessages,
41
+ GatewayIntentBits.MessageContent,
42
+ GatewayIntentBits.DirectMessages,
43
+ ],
44
+ });
45
+ this.client.on("messageCreate", async (msg) => {
46
+ if (msg.author.bot)
47
+ return;
48
+ if (!this.handler)
49
+ return;
50
+ const isMention = msg.mentions.has(this.client.user);
51
+ const isReplyToBot = msg.reference?.messageId
52
+ ? (await msg.channel.messages.fetch(msg.reference.messageId).catch(() => null))?.author?.id === this.client.user.id
53
+ : false;
54
+ const incoming = {
55
+ platform: "discord",
56
+ messageId: msg.id,
57
+ chatId: msg.channel.id,
58
+ userId: msg.author.id,
59
+ userName: msg.author.displayName || msg.author.username,
60
+ userHandle: msg.author.username,
61
+ text: msg.content,
62
+ isGroup: msg.guild !== null,
63
+ isMention,
64
+ isReplyToBot,
65
+ replyToText: undefined,
66
+ };
67
+ // In servers: only respond to mentions or replies
68
+ if (msg.guild && !isMention && !isReplyToBot)
69
+ return;
70
+ // Strip mention from text
71
+ if (isMention) {
72
+ incoming.text = incoming.text.replace(/<@!?\d+>/g, "").trim();
73
+ }
74
+ await this.handler(incoming);
75
+ });
76
+ _discordState.status = "connecting";
77
+ this.client.on("ready", () => {
78
+ _discordState.status = "connected";
79
+ _discordState.botName = this.client.user?.displayName || this.client.user?.username || null;
80
+ _discordState.botTag = this.client.user?.tag || null;
81
+ _discordState.guildCount = this.client.guilds.cache.size;
82
+ _discordState.connectedAt = Date.now();
83
+ });
84
+ await this.client.login(this.token);
85
+ console.log(`🎮 Discord adapter started (${this.client.user?.tag})`);
86
+ }
87
+ catch (err) {
88
+ _discordState.status = "error";
89
+ _discordState.error = err instanceof Error ? err.message : String(err);
90
+ console.error("Discord adapter failed to start:", err);
91
+ throw err;
92
+ }
93
+ }
94
+ async stop() {
95
+ if (this.client) {
96
+ this.client.destroy();
97
+ }
98
+ }
99
+ async sendText(chatId, text, options) {
100
+ if (!this.client)
101
+ return;
102
+ const channel = await this.client.channels.fetch(chatId);
103
+ if (!channel?.isTextBased())
104
+ return;
105
+ // Discord max message length is 2000
106
+ if (text.length > 2000) {
107
+ // Split into chunks
108
+ const chunks = text.match(/.{1,1990}/gs) || [text];
109
+ for (const chunk of chunks) {
110
+ await channel.send({
111
+ content: chunk,
112
+ reply: options?.replyTo ? { messageReference: options.replyTo } : undefined,
113
+ });
114
+ }
115
+ }
116
+ else {
117
+ await channel.send({
118
+ content: text,
119
+ reply: options?.replyTo ? { messageReference: options.replyTo } : undefined,
120
+ });
121
+ }
122
+ }
123
+ async sendPhoto(chatId, photo, caption) {
124
+ if (!this.client)
125
+ return;
126
+ const channel = await this.client.channels.fetch(chatId);
127
+ if (!channel?.isTextBased())
128
+ return;
129
+ // @ts-ignore — discord.js is an optional dependency
130
+ const { AttachmentBuilder } = await import("discord.js");
131
+ const attachment = typeof photo === "string"
132
+ ? new AttachmentBuilder(photo)
133
+ : new AttachmentBuilder(photo, { name: "image.png" });
134
+ await channel.send({ content: caption, files: [attachment] });
135
+ }
136
+ async react(chatId, messageId, emoji) {
137
+ if (!this.client)
138
+ return;
139
+ try {
140
+ const channel = await this.client.channels.fetch(chatId);
141
+ if (!channel?.isTextBased())
142
+ return;
143
+ const msg = await channel.messages.fetch(messageId);
144
+ await msg.react(emoji);
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ async setTyping(chatId) {
149
+ if (!this.client)
150
+ return;
151
+ try {
152
+ const channel = await this.client.channels.fetch(chatId);
153
+ if (channel?.isTextBased())
154
+ await channel.sendTyping();
155
+ }
156
+ catch { /* ignore */ }
157
+ }
158
+ onMessage(handler) {
159
+ this.handler = handler;
160
+ }
161
+ }