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
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
|
+
}
|