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/bin/cli.js
ADDED
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Alvin Bot CLI — Setup, manage, and chat with your AI agent.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* alvin-bot setup — Interactive setup wizard
|
|
8
|
+
* alvin-bot tui — Terminal chat UI
|
|
9
|
+
* alvin-bot doctor — Check configuration
|
|
10
|
+
* alvin-bot audit — Security health check
|
|
11
|
+
* alvin-bot update — Pull latest & rebuild
|
|
12
|
+
* alvin-bot start — Start the bot
|
|
13
|
+
* alvin-bot stop — Stop the bot
|
|
14
|
+
*
|
|
15
|
+
* Flags:
|
|
16
|
+
* --lang en|de — Language (default: en, auto-detects from LANG env)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createInterface } from "readline";
|
|
20
|
+
import { existsSync, writeFileSync, readFileSync, mkdirSync, copyFileSync, readdirSync } from "fs";
|
|
21
|
+
import { resolve, join } from "path";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { execSync } from "child_process";
|
|
24
|
+
import { initI18n, t, getLocale } from "../dist/i18n.js";
|
|
25
|
+
|
|
26
|
+
// Data directory — same logic as src/paths.ts
|
|
27
|
+
const DATA_DIR = process.env.ALVIN_DATA_DIR || join(homedir(), ".alvin-bot");
|
|
28
|
+
|
|
29
|
+
// Init i18n early
|
|
30
|
+
initI18n();
|
|
31
|
+
|
|
32
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
34
|
+
|
|
35
|
+
const LOGO = `
|
|
36
|
+
╔══════════════════════════════════════╗
|
|
37
|
+
║ 🤖 Alvin Bot — Setup Wizard v3.0 ║
|
|
38
|
+
║ Your Personal AI Agent ║
|
|
39
|
+
╚══════════════════════════════════════╝
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
// ── Provider Definitions ────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const PROVIDERS = [
|
|
45
|
+
{
|
|
46
|
+
key: "groq",
|
|
47
|
+
name: "Groq (Llama 3.3 70B)",
|
|
48
|
+
desc: () => t("provider.groq.desc"),
|
|
49
|
+
free: true,
|
|
50
|
+
envKey: "GROQ_API_KEY",
|
|
51
|
+
signup: "https://console.groq.com",
|
|
52
|
+
model: "llama-3.3-70b-versatile",
|
|
53
|
+
needsCLI: false,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "nvidia-kimi-k2.5",
|
|
57
|
+
name: "NVIDIA NIM (Kimi K2.5 — Best Tool Use)",
|
|
58
|
+
desc: () => t("provider.nvidia.desc"),
|
|
59
|
+
free: true,
|
|
60
|
+
envKey: "NVIDIA_API_KEY",
|
|
61
|
+
signup: "https://build.nvidia.com",
|
|
62
|
+
model: "moonshotai/kimi-k2.5",
|
|
63
|
+
needsCLI: false,
|
|
64
|
+
fallbackModel: "meta/llama-3.3-70b-instruct",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "gemini-2.5-flash",
|
|
68
|
+
name: "Google Gemini (2.5 Flash)",
|
|
69
|
+
desc: () => t("provider.gemini.desc"),
|
|
70
|
+
free: true,
|
|
71
|
+
envKey: "GOOGLE_API_KEY",
|
|
72
|
+
signup: "https://aistudio.google.com",
|
|
73
|
+
model: "gemini-2.5-flash",
|
|
74
|
+
needsCLI: false,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "openai",
|
|
78
|
+
name: "OpenAI (GPT-4o)",
|
|
79
|
+
desc: () => t("provider.openai.desc"),
|
|
80
|
+
free: false,
|
|
81
|
+
envKey: "OPENAI_API_KEY",
|
|
82
|
+
signup: "https://platform.openai.com",
|
|
83
|
+
model: "gpt-4o",
|
|
84
|
+
needsCLI: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "openrouter",
|
|
88
|
+
name: "OpenRouter (100+ Models)",
|
|
89
|
+
desc: () => t("provider.openrouter.desc"),
|
|
90
|
+
free: false,
|
|
91
|
+
envKey: "OPENROUTER_API_KEY",
|
|
92
|
+
signup: "https://openrouter.ai",
|
|
93
|
+
model: "anthropic/claude-sonnet-4",
|
|
94
|
+
needsCLI: false,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: "claude-sdk",
|
|
98
|
+
name: "Claude Agent SDK (Premium)",
|
|
99
|
+
desc: () => t("provider.claude.desc"),
|
|
100
|
+
free: false,
|
|
101
|
+
envKey: null,
|
|
102
|
+
signup: "https://claude.ai",
|
|
103
|
+
model: "claude-sonnet-4-20250514",
|
|
104
|
+
needsCLI: true,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// ── Provider Validation ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate a provider's API key or auth by making a lightweight API call.
|
|
112
|
+
* Returns { ok: true, detail: "..." } or { ok: false, error: "..." }.
|
|
113
|
+
*/
|
|
114
|
+
async function validateProviderKey(providerKey, apiKey) {
|
|
115
|
+
const timeout = 10_000;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
switch (providerKey) {
|
|
119
|
+
case "groq": {
|
|
120
|
+
const res = await fetch("https://api.groq.com/openai/v1/models", {
|
|
121
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
122
|
+
signal: AbortSignal.timeout(timeout),
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
|
|
125
|
+
return { ok: true, detail: "Groq API key valid" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "nvidia-llama-3.3-70b":
|
|
129
|
+
case "nvidia-kimi-k2.5": {
|
|
130
|
+
const res = await fetch("https://integrate.api.nvidia.com/v1/models", {
|
|
131
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
132
|
+
signal: AbortSignal.timeout(timeout),
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
|
|
135
|
+
return { ok: true, detail: "NVIDIA API key valid" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "gemini-2.5-flash": {
|
|
139
|
+
const res = await fetch(
|
|
140
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`,
|
|
141
|
+
{ signal: AbortSignal.timeout(timeout) }
|
|
142
|
+
);
|
|
143
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
|
|
144
|
+
return { ok: true, detail: "Google API key valid" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "openai":
|
|
148
|
+
case "gpt-4o": {
|
|
149
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
150
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
151
|
+
signal: AbortSignal.timeout(timeout),
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
|
|
154
|
+
return { ok: true, detail: "OpenAI API key valid" };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "openrouter": {
|
|
158
|
+
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
159
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
160
|
+
signal: AbortSignal.timeout(timeout),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} — ${res.statusText}` };
|
|
163
|
+
return { ok: true, detail: "OpenRouter API key valid" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "claude-sdk": {
|
|
167
|
+
// Find claude binary — check PATH and common locations
|
|
168
|
+
let claudeBin = null;
|
|
169
|
+
try {
|
|
170
|
+
execSync("claude --version", { stdio: "pipe", timeout: 5000 });
|
|
171
|
+
claudeBin = "claude";
|
|
172
|
+
} catch {
|
|
173
|
+
// Not in PATH — try common native install locations
|
|
174
|
+
const candidates = [
|
|
175
|
+
join(homedir(), ".local", "bin", "claude"),
|
|
176
|
+
"/usr/local/bin/claude",
|
|
177
|
+
];
|
|
178
|
+
for (const p of candidates) {
|
|
179
|
+
if (existsSync(p)) { claudeBin = p; break; }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!claudeBin) {
|
|
183
|
+
return { ok: false, error: "Claude CLI not installed" };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
// Use `auth status` instead of `-p "ping"` — faster and doesn't require a full query
|
|
187
|
+
const authJson = execSync(`${claudeBin} auth status`, {
|
|
188
|
+
stdio: "pipe", timeout: 10000, encoding: "utf-8",
|
|
189
|
+
});
|
|
190
|
+
const authData = JSON.parse(authJson);
|
|
191
|
+
if (authData.loggedIn) {
|
|
192
|
+
return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
|
|
193
|
+
}
|
|
194
|
+
return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const msg = err.stdout?.toString() || err.stderr?.toString() || err.message || "";
|
|
197
|
+
// Try parsing JSON from stdout (auth status exits with code 1 when not logged in)
|
|
198
|
+
try {
|
|
199
|
+
const authData = JSON.parse(msg);
|
|
200
|
+
if (authData.loggedIn) {
|
|
201
|
+
return { ok: true, detail: `Claude SDK authenticated (${authData.authMethod || "OK"})` };
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
return { ok: false, error: "Claude CLI not authenticated. Run: claude auth login" };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
return { ok: true, detail: "No validation available for this provider" };
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
if (err.name === "TimeoutError" || err.code === "ABORT_ERR") {
|
|
213
|
+
return { ok: false, error: "Connection timed out — check your internet" };
|
|
214
|
+
}
|
|
215
|
+
return { ok: false, error: err.message || "Unknown error" };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Validate a Telegram bot token by calling getMe.
|
|
221
|
+
* Returns { ok: true, botName: "@username" } or { ok: false, error: "reason" }.
|
|
222
|
+
*/
|
|
223
|
+
async function validateTelegramToken(token) {
|
|
224
|
+
if (!token) return { ok: false, error: "No token provided" };
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, {
|
|
227
|
+
signal: AbortSignal.timeout(5000),
|
|
228
|
+
});
|
|
229
|
+
const data = await res.json();
|
|
230
|
+
if (data.ok) {
|
|
231
|
+
return { ok: true, botName: `@${data.result.username}` };
|
|
232
|
+
}
|
|
233
|
+
return { ok: false, error: data.description || "Invalid token" };
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return { ok: false, error: err.message || "Connection failed" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Run post-setup validation: provider, Telegram, port.
|
|
241
|
+
*/
|
|
242
|
+
async function runPostSetupValidation(providerKey, apiKey, botToken, webPort) {
|
|
243
|
+
console.log(`\n━━━ Validating Setup ━━━\n`);
|
|
244
|
+
let allGood = true;
|
|
245
|
+
|
|
246
|
+
// 1. Provider
|
|
247
|
+
if (providerKey === "claude-sdk" || apiKey) {
|
|
248
|
+
console.log(` Testing provider...`);
|
|
249
|
+
const pResult = await validateProviderKey(providerKey, apiKey);
|
|
250
|
+
if (pResult.ok) {
|
|
251
|
+
console.log(` ✅ Provider — ${pResult.detail}`);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(` ❌ Provider — ${pResult.error}`);
|
|
254
|
+
allGood = false;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
console.log(` ⚠️ Provider: No API key configured`);
|
|
258
|
+
allGood = false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 2. Telegram
|
|
262
|
+
if (botToken) {
|
|
263
|
+
console.log(` Testing Telegram...`);
|
|
264
|
+
const tResult = await validateTelegramToken(botToken);
|
|
265
|
+
if (tResult.ok) {
|
|
266
|
+
console.log(` ✅ Telegram: ${tResult.botName}`);
|
|
267
|
+
} else {
|
|
268
|
+
console.log(` ❌ Telegram: ${tResult.error}`);
|
|
269
|
+
allGood = false;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
console.log(` ℹ️ Telegram: Skipped (no token)`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. Web UI port
|
|
276
|
+
const port = webPort || 3100;
|
|
277
|
+
try {
|
|
278
|
+
const net = await import("net");
|
|
279
|
+
await new Promise((resolve, reject) => {
|
|
280
|
+
const srv = net.createServer();
|
|
281
|
+
srv.once("error", () => { srv.close(); reject(); });
|
|
282
|
+
srv.once("listening", () => { srv.close(); resolve(); });
|
|
283
|
+
srv.listen(port);
|
|
284
|
+
});
|
|
285
|
+
console.log(` ✅ Web UI: Port ${port} available`);
|
|
286
|
+
} catch {
|
|
287
|
+
console.log(` ⚠️ Web UI: Port ${port} in use (another instance running?)`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log("");
|
|
291
|
+
|
|
292
|
+
if (!allGood) {
|
|
293
|
+
console.log(` Some checks failed. Run 'alvin-bot doctor' after fixing to verify.\n`);
|
|
294
|
+
} else {
|
|
295
|
+
console.log(` All checks passed! ✅\n`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return allGood;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Setup Wizard ────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
async function setup() {
|
|
304
|
+
console.log(LOGO);
|
|
305
|
+
|
|
306
|
+
// ── Prerequisites
|
|
307
|
+
console.log(t("setup.checkingPrereqs"));
|
|
308
|
+
|
|
309
|
+
let hasNode = false;
|
|
310
|
+
try {
|
|
311
|
+
const nodeVersion = execSync("node --version", { encoding: "utf-8" }).trim();
|
|
312
|
+
const major = parseInt(nodeVersion.slice(1));
|
|
313
|
+
hasNode = major >= 18;
|
|
314
|
+
console.log(` ${hasNode ? "✅" : "❌"} Node.js ${nodeVersion}${major < 18 ? ` (${t("setup.needVersion")})` : ""}`);
|
|
315
|
+
} catch {
|
|
316
|
+
console.log(` ❌ ${t("setup.nodeNotFound")}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!hasNode) {
|
|
320
|
+
console.log(`\n❌ ${t("setup.nodeRequired")}`);
|
|
321
|
+
rl.close();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Step 1: Telegram Bot
|
|
326
|
+
console.log(`\n━━━ ${t("setup.step1")} ━━━`);
|
|
327
|
+
console.log(t("setup.step1.intro"));
|
|
328
|
+
console.log(` (Press Enter to skip — WebUI-only mode)\n`);
|
|
329
|
+
let botToken = (await ask(t("setup.botToken"))).trim();
|
|
330
|
+
|
|
331
|
+
if (!botToken) {
|
|
332
|
+
console.log(` ℹ️ Skipping Telegram — bot will run in WebUI-only mode.`);
|
|
333
|
+
console.log(` You can add BOT_TOKEN to ~/.alvin-bot/.env later.\n`);
|
|
334
|
+
} else if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
|
|
335
|
+
console.log(`\n ⚠️ That doesn't look like a valid bot token.`);
|
|
336
|
+
console.log(` Expected format: 123456789:ABCdefGHI-jklMNO`);
|
|
337
|
+
console.log(` Get one from @BotFather on Telegram.\n`);
|
|
338
|
+
const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
|
|
339
|
+
if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
|
|
340
|
+
rl.close();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Validate token with Telegram API
|
|
346
|
+
if (botToken && /^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
|
|
347
|
+
console.log(` Validating...`);
|
|
348
|
+
const tgResult = await validateTelegramToken(botToken);
|
|
349
|
+
if (tgResult.ok) {
|
|
350
|
+
console.log(` ✅ Bot: ${tgResult.botName}\n`);
|
|
351
|
+
} else {
|
|
352
|
+
console.log(` ❌ ${tgResult.error}`);
|
|
353
|
+
console.log(` Check your token at @BotFather on Telegram.\n`);
|
|
354
|
+
const retryToken = (await ask(` Re-enter token (or Enter to skip): `)).trim();
|
|
355
|
+
if (retryToken) {
|
|
356
|
+
botToken = retryToken;
|
|
357
|
+
const retry = await validateTelegramToken(botToken);
|
|
358
|
+
if (retry.ok) console.log(` ✅ Bot: ${retry.botName}\n`);
|
|
359
|
+
else console.log(` ⚠️ Still invalid — continuing anyway.\n`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Step 2: User ID
|
|
365
|
+
let userId = "";
|
|
366
|
+
if (botToken) {
|
|
367
|
+
console.log(`\n━━━ ${t("setup.step2")} ━━━`);
|
|
368
|
+
console.log(t("setup.step2.intro"));
|
|
369
|
+
console.log(` 💡 Send /start to @userinfobot on Telegram to find your ID.`);
|
|
370
|
+
console.log(` (Press Enter to skip — you can add it later)\n`);
|
|
371
|
+
userId = (await ask(t("setup.userId"))).trim();
|
|
372
|
+
|
|
373
|
+
if (!userId) {
|
|
374
|
+
console.log(` ℹ️ Skipping — add ALLOWED_USERS to ~/.alvin-bot/.env later.\n`);
|
|
375
|
+
} else {
|
|
376
|
+
// Validate user ID is numeric
|
|
377
|
+
const userIds = userId.split(",").map(s => s.trim());
|
|
378
|
+
const invalidIds = userIds.filter(id => !/^\d+$/.test(id));
|
|
379
|
+
if (invalidIds.length > 0) {
|
|
380
|
+
console.log(`\n ⚠️ User IDs must be numbers, got: ${invalidIds.join(", ")}`);
|
|
381
|
+
console.log(` Send /start to @userinfobot on Telegram to get your numeric ID.\n`);
|
|
382
|
+
const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
|
|
383
|
+
if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
|
|
384
|
+
rl.close();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Warn if user ID matches bot token prefix (common mistake)
|
|
390
|
+
const botIdPrefix = botToken.split(":")[0];
|
|
391
|
+
const userIdList = userId.split(",").map(s => s.trim());
|
|
392
|
+
if (userIdList.includes(botIdPrefix)) {
|
|
393
|
+
console.log(`\n ⚠️ "${botIdPrefix}" looks like the bot's own ID, not yours!`);
|
|
394
|
+
console.log(` The bot token starts with the bot's ID. You need YOUR user ID.`);
|
|
395
|
+
console.log(` Send /start to @userinfobot on Telegram to get your ID.\n`);
|
|
396
|
+
const proceed = (await ask(` Continue anyway? (y/n): `)).trim().toLowerCase();
|
|
397
|
+
if (proceed !== "y" && proceed !== "yes" && proceed !== "j" && proceed !== "ja") {
|
|
398
|
+
userId = "";
|
|
399
|
+
console.log(` ℹ️ Cleared — add ALLOWED_USERS to ~/.alvin-bot/.env later.\n`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Step 3: AI Provider
|
|
406
|
+
console.log(`\n━━━ ${t("setup.step3")} ━━━`);
|
|
407
|
+
console.log(t("setup.step3.intro") + "\n");
|
|
408
|
+
|
|
409
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
410
|
+
const p = PROVIDERS[i];
|
|
411
|
+
const badge = p.free ? "🆓" : "💰";
|
|
412
|
+
const premium = p.needsCLI ? " ⭐" : "";
|
|
413
|
+
console.log(` ${i + 1}. ${badge} ${p.name}${premium}`);
|
|
414
|
+
console.log(` ${p.desc()}`);
|
|
415
|
+
if (p.signup) console.log(` → ${p.signup}`);
|
|
416
|
+
console.log("");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const providerChoice = parseInt((await ask(t("setup.yourChoice"))).trim()) || 1;
|
|
420
|
+
let provider = PROVIDERS[Math.max(0, Math.min(providerChoice - 1, PROVIDERS.length - 1))];
|
|
421
|
+
|
|
422
|
+
console.log(`\n✅ ${t("setup.providerSelected")} ${provider.name}`);
|
|
423
|
+
|
|
424
|
+
// ── Validate Provider ────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
// Claude SDK: show requirements upfront
|
|
427
|
+
if (provider.needsCLI) {
|
|
428
|
+
console.log(`\n ⚠️ Claude SDK requires one of:`);
|
|
429
|
+
console.log(` • Claude Max/Team subscription ($20+/mo)`);
|
|
430
|
+
console.log(` • Anthropic API key with Agent SDK access\n`);
|
|
431
|
+
|
|
432
|
+
const yesChars = getLocale() === "de" ? ["j", "ja", "y", "yes", ""] : ["y", "yes", ""];
|
|
433
|
+
const proceed = (await ask(` Continue with Claude SDK? (Y/n): `)).trim().toLowerCase();
|
|
434
|
+
if (!yesChars.includes(proceed)) {
|
|
435
|
+
console.log(`\n Switching to provider selection...\n`);
|
|
436
|
+
provider = PROVIDERS[0];
|
|
437
|
+
console.log(` ✅ Switched to ${provider.name} (free)\n`);
|
|
438
|
+
} else {
|
|
439
|
+
// Check CLI installed
|
|
440
|
+
let cliInstalled = false;
|
|
441
|
+
try {
|
|
442
|
+
execSync("claude --version", { encoding: "utf-8", stdio: "pipe" });
|
|
443
|
+
cliInstalled = true;
|
|
444
|
+
console.log(` ✅ Claude CLI found`);
|
|
445
|
+
} catch {
|
|
446
|
+
console.log(` ⚠️ Claude CLI not found (native binary required).`);
|
|
447
|
+
console.log(`\n The Claude Agent SDK needs the native Claude Code binary.`);
|
|
448
|
+
console.log(` Install it with:\n`);
|
|
449
|
+
console.log(` curl -fsSL https://claude.ai/install.sh | sh\n`);
|
|
450
|
+
console.log(` (npm install @anthropic-ai/claude-code does NOT work for this)\n`);
|
|
451
|
+
const yc = getLocale() === "de" ? ["j", "ja"] : ["y", "yes"];
|
|
452
|
+
const doInstall = (await ask(` Already installed or want to try now? (y/n): `)).trim().toLowerCase();
|
|
453
|
+
if (yc.includes(doInstall)) {
|
|
454
|
+
console.log(`\n Installing Claude CLI (native)...`);
|
|
455
|
+
try {
|
|
456
|
+
execSync("curl -fsSL https://claude.ai/install.sh | sh", { stdio: "inherit", timeout: 120_000 });
|
|
457
|
+
// Add ~/.local/bin to PATH for this process (installer puts claude there)
|
|
458
|
+
const localBin = join(homedir(), ".local", "bin");
|
|
459
|
+
if (!process.env.PATH.includes(localBin)) {
|
|
460
|
+
process.env.PATH = `${localBin}:${process.env.PATH}`;
|
|
461
|
+
}
|
|
462
|
+
cliInstalled = true;
|
|
463
|
+
console.log(` ✅ Claude CLI installed\n`);
|
|
464
|
+
} catch {
|
|
465
|
+
console.log(` ❌ Installation failed. Try manually: curl -fsSL https://claude.ai/install.sh | sh`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (cliInstalled) {
|
|
471
|
+
console.log(`\n Checking Claude SDK authentication...`);
|
|
472
|
+
const authResult = await validateProviderKey("claude-sdk", null);
|
|
473
|
+
if (!authResult.ok) {
|
|
474
|
+
console.log(` ⚠️ ${authResult.error}`);
|
|
475
|
+
console.log(`\n Logging in to Claude...`);
|
|
476
|
+
console.log(` This will open your browser for authentication.\n`);
|
|
477
|
+
try {
|
|
478
|
+
// Find claude binary for auth login
|
|
479
|
+
let authBin = "claude";
|
|
480
|
+
const authLocalBin = join(homedir(), ".local", "bin", "claude");
|
|
481
|
+
if (existsSync(authLocalBin)) authBin = `"${authLocalBin}"`;
|
|
482
|
+
execSync(`${authBin} auth login --claudeai`, {
|
|
483
|
+
stdio: "inherit",
|
|
484
|
+
timeout: 120000,
|
|
485
|
+
});
|
|
486
|
+
} catch {
|
|
487
|
+
console.log(`\n ⚠️ Auto-login failed. Please run manually in another terminal:`);
|
|
488
|
+
console.log(` claude auth login\n`);
|
|
489
|
+
await ask(` Press Enter when you've logged in...`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const recheck = await validateProviderKey("claude-sdk", null);
|
|
493
|
+
if (recheck.ok) {
|
|
494
|
+
console.log(`\n ✅ Claude SDK authenticated!\n`);
|
|
495
|
+
} else {
|
|
496
|
+
console.log(`\n ❌ Claude SDK still not working: ${recheck.error}`);
|
|
497
|
+
console.log(` Switching to a free provider.\n`);
|
|
498
|
+
provider = PROVIDERS[0];
|
|
499
|
+
console.log(` ✅ Switched to ${provider.name} (free)\n`);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
console.log(` ✅ ${authResult.detail}\n`);
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
console.log(`\n Claude CLI not available. Switching to a free provider.\n`);
|
|
506
|
+
provider = PROVIDERS[0];
|
|
507
|
+
console.log(` ✅ Switched to ${provider.name} (free)\n`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Get and validate API key
|
|
513
|
+
let providerApiKey = "";
|
|
514
|
+
if (provider.envKey) {
|
|
515
|
+
console.log(`\n API key for ${provider.name}:`);
|
|
516
|
+
console.log(` Get one at: ${provider.signup}\n`);
|
|
517
|
+
providerApiKey = (await ask(` ${provider.envKey}: `)).trim();
|
|
518
|
+
|
|
519
|
+
if (providerApiKey) {
|
|
520
|
+
console.log(`\n Validating...`);
|
|
521
|
+
let keyResult = await validateProviderKey(provider.key, providerApiKey);
|
|
522
|
+
if (keyResult.ok) {
|
|
523
|
+
console.log(` ✅ ${keyResult.detail}\n`);
|
|
524
|
+
} else {
|
|
525
|
+
console.log(` ❌ ${keyResult.error}\n`);
|
|
526
|
+
let resolved = false;
|
|
527
|
+
for (let attempt = 0; attempt < 2 && !resolved; attempt++) {
|
|
528
|
+
const choice = (await ask(` 1. Enter new key 2. Switch provider 3. Skip\n Choice: `)).trim();
|
|
529
|
+
if (choice === "1") {
|
|
530
|
+
providerApiKey = (await ask(`\n ${provider.envKey}: `)).trim();
|
|
531
|
+
if (providerApiKey) {
|
|
532
|
+
console.log(` Validating...`);
|
|
533
|
+
keyResult = await validateProviderKey(provider.key, providerApiKey);
|
|
534
|
+
if (keyResult.ok) {
|
|
535
|
+
console.log(` ✅ ${keyResult.detail}\n`);
|
|
536
|
+
resolved = true;
|
|
537
|
+
} else {
|
|
538
|
+
console.log(` ❌ ${keyResult.error}\n`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else if (choice === "2") {
|
|
542
|
+
provider = PROVIDERS[0];
|
|
543
|
+
console.log(`\n Switched to ${provider.name} (free)`);
|
|
544
|
+
console.log(` Get a free key at: ${provider.signup}\n`);
|
|
545
|
+
providerApiKey = (await ask(` ${provider.envKey}: `)).trim();
|
|
546
|
+
if (providerApiKey) {
|
|
547
|
+
const gr = await validateProviderKey(provider.key, providerApiKey);
|
|
548
|
+
if (gr.ok) console.log(` ✅ ${gr.detail}\n`);
|
|
549
|
+
else console.log(` ⚠️ ${gr.error} — continuing anyway\n`);
|
|
550
|
+
}
|
|
551
|
+
resolved = true;
|
|
552
|
+
} else {
|
|
553
|
+
console.log(` ⚠️ Skipping — bot won't work until a valid key is configured.\n`);
|
|
554
|
+
providerApiKey = "";
|
|
555
|
+
resolved = true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
console.log(`\n ⚠️ No API key provided. Bot won't work until configured.`);
|
|
561
|
+
console.log(` Get one at: ${provider.signup}\n`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Step 4: Fallback & Extras
|
|
566
|
+
console.log(`\n━━━ ${t("setup.step4")} ━━━\n`);
|
|
567
|
+
|
|
568
|
+
let groqKey = "";
|
|
569
|
+
if (provider.key !== "groq") {
|
|
570
|
+
console.log(` ${t("setup.groqFallback")}\n`);
|
|
571
|
+
groqKey = (await ask(t("setup.groqKeyPrompt"))).trim();
|
|
572
|
+
if (!groqKey) {
|
|
573
|
+
console.log(` ℹ️ ${t("setup.noGroqKey")}\n`);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
groqKey = providerApiKey;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log(` ${t("setup.extraKeys")}\n`);
|
|
580
|
+
const extraKeys = {};
|
|
581
|
+
if (provider.key !== "nvidia-llama-3.3-70b" && provider.key !== "nvidia-kimi-k2.5") {
|
|
582
|
+
const nk = (await ask(` ${t("setup.nvidiaKeyPrompt")}`)).trim();
|
|
583
|
+
if (nk) extraKeys["NVIDIA_API_KEY"] = nk;
|
|
584
|
+
}
|
|
585
|
+
if (provider.key !== "gemini-2.5-flash") {
|
|
586
|
+
const gk = (await ask(` ${t("setup.googleKeyPrompt")}`)).trim();
|
|
587
|
+
if (gk) extraKeys["GOOGLE_API_KEY"] = gk;
|
|
588
|
+
}
|
|
589
|
+
if (provider.key !== "openai" && provider.key !== "gpt-4o") {
|
|
590
|
+
const ok = (await ask(` ${t("setup.openaiKeyPrompt")}`)).trim();
|
|
591
|
+
if (ok) extraKeys["OPENAI_API_KEY"] = ok;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Fallback order
|
|
595
|
+
console.log(`\n ${t("setup.fallbackOrder")}`);
|
|
596
|
+
const availableFallbacks = [];
|
|
597
|
+
if (groqKey && provider.key !== "groq") availableFallbacks.push("groq");
|
|
598
|
+
if (extraKeys["NVIDIA_API_KEY"]) availableFallbacks.push("nvidia-llama-3.3-70b");
|
|
599
|
+
// If NVIDIA is primary, add llama as fallback automatically
|
|
600
|
+
if (provider.key === "nvidia-kimi-k2.5" && !availableFallbacks.includes("nvidia-llama-3.3-70b")) {
|
|
601
|
+
availableFallbacks.push("nvidia-llama-3.3-70b");
|
|
602
|
+
}
|
|
603
|
+
if (extraKeys["GOOGLE_API_KEY"]) availableFallbacks.push("gemini-2.5-flash");
|
|
604
|
+
if (extraKeys["OPENAI_API_KEY"]) availableFallbacks.push("gpt-4o");
|
|
605
|
+
|
|
606
|
+
if (availableFallbacks.length > 0) {
|
|
607
|
+
console.log(` ${t("setup.defaultOrder")} ${availableFallbacks.join(" → ")}`);
|
|
608
|
+
const customOrder = (await ask(` ${t("setup.customOrder")}`)).trim();
|
|
609
|
+
if (customOrder) {
|
|
610
|
+
availableFallbacks.length = 0;
|
|
611
|
+
availableFallbacks.push(...customOrder.split(",").map(s => s.trim()).filter(Boolean));
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
console.log(` ${t("setup.noFallbacks")}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
console.log("");
|
|
618
|
+
const webPassword = (await ask(t("setup.webPassword"))).trim();
|
|
619
|
+
|
|
620
|
+
// ── Step 5: Platforms
|
|
621
|
+
console.log(`\n━━━ ${t("setup.step5")} ━━━`);
|
|
622
|
+
console.log(`${t("setup.step5.intro")}\n`);
|
|
623
|
+
console.log(` 1. ${t("setup.platform.telegramOnly")}`);
|
|
624
|
+
console.log(` 2. ${t("setup.platform.whatsapp")}`);
|
|
625
|
+
console.log(` 3. ${t("setup.platform.later")}\n`);
|
|
626
|
+
|
|
627
|
+
const platformChoice = parseInt((await ask(t("setup.platformChoice"))).trim()) || 1;
|
|
628
|
+
const enableWhatsApp = platformChoice === 2;
|
|
629
|
+
|
|
630
|
+
// ── Write .env
|
|
631
|
+
console.log(`\n${t("setup.writingConfig")}`);
|
|
632
|
+
|
|
633
|
+
const envLines = [
|
|
634
|
+
"# === Telegram ===",
|
|
635
|
+
`BOT_TOKEN=${botToken || ""}`,
|
|
636
|
+
`ALLOWED_USERS=${userId || ""}`,
|
|
637
|
+
"",
|
|
638
|
+
"# === AI Provider ===",
|
|
639
|
+
`PRIMARY_PROVIDER=${provider.key}`,
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
if (provider.envKey && providerApiKey) {
|
|
643
|
+
envLines.push(`${provider.envKey}=${providerApiKey}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (groqKey && provider.key !== "groq") {
|
|
647
|
+
envLines.push(`GROQ_API_KEY=${groqKey}`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
for (const [envKey, value] of Object.entries(extraKeys)) {
|
|
651
|
+
envLines.push(`${envKey}=${value}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (availableFallbacks.length > 0) {
|
|
655
|
+
envLines.push(`FALLBACK_PROVIDERS=${availableFallbacks.join(",")}`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
envLines.push("");
|
|
659
|
+
envLines.push("# === Agent ===");
|
|
660
|
+
envLines.push(`WORKING_DIR=${os.homedir()}`);
|
|
661
|
+
envLines.push("MAX_BUDGET_USD=5.0");
|
|
662
|
+
|
|
663
|
+
if (webPassword) {
|
|
664
|
+
envLines.push(`WEB_PASSWORD=${webPassword}`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
envLines.push("WEB_PORT=3100");
|
|
668
|
+
|
|
669
|
+
if (enableWhatsApp) {
|
|
670
|
+
envLines.push("");
|
|
671
|
+
envLines.push("# === WhatsApp ===");
|
|
672
|
+
envLines.push("WHATSAPP_ENABLED=true");
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const envContent = envLines.join("\n") + "\n";
|
|
676
|
+
|
|
677
|
+
// Ensure DATA_DIR exists
|
|
678
|
+
if (!existsSync(DATA_DIR)) {
|
|
679
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Write .env to ~/.alvin-bot/.env (works for both global npm install and local dev)
|
|
683
|
+
const envPath = resolve(DATA_DIR, ".env");
|
|
684
|
+
|
|
685
|
+
if (existsSync(envPath)) {
|
|
686
|
+
const backup = `${envPath}.backup-${Date.now()}`;
|
|
687
|
+
writeFileSync(backup, readFileSync(envPath));
|
|
688
|
+
console.log(` ${t("setup.backup")} ${backup}`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
writeFileSync(envPath, envContent);
|
|
692
|
+
console.log(` ✅ Config saved to ${envPath}`);
|
|
693
|
+
|
|
694
|
+
// Also write to cwd if we're in a dev/git environment (convenience)
|
|
695
|
+
const cwdEnvPath = resolve(process.cwd(), ".env");
|
|
696
|
+
const isDevMode = existsSync(resolve(process.cwd(), ".git"));
|
|
697
|
+
if (isDevMode && cwdEnvPath !== envPath) {
|
|
698
|
+
writeFileSync(cwdEnvPath, envContent);
|
|
699
|
+
console.log(` ✅ Dev copy saved to ${cwdEnvPath}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Create ~/.alvin-bot/ data directory
|
|
703
|
+
const memoryDir = resolve(DATA_DIR, "memory");
|
|
704
|
+
if (!existsSync(memoryDir)) {
|
|
705
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Create soul.md if not exists
|
|
709
|
+
const soulPath = resolve(DATA_DIR, "soul.md");
|
|
710
|
+
if (!existsSync(soulPath)) {
|
|
711
|
+
const soulExample = resolve(process.cwd(), "SOUL.example.md");
|
|
712
|
+
if (existsSync(soulExample)) {
|
|
713
|
+
copyFileSync(soulExample, soulPath);
|
|
714
|
+
console.log(" ✅ soul.md initialized from example");
|
|
715
|
+
} else {
|
|
716
|
+
writeFileSync(soulPath, t("soul.default"));
|
|
717
|
+
console.log(` ✅ ${t("setup.soulCreated")}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Initialize memory/MEMORY.md if not exists
|
|
722
|
+
const memoryMdPath = resolve(DATA_DIR, "memory", "MEMORY.md");
|
|
723
|
+
if (!existsSync(memoryMdPath)) {
|
|
724
|
+
writeFileSync(memoryMdPath, "# Long-term Memory\n\n> This file is your agent's long-term memory. Add important context here.\n> It persists across sessions and is read at every startup.\n");
|
|
725
|
+
console.log(" ✅ memory/MEMORY.md created");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Initialize custom-models.json if not exists
|
|
729
|
+
const customModelsPath = resolve(DATA_DIR, "custom-models.json");
|
|
730
|
+
if (!existsSync(customModelsPath)) {
|
|
731
|
+
writeFileSync(customModelsPath, "[]");
|
|
732
|
+
console.log(" ✅ custom-models.json initialized");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Copy TOOLS.example.md → tools.md if not exists
|
|
736
|
+
const toolsMdPath = resolve(DATA_DIR, "tools.md");
|
|
737
|
+
const toolsMdExample = resolve(process.cwd(), "TOOLS.example.md");
|
|
738
|
+
if (!existsSync(toolsMdPath) && existsSync(toolsMdExample)) {
|
|
739
|
+
copyFileSync(toolsMdExample, toolsMdPath);
|
|
740
|
+
console.log(" ✅ Custom tools initialized from example (tools.md)");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Copy CLAUDE.example.md → CLAUDE.md in BOT_ROOT if not exists
|
|
744
|
+
const claudePath = resolve(process.cwd(), "CLAUDE.md");
|
|
745
|
+
const claudeExample = resolve(process.cwd(), "CLAUDE.example.md");
|
|
746
|
+
if (!existsSync(claudePath) && existsSync(claudeExample)) {
|
|
747
|
+
copyFileSync(claudeExample, claudePath);
|
|
748
|
+
console.log(" ✅ CLAUDE.md initialized from example");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ── Build (only for local/dev installs — global npm installs already have dist/)
|
|
752
|
+
const isGlobalInstall = !existsSync(resolve(process.cwd(), "tsconfig.json"));
|
|
753
|
+
if (!isGlobalInstall) {
|
|
754
|
+
console.log(`\n${t("setup.building")}`);
|
|
755
|
+
try {
|
|
756
|
+
execSync("npm run build", { stdio: "inherit" });
|
|
757
|
+
console.log(` ✅ ${t("setup.buildOk")}`);
|
|
758
|
+
} catch {
|
|
759
|
+
console.log(`\n ❌ ${t("setup.buildFailed")}`);
|
|
760
|
+
console.log(` The bot cannot start without a successful build.`);
|
|
761
|
+
console.log(` Try running 'npm run build' manually to see the error.\n`);
|
|
762
|
+
rl.close();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ── Post-Setup Validation ──────────────────────────────────────────────
|
|
768
|
+
await runPostSetupValidation(provider.key, providerApiKey, botToken, 3100);
|
|
769
|
+
|
|
770
|
+
// ── Summary
|
|
771
|
+
const providerInfo = "";
|
|
772
|
+
|
|
773
|
+
const startCmds = isGlobalInstall
|
|
774
|
+
? ` alvin-bot start (start the bot)
|
|
775
|
+
alvin-bot doctor (check configuration)
|
|
776
|
+
|
|
777
|
+
# Keep running permanently:
|
|
778
|
+
npm install -g pm2
|
|
779
|
+
pm2 start "alvin-bot start" --name alvin-bot
|
|
780
|
+
pm2 save && pm2 startup`
|
|
781
|
+
: ` npm run dev (development, hot reload)
|
|
782
|
+
npm start (production)
|
|
783
|
+
pm2 start ecosystem.config.cjs (production, auto-restart)`;
|
|
784
|
+
|
|
785
|
+
console.log(`
|
|
786
|
+
━━━ ${t("setup.done")} ━━━
|
|
787
|
+
|
|
788
|
+
🤖 Provider: ${provider.name}
|
|
789
|
+
💬 Telegram: @... (check @BotFather)
|
|
790
|
+
🌐 Web UI: http://localhost:3100${webPassword ? ` (${t("setup.passwordProtected")})` : ""}
|
|
791
|
+
📁 Config: ${envPath}
|
|
792
|
+
${enableWhatsApp ? ` 📱 ${t("setup.scanQr")}\n` : ""}${providerInfo}
|
|
793
|
+
Start:
|
|
794
|
+
${startCmds}
|
|
795
|
+
|
|
796
|
+
Bot commands:
|
|
797
|
+
/help — Show all commands
|
|
798
|
+
/model — Switch AI model
|
|
799
|
+
/effort — Set thinking depth
|
|
800
|
+
/imagine — Generate images
|
|
801
|
+
/web — Web search
|
|
802
|
+
/cron — Scheduled tasks
|
|
803
|
+
|
|
804
|
+
${t("setup.haveFun")}
|
|
805
|
+
`);
|
|
806
|
+
|
|
807
|
+
rl.close();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ── Doctor ──────────────────────────────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
async function doctor() {
|
|
813
|
+
console.log(`\n━━━ Alvin Bot Health Check ━━━\n`);
|
|
814
|
+
|
|
815
|
+
// ── System ──
|
|
816
|
+
console.log(" System:");
|
|
817
|
+
try {
|
|
818
|
+
const v = execSync("node --version", { encoding: "utf-8" }).trim();
|
|
819
|
+
const major = parseInt(v.slice(1));
|
|
820
|
+
console.log(` ${major >= 18 ? "✅" : "❌"} Node.js ${v}${major < 18 ? " (need ≥ 18)" : ""}`);
|
|
821
|
+
} catch {
|
|
822
|
+
console.log(" ❌ Node.js not found");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Config file
|
|
826
|
+
const dataEnvPath = resolve(DATA_DIR, ".env");
|
|
827
|
+
const cwdEnvPath = resolve(process.cwd(), ".env");
|
|
828
|
+
const envPath = existsSync(dataEnvPath) ? dataEnvPath : existsSync(cwdEnvPath) ? cwdEnvPath : null;
|
|
829
|
+
|
|
830
|
+
if (envPath) {
|
|
831
|
+
console.log(` ✅ Config: ${envPath}`);
|
|
832
|
+
} else {
|
|
833
|
+
console.log(` ❌ No .env found`);
|
|
834
|
+
console.log(` Run: alvin-bot setup\n`);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const env = readFileSync(envPath, "utf-8");
|
|
839
|
+
const getEnv = (key) => {
|
|
840
|
+
const m = env.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
841
|
+
return m?.[1]?.trim() || "";
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// Build
|
|
845
|
+
const distPaths = [
|
|
846
|
+
resolve(process.cwd(), "dist/index.js"),
|
|
847
|
+
resolve(import.meta.dirname || ".", "../dist/index.js"),
|
|
848
|
+
];
|
|
849
|
+
console.log(` ${distPaths.some(p => existsSync(p)) ? "✅" : "❌"} Build present`);
|
|
850
|
+
|
|
851
|
+
// ── Provider ──
|
|
852
|
+
console.log("\n Provider:");
|
|
853
|
+
const primary = getEnv("PRIMARY_PROVIDER");
|
|
854
|
+
if (primary) {
|
|
855
|
+
const apiKeyMap = {
|
|
856
|
+
groq: "GROQ_API_KEY",
|
|
857
|
+
"nvidia-llama-3.3-70b": "NVIDIA_API_KEY",
|
|
858
|
+
"nvidia-kimi-k2.5": "NVIDIA_API_KEY",
|
|
859
|
+
"gemini-2.5-flash": "GOOGLE_API_KEY",
|
|
860
|
+
openai: "OPENAI_API_KEY",
|
|
861
|
+
"gpt-4o": "OPENAI_API_KEY",
|
|
862
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
863
|
+
};
|
|
864
|
+
const keyName = apiKeyMap[primary];
|
|
865
|
+
const key = keyName ? getEnv(keyName) : null;
|
|
866
|
+
|
|
867
|
+
console.log(` Validating ${primary}...`);
|
|
868
|
+
const result = await validateProviderKey(primary, key);
|
|
869
|
+
if (result.ok) {
|
|
870
|
+
console.log(` ✅ ${primary} — ${result.detail}`);
|
|
871
|
+
} else {
|
|
872
|
+
console.log(` ❌ ${primary} — ${result.error}`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const fallbacks = getEnv("FALLBACK_PROVIDERS");
|
|
876
|
+
if (fallbacks) {
|
|
877
|
+
console.log(` ℹ️ Fallbacks: ${fallbacks}`);
|
|
878
|
+
} else {
|
|
879
|
+
console.log(` ⚠️ No fallback providers configured`);
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
console.log(` ❌ PRIMARY_PROVIDER not set`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ── Telegram ──
|
|
886
|
+
console.log("\n Telegram:");
|
|
887
|
+
const botToken = getEnv("BOT_TOKEN");
|
|
888
|
+
if (botToken) {
|
|
889
|
+
const tResult = await validateTelegramToken(botToken);
|
|
890
|
+
if (tResult.ok) {
|
|
891
|
+
console.log(` ✅ Bot: ${tResult.botName}`);
|
|
892
|
+
} else {
|
|
893
|
+
console.log(` ❌ ${tResult.error}`);
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
console.log(` ⚠️ BOT_TOKEN not configured (WebUI-only mode)`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const users = getEnv("ALLOWED_USERS");
|
|
900
|
+
if (users) {
|
|
901
|
+
const ids = users.split(",").map(s => s.trim());
|
|
902
|
+
const invalid = ids.filter(id => !/^\d+$/.test(id));
|
|
903
|
+
if (invalid.length > 0) {
|
|
904
|
+
console.log(` ⚠️ ALLOWED_USERS has non-numeric: ${invalid.join(", ")}`);
|
|
905
|
+
} else {
|
|
906
|
+
console.log(` ✅ ALLOWED_USERS: ${ids.length} user${ids.length > 1 ? "s" : ""}`);
|
|
907
|
+
}
|
|
908
|
+
} else if (botToken) {
|
|
909
|
+
console.log(` ❌ ALLOWED_USERS not set (nobody can message the bot)`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// ── Extras ──
|
|
913
|
+
console.log("\n Extras:");
|
|
914
|
+
|
|
915
|
+
if (existsSync(resolve(DATA_DIR, "soul.md")) || existsSync(resolve(process.cwd(), "SOUL.md"))) {
|
|
916
|
+
console.log(` ✅ Personality (soul.md)`);
|
|
917
|
+
} else {
|
|
918
|
+
console.log(` ⚠️ No soul.md (bot uses default personality)`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const pluginsDir = resolve(process.cwd(), "plugins");
|
|
922
|
+
if (existsSync(pluginsDir)) {
|
|
923
|
+
try {
|
|
924
|
+
const plugins = readdirSync(pluginsDir).filter(d => {
|
|
925
|
+
try { return existsSync(resolve(pluginsDir, d, "index.js")); } catch { return false; }
|
|
926
|
+
});
|
|
927
|
+
if (plugins.length > 0) console.log(` ✅ Plugins: ${plugins.join(", ")}`);
|
|
928
|
+
} catch { /* ignore */ }
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (env.includes("WHATSAPP_ENABLED=true")) {
|
|
932
|
+
const chromePaths = [
|
|
933
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
934
|
+
"/usr/bin/google-chrome", "/usr/bin/chromium",
|
|
935
|
+
];
|
|
936
|
+
const hasChrome = chromePaths.some(p => existsSync(p));
|
|
937
|
+
console.log(` ${hasChrome ? "✅" : "⚠️ "} WhatsApp (Chrome: ${hasChrome ? "found" : "not found"})`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
console.log("");
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Update ──────────────────────────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
async function update() {
|
|
946
|
+
console.log(`${t("update.title")}\n`);
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
const isGit = existsSync(resolve(process.cwd(), ".git"));
|
|
950
|
+
|
|
951
|
+
if (isGit) {
|
|
952
|
+
console.log(` ${t("update.pulling")}`);
|
|
953
|
+
execSync("git pull", { stdio: "inherit" });
|
|
954
|
+
console.log(`\n ${t("update.installing")}`);
|
|
955
|
+
execSync("npm install", { stdio: "inherit" });
|
|
956
|
+
console.log(`\n ${t("update.building")}`);
|
|
957
|
+
execSync("npm run build", { stdio: "inherit" });
|
|
958
|
+
console.log(`\n ✅ ${t("update.done")}`);
|
|
959
|
+
} else {
|
|
960
|
+
console.log(` ${t("update.npm")}`);
|
|
961
|
+
execSync("npm update alvin-bot", { stdio: "inherit" });
|
|
962
|
+
console.log(`\n ✅ ${t("update.done")}`);
|
|
963
|
+
}
|
|
964
|
+
} catch (err) {
|
|
965
|
+
console.error(`\n ❌ ${t("update.failed")} ${err.message}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ── Version ─────────────────────────────────────────────────────────────────
|
|
970
|
+
|
|
971
|
+
async function version() {
|
|
972
|
+
try {
|
|
973
|
+
const pkg = JSON.parse(readFileSync(resolve(import.meta.dirname || ".", "../package.json"), "utf-8"));
|
|
974
|
+
console.log(`Alvin Bot v${pkg.version}`);
|
|
975
|
+
} catch {
|
|
976
|
+
console.log("Alvin Bot (version unknown)");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ── CLI Router ──────────────────────────────────────────────────────────────
|
|
981
|
+
|
|
982
|
+
const cmd = process.argv[2];
|
|
983
|
+
switch (cmd) {
|
|
984
|
+
case "setup":
|
|
985
|
+
setup().catch(console.error);
|
|
986
|
+
break;
|
|
987
|
+
case "doctor":
|
|
988
|
+
doctor().catch(console.error);
|
|
989
|
+
break;
|
|
990
|
+
case "update":
|
|
991
|
+
update().catch(console.error);
|
|
992
|
+
break;
|
|
993
|
+
case "start": {
|
|
994
|
+
const fg = process.argv.includes("--foreground") || process.argv.includes("-f");
|
|
995
|
+
if (fg) {
|
|
996
|
+
import("../dist/index.js");
|
|
997
|
+
} else {
|
|
998
|
+
// Start via PM2 (background, survives terminal close, auto-restart on crash)
|
|
999
|
+
try {
|
|
1000
|
+
execSync("pm2 --version", { stdio: "pipe" });
|
|
1001
|
+
} catch {
|
|
1002
|
+
// PM2 not installed — install it
|
|
1003
|
+
console.log("Installing PM2 for background operation...");
|
|
1004
|
+
try {
|
|
1005
|
+
execSync("npm install -g pm2", { stdio: "inherit", timeout: 60000 });
|
|
1006
|
+
} catch {
|
|
1007
|
+
console.log("Could not install PM2. Starting in foreground instead.");
|
|
1008
|
+
console.log("Tip: Install PM2 manually (npm install -g pm2) to run in background.\n");
|
|
1009
|
+
await import("../dist/index.js");
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const cliPath = resolve(join(import.meta.dirname, "cli.js"));
|
|
1014
|
+
try {
|
|
1015
|
+
// Stop existing instance if running
|
|
1016
|
+
execSync("pm2 delete alvin-bot", { stdio: "pipe" });
|
|
1017
|
+
} catch { /* not running — fine */ }
|
|
1018
|
+
execSync(`pm2 start "${cliPath}" --name alvin-bot -- start --foreground`, {
|
|
1019
|
+
stdio: "inherit",
|
|
1020
|
+
timeout: 15000,
|
|
1021
|
+
});
|
|
1022
|
+
console.log("\n✅ Bot is running in the background.");
|
|
1023
|
+
console.log(" Logs: pm2 logs alvin-bot");
|
|
1024
|
+
console.log(" Stop: alvin-bot stop");
|
|
1025
|
+
console.log(" Restart: alvin-bot start\n");
|
|
1026
|
+
process.exit(0);
|
|
1027
|
+
}
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
case "stop": {
|
|
1031
|
+
try {
|
|
1032
|
+
execSync("pm2 stop alvin-bot", { stdio: "inherit", timeout: 10000 });
|
|
1033
|
+
} catch {
|
|
1034
|
+
console.log("Bot is not running via PM2. If running in foreground, use Ctrl+C.");
|
|
1035
|
+
}
|
|
1036
|
+
process.exit(0);
|
|
1037
|
+
}
|
|
1038
|
+
case "tui":
|
|
1039
|
+
case "chat":
|
|
1040
|
+
import("../dist/tui/index.js").then(m => m.startTUI()).catch(console.error);
|
|
1041
|
+
break;
|
|
1042
|
+
case "search": {
|
|
1043
|
+
const searchQuery = process.argv.slice(3).join(" ");
|
|
1044
|
+
if (!searchQuery) {
|
|
1045
|
+
console.log("Usage: alvin-bot search <query>");
|
|
1046
|
+
console.log('Example: alvin-bot search "cover letter"');
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
const { searchSelf, formatSearchResults } = await import("../dist/services/self-search.js");
|
|
1050
|
+
const results = await searchSelf(searchQuery);
|
|
1051
|
+
console.log(formatSearchResults(results));
|
|
1052
|
+
process.exit(0);
|
|
1053
|
+
}
|
|
1054
|
+
case "audit": {
|
|
1055
|
+
const { runAudit, formatAuditReport } = await import("../dist/services/security-audit.js");
|
|
1056
|
+
const checks = runAudit();
|
|
1057
|
+
console.log(formatAuditReport(checks));
|
|
1058
|
+
process.exit(checks.some(c => c.status === "FAIL") ? 1 : 0);
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "version":
|
|
1062
|
+
case "--version":
|
|
1063
|
+
case "-v":
|
|
1064
|
+
version();
|
|
1065
|
+
break;
|
|
1066
|
+
default:
|
|
1067
|
+
console.log(`
|
|
1068
|
+
${t("cli.title")}
|
|
1069
|
+
|
|
1070
|
+
${t("cli.commands")}
|
|
1071
|
+
setup ${t("cli.setup")}
|
|
1072
|
+
tui ${t("cli.tui")}
|
|
1073
|
+
chat ${t("cli.chatAlias")}
|
|
1074
|
+
doctor ${t("cli.doctorDesc")}
|
|
1075
|
+
audit Security health check (permissions, secrets, config)
|
|
1076
|
+
search Search your assets, memories, and skills
|
|
1077
|
+
update ${t("cli.updateDesc")}
|
|
1078
|
+
start ${t("cli.startDesc")} (background via PM2)
|
|
1079
|
+
start -f Start in foreground (for debugging)
|
|
1080
|
+
stop Stop the bot
|
|
1081
|
+
version ${t("cli.versionDesc")}
|
|
1082
|
+
|
|
1083
|
+
${t("cli.example")}
|
|
1084
|
+
alvin-bot setup
|
|
1085
|
+
alvin-bot tui
|
|
1086
|
+
alvin-bot tui --lang de
|
|
1087
|
+
`);
|
|
1088
|
+
}
|