alvin-bot 4.7.0 โ†’ 4.8.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/CHANGELOG.md CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.8.1] โ€” 2026-04-11
6
+
7
+ ### ๐Ÿ› Offline setup: Homebrew preferred on macOS
8
+
9
+ Caught during the first real run of the new offline setup wizard on a fresh test MacBook: the official Ollama `install.sh` script on macOS wants to drop `Ollama.app` into `/Applications` and start it as a GUI app. That requires a real user session with sudo and completely breaks over SSH or any non-interactive context. The install downloads the 25 MB .app, then fails at `Unable to find application named 'Ollama'` and drops the wizard back to the fallback provider picker.
10
+
11
+ Fix in `bin/cli.js` `installOllama()`:
12
+
13
+ - **macOS preferred path**: if Homebrew is available (`brew --version` succeeds), use `brew install ollama`. Brew installs `/opt/homebrew/bin/ollama` as a CLI binary with no sudo prompt, no /Applications drop, no GUI dependency โ€” works over SSH and in any CI/non-interactive context.
14
+ - **Fallback**: if brew is not installed or `brew install` itself fails, fall through to the official `install.sh` with an explicit heads-up that the installer may prompt for admin password and may only work in a local terminal.
15
+ - **Better error messaging**: on macOS install failure, suggest `brew install ollama` or the `.dmg` from ollama.com/download as alternatives. On Linux, unchanged.
16
+
17
+ Linux always uses `install.sh` โ€” systemd user units work non-interactively there.
18
+
19
+ ## [4.8.0] โ€” 2026-04-11
20
+
21
+ ### โœจ Offline mode โ€” Gemma 4 E4B via Ollama in the setup wizard
22
+
23
+ Fresh installs on a machine without any AI-provider key can now pick **Offline mode** as the first option in the setup wizard. It runs **Google Gemma 4 E4B** locally via Ollama โ€” no API key, zero running cost, works 100% offline once downloaded.
24
+
25
+ New in `bin/cli.js`:
26
+
27
+ - `PROVIDERS[0]` is now `offline-gemma4`, labeled prominently with the `~10 GB one-time download` so users can't miss the size.
28
+ - `setupOfflineGemma4()` helper walks the user through:
29
+ 1. **Warning** about download size (15โ€“70 min depending on connection) and on-disk footprint (~10 GB in `~/.ollama/models`)
30
+ 2. **Confirmation prompt** โ€” if the user declines, the wizard loops back to the normal provider picker (no dead ends)
31
+ 3. **Ollama install** via the official `curl -fsSL https://ollama.com/install.sh | sh` if the `ollama` binary is missing
32
+ 4. **Daemon check** โ€” ensures Ollama is listening, spawns it in the background if not
33
+ 5. **Cache check** โ€” if `gemma4:e4b` is already pulled, skips the download
34
+ 6. **Model pull** with a second confirmation before the 10 GB actually starts, streaming progress output so the user sees every layer land
35
+ - `.env` gets `PRIMARY_PROVIDER=ollama`. The registry's Ollama preset in `src/providers/types.ts` already defaults to `gemma4:e4b`, so no extra environment variable is needed.
36
+
37
+ macOS + Linux only. Windows users get pointed at https://ollama.com/download.
38
+
39
+ ### โœจ `/version` command + version display in `/status`
40
+
41
+ - New `/version` command in both **Telegram** and **TUI**. Shows `Alvin Bot vX.Y.Z ยท Node vN ยท platform/arch`. Registered in `setMyCommands` so Telegram shows it in the autocomplete menu.
42
+ - `/status` header on Telegram now reads `๐Ÿค– Alvin Bot vX.Y.Z` instead of just `Alvin Bot Status`.
43
+ - TUI `/status` header also carries the version.
44
+ - **Bug fix**: `/api/status` used to hard-code `version: "3.0.0"` (a leftover from v3). It now reads `BOT_VERSION` dynamically, so the TUI and Web UI see the actual running version.
45
+
46
+ Implementation: new `src/version.ts` module reads `package.json` once at module load, exports `BOT_VERSION` as a const. Path resolution uses `import.meta.url` so the cwd can't break it.
47
+
48
+ ### ๐Ÿ› `alvin-bot launchd install` preserves other pm2 projects
49
+
50
+ The initial 4.7.0 release called `pm2 kill` during `launchd install` to stop the pm2 daemon. That's wrong for users who have **other** pm2-managed projects (e.g. `polyseus`) alongside `alvin-bot` โ€” their other work would go down with the switch.
51
+
52
+ New behavior in `bin/cli.js`:
53
+
54
+ - Parse `pm2 jlist` JSON to detect (a) whether `alvin-bot` is pm2-managed and (b) whether any other pm2 projects exist.
55
+ - Only run `pm2 delete alvin-bot` โ€” never `pm2 kill`. The daemon keeps running for the other projects.
56
+ - Post-install hint is smarter:
57
+ - **pm2 now empty** โ†’ *"pm2 now has zero managed processes. Remove it with: `npm uninstall -g pm2`"*
58
+ - **pm2 still has other projects** โ†’ *"pm2 still has other projects running โ€” leaving it installed."*
59
+
60
+ Caught immediately after 4.7.0 shipped when Ali pointed out his Mac mini has `polyseus` in pm2 alongside `alvin-bot` and didn't want it touched.
61
+
5
62
  ## [4.7.0] โ€” 2026-04-11
