alvin-bot 4.7.0 → 4.8.0
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 +43 -0
- package/bin/cli.js +235 -1
- package/dist/handlers/commands.js +7 -1
- package/dist/tui/index.js +8 -1
- package/dist/version.js +24 -0
- package/dist/web/server.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.8.0] — 2026-04-11
|
|
6
|
+
|
|
7
|
+
### ✨ Offline mode — Gemma 4 E4B via Ollama in the setup wizard
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
New in `bin/cli.js`:
|
|
12
|
+
|
|
13
|
+
- `PROVIDERS[0]` is now `offline-gemma4`, labeled prominently with the `~10 GB one-time download` so users can't miss the size.
|
|
14
|
+
- `setupOfflineGemma4()` helper walks the user through:
|
|
15
|
+
1. **Warning** about download size (15–70 min depending on connection) and on-disk footprint (~10 GB in `~/.ollama/models`)
|
|
16
|
+
2. **Confirmation prompt** — if the user declines, the wizard loops back to the normal provider picker (no dead ends)
|
|
17
|
+
3. **Ollama install** via the official `curl -fsSL https://ollama.com/install.sh | sh` if the `ollama` binary is missing
|
|
18
|
+
4. **Daemon check** — ensures Ollama is listening, spawns it in the background if not
|
|
19
|
+
5. **Cache check** — if `gemma4:e4b` is already pulled, skips the download
|
|
20
|
+
6. **Model pull** with a second confirmation before the 10 GB actually starts, streaming progress output so the user sees every layer land
|
|
21
|
+
- `.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.
|
|
22
|
+
|
|
23
|
+
macOS + Linux only. Windows users get pointed at https://ollama.com/download.
|
|
24
|
+
|
|
25
|
+
### ✨ `/version` command + version display in `/status`
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
- `/status` header on Telegram now reads `🤖 Alvin Bot vX.Y.Z` instead of just `Alvin Bot Status`.
|
|
29
|
+
- TUI `/status` header also carries the version.
|
|
30
|
+
- **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.
|
|
31
|
+
|
|
32
|
+
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.
|
|
33
|
+
|
|
34
|
+
### 🐛 `alvin-bot launchd install` preserves other pm2 projects
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
New behavior in `bin/cli.js`:
|
|
39
|
+
|
|
40
|
+
- Parse `pm2 jlist` JSON to detect (a) whether `alvin-bot` is pm2-managed and (b) whether any other pm2 projects exist.
|
|
41
|
+
- Only run `pm2 delete alvin-bot` — never `pm2 kill`. The daemon keeps running for the other projects.
|
|
42
|
+
- Post-install hint is smarter:
|
|
43
|
+
- **pm2 now empty** → *"pm2 now has zero managed processes. Remove it with: `npm uninstall -g pm2`"*
|
|
44
|
+
- **pm2 still has other projects** → *"pm2 still has other projects running — leaving it installed."*
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
5
48
|
## [4.7.0] — 2026-04-11
|
|
6
49
|
|
|
7
50
|
### ✨ 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,165 @@ 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
|
+
* Install Ollama via the official installer. Prints progress to stdout.
|
|
147
|
+
* Returns true on success, false on failure.
|
|
148
|
+
*/
|
|
149
|
+
function installOllama() {
|
|
150
|
+
console.log("\n📥 Installing Ollama (official installer)...");
|
|
151
|
+
try {
|
|
152
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
153
|
+
execSync("curl -fsSL https://ollama.com/install.sh | sh", {
|
|
154
|
+
stdio: "inherit",
|
|
155
|
+
timeout: 300_000, // 5 minutes
|
|
156
|
+
});
|
|
157
|
+
return hasOllama();
|
|
158
|
+
} else {
|
|
159
|
+
console.log(" ❌ Offline mode only supported on macOS and Linux.");
|
|
160
|
+
console.log(" Windows users: download from https://ollama.com/download");
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.log(`\n ❌ Ollama install failed: ${err.message || err}`);
|
|
165
|
+
console.log(" Try manually: curl -fsSL https://ollama.com/install.sh | sh");
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Ensure the Ollama daemon is running. Spawns it in the background if not.
|
|
172
|
+
*/
|
|
173
|
+
function ensureOllamaServe() {
|
|
174
|
+
try {
|
|
175
|
+
// 'ollama list' needs the daemon running
|
|
176
|
+
execSync("ollama list", { stdio: "pipe", timeout: 5000 });
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
// Daemon not running — spawn it
|
|
180
|
+
try {
|
|
181
|
+
execSync("nohup ollama serve > /tmp/ollama-setup.log 2>&1 &", {
|
|
182
|
+
stdio: "pipe",
|
|
183
|
+
shell: "/bin/sh",
|
|
184
|
+
});
|
|
185
|
+
// Give it a moment
|
|
186
|
+
execSync("sleep 2", { stdio: "pipe" });
|
|
187
|
+
execSync("ollama list", { stdio: "pipe", timeout: 5000 });
|
|
188
|
+
return true;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check whether gemma4:e4b is already pulled into Ollama's model cache.
|
|
197
|
+
*/
|
|
198
|
+
function hasGemma4E4b() {
|
|
199
|
+
try {
|
|
200
|
+
const out = execSync("ollama list", { encoding: "utf-8", timeout: 5000 });
|
|
201
|
+
return /gemma4[:\s].*e4b/i.test(out);
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Pull gemma4:e4b from the Ollama registry. Streams progress to stdout.
|
|
209
|
+
* Returns true on success, false on failure.
|
|
210
|
+
*/
|
|
211
|
+
function pullGemma4E4b() {
|
|
212
|
+
console.log("\n📥 Downloading gemma4:e4b (~10 GB — this can take 10-30 min)...\n");
|
|
213
|
+
try {
|
|
214
|
+
execSync("ollama pull gemma4:e4b", {
|
|
215
|
+
stdio: "inherit",
|
|
216
|
+
timeout: 45 * 60_000, // 45 minutes
|
|
217
|
+
});
|
|
218
|
+
return hasGemma4E4b();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.log(`\n ❌ Pull failed: ${err.message || err}`);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Full offline-mode setup flow: warn about download size, confirm, install
|
|
227
|
+
* Ollama if missing, pull the model, verify. Returns true on success,
|
|
228
|
+
* false if the user bailed or something broke (caller falls back to
|
|
229
|
+
* interactive provider selection).
|
|
230
|
+
*/
|
|
231
|
+
async function setupOfflineGemma4() {
|
|
232
|
+
console.log("\n ⚠️ Offline mode uses Google Gemma 4 E4B via Ollama.");
|
|
233
|
+
console.log(" • One-time download: ~10 GB");
|
|
234
|
+
console.log(" • On a 100 Mbps connection: ~15 minutes");
|
|
235
|
+
console.log(" • On a 20 Mbps connection: ~70 minutes");
|
|
236
|
+
console.log(" • Disk usage: ~10 GB in ~/.ollama/models");
|
|
237
|
+
console.log(" • Runs on CPU + GPU via Metal (macOS) / CUDA (Linux)");
|
|
238
|
+
console.log(" • Works 100% offline once downloaded\n");
|
|
239
|
+
|
|
240
|
+
const yesChars = getLocale() === "de" ? ["j", "ja", "y", "yes"] : ["y", "yes"];
|
|
241
|
+
const proceed = (await ask(" Continue with offline mode? (y/N): ")).trim().toLowerCase();
|
|
242
|
+
if (!yesChars.includes(proceed)) {
|
|
243
|
+
console.log("\n ℹ️ Offline mode declined — returning to provider selection.\n");
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Step 1: Ollama binary
|
|
248
|
+
if (!hasOllama()) {
|
|
249
|
+
console.log("\n ℹ️ Ollama not installed.");
|
|
250
|
+
const installProceed = (await ask(" Install Ollama now? (y/N): ")).trim().toLowerCase();
|
|
251
|
+
if (!yesChars.includes(installProceed)) {
|
|
252
|
+
console.log("\n ℹ️ Offline mode cancelled — Ollama is required.\n");
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (!installOllama()) return false;
|
|
256
|
+
console.log(" ✅ Ollama installed");
|
|
257
|
+
} else {
|
|
258
|
+
console.log("\n ✅ Ollama already installed");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Step 2: Ensure daemon is running
|
|
262
|
+
if (!ensureOllamaServe()) {
|
|
263
|
+
console.log("\n ⚠️ Could not start Ollama daemon. Try manually:");
|
|
264
|
+
console.log(" ollama serve");
|
|
265
|
+
console.log(" (in a separate terminal, then re-run alvin-bot setup)\n");
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
console.log(" ✅ Ollama daemon responding");
|
|
269
|
+
|
|
270
|
+
// Step 3: Model already present?
|
|
271
|
+
if (hasGemma4E4b()) {
|
|
272
|
+
console.log(" ✅ gemma4:e4b already downloaded — skipping pull");
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 4: Pull the model (big download)
|
|
277
|
+
console.log("\n 📦 gemma4:e4b not in cache yet.");
|
|
278
|
+
const pullProceed = (await ask(" Start 10 GB download now? (y/N): ")).trim().toLowerCase();
|
|
279
|
+
if (!yesChars.includes(pullProceed)) {
|
|
280
|
+
console.log("\n ℹ️ Pull cancelled. You can run this later:");
|
|
281
|
+
console.log(" ollama pull gemma4:e4b\n");
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!pullGemma4E4b()) return false;
|
|
286
|
+
console.log("\n ✅ gemma4:e4b downloaded and ready\n");
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
120
290
|
// ── Provider Validation ────────────────────────────────────────────────────
|
|
121
291
|
|
|
122
292
|
/**
|
|
@@ -594,6 +764,32 @@ async function setup() {
|
|
|
594
764
|
|
|
595
765
|
console.log(`\n✅ ${t("setup.providerSelected")} ${provider.name}`);
|
|
596
766
|
|
|
767
|
+
// ── Offline mode: Gemma 4 E4B via Ollama ────────────────────────
|
|
768
|
+
// Handled specially because it needs a 10 GB model download, not an
|
|
769
|
+
// API key. If the user bails out anywhere in the flow, we loop back
|
|
770
|
+
// to the normal provider picker so setup isn't a dead-end.
|
|
771
|
+
if (provider.offline) {
|
|
772
|
+
const ok = await setupOfflineGemma4();
|
|
773
|
+
if (!ok) {
|
|
774
|
+
// User declined or something failed — pick a different provider
|
|
775
|
+
console.log(`\n Choose a different provider:\n`);
|
|
776
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
777
|
+
if (PROVIDERS[i].offline) continue;
|
|
778
|
+
const p = PROVIDERS[i];
|
|
779
|
+
const badge = p.free ? "🆓" : "💰";
|
|
780
|
+
const premium = p.needsCLI ? " ⭐" : "";
|
|
781
|
+
console.log(` ${i + 1}. ${badge} ${p.name}${premium}`);
|
|
782
|
+
}
|
|
783
|
+
console.log("");
|
|
784
|
+
const fallbackChoice = parseInt((await ask(t("setup.yourChoice"))).trim()) || 2;
|
|
785
|
+
provider = PROVIDERS[Math.max(1, Math.min(fallbackChoice - 1, PROVIDERS.length - 1))];
|
|
786
|
+
console.log(`\n✅ ${t("setup.providerSelected")} ${provider.name}`);
|
|
787
|
+
}
|
|
788
|
+
// Note: if setupOfflineGemma4 succeeded, we skip further API-key
|
|
789
|
+
// validation below — offline mode doesn't need a key. The .env
|
|
790
|
+
// write step reads provider.offline and sets PRIMARY_PROVIDER=ollama.
|
|
791
|
+
}
|
|
792
|
+
|
|
597
793
|
// ── Validate Provider ────────────────────────────────────────────
|
|
598
794
|
|
|
599
795
|
// Claude SDK: show requirements upfront
|
|
@@ -803,13 +999,17 @@ async function setup() {
|
|
|
803
999
|
// ── Write .env
|
|
804
1000
|
console.log(`\n${t("setup.writingConfig")}`);
|
|
805
1001
|
|
|
1002
|
+
// Offline mode translates to PRIMARY_PROVIDER=ollama — the registry's
|
|
1003
|
+
// ollama preset already points at gemma4:e4b, so no extra env needed.
|
|
1004
|
+
const primaryKey = provider.offline ? "ollama" : provider.key;
|
|
1005
|
+
|
|
806
1006
|
const envLines = [
|
|
807
1007
|
"# === Telegram ===",
|
|
808
1008
|
`BOT_TOKEN=${botToken || ""}`,
|
|
809
1009
|
`ALLOWED_USERS=${userId || ""}`,
|
|
810
1010
|
"",
|
|
811
1011
|
"# === AI Provider ===",
|
|
812
|
-
`PRIMARY_PROVIDER=${
|
|
1012
|
+
`PRIMARY_PROVIDER=${primaryKey}`,
|
|
813
1013
|
];
|
|
814
1014
|
|
|
815
1015
|
if (provider.envKey && providerApiKey) {
|
|
@@ -1261,6 +1461,32 @@ async function launchdInstall() {
|
|
|
1261
1461
|
execSync(`launchctl unload -w "${plistPath}"`, { stdio: "pipe" });
|
|
1262
1462
|
} catch { /* not loaded yet — fine */ }
|
|
1263
1463
|
|
|
1464
|
+
// If pm2 is managing an alvin-bot process, tear that one process down.
|
|
1465
|
+
// We deliberately do NOT `pm2 kill` the whole daemon — the user may
|
|
1466
|
+
// have other pm2-managed projects (polyseus, etc.) and we must not
|
|
1467
|
+
// nuke those. Only the alvin-bot entry is removed.
|
|
1468
|
+
let pm2HadAlvinBot = false;
|
|
1469
|
+
let pm2StillHasOtherProcesses = false;
|
|
1470
|
+
try {
|
|
1471
|
+
execSync("pm2 --version", { stdio: "pipe" });
|
|
1472
|
+
// Check whether alvin-bot is currently pm2-managed
|
|
1473
|
+
try {
|
|
1474
|
+
const lsOut = execSync("pm2 jlist", { stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
|
|
1475
|
+
const procs = JSON.parse(lsOut);
|
|
1476
|
+
if (Array.isArray(procs)) {
|
|
1477
|
+
pm2HadAlvinBot = procs.some((p) => p && p.name === "alvin-bot");
|
|
1478
|
+
pm2StillHasOtherProcesses = procs.some((p) => p && p.name !== "alvin-bot");
|
|
1479
|
+
}
|
|
1480
|
+
} catch { /* pm2 jlist can fail on empty list or missing daemon — ignore */ }
|
|
1481
|
+
|
|
1482
|
+
if (pm2HadAlvinBot) {
|
|
1483
|
+
try {
|
|
1484
|
+
execSync("pm2 delete alvin-bot", { stdio: "pipe" });
|
|
1485
|
+
console.log("🧹 Removed alvin-bot from pm2 (other pm2 projects left intact).");
|
|
1486
|
+
} catch { /* already gone */ }
|
|
1487
|
+
}
|
|
1488
|
+
} catch { /* pm2 not installed — nothing to clean up */ }
|
|
1489
|
+
|
|
1264
1490
|
// Stop any nohup'd bot that might still be running
|
|
1265
1491
|
try {
|
|
1266
1492
|
execSync(`pkill -TERM -f 'node.*dist/index.js' || true`, { stdio: "pipe" });
|
|
@@ -1289,6 +1515,14 @@ async function launchdInstall() {
|
|
|
1289
1515
|
console.log(" the macOS Keychain is automatically unlocked — Claude Code");
|
|
1290
1516
|
console.log(" OAuth tokens (Max subscription) just work, no SSH keychain");
|
|
1291
1517
|
console.log(" dance needed anymore.");
|
|
1518
|
+
if (pm2HadAlvinBot && !pm2StillHasOtherProcesses) {
|
|
1519
|
+
console.log("");
|
|
1520
|
+
console.log("💡 pm2 now has zero managed processes. You can remove it entirely:");
|
|
1521
|
+
console.log(" npm uninstall -g pm2");
|
|
1522
|
+
} else if (pm2HadAlvinBot && pm2StillHasOtherProcesses) {
|
|
1523
|
+
console.log("");
|
|
1524
|
+
console.log("💡 pm2 still has other projects running — leaving it installed.");
|
|
1525
|
+
}
|
|
1292
1526
|
process.exit(0);
|
|
1293
1527
|
}
|
|
1294
1528
|
|
|
@@ -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
|
|
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}${
|
|
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 || "?"}`);
|
package/dist/version.js
ADDED
|
@@ -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();
|
package/dist/web/server.js
CHANGED
|
@@ -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:
|
|
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,
|