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 +57 -0
- package/bin/cli.js +286 -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,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=${
|
|
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
|
|
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,
|