6
63
 
7
64
  ### โœจ Sub-Agents Stufe 2 โ€” live-stream, bounded queue, 24h stats
package/bin/cli.js CHANGED
@@ -54,6 +54,17 @@ const LOGO = `
54
54
  // โ”€โ”€ Provider Definitions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
55
55
 
56
56
  const PROVIDERS = [
57
+ {
58
+ key: "offline-gemma4",
59
+ name: "๐Ÿ”’ Offline โ€” Gemma 4 E4B (no API key, ~10 GB one-time download)",
60
+ desc: () => "Works without internet. Runs Google Gemma 4 E4B locally via Ollama. Big first-time download, zero running cost, works forever offline.",
61
+ free: true,
62
+ envKey: null,
63
+ signup: null,
64
+ model: "gemma4:e4b",
65
+ needsCLI: false,
66
+ offline: true,
67
+ },
57
68
  {
58
69
  key: "groq",
59
70
  name: "Groq (Llama 3.3 70B)",
@@ -117,6 +128,216 @@ const PROVIDERS = [
117
128
  },
118
129
  ];
119
130
 
131
+ // โ”€โ”€ Offline mode: Ollama + Gemma 4 E4B โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
132
+
133
+ /**
134
+ * Check whether the `ollama` binary is present on PATH.
135
+ */
136
+ function hasOllama() {
137
+ try {
138
+ execSync("ollama --version", { stdio: "pipe" });
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check whether Homebrew is available on PATH (macOS only path normally).
147
+ */
148
+ function hasBrew() {
149
+ try {
150
+ execSync("brew --version", { stdio: "pipe" });
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Install Ollama. On macOS prefers `brew install ollama` because the
159
+ * official install.sh wants to drop Ollama.app into /Applications and
160
+ * start it as a GUI app โ€” that needs a real user session with sudo and
161
+ * breaks over SSH or in any non-interactive context.
162
+ *
163
+ * Linux always uses the official install.sh (systemd user services work
164
+ * non-interactively).
165
+ *
166
+ * Returns true on success, false on failure.
167
+ */
168
+ function installOllama() {
169
+ if (process.platform !== "darwin" && process.platform !== "linux") {
170
+ console.log(" โŒ Offline mode only supported on macOS and Linux.");
171
+ console.log(" Windows users: download from https://ollama.com/download");
172
+ return false;
173
+ }
174
+
175
+ // macOS preferred path: Homebrew (non-interactive, no sudo, no GUI dependency)
176
+ if (process.platform === "darwin" && hasBrew()) {
177
+ console.log("\n๐Ÿ“ฅ Installing Ollama via Homebrew (non-interactive)...");
178
+ try {
179
+ execSync("brew install ollama", {
180
+ stdio: "inherit",
181
+ timeout: 300_000,
182
+ });
183
+ if (hasOllama()) {
184
+ console.log(" โœ… Ollama installed via Homebrew");
185
+ return true;
186
+ }
187
+ console.log(" โš ๏ธ Homebrew finished but `ollama` not on PATH yet.");
188
+ } catch (err) {
189
+ console.log(`\n โš ๏ธ brew install ollama failed: ${err.message || err}`);
190
+ console.log(" Falling back to the official install.sh โ€” this may need sudo and a GUI session.\n");
191
+ }
192
+ }
193
+
194
+ // Fallback: official installer
195
+ console.log("\n๐Ÿ“ฅ Installing Ollama (official installer)...");
196
+ if (process.platform === "darwin") {
197
+ console.log(" โš ๏ธ Heads-up: on macOS the installer drops Ollama.app into");
198
+ console.log(" /Applications and wants to start it โ€” this may prompt for");
199
+ console.log(" your admin password and only works in a local terminal,");
200
+ console.log(" not over SSH.\n");
201
+ }
202
+ try {
203
+ execSync("curl -fsSL https://ollama.com/install.sh | sh", {
204
+ stdio: "inherit",
205
+ timeout: 300_000,
206
+ });
207
+ return hasOllama();
208
+ } catch (err) {
209
+ console.log(`\n โŒ Ollama install failed: ${err.message || err}`);
210
+ if (process.platform === "darwin") {
211
+ console.log(" On macOS, try one of:");
212
+ console.log(" โ€ข brew install ollama (recommended)");
213
+ console.log(" โ€ข download the .dmg from https://ollama.com/download");
214
+ } else {
215
+ console.log(" Try manually: curl -fsSL https://ollama.com/install.sh | sh");
216
+ }
217
+ return false;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Ensure the Ollama daemon is running. Spawns it in the background if not.
223
+ */
224
+ function ensureOllamaServe() {
225
+ try {
226
+ // 'ollama list' needs the daemon running
227
+ execSync("ollama list", { stdio: "pipe", timeout: 5000 });
228
+ return true;
229
+ } catch {
230
+ // Daemon not running โ€” spawn it
231
+ try {
232
+ execSync("nohup ollama serve > /tmp/ollama-setup.log 2>&1 &", {
233
+ stdio: "pipe",
234
+ shell: "/bin/sh",
235
+ });
236
+ // Give it a moment
237
+ execSync("sleep 2", { stdio: "pipe" });
238
+ execSync("ollama list", { stdio: "pipe", timeout: 5000 });
239
+ return true;
240
+ } catch {
241
+ return false;
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Check whether gemma4:e4b is already pulled into Ollama's model cache.
248
+ */
249
+ function hasGemma4E4b() {
250
+ try {
251
+ const out = execSync("ollama list", { encoding: "utf-8", timeout: 5000 });
252
+ return /gemma4[:\s].*e4b/i.test(out);
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Pull gemma4:e4b from the Ollama registry. Streams progress to stdout.
260
+ * Returns true on success, false on failure.
261
+ */
262
+ function pullGemma4E4b() {
263
+ console.log("\n๐Ÿ“ฅ Downloading gemma4:e4b (~10 GB โ€” this can take 10-30 min)...\n");
264
+ try {
265
+ execSync("ollama pull gemma4:e4b", {
266
+ stdio: "inherit",
267
+ timeout: 45 * 60_000, // 45 minutes
268
+ });
269
+ return hasGemma4E4b();
270
+ } catch (err) {
271
+ console.log(`\n โŒ Pull failed: ${err.message || err}`);
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Full offline-mode setup flow: warn about download size, confirm, install
278
+ * Ollama if missing, pull the model, verify. Returns true on success,
279
+ * false if the user bailed or something broke (caller falls back to
280
+ * interactive provider selection).
281
+ */
282
+ async function setupOfflineGemma4() {
283
+ console.log("\n โš ๏ธ Offline mode uses Google Gemma 4 E4B via Ollama.");
284
+ console.log(" โ€ข One-time download: ~10 GB");
285
+ console.log(" โ€ข On a 100 Mbps connection: ~15 minutes");
286
+ console.log(" โ€ข On a 20 Mbps connection: ~70 minutes");
287
+ console.log(" โ€ข Disk usage: ~10 GB in ~/.ollama/models");
288
+ console.log(" โ€ข Runs on CPU + GPU via Metal (macOS) / CUDA (Linux)");
289
+ console.log(" โ€ข Works 100% offline once downloaded\n");
290
+
291
+ const yesChars = getLocale() === "de" ? ["j", "ja", "y", "yes"] : ["y", "yes"];
292
+ const proceed = (await ask(" Continue with offline mode? (y/N): ")).trim().toLowerCase();
293
+ if (!yesChars.includes(proceed)) {
294
+ console.log("\n โ„น๏ธ Offline mode declined โ€” returning to provider selection.\n");
295
+ return false;
296
+ }
297
+
298
+ // Step 1: Ollama binary
299
+ if (!hasOllama()) {
300
+ console.log("\n โ„น๏ธ Ollama not installed.");
301
+ const installProceed = (await ask(" Install Ollama now? (y/N): ")).trim().toLowerCase();
302
+ if (!yesChars.includes(installProceed)) {
303
+ console.log("\n โ„น๏ธ Offline mode cancelled โ€” Ollama is required.\n");
304
+ return false;
305
+ }
306
+ if (!installOllama()) return false;
307
+ console.log(" โœ… Ollama installed");
308
+ } else {
309
+ console.log("\n โœ… Ollama already installed");
310
+ }
311
+
312
+ // Step 2: Ensure daemon is running
313
+ if (!ensureOllamaServe()) {
314
+ console.log("\n โš ๏ธ Could not start Ollama daemon. Try manually:");
315
+ console.log(" ollama serve");
316
+ console.log(" (in a separate terminal, then re-run alvin-bot setup)\n");
317
+ return false;
318
+ }
319
+ console.log(" โœ… Ollama daemon responding");
320
+
321
+ // Step 3: Model already present?
322
+ if (hasGemma4E4b()) {
323
+ console.log(" โœ… gemma4:e4b already downloaded โ€” skipping pull");
324
+ return true;
325
+ }
326
+
327
+ // Step 4: Pull the model (big download)
328
+ console.log("\n ๐Ÿ“ฆ gemma4:e4b not in cache yet.");
329
+ const pullProceed = (await ask(" Start 10 GB download now? (y/N): ")).trim().toLowerCase();
330
+ if (!yesChars.includes(pullProceed)) {
331
+ console.log("\n โ„น๏ธ Pull cancelled. You can run this later:");
332
+ console.log(" ollama pull gemma4:e4b\n");
333
+ return false;
334
+ }
335
+
336
+ if (!pullGemma4E4b()) return false;
337
+ console.log("\n โœ… gemma4:e4b downloaded and ready\n");
338
+ return true;
339
+ }
340
+
120
341
  // โ”€โ”€ Provider Validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
121
342
 
122
343
  /**
@@ -594,6 +815,32 @@ async function setup() {
594
815
 
595
816
  console.log(`\nโœ… ${t("setup.providerSelected")} ${provider.name}`);
596
817
 
818
+ // โ”€โ”€ Offline mode: Gemma 4 E4B via Ollama โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
819
+ // Handled specially because it needs a 10 GB model download, not an
820
+ // API key. If the user bails out anywhere in the flow, we loop back
821
+ // to the normal provider picker so setup isn't a dead-end.
822
+ if (provider.offline) {
823
+ const ok = await setupOfflineGemma4();
824
+ if (!ok) {
825
+ // User declined or something failed โ€” pick a different provider
826
+ console.log(`\n Choose a different provider:\n`);
827
+ for (let i = 0; i < PROVIDERS.length; i++) {
828
+ if (PROVIDERS[i].offline) continue;
829
+ const p = PROVIDERS[i];
830
+ const badge = p.free ? "๐Ÿ†“" : "๐Ÿ’ฐ";
831
+ const premium = p.needsCLI ? " โญ" : "";
832
+ console.log(` ${i + 1}. ${badge} ${p.name}${premium}`);
833
+ }
834
+ console.log("");
835
+ const fallbackChoice = parseInt((await ask(t("setup.yourChoice"))).trim()) || 2;
836
+ provider = PROVIDERS[Math.max(1, Math.min(fallbackChoice - 1, PROVIDERS.length - 1))];
837
+ console.log(`\nโœ… ${t("setup.providerSelected")} ${provider.name}`);
838
+ }
839
+ // Note: if setupOfflineGemma4 succeeded, we skip further API-key
840
+ // validation below โ€” offline mode doesn't need a key. The .env
841
+ // write step reads provider.offline and sets PRIMARY_PROVIDER=ollama.
842
+ }
843
+
597
844
  // โ”€โ”€ Validate Provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
598
845
 
599
846
  // Claude SDK: show requirements upfront
@@ -803,13 +1050,17 @@ async function setup() {
803
1050
  // โ”€โ”€ Write .env
804
1051
  console.log(`\n${t("setup.writingConfig")}`);
805
1052
 
1053
+ // Offline mode translates to PRIMARY_PROVIDER=ollama โ€” the registry's
1054
+ // ollama preset already points at gemma4:e4b, so no extra env needed.
1055
+ const primaryKey = provider.offline ? "ollama" : provider.key;
1056
+
806
1057
  const envLines = [
807
1058
  "# === Telegram ===",
808
1059
  `BOT_TOKEN=${botToken || ""}`,
809
1060
  `ALLOWED_USERS=${userId || ""}`,
810
1061
  "",
811
1062
  "# === AI Provider ===",
812
- `PRIMARY_PROVIDER=${provider.key}`,
1063
+ `PRIMARY_PROVIDER=${primaryKey}`,
813
1064
  ];
814
1065
 
815
1066
  if (provider.envKey && providerApiKey) {
@@ -1261,6 +1512,32 @@ async function launchdInstall() {
1261
1512
  execSync(`launchctl unload -w "${plistPath}"`, { stdio: "pipe" });
1262
1513
  } catch { /* not loaded yet โ€” fine */ }
1263
1514
 
1515
+ // If pm2 is managing an alvin-bot process, tear that one process down.
1516
+ // We deliberately do NOT `pm2 kill` the whole daemon โ€” the user may
1517
+ // have other pm2-managed projects (polyseus, etc.) and we must not
1518
+ // nuke those. Only the alvin-bot entry is removed.
1519
+ let pm2HadAlvinBot = false;
1520
+ let pm2StillHasOtherProcesses = false;
1521
+ try {
1522
+ execSync("pm2 --version", { stdio: "pipe" });
1523
+ // Check whether alvin-bot is currently pm2-managed
1524
+ try {
1525
+ const lsOut = execSync("pm2 jlist", { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
1526
+ const procs = JSON.parse(lsOut);
1527
+ if (Array.isArray(procs)) {
1528
+ pm2HadAlvinBot = procs.some((p) => p && p.name === "alvin-bot");
1529
+ pm2StillHasOtherProcesses = procs.some((p) => p && p.name !== "alvin-bot");
1530
+ }
1531
+ } catch { /* pm2 jlist can fail on empty list or missing daemon โ€” ignore */ }
1532
+
1533
+ if (pm2HadAlvinBot) {
1534
+ try {
1535
+ execSync("pm2 delete alvin-bot", { stdio: "pipe" });
1536
+ console.log("๐Ÿงน Removed alvin-bot from pm2 (other pm2 projects left intact).");
1537
+ } catch { /* already gone */ }
1538
+ }
1539
+ } catch { /* pm2 not installed โ€” nothing to clean up */ }
1540
+
1264
1541
  // Stop any nohup'd bot that might still be running
1265
1542
  try {
1266
1543
  execSync(`pkill -TERM -f 'node.*dist/index.js' || true`, { stdio: "pipe" });
@@ -1289,6 +1566,14 @@ async function launchdInstall() {
1289
1566
  console.log(" the macOS Keychain is automatically unlocked โ€” Claude Code");
1290
1567
  console.log(" OAuth tokens (Max subscription) just work, no SSH keychain");
1291
1568
  console.log(" dance needed anymore.");
1569
+ if (pm2HadAlvinBot && !pm2StillHasOtherProcesses) {
1570
+ console.log("");
1571
+ console.log("๐Ÿ’ก pm2 now has zero managed processes. You can remove it entirely:");
1572
+ console.log(" npm uninstall -g pm2");
1573
+ } else if (pm2HadAlvinBot && pm2StillHasOtherProcesses) {
1574
+ console.log("");
1575
+ console.log("๐Ÿ’ก pm2 still has other projects running โ€” leaving it installed.");
1576
+ }
1292
1577
  process.exit(0);
1293
1578
  }
1294
1579
 
@@ -18,6 +18,7 @@ import { screenshotUrl, extractText, generatePdf, hasPlaywright } from "../servi
18
18
  import { listJobs, createJob, deleteJob, toggleJob, runJobNow, formatNextRun, humanReadableSchedule } from "../services/cron.js";
19
19
  import { storePassword, revokePassword, getSudoStatus, verifyPassword } from "../services/sudo.js";
20
20
  import { config } from "../config.js";
21
+ import { BOT_VERSION } from "../version.js";
21
22
  import { getWebPort } from "../web/server.js";
22
23
  import { getUsageSummary, getAllRateLimits, formatTokens } from "../services/usage-tracker.js";
23
24
  import { runUpdate, getAutoUpdate, setAutoUpdate, startAutoUpdateLoop } from "../services/updater.js";
@@ -141,6 +142,7 @@ export function registerCommands(bot) {
141
142
  { command: "effort", description: "Set reasoning depth" },
142
143
  { command: "voice", description: "Voice replies on/off" },
143
144
  { command: "status", description: "Current status" },
145
+ { command: "version", description: "Show Alvin Bot version" },
144
146
  { command: "new", description: "Start new session" },
145
147
  { command: "dir", description: "Change working directory" },
146
148
  { command: "web", description: "Quick web search" },
@@ -219,6 +221,10 @@ export function registerCommands(bot) {
219
221
  await ctx.reply(`Directory not found: ${resolved}`);
220
222
  }
221
223
  });
224
+ bot.command("version", async (ctx) => {
225
+ await ctx.reply(`๐Ÿค– *Alvin Bot* \`v${BOT_VERSION}\`\n` +
226
+ `Node ${process.version} ยท ${process.platform}/${process.arch}`, { parse_mode: "Markdown" });
227
+ });
222
228
  bot.command("status", async (ctx) => {
223
229
  const userId = ctx.from.id;
224
230
  const session = getSession(userId);
@@ -371,7 +377,7 @@ export function registerCommands(bot) {
371
377
  const failoverBadge = failedOver ? ` ${t("bot.status.failedOver", lang)}` : "";
372
378
  healthLines = `\n${t("bot.status.providerHealth", lang)}${failoverBadge}\n${rows.join("\n")}\n`;
373
379
  }
374
- await ctx.reply(`๐Ÿค– *Alvin Bot Status*\n\n` +
380
+ await ctx.reply(`๐Ÿค– *Alvin Bot* \`v${BOT_VERSION}\`\n\n` +
375
381
  `*Model:* ${info.name} ${providerTag}\n` +
376
382
  `*Effort:* ${EFFORT_LABELS[session.effort]}\n` +
377
383
  `*Voice:* ${session.voiceReply ? "on" : "off"}\n` +
package/dist/tui/index.js CHANGED
@@ -20,6 +20,7 @@ import { createInterface, cursorTo, clearLine as rlClearLine } from "readline";
20
20
  import WebSocket from "ws";
21
21
  import http from "http";
22
22
  import { initI18n, t } from "../i18n.js";
23
+ import { BOT_VERSION } from "../version.js";
23
24
  // Init i18n before anything else
24
25
  initI18n();
25
26
  // โ”€โ”€ ANSI Colors & Styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -410,11 +411,17 @@ async function handleCommand(cmd) {
410
411
  }
411
412
  break;
412
413
  }
414
+ case "version":
415
+ case "v": {
416
+ console.log(`\n${C.bold}${C.brightCyan}๐Ÿค– Alvin Bot${C.reset} ${C.dim}v${BOT_VERSION}${C.reset}`);
417
+ console.log(`${C.dim}Node ${process.version} ยท ${process.platform}/${process.arch}${C.reset}\n`);
418
+ break;
419
+ }
413
420
  case "status":
414
421
  case "s": {
415
422
  try {
416
423
  const data = await apiGet("/api/status");
417
- console.log(`\n${C.bold}${C.brightCyan}${t("status.title")}${C.reset}`);
424
+ console.log(`\n${C.bold}${C.brightCyan}๐Ÿค– Alvin Bot${C.reset} ${C.dim}v${BOT_VERSION}${C.reset}`);
418
425
  console.log(`${C.gray}${"โ”€".repeat(40)}${C.reset}`);
419
426
  if (data.model) {
420
427
  console.log(` ${C.cyan}${t("status.model")}${C.reset} ${data.model.model || data.model.name || "?"}`);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Central source of truth for the running Alvin Bot version.
3
+ * Read from package.json once at module load โ€” subsequent imports
4
+ * return the cached string without touching disk.
5
+ */
6
+ import { readFileSync } from "fs";
7
+ import { dirname, resolve } from "path";
8
+ import { fileURLToPath } from "url";
9
+ function readVersion() {
10
+ try {
11
+ // dist/version.js is two levels deep; package.json sits at the root
12
+ const here = dirname(fileURLToPath(import.meta.url));
13
+ const pkgPath = resolve(here, "..", "package.json");
14
+ const raw = readFileSync(pkgPath, "utf-8");
15
+ const parsed = JSON.parse(raw);
16
+ if (typeof parsed.version === "string")
17
+ return parsed.version;
18
+ }
19
+ catch {
20
+ /* fall through to unknown */
21
+ }
22
+ return "unknown";
23
+ }
24
+ export const BOT_VERSION = readVersion();
@@ -29,6 +29,7 @@ import { handleOpenAICompat } from "./openai-compat.js";
29
29
  import { addCanvasClient } from "./canvas.js";
30
30
  import { BOT_ROOT, ENV_FILE, PUBLIC_DIR, MEMORY_DIR, MEMORY_FILE, SOUL_FILE, DATA_DIR, MCP_CONFIG, SKILLS_DIR } from "../paths.js";
31
31
  import { broadcast } from "../services/broadcast.js";
32
+ import { BOT_VERSION } from "../version.js";
32
33
  const WEB_PORT = parseInt(process.env.WEB_PORT || "3100");
33
34
  const WEB_PASSWORD = process.env.WEB_PASSWORD || "";
34
35
  /** The actual port the Web UI is running on (may differ from WEB_PORT if busy). */
@@ -259,7 +260,7 @@ async function handleAPI(req, res, urlPath, body) {
259
260
  }
260
261
  const { config: appConfig } = await import("../config.js");
261
262
  res.end(JSON.stringify({
262
- bot: { version: "3.0.0", uptime: process.uptime() },
263
+ bot: { version: BOT_VERSION, uptime: process.uptime() },
263
264
  model: modelInfo,
264
265
  memory: { ...memory, vectors: index.entries, indexSize: index.sizeBytes },
265
266
  plugins: plugins.length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alvin-bot",
3
- "version": "4.7.0",
3
+ "version": "4.8.1",
4
4
  "description": "Alvin Bot โ€” Your personal AI agent on Telegram, WhatsApp, Discord, Signal, and Web.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",