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