alvin-bot 4.24.0 → 4.25.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 +64 -0
- package/bin/cli.js +184 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.25.1] — 2026-05-13
|
|
6
|
+
|
|
7
|
+
### Fixed: `alvin-bot launchd install` now persists the PM2 cleanup
|
|
8
|
+
|
|
9
|
+
The launchd installer correctly called `pm2 delete alvin-bot` when migrating a previously-PM2-managed bot to launchd — but `pm2 delete` only mutates the live process list, **not** the persisted `~/.pm2/dump.pm2`. On the next login, PM2's startup script would call `pm2 resurrect`, see alvin-bot in the stale dump, and re-spawn it. The resurrected pm2-managed instance and the new launchd-managed instance then both polled Telegram with the same `BOT_TOKEN`, racking up `409 Conflict: terminated by other getUpdates request` errors until one of them crashed.
|
|
10
|
+
|
|
11
|
+
The fix adds `pm2 save --force` immediately after `pm2 delete`, so the dump file reflects reality. `--force` is needed because plain `pm2 save` refuses to persist an empty list with a warning. Other PM2-managed processes (e.g. unrelated projects in the user's PM2) are preserved correctly — the save only mutates entries for alvin-bot.
|
|
12
|
+
|
|
13
|
+
### How it surfaced
|
|
14
|
+
|
|
15
|
+
A maintainer's local Mac that had been running alvin-bot under PM2 *before* the v4.x migration to launchd hit `409 Conflict` errors out of nowhere — turns out PM2 had been silently resurrecting alvin-bot on every reboot for months (106,000+ restart counter), invocations without args printed help and exited, PM2 restarted it instantly, the cycle continued. Today the cycle accidentally aligned with a Telegram getUpdates call, causing the conflict. Cleanup steps:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pm2 delete polyseus # any other PM2 entries
|
|
19
|
+
pm2 save --force # empty dump
|
|
20
|
+
launchctl unload ~/Library/LaunchAgents/pm2.alvin_de.plist 2>/dev/null
|
|
21
|
+
rm -f ~/Library/LaunchAgents/pm2.alvin_de.plist
|
|
22
|
+
pm2 kill
|
|
23
|
+
npm uninstall -g pm2
|
|
24
|
+
rm -rf ~/.pm2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
After this patch, future users migrating from PM2 to launchd via `alvin-bot launchd install` won't accumulate the stale-dump time bomb — the cleanup is persistent immediately.
|
|
28
|
+
|
|
29
|
+
### End-to-end verified on a real Apple Silicon Mac (.75)
|
|
30
|
+
|
|
31
|
+
1. Reproduced the broken pre-condition: install PM2, add alvin-bot to its dump via `pm2 start … && pm2 save`. `grep -c alvin-bot ~/.pm2/dump.pm2` → 7 hits.
|
|
32
|
+
2. Ran `alvin-bot launchd install` with the fix applied.
|
|
33
|
+
3. Verified `grep -c alvin-bot ~/.pm2/dump.pm2` → **0 hits**. PM2 live list is also empty.
|
|
34
|
+
|
|
35
|
+
The installer's output now ends with `🧹 Removed alvin-bot from pm2 and persisted (other pm2 projects left intact).` and includes a `💡 pm2 now has zero managed processes. You can remove it entirely:` hint when no other pm2 projects remain.
|
|
36
|
+
|
|
37
|
+
## [4.25.0] — 2026-05-13
|
|
38
|
+
|
|
39
|
+
### Auto-bootstrap media tools (yt-dlp + ffmpeg) on every setup and update
|
|
40
|
+
|
|
41
|
+
`yt-dlp` and `ffmpeg` are now installed (or upgraded) automatically whenever the user runs `alvin-bot setup` or `alvin-bot update` — silently, one line of status per tool, non-fatal on failure. These are the two foundational media tools used by the transcribe / download / video-generate / voice skills and they break or need patches often enough that "whatever brew shipped last week" is materially worse than "current latest" for ordinary users.
|
|
42
|
+
|
|
43
|
+
**No bundling. No redistribution.** alvin-bot shells out to the user's own system package manager (Homebrew on macOS, `pipx`/`apt` on Linux). The user remains the licensee under each tool's upstream license. License notes are recorded in `BOOTSTRAP_TOOLS` for transparency:
|
|
44
|
+
|
|
45
|
+
- **yt-dlp** — Unlicense (public-domain equivalent)
|
|
46
|
+
- **ffmpeg** — LGPL-2.1+ (Brew default build; no GPL-only codecs are pulled in by the default formula)
|
|
47
|
+
|
|
48
|
+
### Behaviour
|
|
49
|
+
|
|
50
|
+
| User action | What happens |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `alvin-bot setup` (interactive) | At the very start, before any prompt: tries `brew install yt-dlp` + `brew install ffmpeg` (or `pipx`/`apt` on Linux) if missing, runs `brew upgrade` if already installed. One status line per tool. |
|
|
53
|
+
| `alvin-bot update` | After `npm install -g` / `git pull` completes, the same bootstrap step runs — so an update of the bot also refreshes media tools. |
|
|
54
|
+
| `alvin-bot setup --no-bootstrap-tools` | Skips the bootstrap step entirely. For users who want full manual control. |
|
|
55
|
+
| Failure (no brew, no network, etc.) | Setup/update continues; user sees a `⚠` warning line; bot still works for everything that doesn't need media tools. |
|
|
56
|
+
|
|
57
|
+
### Implementation detail that matters
|
|
58
|
+
|
|
59
|
+
On macOS, `brew upgrade <pkg>` does **not** automatically refresh the formula database before checking versions — meaning a user on stale brew (haven't run `brew update` in weeks) would see "already at latest" even when newer versions exist. The bootstrap step runs `brew update --quiet` **once per session** before any brew upgrade, memoized so multiple tools share the refresh cost. Cost: 5-30 s on first invocation, zero on subsequent calls.
|
|
60
|
+
|
|
61
|
+
### End-to-end verified on a real Apple Silicon Mac
|
|
62
|
+
|
|
63
|
+
Two scenarios run manually after the code change:
|
|
64
|
+
|
|
65
|
+
1. **Both tools present, brew formulas stale.** Bootstrap ran `brew update --quiet` + `brew upgrade yt-dlp/ffmpeg`. yt-dlp's brew-listed and upstream latest both confirmed at `2026.03.17` (verified via `brew info yt-dlp` and `yt-dlp -U`'s self-check) — code correctly reported "up-to-date". ffmpeg's pre-existing dylib-link failure (`libx265.215.dylib not found`) was incidentally fixed by `brew upgrade ffmpeg`, which is the exact kind of bit-rot the bootstrap is meant to catch.
|
|
66
|
+
|
|
67
|
+
2. **yt-dlp deliberately uninstalled, run `alvin-bot update` again.** Bootstrap correctly detected `command -v yt-dlp` → not found, ran `brew install yt-dlp`, reported "installed (2026.03.17)" (note the differentiated wording vs "up-to-date" — same versioning convention as `npm install` vs `npm update`). Post-state: `/opt/homebrew/bin/yt-dlp` back and working.
|
|
68
|
+
|
|
5
69
|
## [4.24.0] — 2026-05-12
|
|
6
70
|
|
|
7
71
|
### Switch AI providers cleanly after install — new `alvin-bot provider` command
|
package/bin/cli.js
CHANGED
|
@@ -235,6 +235,165 @@ function hasCommand(name) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// ─── Auto-bootstrap media tools ───────────────────────────────────────────
|
|
239
|
+
//
|
|
240
|
+
// These two are universally useful for the media-related skills (transcribe,
|
|
241
|
+
// download, video-generate, voice) and they break or need patches often enough
|
|
242
|
+
// (yt-dlp especially — YouTube changes its extractors weekly) that "always
|
|
243
|
+
// latest" is materially better than "whatever brew shipped last week".
|
|
244
|
+
//
|
|
245
|
+
// The installer here is NOT redistribution: we shell out to the user's own
|
|
246
|
+
// system package manager (brew/apt) to install or upgrade. The user remains
|
|
247
|
+
// the licensee under the tool's upstream license. License notes per tool are
|
|
248
|
+
// in BOOTSTRAP_TOOLS for transparency — and `--no-bootstrap-tools` lets users
|
|
249
|
+
// opt out entirely.
|
|
250
|
+
//
|
|
251
|
+
// Output is intentionally quiet: a single status line per tool, install output
|
|
252
|
+
// only surfaces on failure. Setup-flow is never blocked by a bootstrap-tool
|
|
253
|
+
// failure — the user gets a warning, the wizard continues.
|
|
254
|
+
|
|
255
|
+
const BOOTSTRAP_TOOLS = [
|
|
256
|
+
{
|
|
257
|
+
cmd: "yt-dlp",
|
|
258
|
+
name: "yt-dlp",
|
|
259
|
+
license: "Unlicense (public domain equivalent)",
|
|
260
|
+
install: { macos: "brew install yt-dlp", linux: "pipx install yt-dlp" },
|
|
261
|
+
// yt-dlp ships its own `-U` self-updater but it bails out for binaries
|
|
262
|
+
// installed via brew (can't write to /opt/homebrew/). Always go through
|
|
263
|
+
// the package manager that originally installed it.
|
|
264
|
+
upgrade: { macos: "brew upgrade yt-dlp", linux: "pipx upgrade yt-dlp" },
|
|
265
|
+
// Linux fallback if pipx isn't available
|
|
266
|
+
linuxSkipIf: "pipx",
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
cmd: "ffmpeg",
|
|
270
|
+
name: "ffmpeg",
|
|
271
|
+
license: "LGPL-2.1+ (brew default; auto-install via your package manager — you remain the licensee)",
|
|
272
|
+
install: { macos: "brew install ffmpeg", linux: "sudo apt-get install -y ffmpeg" },
|
|
273
|
+
upgrade: { macos: "brew upgrade ffmpeg", linux: "sudo apt-get install --only-upgrade -y ffmpeg" },
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
// Memoized: `brew update` is slow (5-30s) but needs to run at least once
|
|
278
|
+
// per session before `brew upgrade` so we get the latest formula info,
|
|
279
|
+
// not whatever was cached locally weeks ago. Without this we hit the
|
|
280
|
+
// "brew thinks yt-dlp is at 2026-03-17 because formula DB is stale" trap.
|
|
281
|
+
let brewFormulasRefreshed = false;
|
|
282
|
+
function refreshBrewFormulas() {
|
|
283
|
+
if (brewFormulasRefreshed) return;
|
|
284
|
+
if (!hasCommand("brew")) return;
|
|
285
|
+
try {
|
|
286
|
+
execSync("brew update --quiet", { stdio: "pipe", timeout: 90_000 });
|
|
287
|
+
} catch {
|
|
288
|
+
// brew update can fail transiently (rate limits, network). Non-fatal:
|
|
289
|
+
// we'll fall through with whatever cached formulas brew has.
|
|
290
|
+
}
|
|
291
|
+
brewFormulasRefreshed = true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Detect the major package manager available for non-interactive installs.
|
|
296
|
+
* Returns "macos" (homebrew), "linux" (apt-based), or null.
|
|
297
|
+
*/
|
|
298
|
+
function detectPlatformPm() {
|
|
299
|
+
if (process.platform === "darwin") return "macos";
|
|
300
|
+
if (process.platform === "linux") return "linux";
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Quietly install or upgrade a single bootstrap tool. Returns a brief status
|
|
306
|
+
* string for the caller to print. Never throws — failures are reported as
|
|
307
|
+
* { ok: false, message } so the calling step keeps going.
|
|
308
|
+
*/
|
|
309
|
+
function bootstrapOneTool(tool, platform) {
|
|
310
|
+
const cmdAvailable = hasCommand(tool.cmd);
|
|
311
|
+
|
|
312
|
+
// Linux-only prerequisite check (e.g. pipx for yt-dlp).
|
|
313
|
+
if (platform === "linux" && tool.linuxSkipIf && !hasCommand(tool.linuxSkipIf)) {
|
|
314
|
+
return { ok: false, message: `${tool.name} skipped — needs '${tool.linuxSkipIf}' on Linux` };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Decide whether the install/upgrade command goes through brew on macOS.
|
|
318
|
+
// If so, refresh the formula DB once before any upgrade attempts so we
|
|
319
|
+
// don't end up running `brew upgrade` against weeks-old formula info.
|
|
320
|
+
const installCmd = tool.install[platform];
|
|
321
|
+
const upgradeCmd = tool.upgrade && tool.upgrade[platform];
|
|
322
|
+
if (platform === "macos" && (String(installCmd).startsWith("brew") || String(upgradeCmd).startsWith("brew"))) {
|
|
323
|
+
refreshBrewFormulas();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Upgrade path: tool is already installed ────────────────────────────
|
|
327
|
+
if (cmdAvailable) {
|
|
328
|
+
if (!upgradeCmd) {
|
|
329
|
+
return { ok: true, message: `${tool.name} (already installed, no upgrade path on ${platform})` };
|
|
330
|
+
}
|
|
331
|
+
let beforeVersion = "";
|
|
332
|
+
try {
|
|
333
|
+
beforeVersion = execSync(`${tool.cmd} --version 2>&1 | head -1`, { stdio: "pipe", encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
334
|
+
} catch {}
|
|
335
|
+
try {
|
|
336
|
+
execSync(upgradeCmd, { stdio: "pipe", timeout: 120_000, encoding: "utf-8" });
|
|
337
|
+
let afterVersion = "";
|
|
338
|
+
try {
|
|
339
|
+
afterVersion = execSync(`${tool.cmd} --version 2>&1 | head -1`, { stdio: "pipe", encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
340
|
+
} catch {}
|
|
341
|
+
const upgraded = beforeVersion && afterVersion && beforeVersion !== afterVersion;
|
|
342
|
+
const verbInfix = upgraded ? "upgraded" : "up-to-date";
|
|
343
|
+
const versionTag = afterVersion ? ` (${afterVersion})` : "";
|
|
344
|
+
return { ok: true, message: `${tool.name} ${verbInfix}${versionTag}` };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const msg = String(err.stderr || err.message || err).split("\n")[0].slice(0, 120);
|
|
347
|
+
return { ok: true, message: `${tool.name} present (upgrade attempt failed: ${msg})` };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Fresh install ──────────────────────────────────────────────────────
|
|
352
|
+
if (!installCmd) {
|
|
353
|
+
return { ok: false, message: `${tool.name} skipped — no install command for ${platform}` };
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
execSync(installCmd, { stdio: "pipe", timeout: 240_000, encoding: "utf-8" });
|
|
357
|
+
let version = "";
|
|
358
|
+
try {
|
|
359
|
+
version = execSync(`${tool.cmd} --version 2>&1 | head -1`, { stdio: "pipe", encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
360
|
+
} catch {}
|
|
361
|
+
return { ok: true, message: `${tool.name} installed${version ? ` (${version})` : ""}` };
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const msg = String(err.stderr || err.message || err).split("\n")[0].slice(0, 120);
|
|
364
|
+
return { ok: false, message: `${tool.name} install failed (${msg})` };
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Run all bootstrap tools quietly. Used at the start of `alvin-bot setup` and
|
|
370
|
+
* in `alvin-bot update`. Skipped entirely if the caller passes
|
|
371
|
+
* `--no-bootstrap-tools`. On unsupported platforms (Windows etc.) this is a
|
|
372
|
+
* silent no-op — the user can install manually.
|
|
373
|
+
*/
|
|
374
|
+
async function ensureBootstrapTools(opts = {}) {
|
|
375
|
+
if (opts.skip) return;
|
|
376
|
+
const platform = detectPlatformPm();
|
|
377
|
+
if (!platform) return;
|
|
378
|
+
|
|
379
|
+
console.log("\n🎬 Setting up media tools (yt-dlp + ffmpeg)...");
|
|
380
|
+
|
|
381
|
+
// macOS needs brew on PATH — same trick as ensureBrewOnPath() uses.
|
|
382
|
+
if (platform === "macos" && !hasCommand("brew")) {
|
|
383
|
+
if (!ensureBrewOnPath()) {
|
|
384
|
+
console.log(" ⚠️ Skipping media-tool bootstrap — Homebrew not installed.");
|
|
385
|
+
console.log(" To enable: install brew from https://brew.sh and re-run setup.");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const tool of BOOTSTRAP_TOOLS) {
|
|
391
|
+
const result = bootstrapOneTool(tool, platform);
|
|
392
|
+
console.log(` ${result.ok ? "✓" : "⚠"} ${result.message}`);
|
|
393
|
+
}
|
|
394
|
+
console.log("");
|
|
395
|
+
}
|
|
396
|
+
|
|
238
397
|
function tryNpx(args) {
|
|
239
398
|
try {
|
|
240
399
|
execSync(`npx --no-install ${args}`, { stdio: "pipe", timeout: 5000 });
|
|
@@ -1405,7 +1564,7 @@ async function runPostSetupValidation(providerKey, apiKey, botToken, webPort) {
|
|
|
1405
1564
|
*/
|
|
1406
1565
|
function parseSetupArgs(argv) {
|
|
1407
1566
|
const args = {};
|
|
1408
|
-
const flags = new Set(["--non-interactive", "-y", "--yes", "--skip-validation"]);
|
|
1567
|
+
const flags = new Set(["--non-interactive", "-y", "--yes", "--skip-validation", "--no-bootstrap-tools"]);
|
|
1409
1568
|
const valueFlags = [
|
|
1410
1569
|
"--bot-token", "--allowed-users", "--primary-provider",
|
|
1411
1570
|
"--groq-key", "--google-key", "--openai-key", "--nvidia-key",
|
|
@@ -1558,6 +1717,11 @@ async function setup() {
|
|
|
1558
1717
|
return;
|
|
1559
1718
|
}
|
|
1560
1719
|
|
|
1720
|
+
// ── Auto-bootstrap media tools (yt-dlp + ffmpeg) ───────────────────────
|
|
1721
|
+
// Runs before any prompts so the user doesn't have to wait at the end.
|
|
1722
|
+
// Single-line status per tool. Failure is non-fatal — setup continues.
|
|
1723
|
+
await ensureBootstrapTools({ skip: !!args["no-bootstrap-tools"] });
|
|
1724
|
+
|
|
1561
1725
|
// ── Step 1: Telegram Bot
|
|
1562
1726
|
console.log(`\n━━━ ${t("setup.step1")} ━━━`);
|
|
1563
1727
|
console.log(t("setup.step1.intro"));
|
|
@@ -2360,6 +2524,8 @@ async function doctor() {
|
|
|
2360
2524
|
async function update() {
|
|
2361
2525
|
console.log(`${t("update.title")}\n`);
|
|
2362
2526
|
|
|
2527
|
+
const skipBootstrap = process.argv.includes("--no-bootstrap-tools");
|
|
2528
|
+
|
|
2363
2529
|
try {
|
|
2364
2530
|
const isGit = existsSync(resolve(process.cwd(), ".git"));
|
|
2365
2531
|
|
|
@@ -2376,6 +2542,10 @@ async function update() {
|
|
|
2376
2542
|
execSync("npm update alvin-bot", { stdio: "inherit" });
|
|
2377
2543
|
console.log(`\n ✅ ${t("update.done")}`);
|
|
2378
2544
|
}
|
|
2545
|
+
|
|
2546
|
+
// Refresh bootstrap media tools (yt-dlp / ffmpeg) — keeps them in sync
|
|
2547
|
+
// with the alvin-bot version that just got installed.
|
|
2548
|
+
await ensureBootstrapTools({ skip: skipBootstrap });
|
|
2379
2549
|
} catch (err) {
|
|
2380
2550
|
console.error(`\n ❌ ${t("update.failed")} ${err.message}`);
|
|
2381
2551
|
}
|
|
@@ -2573,7 +2743,19 @@ async function launchdInstall() {
|
|
|
2573
2743
|
if (pm2HadAlvinBot) {
|
|
2574
2744
|
try {
|
|
2575
2745
|
execSync("pm2 delete alvin-bot", { stdio: "pipe" });
|
|
2576
|
-
|
|
2746
|
+
// CRITICAL: `pm2 delete` only mutates the live process list; the
|
|
2747
|
+
// persisted dump (~/.pm2/dump.pm2) keeps a stale copy. Without
|
|
2748
|
+
// `pm2 save --force` here, the next time PM2 resurrects on
|
|
2749
|
+
// login it will re-spawn alvin-bot from the dump — fighting with
|
|
2750
|
+
// the new launchd-managed bot for the same BOT_TOKEN and racking
|
|
2751
|
+
// up Telegram getUpdates 409 conflicts (six-figure restart counts
|
|
2752
|
+
// in extreme cases). `--force` ensures the save runs even when
|
|
2753
|
+
// the resulting list is empty (default `pm2 save` warns and
|
|
2754
|
+
// skips the write on an empty list).
|
|
2755
|
+
try {
|
|
2756
|
+
execSync("pm2 save --force", { stdio: "pipe" });
|
|
2757
|
+
} catch { /* save failed — non-fatal, user has clean live list either way */ }
|
|
2758
|
+
console.log("🧹 Removed alvin-bot from pm2 and persisted (other pm2 projects left intact).");
|
|
2577
2759
|
} catch { /* already gone */ }
|
|
2578
2760
|
}
|
|
2579
2761
|
} catch { /* pm2 not installed — nothing to clean up */ }
|