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,1078 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup API — Platform & Model configuration endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Platform setup (Discord, WhatsApp, Signal tokens + dependency installation)
|
|
6
|
+
* - Model/Provider management (API keys, custom models, presets)
|
|
7
|
+
* - Runtime activation/deactivation
|
|
8
|
+
*/
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { getRegistry } from "../engine.js";
|
|
13
|
+
import { listJobs, createJob, deleteJob, toggleJob, updateJob, runJobNow, formatNextRun, humanReadableSchedule } from "../services/cron.js";
|
|
14
|
+
import { storePassword, revokePassword, getSudoStatus, verifyPassword, sudoExec, requestAdminViaDialog, openSystemSettings } from "../services/sudo.js";
|
|
15
|
+
import { ENV_FILE, CUSTOM_MODELS as CUSTOM_MODELS_FILE, BOT_ROOT, WHATSAPP_AUTH } from "../paths.js";
|
|
16
|
+
// ── Env Helpers ─────────────────────────────────────────
|
|
17
|
+
function readEnv() {
|
|
18
|
+
if (!fs.existsSync(ENV_FILE))
|
|
19
|
+
return {};
|
|
20
|
+
const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
21
|
+
const env = {};
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.startsWith("#") || !line.includes("="))
|
|
24
|
+
continue;
|
|
25
|
+
const idx = line.indexOf("=");
|
|
26
|
+
env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
27
|
+
}
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
function writeEnvVar(key, value) {
|
|
31
|
+
let content = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, "utf-8") : "";
|
|
32
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
33
|
+
if (regex.test(content)) {
|
|
34
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
38
|
+
}
|
|
39
|
+
fs.writeFileSync(ENV_FILE, content);
|
|
40
|
+
}
|
|
41
|
+
function removeEnvVar(key) {
|
|
42
|
+
if (!fs.existsSync(ENV_FILE))
|
|
43
|
+
return;
|
|
44
|
+
let content = fs.readFileSync(ENV_FILE, "utf-8");
|
|
45
|
+
content = content.replace(new RegExp(`^${key}=.*\n?`, "m"), "");
|
|
46
|
+
fs.writeFileSync(ENV_FILE, content);
|
|
47
|
+
}
|
|
48
|
+
function loadCustomModels() {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(CUSTOM_MODELS_FILE, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function saveCustomModels(models) {
|
|
57
|
+
fs.writeFileSync(CUSTOM_MODELS_FILE, JSON.stringify(models, null, 2));
|
|
58
|
+
}
|
|
59
|
+
const PLATFORMS = [
|
|
60
|
+
{
|
|
61
|
+
id: "telegram",
|
|
62
|
+
name: "Telegram",
|
|
63
|
+
icon: "📱",
|
|
64
|
+
description: "Telegram Bot via BotFather. The default messaging channel.",
|
|
65
|
+
envVars: [
|
|
66
|
+
{ key: "BOT_TOKEN", label: "Bot Token", placeholder: "123456:ABC-DEF...", secret: true },
|
|
67
|
+
{ key: "ALLOWED_USERS", label: "Allowed User IDs", placeholder: "123456789,987654321" },
|
|
68
|
+
],
|
|
69
|
+
setupUrl: "https://t.me/BotFather",
|
|
70
|
+
setupSteps: [
|
|
71
|
+
"Open @BotFather on Telegram",
|
|
72
|
+
"Send /newbot and follow the instructions",
|
|
73
|
+
"Copy the bot token here",
|
|
74
|
+
"For your User ID: Send a message to @userinfobot",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "discord",
|
|
79
|
+
name: "Discord",
|
|
80
|
+
icon: "🎮",
|
|
81
|
+
description: "Discord bot for servers and DMs. Requires discord.js.",
|
|
82
|
+
envVars: [
|
|
83
|
+
{ key: "DISCORD_TOKEN", label: "Bot Token", placeholder: "MTIz...abc", secret: true },
|
|
84
|
+
],
|
|
85
|
+
npmPackages: ["discord.js"],
|
|
86
|
+
setupUrl: "https://discord.com/developers/applications",
|
|
87
|
+
setupSteps: [
|
|
88
|
+
"Create an Application on discord.com/developers",
|
|
89
|
+
"Go to Bot → Reset Token → Copy token",
|
|
90
|
+
"Enable Message Content Intent under Bot → Privileged Intents",
|
|
91
|
+
"Invite the bot to your server: OAuth2 → URL Generator → bot + messages.read + messages.write",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "whatsapp",
|
|
96
|
+
name: "WhatsApp",
|
|
97
|
+
icon: "💬",
|
|
98
|
+
description: "WhatsApp Multi-Device connection via Baileys (no Chrome needed). QR code scan on first start.",
|
|
99
|
+
envVars: [
|
|
100
|
+
{ key: "WHATSAPP_ENABLED", label: "Enable", placeholder: "true", type: "toggle" },
|
|
101
|
+
{ key: "WHATSAPP_SELF_CHAT_ONLY", label: "Self-chat only (recommended)", placeholder: "true", type: "toggle" },
|
|
102
|
+
{ key: "WHATSAPP_ALLOW_GROUPS", label: "Reply in groups (on @mention)", placeholder: "", type: "toggle" },
|
|
103
|
+
{ key: "WHATSAPP_ALLOW_DMS", label: "Reply to private messages", placeholder: "", type: "toggle" },
|
|
104
|
+
],
|
|
105
|
+
npmPackages: ["@whiskeysockets/baileys"],
|
|
106
|
+
setupSteps: [
|
|
107
|
+
"Click 'Install Dependencies' (if needed)",
|
|
108
|
+
"Enable WhatsApp (toggle above) and click 'Save'",
|
|
109
|
+
"Restart the bot (Maintenance → Restart bot)",
|
|
110
|
+
"The QR code will appear below — scan it with WhatsApp → Linked Devices → Link a Device",
|
|
111
|
+
"The connection is persisted (data/whatsapp-auth/)",
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "slack",
|
|
116
|
+
name: "Slack",
|
|
117
|
+
icon: "💼",
|
|
118
|
+
description: "Slack workspace integration via Socket Mode (no public URL needed). DMs and @mentions in channels.",
|
|
119
|
+
envVars: [
|
|
120
|
+
{ key: "SLACK_BOT_TOKEN", label: "Bot Token (xoxb-...)", placeholder: "xoxb-...", secret: true },
|
|
121
|
+
{ key: "SLACK_APP_TOKEN", label: "App Token (xapp-...)", placeholder: "xapp-...", secret: true },
|
|
122
|
+
],
|
|
123
|
+
npmPackages: ["@slack/bolt"],
|
|
124
|
+
setupUrl: "https://api.slack.com/apps",
|
|
125
|
+
setupSteps: [
|
|
126
|
+
"Create a new App at api.slack.com/apps (From scratch)",
|
|
127
|
+
"Enable Socket Mode (Settings → Socket Mode → Enable)",
|
|
128
|
+
"Generate App-Level Token with 'connections:write' scope → copy as SLACK_APP_TOKEN",
|
|
129
|
+
"Go to OAuth & Permissions → add Bot Token Scopes: chat:write, channels:history, groups:history, im:history, mpim:history, app_mentions:read, files:write, reactions:write",
|
|
130
|
+
"Install App to Workspace → copy Bot User OAuth Token as SLACK_BOT_TOKEN",
|
|
131
|
+
"Subscribe to Events: message.im, message.groups, message.channels, app_mention",
|
|
132
|
+
"Invite the bot to channels with /invite @botname",
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "signal",
|
|
137
|
+
name: "Signal",
|
|
138
|
+
icon: "🔒",
|
|
139
|
+
description: "Signal Messenger via signal-cli REST API. Requires a separate signal-cli container.",
|
|
140
|
+
envVars: [
|
|
141
|
+
{ key: "SIGNAL_API_URL", label: "signal-cli REST API URL", placeholder: "http://localhost:8080" },
|
|
142
|
+
{ key: "SIGNAL_NUMBER", label: "Signal Number", placeholder: "+491234567890" },
|
|
143
|
+
],
|
|
144
|
+
setupUrl: "https://github.com/bbernhard/signal-cli-rest-api",
|
|
145
|
+
setupSteps: [
|
|
146
|
+
"Start signal-cli REST API (Docker recommended):",
|
|
147
|
+
"docker run -p 8080:8080 bbernhard/signal-cli-rest-api",
|
|
148
|
+
"Register your number via the API",
|
|
149
|
+
"Enter URL and number above",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const PROVIDERS = [
|
|
154
|
+
{
|
|
155
|
+
id: "claude-sdk",
|
|
156
|
+
name: "Claude Agent SDK",
|
|
157
|
+
icon: "🟣",
|
|
158
|
+
description: "Full tool use via Agent SDK. Requires Claude CLI login (Max plan or API key).",
|
|
159
|
+
envKey: "",
|
|
160
|
+
models: [
|
|
161
|
+
{ key: "claude-sdk", name: "Claude (Agent SDK)", model: "claude-opus-4-6" },
|
|
162
|
+
],
|
|
163
|
+
signupUrl: "https://console.anthropic.com",
|
|
164
|
+
docsUrl: "https://docs.anthropic.com/en/docs/claude-code",
|
|
165
|
+
setupSteps: [
|
|
166
|
+
"npm install -g @anthropic-ai/claude-code",
|
|
167
|
+
"claude login (browser auth or API key)",
|
|
168
|
+
"Full tool use: read/write files, shell commands, browser",
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "anthropic",
|
|
173
|
+
name: "Anthropic API",
|
|
174
|
+
icon: "🟣",
|
|
175
|
+
description: "Claude Opus, Sonnet, Haiku directly via API key. OpenAI-compatible.",
|
|
176
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
177
|
+
models: [
|
|
178
|
+
{ key: "claude-opus", name: "Claude Opus 4", model: "claude-opus-4-6" },
|
|
179
|
+
{ key: "claude-sonnet", name: "Claude Sonnet 4", model: "claude-sonnet-4-20250514" },
|
|
180
|
+
{ key: "claude-haiku", name: "Claude 3.5 Haiku", model: "claude-3-5-haiku-20241022" },
|
|
181
|
+
],
|
|
182
|
+
signupUrl: "https://console.anthropic.com/settings/keys",
|
|
183
|
+
docsUrl: "https://docs.anthropic.com/en/api",
|
|
184
|
+
setupSteps: [
|
|
185
|
+
"Create account on console.anthropic.com",
|
|
186
|
+
"Generate API key under Settings → API Keys",
|
|
187
|
+
"Add credits (pay-as-you-go) or use subscription",
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "openai",
|
|
192
|
+
name: "OpenAI",
|
|
193
|
+
icon: "🟢",
|
|
194
|
+
description: "GPT-4o, GPT-4.1, o3/o4 and other OpenAI models.",
|
|
195
|
+
envKey: "OPENAI_API_KEY",
|
|
196
|
+
models: [
|
|
197
|
+
{ key: "gpt-4o", name: "GPT-4o", model: "gpt-4o" },
|
|
198
|
+
{ key: "gpt-4o-mini", name: "GPT-4o Mini", model: "gpt-4o-mini" },
|
|
199
|
+
{ key: "gpt-4.1", name: "GPT-4.1", model: "gpt-4.1" },
|
|
200
|
+
{ key: "gpt-4.1-mini", name: "GPT-4.1 Mini", model: "gpt-4.1-mini" },
|
|
201
|
+
{ key: "o3-mini", name: "o3 Mini", model: "o3-mini" },
|
|
202
|
+
],
|
|
203
|
+
signupUrl: "https://platform.openai.com/api-keys",
|
|
204
|
+
docsUrl: "https://platform.openai.com/docs",
|
|
205
|
+
setupSteps: [
|
|
206
|
+
"Create account on platform.openai.com",
|
|
207
|
+
"Generate API key under API Keys",
|
|
208
|
+
"Add credits (pay-as-you-go)",
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "google",
|
|
213
|
+
name: "Google Gemini",
|
|
214
|
+
icon: "🔵",
|
|
215
|
+
description: "Gemini 2.5/3 Pro/Flash via Google AI Studio. Free tier available.",
|
|
216
|
+
envKey: "GOOGLE_API_KEY",
|
|
217
|
+
models: [
|
|
218
|
+
{ key: "gemini-2.5-pro", name: "Gemini 2.5 Pro", model: "gemini-2.5-pro" },
|
|
219
|
+
{ key: "gemini-2.5-flash", name: "Gemini 2.5 Flash", model: "gemini-2.5-flash" },
|
|
220
|
+
{ key: "gemini-3-pro", name: "Gemini 3 Pro (Preview)", model: "gemini-3-pro-preview" },
|
|
221
|
+
{ key: "gemini-3-flash", name: "Gemini 3 Flash (Preview)", model: "gemini-3-flash-preview" },
|
|
222
|
+
],
|
|
223
|
+
signupUrl: "https://aistudio.google.com/apikey",
|
|
224
|
+
docsUrl: "https://ai.google.dev/docs",
|
|
225
|
+
setupSteps: [
|
|
226
|
+
"Open Google AI Studio (aistudio.google.com)",
|
|
227
|
+
"Create API key → ready to use immediately",
|
|
228
|
+
"Free tier: 15 RPM, 1M TPM",
|
|
229
|
+
],
|
|
230
|
+
free: true,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "nvidia",
|
|
234
|
+
name: "NVIDIA NIM",
|
|
235
|
+
icon: "🟩",
|
|
236
|
+
description: "150+ models free (Llama, Kimi, Mistral, etc.) via NVIDIA API.",
|
|
237
|
+
envKey: "NVIDIA_API_KEY",
|
|
238
|
+
models: [
|
|
239
|
+
{ key: "nvidia-llama-3.3-70b", name: "Llama 3.3 70B", model: "meta/llama-3.3-70b-instruct" },
|
|
240
|
+
{ key: "nvidia-kimi-k2.5", name: "Kimi K2.5", model: "moonshotai/kimi-k2.5" },
|
|
241
|
+
],
|
|
242
|
+
signupUrl: "https://build.nvidia.com",
|
|
243
|
+
docsUrl: "https://docs.api.nvidia.com",
|
|
244
|
+
setupSteps: [
|
|
245
|
+
"Create account on build.nvidia.com",
|
|
246
|
+
"Generate free API key",
|
|
247
|
+
"150+ models available for free (1000 credits/month)",
|
|
248
|
+
],
|
|
249
|
+
free: true,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "groq",
|
|
253
|
+
name: "Groq",
|
|
254
|
+
icon: "⚡",
|
|
255
|
+
description: "Ultra-fast inference. Llama, Mixtral, Gemma — free and lightning fast.",
|
|
256
|
+
envKey: "GROQ_API_KEY",
|
|
257
|
+
models: [
|
|
258
|
+
{ key: "groq", name: "Llama 3.3 70B (Groq)", model: "llama-3.3-70b-versatile" },
|
|
259
|
+
{ key: "groq-llama-3.1-8b", name: "Llama 3.1 8B (Groq)", model: "llama-3.1-8b-instant" },
|
|
260
|
+
{ key: "groq-mixtral", name: "Mixtral 8x7B (Groq)", model: "mixtral-8x7b-32768" },
|
|
261
|
+
],
|
|
262
|
+
signupUrl: "https://console.groq.com",
|
|
263
|
+
docsUrl: "https://console.groq.com/docs",
|
|
264
|
+
setupSteps: [
|
|
265
|
+
"Create account on console.groq.com (no credit card needed)",
|
|
266
|
+
"Generate API key",
|
|
267
|
+
"Ready to use immediately — free tier with rate limits",
|
|
268
|
+
],
|
|
269
|
+
free: true,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "openrouter",
|
|
273
|
+
name: "OpenRouter",
|
|
274
|
+
icon: "🌐",
|
|
275
|
+
description: "One API key, 200+ models. Claude, GPT, Gemini, Llama — all via one API.",
|
|
276
|
+
envKey: "OPENROUTER_API_KEY",
|
|
277
|
+
models: [
|
|
278
|
+
{ key: "openrouter", name: "OpenRouter (Standard)", model: "anthropic/claude-sonnet-4" },
|
|
279
|
+
],
|
|
280
|
+
signupUrl: "https://openrouter.ai/keys",
|
|
281
|
+
docsUrl: "https://openrouter.ai/docs",
|
|
282
|
+
setupSteps: [
|
|
283
|
+
"Create account on openrouter.ai",
|
|
284
|
+
"Generate API key",
|
|
285
|
+
"Add credits or use free models",
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "ollama",
|
|
290
|
+
name: "Ollama (Local)",
|
|
291
|
+
icon: "🦙",
|
|
292
|
+
description: "Local models on your machine. No API key needed, runs offline.",
|
|
293
|
+
envKey: "",
|
|
294
|
+
models: [
|
|
295
|
+
{ key: "ollama", name: "Ollama (Local)", model: "llama3.2" },
|
|
296
|
+
],
|
|
297
|
+
signupUrl: "https://ollama.com/download",
|
|
298
|
+
docsUrl: "https://ollama.com/library",
|
|
299
|
+
setupSteps: [
|
|
300
|
+
"Install Ollama: brew install ollama (macOS) or ollama.com/download",
|
|
301
|
+
"Pull a model: ollama pull llama3.2",
|
|
302
|
+
"Runs automatically on localhost:11434",
|
|
303
|
+
],
|
|
304
|
+
free: true,
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
// ── API Handler ─────────────────────────────────────────
|
|
308
|
+
export async function handleSetupAPI(req, res, urlPath, body) {
|
|
309
|
+
res.setHeader("Content-Type", "application/json");
|
|
310
|
+
// ── Platforms ───────────────────────────────────────
|
|
311
|
+
// GET /api/platforms/setup — full setup info for all platforms
|
|
312
|
+
if (urlPath === "/api/platforms/setup") {
|
|
313
|
+
const env = readEnv();
|
|
314
|
+
const platforms = PLATFORMS.map(p => ({
|
|
315
|
+
...p,
|
|
316
|
+
configured: (() => {
|
|
317
|
+
// A platform is "configured" if its primary env var(s) are set
|
|
318
|
+
// Toggles: the first toggle being true is enough (e.g., WHATSAPP_ENABLED)
|
|
319
|
+
// Text fields: all non-toggle fields must have a value
|
|
320
|
+
const required = p.envVars.filter(v => v.type !== "toggle");
|
|
321
|
+
const toggles = p.envVars.filter(v => v.type === "toggle");
|
|
322
|
+
if (required.length > 0)
|
|
323
|
+
return required.every(v => !!env[v.key]);
|
|
324
|
+
if (toggles.length > 0)
|
|
325
|
+
return toggles[0] && env[toggles[0].key] === "true";
|
|
326
|
+
return false;
|
|
327
|
+
})(),
|
|
328
|
+
values: Object.fromEntries(p.envVars.map(v => [v.key, v.secret && env[v.key] ? maskSecret(env[v.key]) : (env[v.key] || "")])),
|
|
329
|
+
depsInstalled: p.npmPackages ? checkNpmDeps(p.npmPackages) : true,
|
|
330
|
+
}));
|
|
331
|
+
res.end(JSON.stringify({ platforms }));
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
// POST /api/platforms/configure — save platform env vars
|
|
335
|
+
if (urlPath === "/api/platforms/configure" && req.method === "POST") {
|
|
336
|
+
try {
|
|
337
|
+
const { platformId, values } = JSON.parse(body);
|
|
338
|
+
const platform = PLATFORMS.find(p => p.id === platformId);
|
|
339
|
+
if (!platform) {
|
|
340
|
+
res.statusCode = 400;
|
|
341
|
+
res.end(JSON.stringify({ error: "Unknown platform" }));
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
for (const v of platform.envVars) {
|
|
345
|
+
if (values[v.key] !== undefined && values[v.key] !== "") {
|
|
346
|
+
writeEnvVar(v.key, values[v.key]);
|
|
347
|
+
process.env[v.key] = values[v.key]; // Hot-apply for toggle changes
|
|
348
|
+
}
|
|
349
|
+
else if (values[v.key] === "") {
|
|
350
|
+
removeEnvVar(v.key);
|
|
351
|
+
delete process.env[v.key]; // Hot-remove
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// WhatsApp toggle-only changes (self-chat, groups, DMs) don't need restart
|
|
355
|
+
const onlyToggles = platform.envVars.every(v => v.type === "toggle") ||
|
|
356
|
+
(platformId === "whatsapp" && platform.envVars.filter(v => v.type !== "toggle").every(v => !values[v.key]));
|
|
357
|
+
const restartNeeded = !onlyToggles;
|
|
358
|
+
res.end(JSON.stringify({ ok: true, restartNeeded, note: restartNeeded ? "Restart required to apply changes." : "Saved." }));
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
res.statusCode = 400;
|
|
362
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
// POST /api/platforms/install-deps — install npm packages for a platform
|
|
367
|
+
if (urlPath === "/api/platforms/install-deps" && req.method === "POST") {
|
|
368
|
+
try {
|
|
369
|
+
const { platformId } = JSON.parse(body);
|
|
370
|
+
const platform = PLATFORMS.find(p => p.id === platformId);
|
|
371
|
+
if (!platform?.npmPackages?.length) {
|
|
372
|
+
res.end(JSON.stringify({ ok: true, note: "No dependencies needed." }));
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
const pkgs = platform.npmPackages.join(" ");
|
|
376
|
+
const output = execSync(`cd "${BOT_ROOT}" && npm install ${pkgs} --save-optional 2>&1`, {
|
|
377
|
+
timeout: 120000,
|
|
378
|
+
env: { ...process.env, PATH: process.env.PATH + ":/opt/homebrew/bin:/usr/local/bin" },
|
|
379
|
+
}).toString();
|
|
380
|
+
res.end(JSON.stringify({ ok: true, output: output.slice(0, 5000) }));
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
384
|
+
res.end(JSON.stringify({ error }));
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
// ── Models / Providers ─────────────────────────────
|
|
389
|
+
// GET /api/providers/setup — full setup info for all providers
|
|
390
|
+
if (urlPath === "/api/providers/setup") {
|
|
391
|
+
const env = readEnv();
|
|
392
|
+
const registry = getRegistry();
|
|
393
|
+
const activeKey = registry.getActiveKey();
|
|
394
|
+
const registeredModels = await registry.listAll();
|
|
395
|
+
const providers = PROVIDERS.map(p => ({
|
|
396
|
+
...p,
|
|
397
|
+
hasKey: p.envKey ? !!env[p.envKey] : true, // Ollama doesn't need key
|
|
398
|
+
keyPreview: p.envKey && env[p.envKey] ? maskSecret(env[p.envKey]) : "",
|
|
399
|
+
modelsActive: p.models.map(m => ({
|
|
400
|
+
...m,
|
|
401
|
+
registered: registeredModels.some(rm => rm.key === m.key),
|
|
402
|
+
active: activeKey === m.key,
|
|
403
|
+
status: registeredModels.find(rm => rm.key === m.key)?.status || "not configured",
|
|
404
|
+
})),
|
|
405
|
+
}));
|
|
406
|
+
const customModels = loadCustomModels();
|
|
407
|
+
res.end(JSON.stringify({ providers, customModels, activeModel: activeKey }));
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
// POST /api/providers/set-key — save an API key
|
|
411
|
+
if (urlPath === "/api/providers/set-key" && req.method === "POST") {
|
|
412
|
+
try {
|
|
413
|
+
const { providerId, apiKey } = JSON.parse(body);
|
|
414
|
+
const provider = PROVIDERS.find(p => p.id === providerId);
|
|
415
|
+
if (!provider?.envKey) {
|
|
416
|
+
res.statusCode = 400;
|
|
417
|
+
res.end(JSON.stringify({ error: "Provider does not need an API key" }));
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
writeEnvVar(provider.envKey, apiKey);
|
|
421
|
+
res.end(JSON.stringify({ ok: true, note: "Restart required to activate the new key." }));
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
res.statusCode = 400;
|
|
425
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
// POST /api/providers/set-primary — set primary provider
|
|
430
|
+
if (urlPath === "/api/providers/set-primary" && req.method === "POST") {
|
|
431
|
+
try {
|
|
432
|
+
const { key } = JSON.parse(body);
|
|
433
|
+
writeEnvVar("PRIMARY_PROVIDER", key);
|
|
434
|
+
// Also switch runtime
|
|
435
|
+
const registry = getRegistry();
|
|
436
|
+
registry.switchTo(key);
|
|
437
|
+
res.end(JSON.stringify({ ok: true }));
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
res.statusCode = 400;
|
|
441
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
// POST /api/providers/set-fallbacks — set fallback chain
|
|
446
|
+
if (urlPath === "/api/providers/set-fallbacks" && req.method === "POST") {
|
|
447
|
+
try {
|
|
448
|
+
const { keys } = JSON.parse(body);
|
|
449
|
+
writeEnvVar("FALLBACK_PROVIDERS", keys.join(","));
|
|
450
|
+
res.end(JSON.stringify({ ok: true, note: "Restart required." }));
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
res.statusCode = 400;
|
|
454
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
// GET /api/providers/live-models?id=<providerId> — fetch available models from provider API
|
|
459
|
+
if (urlPath?.startsWith("/api/providers/live-models") && req.method === "GET") {
|
|
460
|
+
try {
|
|
461
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
462
|
+
const providerId = url.searchParams.get("id") || "";
|
|
463
|
+
const models = await fetchLiveModels(providerId);
|
|
464
|
+
res.end(JSON.stringify({ ok: true, providerId, models }));
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err), models: [] }));
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
// POST /api/providers/add-custom — add a custom model
|
|
472
|
+
if (urlPath === "/api/providers/add-custom" && req.method === "POST") {
|
|
473
|
+
try {
|
|
474
|
+
const model = JSON.parse(body);
|
|
475
|
+
if (!model.key || !model.name || !model.baseUrl || !model.model) {
|
|
476
|
+
res.statusCode = 400;
|
|
477
|
+
res.end(JSON.stringify({ error: "key, name, baseUrl and model are required fields" }));
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
model.type = "openai-compatible";
|
|
481
|
+
const models = loadCustomModels();
|
|
482
|
+
// Upsert
|
|
483
|
+
const idx = models.findIndex(m => m.key === model.key);
|
|
484
|
+
if (idx >= 0)
|
|
485
|
+
models[idx] = model;
|
|
486
|
+
else
|
|
487
|
+
models.push(model);
|
|
488
|
+
saveCustomModels(models);
|
|
489
|
+
// Save API key if provided
|
|
490
|
+
if (model.apiKeyEnv && model.apiKey) {
|
|
491
|
+
writeEnvVar(model.apiKeyEnv, model.apiKey);
|
|
492
|
+
}
|
|
493
|
+
res.end(JSON.stringify({ ok: true, note: "Restart required to activate the model." }));
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
res.statusCode = 400;
|
|
497
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
498
|
+
}
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
// DELETE /api/providers/remove-custom — remove a custom model
|
|
502
|
+
if (urlPath === "/api/providers/remove-custom" && req.method === "POST") {
|
|
503
|
+
try {
|
|
504
|
+
const { key } = JSON.parse(body);
|
|
505
|
+
const models = loadCustomModels().filter(m => m.key !== key);
|
|
506
|
+
saveCustomModels(models);
|
|
507
|
+
res.end(JSON.stringify({ ok: true }));
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
res.statusCode = 400;
|
|
511
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
// POST /api/providers/test-key — quick API key validation
|
|
516
|
+
if (urlPath === "/api/providers/test-key" && req.method === "POST") {
|
|
517
|
+
try {
|
|
518
|
+
const { providerId, apiKey } = JSON.parse(body);
|
|
519
|
+
const result = await testApiKey(providerId, apiKey);
|
|
520
|
+
res.end(JSON.stringify(result));
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
524
|
+
res.end(JSON.stringify({ ok: false, error }));
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
// ── Sudo / Elevated Access ─────────────────────────
|
|
529
|
+
// GET /api/sudo/status — check sudo configuration
|
|
530
|
+
if (urlPath === "/api/sudo/status") {
|
|
531
|
+
const status = await getSudoStatus();
|
|
532
|
+
res.end(JSON.stringify(status));
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
// POST /api/sudo/setup — store sudo password
|
|
536
|
+
if (urlPath === "/api/sudo/setup" && req.method === "POST") {
|
|
537
|
+
try {
|
|
538
|
+
const { password } = JSON.parse(body);
|
|
539
|
+
if (!password) {
|
|
540
|
+
res.statusCode = 400;
|
|
541
|
+
res.end(JSON.stringify({ error: "Password required" }));
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
const result = storePassword(password);
|
|
545
|
+
if (result.ok) {
|
|
546
|
+
// Verify it works
|
|
547
|
+
const verify = await verifyPassword();
|
|
548
|
+
if (verify.ok) {
|
|
549
|
+
res.end(JSON.stringify({ ok: true, method: result.method, verified: true }));
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
revokePassword(); // Clean up if wrong password
|
|
553
|
+
res.end(JSON.stringify({ ok: false, error: "Password stored but verification failed: " + verify.error }));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
res.end(JSON.stringify({ ok: false, error: result.error }));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
res.statusCode = 400;
|
|
562
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
563
|
+
}
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
// POST /api/sudo/revoke — delete stored password
|
|
567
|
+
if (urlPath === "/api/sudo/revoke" && req.method === "POST") {
|
|
568
|
+
const ok = revokePassword();
|
|
569
|
+
res.end(JSON.stringify({ ok }));
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
// POST /api/sudo/verify — test if stored password works
|
|
573
|
+
if (urlPath === "/api/sudo/verify" && req.method === "POST") {
|
|
574
|
+
const result = await verifyPassword();
|
|
575
|
+
res.end(JSON.stringify(result));
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
// POST /api/sudo/exec — execute a command with sudo
|
|
579
|
+
if (urlPath === "/api/sudo/exec" && req.method === "POST") {
|
|
580
|
+
try {
|
|
581
|
+
const { command } = JSON.parse(body);
|
|
582
|
+
if (!command) {
|
|
583
|
+
res.statusCode = 400;
|
|
584
|
+
res.end(JSON.stringify({ error: "No command specified" }));
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
const result = await sudoExec(command);
|
|
588
|
+
res.end(JSON.stringify(result));
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
res.statusCode = 400;
|
|
592
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
// POST /api/sudo/admin-dialog — show macOS admin dialog
|
|
597
|
+
if (urlPath === "/api/sudo/admin-dialog" && req.method === "POST") {
|
|
598
|
+
try {
|
|
599
|
+
const { reason } = JSON.parse(body);
|
|
600
|
+
const result = await requestAdminViaDialog(reason || "Alvin Bot requires administrator privileges");
|
|
601
|
+
res.end(JSON.stringify(result));
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
res.statusCode = 400;
|
|
605
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
606
|
+
}
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
// POST /api/sudo/open-settings — open macOS system settings
|
|
610
|
+
if (urlPath === "/api/sudo/open-settings" && req.method === "POST") {
|
|
611
|
+
try {
|
|
612
|
+
const { pane } = JSON.parse(body);
|
|
613
|
+
const ok = openSystemSettings(pane || "security");
|
|
614
|
+
res.end(JSON.stringify({ ok }));
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
res.statusCode = 400;
|
|
618
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
// ── Skills ────────────────────────────────────────────
|
|
623
|
+
// GET /api/skills — list all loaded skills
|
|
624
|
+
if (urlPath === "/api/skills") {
|
|
625
|
+
const { getSkills } = await import("../services/skills.js");
|
|
626
|
+
const skills = getSkills().map(s => ({
|
|
627
|
+
id: s.id,
|
|
628
|
+
name: s.name,
|
|
629
|
+
description: s.description,
|
|
630
|
+
triggers: s.triggers,
|
|
631
|
+
priority: s.priority,
|
|
632
|
+
category: s.category,
|
|
633
|
+
}));
|
|
634
|
+
res.end(JSON.stringify({ skills }));
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
// ── Cron Jobs ───────────────────────────────────────
|
|
638
|
+
// GET /api/cron — list all jobs
|
|
639
|
+
if (urlPath === "/api/cron") {
|
|
640
|
+
const jobs = listJobs();
|
|
641
|
+
const enriched = jobs.map(j => ({
|
|
642
|
+
...j,
|
|
643
|
+
nextRunFormatted: formatNextRun(j.nextRunAt),
|
|
644
|
+
lastRunFormatted: j.lastRunAt ? new Date(j.lastRunAt).toLocaleString("de-DE") : null,
|
|
645
|
+
scheduleReadable: humanReadableSchedule(j.schedule),
|
|
646
|
+
}));
|
|
647
|
+
res.end(JSON.stringify({ jobs: enriched }));
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
// POST /api/cron/create — create a new job
|
|
651
|
+
if (urlPath === "/api/cron/create" && req.method === "POST") {
|
|
652
|
+
try {
|
|
653
|
+
const data = JSON.parse(body);
|
|
654
|
+
const job = createJob({
|
|
655
|
+
name: data.name,
|
|
656
|
+
type: data.type,
|
|
657
|
+
schedule: data.schedule,
|
|
658
|
+
oneShot: data.oneShot || false,
|
|
659
|
+
payload: data.payload || {},
|
|
660
|
+
target: data.target || { platform: "web", chatId: "dashboard" },
|
|
661
|
+
createdBy: "web-ui",
|
|
662
|
+
});
|
|
663
|
+
res.end(JSON.stringify({ ok: true, job }));
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
res.statusCode = 400;
|
|
667
|
+
const error = err instanceof Error ? err.message : "Invalid request";
|
|
668
|
+
res.end(JSON.stringify({ error }));
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
// POST /api/cron/delete — delete a job
|
|
673
|
+
if (urlPath === "/api/cron/delete" && req.method === "POST") {
|
|
674
|
+
try {
|
|
675
|
+
const { id } = JSON.parse(body);
|
|
676
|
+
const ok = deleteJob(id);
|
|
677
|
+
res.end(JSON.stringify({ ok }));
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
res.statusCode = 400;
|
|
681
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
// POST /api/cron/update — update job fields (schedule, name, oneShot)
|
|
686
|
+
if (urlPath === "/api/cron/update" && req.method === "POST") {
|
|
687
|
+
try {
|
|
688
|
+
const { id, ...updates } = JSON.parse(body);
|
|
689
|
+
if (!id) {
|
|
690
|
+
res.statusCode = 400;
|
|
691
|
+
res.end(JSON.stringify({ error: "id required" }));
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
// Only allow safe fields
|
|
695
|
+
const allowed = {};
|
|
696
|
+
if (updates.schedule !== undefined)
|
|
697
|
+
allowed.schedule = updates.schedule;
|
|
698
|
+
if (updates.name !== undefined)
|
|
699
|
+
allowed.name = updates.name;
|
|
700
|
+
if (updates.oneShot !== undefined)
|
|
701
|
+
allowed.oneShot = updates.oneShot;
|
|
702
|
+
const job = updateJob(id, allowed);
|
|
703
|
+
if (!job) {
|
|
704
|
+
res.statusCode = 404;
|
|
705
|
+
res.end(JSON.stringify({ error: "Job not found" }));
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
res.end(JSON.stringify({ ok: true, job }));
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
res.statusCode = 400;
|
|
712
|
+
const error = err instanceof Error ? err.message : "Invalid request";
|
|
713
|
+
res.end(JSON.stringify({ error }));
|
|
714
|
+
}
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
// POST /api/cron/toggle — enable/disable a job
|
|
718
|
+
if (urlPath === "/api/cron/toggle" && req.method === "POST") {
|
|
719
|
+
try {
|
|
720
|
+
const { id } = JSON.parse(body);
|
|
721
|
+
const job = toggleJob(id);
|
|
722
|
+
res.end(JSON.stringify({ ok: !!job, job }));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
res.statusCode = 400;
|
|
726
|
+
res.end(JSON.stringify({ error: "Invalid request" }));
|
|
727
|
+
}
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
// POST /api/cron/run — run a job immediately
|
|
731
|
+
if (urlPath === "/api/cron/run" && req.method === "POST") {
|
|
732
|
+
try {
|
|
733
|
+
const { id } = JSON.parse(body);
|
|
734
|
+
const result = await (runJobNow(id) || Promise.resolve({ output: "", error: "Job not found" }));
|
|
735
|
+
res.end(JSON.stringify(result));
|
|
736
|
+
}
|
|
737
|
+
catch (err) {
|
|
738
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
739
|
+
res.end(JSON.stringify({ error }));
|
|
740
|
+
}
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
// ── Platform Connection Status ─────────────────────────
|
|
744
|
+
// GET /api/platforms/status — live connection status for all platforms
|
|
745
|
+
if (urlPath === "/api/platforms/status") {
|
|
746
|
+
const statuses = {};
|
|
747
|
+
// Telegram
|
|
748
|
+
try {
|
|
749
|
+
const { getTelegramState } = await import("../platforms/telegram.js");
|
|
750
|
+
statuses.telegram = getTelegramState();
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
statuses.telegram = { status: !!process.env.BOT_TOKEN ? "unknown" : "not_configured" };
|
|
754
|
+
}
|
|
755
|
+
// Discord
|
|
756
|
+
try {
|
|
757
|
+
const { getDiscordState } = await import("../platforms/discord.js");
|
|
758
|
+
statuses.discord = getDiscordState();
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
statuses.discord = { status: !!process.env.DISCORD_TOKEN ? "unknown" : "not_configured" };
|
|
762
|
+
}
|
|
763
|
+
// WhatsApp
|
|
764
|
+
try {
|
|
765
|
+
const { getWhatsAppState } = await import("../platforms/whatsapp.js");
|
|
766
|
+
statuses.whatsapp = getWhatsAppState();
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
statuses.whatsapp = { status: process.env.WHATSAPP_ENABLED === "true" ? "unknown" : "not_configured" };
|
|
770
|
+
}
|
|
771
|
+
// Signal
|
|
772
|
+
try {
|
|
773
|
+
const { getSignalState } = await import("../platforms/signal.js");
|
|
774
|
+
statuses.signal = getSignalState();
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
statuses.signal = { status: !!process.env.SIGNAL_API_URL ? "unknown" : "not_configured" };
|
|
778
|
+
}
|
|
779
|
+
res.end(JSON.stringify(statuses));
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
// GET /api/whatsapp/status — WhatsApp-specific (QR code needs its own endpoint)
|
|
783
|
+
if (urlPath === "/api/whatsapp/status") {
|
|
784
|
+
try {
|
|
785
|
+
const { getWhatsAppState } = await import("../platforms/whatsapp.js");
|
|
786
|
+
const state = getWhatsAppState();
|
|
787
|
+
res.end(JSON.stringify(state));
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
res.end(JSON.stringify({ status: "disconnected", qrString: null, error: "WhatsApp adapter not loaded" }));
|
|
791
|
+
}
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
// POST /api/whatsapp/disconnect — clear auth and disconnect
|
|
795
|
+
if (urlPath === "/api/whatsapp/disconnect" && req.method === "POST") {
|
|
796
|
+
try {
|
|
797
|
+
const authDir = WHATSAPP_AUTH;
|
|
798
|
+
if (fs.existsSync(authDir)) {
|
|
799
|
+
fs.rmSync(authDir, { recursive: true });
|
|
800
|
+
}
|
|
801
|
+
res.end(JSON.stringify({ ok: true, note: "Auth data cleared. Restart required for new connection." }));
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
805
|
+
res.end(JSON.stringify({ ok: false, error }));
|
|
806
|
+
}
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
// POST /api/platforms/test-connection — test a specific platform
|
|
810
|
+
if (urlPath === "/api/platforms/test-connection" && req.method === "POST") {
|
|
811
|
+
try {
|
|
812
|
+
const { platformId } = JSON.parse(body);
|
|
813
|
+
if (platformId === "telegram") {
|
|
814
|
+
const token = process.env.BOT_TOKEN;
|
|
815
|
+
if (!token) {
|
|
816
|
+
res.end(JSON.stringify({ ok: false, error: "BOT_TOKEN not set" }));
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
const apiRes = await fetch(`https://api.telegram.org/bot${token}/getMe`);
|
|
820
|
+
const data = await apiRes.json();
|
|
821
|
+
if (data.ok) {
|
|
822
|
+
res.end(JSON.stringify({ ok: true, info: `@${data.result.username} (${data.result.first_name})` }));
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
res.end(JSON.stringify({ ok: false, error: data.description || "Invalid token" }));
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
if (platformId === "discord") {
|
|
830
|
+
const token = process.env.DISCORD_TOKEN;
|
|
831
|
+
if (!token) {
|
|
832
|
+
res.end(JSON.stringify({ ok: false, error: "DISCORD_TOKEN not set" }));
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
const apiRes = await fetch("https://discord.com/api/v10/users/@me", {
|
|
836
|
+
headers: { Authorization: `Bot ${token}` },
|
|
837
|
+
});
|
|
838
|
+
const data = await apiRes.json();
|
|
839
|
+
if (data.id) {
|
|
840
|
+
res.end(JSON.stringify({ ok: true, info: `${data.username}#${data.discriminator || '0'} (ID: ${data.id})` }));
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
res.end(JSON.stringify({ ok: false, error: data.message || "Invalid token" }));
|
|
844
|
+
}
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
if (platformId === "signal") {
|
|
848
|
+
const apiUrl = process.env.SIGNAL_API_URL;
|
|
849
|
+
if (!apiUrl) {
|
|
850
|
+
res.end(JSON.stringify({ ok: false, error: "SIGNAL_API_URL not set" }));
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
const apiRes = await fetch(`${apiUrl.replace(/\/$/, '')}/v1/about`);
|
|
854
|
+
if (apiRes.ok) {
|
|
855
|
+
const data = await apiRes.json();
|
|
856
|
+
res.end(JSON.stringify({ ok: true, info: `signal-cli API v${data.version || '?'} reachable` }));
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
res.end(JSON.stringify({ ok: false, error: `API responded with ${apiRes.status}` }));
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
if (platformId === "whatsapp") {
|
|
864
|
+
try {
|
|
865
|
+
const { getWhatsAppState } = await import("../platforms/whatsapp.js");
|
|
866
|
+
const state = getWhatsAppState();
|
|
867
|
+
res.end(JSON.stringify({ ok: state.status === "connected", info: `Status: ${state.status}` }));
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
res.end(JSON.stringify({ ok: false, error: "WhatsApp adapter not loaded" }));
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
res.end(JSON.stringify({ ok: false, error: "Unknown platform" }));
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
878
|
+
res.end(JSON.stringify({ ok: false, error }));
|
|
879
|
+
}
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
return false; // Not handled
|
|
883
|
+
}
|
|
884
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
885
|
+
function maskSecret(value) {
|
|
886
|
+
if (value.length <= 8)
|
|
887
|
+
return "****";
|
|
888
|
+
return value.slice(0, 4) + "..." + value.slice(-4);
|
|
889
|
+
}
|
|
890
|
+
function checkNpmDeps(packages) {
|
|
891
|
+
const nodeModules = resolve(BOT_ROOT, "node_modules");
|
|
892
|
+
return packages.every(pkg => {
|
|
893
|
+
try {
|
|
894
|
+
return fs.existsSync(resolve(nodeModules, pkg.split("/")[0]));
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async function testApiKey(providerId, apiKey) {
|
|
902
|
+
try {
|
|
903
|
+
const provider = PROVIDERS.find(p => p.id === providerId);
|
|
904
|
+
if (!provider)
|
|
905
|
+
return { ok: false, error: "Unknown provider" };
|
|
906
|
+
// Use stored key if requested (input was empty but key already configured)
|
|
907
|
+
// Skip for providers that don't use API keys (e.g. claude-sdk uses CLI auth)
|
|
908
|
+
if (apiKey === "__USE_STORED__") {
|
|
909
|
+
if (providerId === "claude-sdk" || providerId === "ollama") {
|
|
910
|
+
apiKey = ""; // These don't need keys — test will check CLI/service availability
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
const envKey = provider.envKey;
|
|
914
|
+
const storedKey = envKey ? process.env[envKey] : undefined;
|
|
915
|
+
if (!storedKey)
|
|
916
|
+
return { ok: false, error: "No stored key available" };
|
|
917
|
+
apiKey = storedKey;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
switch (providerId) {
|
|
921
|
+
case "openai": {
|
|
922
|
+
const r = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
923
|
+
if (!r.ok)
|
|
924
|
+
return { ok: false, error: `HTTP ${r.status}: ${await r.text()}` };
|
|
925
|
+
return { ok: true, model: "gpt-4o" };
|
|
926
|
+
}
|
|
927
|
+
case "google": {
|
|
928
|
+
const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
|
|
929
|
+
if (!r.ok)
|
|
930
|
+
return { ok: false, error: `HTTP ${r.status}: ${await r.text()}` };
|
|
931
|
+
return { ok: true, model: "gemini-2.5-pro" };
|
|
932
|
+
}
|
|
933
|
+
case "nvidia": {
|
|
934
|
+
const r = await fetch("https://integrate.api.nvidia.com/v1/models", { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
935
|
+
if (!r.ok)
|
|
936
|
+
return { ok: false, error: `HTTP ${r.status}: ${await r.text()}` };
|
|
937
|
+
return { ok: true, model: "meta/llama-3.3-70b-instruct" };
|
|
938
|
+
}
|
|
939
|
+
case "openrouter": {
|
|
940
|
+
const r = await fetch("https://openrouter.ai/api/v1/models", { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
941
|
+
if (!r.ok)
|
|
942
|
+
return { ok: false, error: `HTTP ${r.status}: ${await r.text()}` };
|
|
943
|
+
return { ok: true, model: "anthropic/claude-sonnet-4" };
|
|
944
|
+
}
|
|
945
|
+
case "groq": {
|
|
946
|
+
const r = await fetch("https://api.groq.com/openai/v1/models", { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
947
|
+
if (!r.ok)
|
|
948
|
+
return { ok: false, error: `HTTP ${r.status}: ${await r.text()}` };
|
|
949
|
+
return { ok: true, model: "llama-3.3-70b-versatile" };
|
|
950
|
+
}
|
|
951
|
+
case "claude-sdk": {
|
|
952
|
+
// Claude SDK uses CLI auth, not an API key — check if CLI is available
|
|
953
|
+
const { execSync } = await import("child_process");
|
|
954
|
+
try {
|
|
955
|
+
execSync("claude --version", { timeout: 5000, stdio: "pipe" });
|
|
956
|
+
return { ok: true, model: "claude-opus-4-6" };
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
return { ok: false, error: "Claude CLI not installed or not logged in" };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
case "anthropic": {
|
|
963
|
+
// Anthropic API via OpenAI-compatible endpoint
|
|
964
|
+
const r = await fetch("https://api.anthropic.com/v1/models", {
|
|
965
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
966
|
+
});
|
|
967
|
+
if (!r.ok)
|
|
968
|
+
return { ok: false, error: `HTTP ${r.status}: ${(await r.text()).substring(0, 200)}` };
|
|
969
|
+
return { ok: true, model: "claude-sonnet-4" };
|
|
970
|
+
}
|
|
971
|
+
default:
|
|
972
|
+
return { ok: false, error: "Key test not available for this provider" };
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async function fetchLiveModels(providerId) {
|
|
980
|
+
const env = process.env;
|
|
981
|
+
switch (providerId) {
|
|
982
|
+
case "anthropic": {
|
|
983
|
+
const key = env.ANTHROPIC_API_KEY;
|
|
984
|
+
if (!key)
|
|
985
|
+
return [];
|
|
986
|
+
const r = await fetch("https://api.anthropic.com/v1/models", {
|
|
987
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
988
|
+
});
|
|
989
|
+
if (!r.ok)
|
|
990
|
+
return [];
|
|
991
|
+
const data = await r.json();
|
|
992
|
+
return (data.data || [])
|
|
993
|
+
.filter((m) => m.id && !m.id.includes("pdfs"))
|
|
994
|
+
.map((m) => ({ id: m.id, name: m.display_name || m.id, owned_by: "anthropic" }))
|
|
995
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
996
|
+
}
|
|
997
|
+
case "openai": {
|
|
998
|
+
const key = env.OPENAI_API_KEY;
|
|
999
|
+
if (!key)
|
|
1000
|
+
return [];
|
|
1001
|
+
const r = await fetch("https://api.openai.com/v1/models", {
|
|
1002
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1003
|
+
});
|
|
1004
|
+
if (!r.ok)
|
|
1005
|
+
return [];
|
|
1006
|
+
const data = await r.json();
|
|
1007
|
+
// Filter to chat-relevant models only
|
|
1008
|
+
const chatPrefixes = ["gpt-4", "gpt-3.5", "o1", "o3", "o4", "chatgpt"];
|
|
1009
|
+
return (data.data || [])
|
|
1010
|
+
.filter((m) => chatPrefixes.some(p => m.id.startsWith(p)))
|
|
1011
|
+
.map((m) => ({ id: m.id, name: m.id, owned_by: m.owned_by || "openai" }))
|
|
1012
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1013
|
+
}
|
|
1014
|
+
case "google": {
|
|
1015
|
+
const key = env.GOOGLE_API_KEY;
|
|
1016
|
+
if (!key)
|
|
1017
|
+
return [];
|
|
1018
|
+
const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`);
|
|
1019
|
+
if (!r.ok)
|
|
1020
|
+
return [];
|
|
1021
|
+
const data = await r.json();
|
|
1022
|
+
return (data.models || [])
|
|
1023
|
+
.filter((m) => m.name && m.supportedGenerationMethods?.includes("generateContent"))
|
|
1024
|
+
.map((m) => ({
|
|
1025
|
+
id: m.name.replace("models/", ""),
|
|
1026
|
+
name: m.displayName || m.name.replace("models/", ""),
|
|
1027
|
+
owned_by: "google",
|
|
1028
|
+
}))
|
|
1029
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1030
|
+
}
|
|
1031
|
+
case "groq": {
|
|
1032
|
+
const key = env.GROQ_API_KEY;
|
|
1033
|
+
if (!key)
|
|
1034
|
+
return [];
|
|
1035
|
+
const r = await fetch("https://api.groq.com/openai/v1/models", {
|
|
1036
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1037
|
+
});
|
|
1038
|
+
if (!r.ok)
|
|
1039
|
+
return [];
|
|
1040
|
+
const data = await r.json();
|
|
1041
|
+
return (data.data || [])
|
|
1042
|
+
.filter((m) => m.id && m.active !== false)
|
|
1043
|
+
.map((m) => ({ id: m.id, name: m.id, owned_by: m.owned_by || "groq" }))
|
|
1044
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1045
|
+
}
|
|
1046
|
+
case "nvidia": {
|
|
1047
|
+
const key = env.NVIDIA_API_KEY;
|
|
1048
|
+
if (!key)
|
|
1049
|
+
return [];
|
|
1050
|
+
const r = await fetch("https://integrate.api.nvidia.com/v1/models", {
|
|
1051
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1052
|
+
});
|
|
1053
|
+
if (!r.ok)
|
|
1054
|
+
return [];
|
|
1055
|
+
const data = await r.json();
|
|
1056
|
+
return (data.data || [])
|
|
1057
|
+
.map((m) => ({ id: m.id, name: m.id, owned_by: m.owned_by || "nvidia" }))
|
|
1058
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1059
|
+
}
|
|
1060
|
+
case "openrouter": {
|
|
1061
|
+
const key = env.OPENROUTER_API_KEY;
|
|
1062
|
+
if (!key)
|
|
1063
|
+
return [];
|
|
1064
|
+
const r = await fetch("https://openrouter.ai/api/v1/models", {
|
|
1065
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
1066
|
+
});
|
|
1067
|
+
if (!r.ok)
|
|
1068
|
+
return [];
|
|
1069
|
+
const data = await r.json();
|
|
1070
|
+
return (data.data || [])
|
|
1071
|
+
.slice(0, 100) // OpenRouter has 200+ models, limit display
|
|
1072
|
+
.map((m) => ({ id: m.id, name: m.name || m.id, owned_by: "openrouter" }))
|
|
1073
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1074
|
+
}
|
|
1075
|
+
default:
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
}
|