alvin-bot 4.15.2 β†’ 4.16.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,58 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.16.1] β€” 2026-04-20
6
+
7
+ ### πŸ†• Feature: /update shows release highlights
8
+
9
+ After a successful `/update`, the bot now sends a second short message with a bullet-point summary of what actually changed in the newly installed version. Pulled from the CHANGELOG entry matching the version string in the update result.
10
+
11
+ **Implementation:**
12
+ - New module `src/services/release-highlights.ts` parses the CHANGELOG block for a given version and returns at most 5 bullet points, ≀500 chars total.
13
+ - Strategy: prefer `### ` subsection headlines (feature/fix titles); fall back to first non-empty paragraph lines.
14
+ - Telegram-friendly output: plain bullets (`β€’ ...`), no tables, no code blocks, truncates gracefully with an ellipsis line if too long.
15
+
16
+ **Result format in chat:**
17
+ ```
18
+ βœ… Installed v4.16.1 (was v4.16.0). Restarting...
19
+ πŸ“ What's new in v4.16.1
20
+
21
+ β€’ Feature: /update shows release highlights
22
+ ```
23
+
24
+ ## [4.16.0] β€” 2026-04-20
25
+
26
+ ### πŸš€ Feature: bot-owned CDP Chromium β€” no more hub dependency
27
+
28
+ **Problem for new users:** The bot's CDP strategy and the `browse` / `social-fetch` skills referenced `~/.claude/hub/SCRIPTS/browser.sh` β€” a private tooling setup that only the maintainer has. New npm installs silently lacked a working CDP path; the skill-documented commands errored with "file not found". A second failure mode: when a user followed any online guide to start Chrome with `--remote-debugging-port` while their daily Chrome was already running, macOS LaunchServices silently routed the call to the existing instance without applying the flag (log: "Wird in einer aktuellen Browsersitzung geΓΆffnet"), and no CDP endpoint came up.
29
+
30
+ **Fix β€” three additions:**
31
+
32
+ 1. **`src/services/cdp-bootstrap.ts` (new):** Spawns Playwright's bundled *Google Chrome for Testing* binary with a distinct bundle ID β€” zero conflict with the user's daily Chrome. Dynamic binary resolution walks the latest `chromium-NNNN/` cache directory; cross-platform (macOS arm64/x64, Linux, Windows). Idempotent `ensureRunning()` β€” safe to call from multiple concurrent code paths, serialized via a single-flight lock. Cleans stale PID files, verifies liveness via both process signal and CDP `/json/version` probe, captures Chromium stderr to `~/.alvin-bot/browser/chrome-cdp.log` for diagnosis.
33
+
34
+ 2. **`alvin-bot browser` CLI subcommand (new):** Stable shell interface that works on every install β€” `start`, `stop`, `status`, `goto`, `shot`, `eval`, `tabs`, `doctor`. Wraps the bootstrap so agents in skills have a single, documented command. Screenshots default to `~/.alvin-bot/browser/screenshots/`.
35
+
36
+ 3. **`browser-manager` rewired:** The `cdp` strategy now calls `cdp-bootstrap.ensureRunning()` first (works for every install), and only falls back to the hub script if present (maintainer-only dev convenience). The whole cascade still works with no hub at all.
37
+
38
+ **Skills updated:**
39
+ - `skills/browse/SKILL.md` β€” rewritten to use `alvin-bot browser ...` commands; hub-script references removed (kept as "if present" note for dev environments).
40
+ - `skills/social-fetch/SKILL.md` β€” CDP fallback line uses `alvin-bot browser goto/shot`.
41
+
42
+ **Docs:**
43
+ - `CLAUDE.md` β€” browser automation section switched to `alvin-bot browser` everywhere. Tier 0 (curl/WebFetch) now explicit as the cheapest path. Tier 1 example uses inline `node -e` + Playwright (no hub dependency).
44
+ - `src/paths.ts` β€” `HUB_BROWSER_SH` annotated as dev-only optional. New paths: `CDP_PROFILE_DIR`, `CDP_SCREENSHOTS_DIR`, `CDP_PID_FILE`, `CDP_LOG_FILE` under `~/.alvin-bot/browser/`.
45
+
46
+ **First-run setup (one-time):**
47
+ ```bash
48
+ npx playwright install chromium
49
+ ```
50
+
51
+ **Verified on 2026-04-20 with user's daily Chrome running:**
52
+ - `alvin-bot browser start` β†’ PID + endpoint, no LaunchServices hijack
53
+ - `alvin-bot browser stop` + immediate `alvin-bot browser shot <url>` β†’ CDP auto-starts, screenshot written (15 KB PNG in `~/.alvin-bot/browser/screenshots/`)
54
+ - `alvin-bot browser doctor` β†’ all 4 checks green (binary, endpoint, PID, profile lock)
55
+ - `npm test` β†’ 504/504 tests passing
56
+
5
57
  ## [4.15.2] β€” 2026-04-17
6
58
 
7
59
  ### πŸ› Fix: sleep-aware heartbeat prevents false failover after macOS wake
@@ -124,7 +176,7 @@ Four hardcoded Claude model IDs replaced with current strings: `claude-sonnet-4-
124
176
 
125
177
  ### πŸ› Patch: watcher zombie-entry fix (missing outputFile > 10 min = failed)
126
178
 
127
- **Edge case Ali caught today:** a pending async-agent entry stuck in `/subagents list` for 3+ hours showing "running" β€” but the underlying `alvin_dispatch_agent` subprocess had already died (its output file was gone). The entry would have continued haunting the list until the 12-hour `giveUpAt` ceiling fired.
179
+ **Edge case the maintainer caught today:** a pending async-agent entry stuck in `/subagents list` for 3+ hours showing "running" β€” but the underlying `alvin_dispatch_agent` subprocess had already died (its output file was gone). The entry would have continued haunting the list until the 12-hour `giveUpAt` ceiling fired.
128
180
 
129
181
  **Root cause:** `async-agent-watcher`'s `pollOnce` handled four states from `parseOutputFileStatus` β€” `completed` / `failed` / `running` / `missing`. For `missing` (file doesn't exist or is empty), the watcher just kept polling forever, on the assumption that a slow subprocess might eventually write. If the subprocess crashed before writing ANY output, the file never appeared, and we polled for 12 hours before timing out.
130
182
 
@@ -167,7 +219,7 @@ Four hardcoded Claude model IDs replaced with current strings: `claude-sonnet-4-
167
219
 
168
220
  ### πŸ› Patch: `/subagents list` now shows v4.13+ dispatch agents too
169
221
 
170
- **Bug Ali caught:** typing `/subagents list` in Telegram while a `alvin_dispatch_agent` sub-agent was actively running returned "no agents running" β€” even though the user could see the agent finish and deliver a result shortly after. Cross-platform effect too: `/alvin` slash command on Slack had the same display gap.
222
+ **Bug the maintainer caught:** typing `/subagents list` in Telegram while a `alvin_dispatch_agent` sub-agent was actively running returned "no agents running" β€” even though the user could see the agent finish and deliver a result shortly after. Cross-platform effect too: `/alvin` slash command on Slack had the same display gap.
171
223
 
172
224
  **Root cause:** two separate registries for sub-agents:
173
225
  - `src/services/subagents.ts` `activeAgents` Map β€” used since v4.0.0 for bot-level sub-agents (cron spawns, implicit Task tool children, `/sub-agents spawn` CLI)
@@ -422,7 +474,7 @@ This matches the OpenClaw experience the user was asking about β€” except it's b
422
474
 
423
475
  ### πŸ› Patch: recover partial output from interrupted background sub-agents
424
476
 
425
- **The bug Ali saw:** Two Telegram messages appeared hours apart: `⏱️ Background agent a5bf8c74 timeout · 720m 3s · 0 in / 0 out` and `... ab9372d4 timeout · 720m 1s · 0 in / 0 out`, both with `(empty output)`. Three more agents were still pending, all interrupted mid-execution with hundreds of KB of real work sitting on disk.
477
+ **The bug the maintainer saw:** Two Telegram messages appeared hours apart: `⏱️ Background agent a5bf8c74 timeout · 720m 3s · 0 in / 0 out` and `... ab9372d4 timeout · 720m 1s · 0 in / 0 out`, both with `(empty output)`. Three more agents were still pending, all interrupted mid-execution with hundreds of KB of real work sitting on disk.
426
478
 
427
479
  **Root cause:** v4.12.3's bypass-abort calls `session.abortController.abort()`, which propagates through `claude-sdk-provider.ts`'s `internalAbortController` into the SDK's CLI subprocess, which in turn propagates into any in-flight `Agent(run_in_background: true)` tool executions. Evidence from the disk:
428
480
 
@@ -471,7 +523,7 @@ Result: on the next `pollOnce()` after v4.12.4 ships, the three stuck agents get
471
523
 
472
524
  ### πŸ› Patch: Background sub-agent no longer blocks the main Telegram session
473
525
 
474
- **The bug Ali reported:** After launching an async sub-agent (`run_in_background: true`), sending any follow-up message to the bot silently stalled for 2+ minutes before being processed. v4.12.1/v4.12.2 attempted a prompt-hint mitigation but did NOT address the architectural root cause.
526
+ **The bug the maintainer reported:** After launching an async sub-agent (`run_in_background: true`), sending any follow-up message to the bot silently stalled for 2+ minutes before being processed. v4.12.1/v4.12.2 attempted a prompt-hint mitigation but did NOT address the architectural root cause.
475
527
 
476
528
  **Root cause (re-diagnosed with live SDK event logs):** The Claude Agent SDK's CLI subprocess stays alive for the full duration of a background task so it can inject the `<task-notification>` inline into the NEXT assistant turn. While that subprocess idles, Alvin's query iterator is still being drained, `session.isProcessing` stays `true`, and every new user message gets pushed into the 3-slot queue β€” which doesn't auto-drain. From the user's perspective: send "A" β†’ nothing happens for 2 minutes.
477
529
 
@@ -693,7 +745,7 @@ Both the platform handler (Slack/Discord/WhatsApp) and the Telegram main handler
693
745
 
694
746
  #### P0 #4 β€” Slack Setup Documentation (`docs/install/slack-setup.md`, `docs/install/slack-manifest.json`)
695
747
 
696
- Step-by-step guide: create Slack App from manifest β†’ Socket Mode β†’ App-Level Token β†’ Bot Token β†’ `~/.alvin-bot/.env` β†’ restart β†’ invite bot β†’ create workspace files. Covers troubleshooting for common issues. The `slack-manifest.json` is copy-paste-ready: pre-configured bot user, all required scopes, event subscriptions, Socket Mode enabled. Both files are gitignored (Ali's docs/install/ convention) and ship via GitHub Release assets.
748
+ Step-by-step guide: create Slack App from manifest β†’ Socket Mode β†’ App-Level Token β†’ Bot Token β†’ `~/.alvin-bot/.env` β†’ restart β†’ invite bot β†’ create workspace files. Covers troubleshooting for common issues. The `slack-manifest.json` is copy-paste-ready: pre-configured bot user, all required scopes, event subscriptions, Socket Mode enabled. Both files are gitignored (the maintainer's docs/install/ convention) and ship via GitHub Release assets.
697
749
 
698
750
  #### P1 #1 β€” Slack Progress Ticker (`src/platforms/slack.ts`)
699
751
 
@@ -707,7 +759,7 @@ Step-by-step guide: create Slack App from manifest β†’ Socket Mode β†’ App-Level
707
759
 
708
760
  `SlackAdapter.setTyping()` now calls `assistant.threads.setStatus` so Slack shows "Alvin is thinking…" under the message during long queries. Silently no-ops in channels where the assistant scope isn't granted.
709
761
 
710
- New `SlackAdapter.getChannelName(channelId)` resolves + caches channel names via `conversations.info`. `platform-message.ts` detects this helper via duck-typing on the adapter and passes the resolved name to `resolveWorkspaceOrDefault` β€” enabling channel-name matching (`#alev-b` β†’ `workspaces/alev-b.md`) without hardcoding the Slack type in the platform handler.
762
+ New `SlackAdapter.getChannelName(channelId)` resolves + caches channel names via `conversations.info`. `platform-message.ts` detects this helper via duck-typing on the adapter and passes the resolved name to `resolveWorkspaceOrDefault` β€” enabling channel-name matching (`#my-project` β†’ `workspaces/my-project.md`) without hardcoding the Slack type in the platform handler.
711
763
 
712
764
  #### P1 #3 β€” Telegram `/workspace` + `/workspaces` Commands
713
765
 
@@ -823,7 +875,7 @@ Inspired by Mem0's auto-extraction. When `compactSession()` archives old message
823
875
 
824
876
  - **mempalace as MCP server: rejected.** Considered installing mempalace as a Python MCP service. Rejected because (1) Alvin is all-TypeScript and adding a 2nd Python service to launchd is operational complexity, (2) Alvin already has an embeddings vector index β€” mempalace would be a parallel duplicate, (3) mempalace's MCP tools are only consumed by the SDK; cron jobs, sub-agents, and non-SDK providers wouldn't see them. Conclusion: **adopt the patterns natively** (L0–L3 layering, AAAK-style structured extraction) rather than running a second service.
825
877
  - **SQLite migration deferred.** The 128 MB JSON embeddings index is a known performance issue and is already noted in `~/.claude/projects/-Users-alvin-de/memory/project_alvinbot_sqlite_migration.md` for v4.12+. Orthogonal to the "frickelig nach Restart" UX problem this release targets.
826
- - **Multi-user isolation deferred.** Memories are still global per data dir. Single-user use case, not a privacy concern for Ali's setup.
878
+ - **Multi-user isolation deferred.** Memories are still global per data dir. Single-user use case, not a privacy concern for the maintainer's setup.
827
879
  - **Decay/aging deferred.** Daily logs grow monotonically. Will be addressed alongside SQLite migration.
828
880
 
829
881
  #### Testing
@@ -898,11 +950,11 @@ Live-verified via isolated SDK probe (`node sdk-probe.mjs` inside the repo) whic
898
950
 
899
951
  #### What you'll see as a user
900
952
 
901
- Send: *"Make a SEO audit of gethomes.io and alev-b.com in parallel"*
953
+ Send: *"Make a SEO audit of example.com and example.com in parallel"*
902
954
 
903
955
  - **0 s** β€” Claude responds: *"Starting both audits in the background β€” I'll send the reports when done."* Main session **unlocks**.
904
956
  - **1–10 min later** β€” You can chat about anything else. The bot answers immediately.
905
- - **~13 min** (when each agent finishes) β€” Two separate banner messages arrive: *"βœ… SEO audit gethomes.io completed Β· 13m 17s Β· 2.6M in / 28k out"* + the full report body, delivered via the v4.9.3 Markdownβ†’plain-text fallback path.
957
+ - **~13 min** (when each agent finishes) β€” Two separate banner messages arrive: *"βœ… SEO audit example.com completed Β· 13m 17s Β· 2.6M in / 28k out"* + the full report body, delivered via the v4.9.3 Markdownβ†’plain-text fallback path.
906
958
 
907
959
  #### Non-goals
908
960
 
@@ -961,7 +1013,7 @@ He was right. My v4.9.0 `stopWebServer()` fix was *prevention* β€” it stopped th
961
1013
 
962
1014
  ### πŸ›  Two UX bugs found in production after v4.9.2 β€” now closed
963
1015
 
964
- Ali triggered `/cron run Daily Job Alert` after the v4.9.2 deploy and saw 13 minutes of chat silence followed by nothing. Forensics on the live bot revealed two distinct problems on top of an already-successful run:
1016
+ the maintainer triggered `/cron run Daily Job Alert` after the v4.9.2 deploy and saw 13 minutes of chat silence followed by nothing. Forensics on the live bot revealed two distinct problems on top of an already-successful run:
965
1017
 
966
1018
  **1. `subagent-delivery` has been silently dropping every banner for days.** Err.log: `GrammyError: Call to 'sendMessage' failed! (400: Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 2636)`. The daily-job-alert sub-agent produces markdown-dense output (`|` tables, `**bold**`, `\|` escapes, mixed asterisks). Telegram's Markdown parser refuses it, `api.sendMessage(..., parse_mode: "Markdown")` throws, and the bare try/catch in `deliverSubAgentResult` logs + bails. **Result: the user has never seen a sub-agent-delivery banner, even when the underlying run succeeded perfectly and emailed the HTML report correctly.**
967
1019
 
@@ -1090,12 +1142,12 @@ The `browse` skill used to instruct the agent to start `node scripts/browse-serv
1090
1142
  - **Tier 1** β€” `browser.sh stealth <url>` (Playwright + stealth plugin, headless, Cloudflare-masking)
1091
1143
  - **Tier 2** β€” `browser.sh cdp {start|goto|shot|tabs|stop}` (real Chrome with persistent profile at `~/.claude/hub/BROWSER/profile/`, login cookies survive restarts)
1092
1144
  - **Tier 3** β€” Claude-in-Chrome extension via MCP tools (interactive CLI only)
1093
- - Explicit escalation ladder (WebFetch β†’ stealth β†’ CDP β†’ ask Ali to log in) and a `NIEMALS browse-server.cjs nutzen` anti-rule.
1145
+ - Explicit escalation ladder (WebFetch β†’ stealth β†’ CDP β†’ ask the maintainer to log in) and a `NIEMALS browse-server.cjs nutzen` anti-rule.
1094
1146
  - Concrete working targets (StepStone βœ…, Michael Page βœ…, LinkedIn βœ… with login, Indeed ❌) so the agent knows what to try where.
1095
1147
 
1096
1148
  - **`src/services/browser-manager.ts` β€” hardened fallback chain.** The multi-strategy manager already had the right *shape* (`gateway β†’ cdp β†’ hub-stealth β†’ cli`) but several ops silently broke or hung:
1097
1149
  - **`gatewayRequest` now has a 15 s timeout** (`req.destroy` on elapse). Previously a hung gateway would wedge the caller forever.
1098
- - **CDP fallback for interactive ops.** `click`, `fill`, `type`, `press`, `scroll`, `evaluate`, `info`, and `getTree` used to hard-throw `"requires gateway"` when `browse-server.cjs` wasn't running. They now try the gateway first, then a short-lived `chromium.connectOverCDP()` via a new `withCdpPage()` helper that reuses Ali's live Chrome on port 9222. Refs are interpreted as CSS selectors when gateway is absent.
1150
+ - **CDP fallback for interactive ops.** `click`, `fill`, `type`, `press`, `scroll`, `evaluate`, `info`, and `getTree` used to hard-throw `"requires gateway"` when `browse-server.cjs` wasn't running. They now try the gateway first, then a short-lived `chromium.connectOverCDP()` via a new `withCdpPage()` helper that reuses the maintainer's live Chrome on port 9222. Refs are interpreted as CSS selectors when gateway is absent.
1099
1151
  - **Explicit PNG extension** on auto-generated screenshot filenames (`shot_<ts>.png`) so Playwright's format inference is unambiguous.
1100
1152
  - **Better error messages** β€” every "needs interactive" throw now includes the exact command to start CDP Chrome (`~/.claude/hub/SCRIPTS/browser.sh cdp start headless`).
1101
1153
 
@@ -1124,7 +1176,7 @@ Sub-agents and `ai-query` cron jobs used to hard-cap at 5 minutes (`SUBAGENT_TIM
1124
1176
 
1125
1177
  ### πŸ› Silenced harmless `message is not modified` Telegram errors
1126
1178
 
1127
- Occasionally Ali would see a red banner at the bottom of an Alvin message:
1179
+ Occasionally the maintainer would see a red banner at the bottom of an Alvin message:
1128
1180
 
1129
1181
  > Error: Call to 'editMessageText' failed! (400: Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message)
1130
1182
 
@@ -1159,7 +1211,7 @@ After 4.8.7, running `/update` after a manual rebuild will correctly say *"Disk
1159
1211
 
1160
1212
  ### ✨ Internal watchdog with crash-loop brake (`src/services/watchdog.ts`)
1161
1213
 
1162
- Ali asked for "derbe persistent" β€” already 95% there with `KeepAlive: true` from 4.8.6, but the missing piece was a brake to stop the bot from infinite-restart-looping if a deterministic crash happens (corrupt state file, missing dependency, broken upgrade).
1214
+ the maintainer asked for "derbe persistent" β€” already 95% there with `KeepAlive: true` from 4.8.6, but the missing piece was a brake to stop the bot from infinite-restart-looping if a deterministic crash happens (corrupt state file, missing dependency, broken upgrade).
1163
1215
 
1164
1216
  **New module**: `src/services/watchdog.ts`. Two responsibilities:
1165
1217
 
@@ -1274,7 +1326,7 @@ After 4.8.5, `/update` on the test MacBook will correctly detect the npm install
1274
1326
 
1275
1327
  ### πŸ› WhatsApp self-chat detection for the new `@lid` identity format
1276
1328
 
1277
- Ali reported that the WhatsApp bot wasn't responding to "Hi" in his self-chat even after enabling both `Self-chat only` and `Reply to private messages` in the Web UI. Debug logging showed the bot receiving the message correctly and detecting `fromMe=true`, but then hitting the "skip: own message in group/DM" branch because `isSelfChat()` was returning `false`.
1329
+ the maintainer reported that the WhatsApp bot wasn't responding to "Hi" in his self-chat even after enabling both `Self-chat only` and `Reply to private messages` in the Web UI. Debug logging showed the bot receiving the message correctly and detecting `fromMe=true`, but then hitting the "skip: own message in group/DM" branch because `isSelfChat()` was returning `false`.
1278
1330
 
1279
1331
  **Root cause**: WhatsApp has rolled out a new privacy feature that replaces phone-number JIDs in self-chats (and some groups) with a **LID β€” Linked Identity**. Instead of `4917661236656@s.whatsapp.net`, messages in a self-chat now arrive with `jid = "162805718225143@lid"` β€” a completely opaque identifier that looks nothing like the phone number.
1280
1332
 
@@ -1346,7 +1398,7 @@ Offline-friendly status command β€” no running bot required. Prints:
1346
1398
  - On Linux/Windows: `pm2 jlist` check for the `alvin-bot` process
1347
1399
  - **Live info** (when the bot is running with the web UI on :3100): Uptime, active model
1348
1400
 
1349
- Answers Ali's request: *"alvin-bot status im Terminal soll auch die Version anzeigen"*. The command prominently features the version at the top so it's the first thing you see.
1401
+ Answers the maintainer's request: *"alvin-bot status im Terminal soll auch die Version anzeigen"*. The command prominently features the version at the top so it's the first thing you see.
1350
1402
 
1351
1403
  Example:
1352
1404
 
@@ -1449,7 +1501,7 @@ New behavior in `bin/cli.js`:
1449
1501
  - **pm2 now empty** β†’ *"pm2 now has zero managed processes. Remove it with: `npm uninstall -g pm2`"*
1450
1502
  - **pm2 still has other projects** β†’ *"pm2 still has other projects running β€” leaving it installed."*
1451
1503
 
1452
- 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.
1504
+ Caught immediately after 4.7.0 shipped when the maintainer pointed out his Mac mini has `polyseus` in pm2 alongside `alvin-bot` and didn't want it touched.
1453
1505
 
1454
1506
  ## [4.7.0] β€” 2026-04-11
1455
1507
 
@@ -1829,7 +1881,7 @@ Remaining unaddressed (by design, require breaking upgrades or overrides):
1829
1881
 
1830
1882
  ### ✨ Stability Improvements
1831
1883
 
1832
- **Session memory hygiene (`src/services/session.ts`)** β€” The in-memory `sessions` Map grew unbounded: every user that ever messaged the bot kept a full session object (including conversation history, cost breakdown, abort controller) forever. On a single-user bot like Ali's this is a non-issue; on any multi-user deployment it's a steady leak.
1884
+ **Session memory hygiene (`src/services/session.ts`)** β€” The in-memory `sessions` Map grew unbounded: every user that ever messaged the bot kept a full session object (including conversation history, cost breakdown, abort controller) forever. On a single-user bot like the maintainer's this is a non-issue; on any multi-user deployment it's a steady leak.
1833
1885
 
1834
1886
  New behavior:
1835
1887
  - **Conservative 7-day TTL**: a session is only eligible for cleanup after 7 full days of complete inactivity. Configurable via `ALVIN_SESSION_TTL_DAYS` env var.
package/README.md CHANGED
@@ -335,32 +335,32 @@ alvin-bot/
335
335
 
336
336
  ### Why you'd want this
337
337
 
338
- Without workspaces, Alvin has one big blob of context. If you ask about Alev-B deployment right after debugging a trading bot, Claude pollutes one context with the other. Workspaces solve this: **Slack channel = session**, or on Telegram, **`/workspace alev-b` = session**. Each one has its own Claude SDK `resume` token, history, and current project CLAUDE.md loaded via its working directory.
338
+ Without workspaces, Alvin has one big blob of context. If you ask about one project's deployment right after debugging a completely unrelated service, Claude pollutes one context with the other. Workspaces solve this: **Slack channel = session**, or on Telegram, **`/workspace my-project` = session**. Each one has its own Claude SDK `resume` token, history, and current project CLAUDE.md loaded via its working directory.
339
339
 
340
340
  ### How it works
341
341
 
342
342
  1. **Drop a markdown file** into `~/.alvin-bot/workspaces/<name>.md` with YAML frontmatter.
343
343
  2. **Alvin hot-reloads** the workspace registry (no restart needed β€” same pattern as skills).
344
- 3. On **Slack**, workspaces resolve by explicit channel ID first, then by channel name match (`#alev-b` β†’ `workspaces/alev-b.md`, case-insensitive).
344
+ 3. On **Slack**, workspaces resolve by explicit channel ID first, then by channel name match (`#my-project` β†’ `workspaces/my-project.md`, case-insensitive).
345
345
  4. On **Telegram**, run `/workspace <name>` to switch β€” next message uses the new persona and cwd.
346
346
  5. Nothing configured? Alvin falls back to the "default" workspace exactly like pre-v4.12 β€” **no breaking changes**.
347
347
 
348
348
  ### Example workspace file
349
349
 
350
- Create `~/.alvin-bot/workspaces/alev-b.md`:
350
+ Create `~/.alvin-bot/workspaces/my-project.md`:
351
351
 
352
352
  ```markdown
353
353
  ---
354
- purpose: Alev-B consulting website dev
355
- cwd: ~/Projects/alev-b-website
354
+ purpose: my-project website dev
355
+ cwd: ~/Projects/my-project
356
356
  emoji: "🏒"
357
357
  color: "#6366f1"
358
358
  channels: ["C01ABCDEF"]
359
359
  ---
360
- You are focused on the Alev-B consulting website. Stack: React + Express +
361
- Drizzle + MySQL. Production VPS 72.62.34.230, deploy via rsync. Prefer
362
- concise, directly actionable answers about features, deployment, and
363
- Stripe integration.
360
+ You are focused on the my-project website. Stack: React + Express +
361
+ Drizzle + MySQL. Production VPS at your-vps.example.com, deploy via rsync.
362
+ Prefer concise, directly actionable answers about features, deployment,
363
+ and Stripe integration.
364
364
  ```
365
365
 
366
366
  The `cwd` auto-loads the project-specific `CLAUDE.md` via Claude SDK's `settingSources: ["user", "project"]`, so each workspace inherits its project's conventions automatically. `channels` is optional β€” omit it to match by filename.
@@ -405,7 +405,7 @@ curl -s http://localhost:3100/api/workspaces | jq
405
405
 
406
406
  ### Architecture guarantees
407
407
 
408
- - **Memory is global.** Facts Alvin learns in `#alev-b` are visible in `#homes` via the shared `MEMORY.md` and embeddings index. Per-workspace memory layer is on the v4.13 roadmap.
408
+ - **Memory is global.** Facts Alvin learns in one workspace are visible in every other workspace via the shared `MEMORY.md` and embeddings index. Per-workspace memory layer is on the v4.13 roadmap.
409
409
  - **Sub-agents are per-session.** Each workspace can dispatch its own detached sub-agents via `alvin_dispatch_agent` β€” results come back to the originating channel on any platform (Telegram, Slack, Discord, WhatsApp), visible in `/subagents list` (v4.13.0+ dispatch, v4.14.0 cross-platform, v4.14.1 unified list view).
410
410
  - **Session state survives restart.** Claude SDK `resume` tokens, conversation history, language, effort, and `workspaceName` all persist via `session-persistence.ts` (v4.11.0).
411
411
  - **Backwards compatible.** If you don't create any workspace files, everything behaves exactly like v4.11. Upgrade is a no-op.
@@ -658,11 +658,11 @@ alvin-bot version # Show version
658
658
  - [x] Watcher zombie guard β€” missing outputFile > 10 min delivers as failed instead of 12h timeout (v4.14.2)
659
659
  - [x] Staleness-based partial output recovery for interrupted sub-agents (v4.12.4)
660
660
  - [ ] SQLite migration of the embeddings index (currently 128 MB JSON)
661
- - [ ] Per-workspace memory layer (additive over global) β€” facts learned in `#alev-b` stay in `alev-b` unless explicitly promoted to global
662
- - [ ] Per-workspace provider override (`provider:` in frontmatter) β€” e.g. Alev-B uses Claude Opus, JobSnack uses cheap Gemini
661
+ - [ ] Per-workspace memory layer (additive over global) β€” facts learned in one workspace stay there unless explicitly promoted to global
662
+ - [ ] Per-workspace provider override (`provider:` in frontmatter) β€” e.g. one workspace uses Claude Opus, another uses a cheaper model
663
663
  - [ ] Per-workspace skill allowlist β€” scope Apple Notes to personal workspace, sysadmin only to devops workspace, etc.
664
664
  - [ ] Multi-User Slack (real `per-channel-peer` mode) β€” different users in the same Slack channel get their own sub-sessions
665
- - [ ] Workspace cloning / templates β€” `/workspace clone alev-b as homes-dev` spins up a new workspace from an existing one
665
+ - [ ] Workspace cloning / templates β€” `/workspace clone my-project as my-fork` spins up a new workspace from an existing one
666
666
  - [ ] Daily log decay / archive β€” older daily logs move to cold storage after N days
667
667
  - [ ] **Phase 18** β€” Security + Platform hardening (from v4.12.1 audit, prioritized)
668
668
  - [ ] **P1 β€” Electron major upgrade** (35 β†’ 41+) β€” fixes 1 HIGH + 5 MODERATE Electron CVEs in the Desktop-Build path. Major version jump, requires full rebuild + test of `.dmg` flow. Separate release (likely bundled with Windows `.exe` work).
package/bin/cli.js CHANGED
@@ -1928,6 +1928,129 @@ switch (cmd) {
1928
1928
  console.log("");
1929
1929
  process.exit(0);
1930
1930
  }
1931
+ case "browser": {
1932
+ // Browser subcommands: wraps cdp-bootstrap so Skills + humans have a
1933
+ // stable shell interface that works everywhere the bot is installed.
1934
+ const sub = process.argv[3];
1935
+ const { dist } = await import("../dist/services/cdp-bootstrap.js").then(
1936
+ (m) => ({ dist: m }),
1937
+ async () => {
1938
+ console.error("❌ dist/services/cdp-bootstrap.js not found. Run: npm run build");
1939
+ process.exit(1);
1940
+ }
1941
+ );
1942
+ try {
1943
+ switch (sub) {
1944
+ case "start": {
1945
+ const mode = process.argv[4] === "headful" ? "headful" : "headless";
1946
+ const st = await dist.ensureRunning({ mode });
1947
+ console.log(`βœ… CDP running β€” PID ${st.pid} β€” ${st.endpoint}`);
1948
+ if (st.binary) console.log(` Binary: ${st.binary}`);
1949
+ break;
1950
+ }
1951
+ case "stop": {
1952
+ await dist.stop();
1953
+ console.log("βœ… CDP stopped");
1954
+ break;
1955
+ }
1956
+ case "status": {
1957
+ const st = await dist.status();
1958
+ if (st.running) {
1959
+ console.log(`βœ… CDP running β€” PID ${st.pid}`);
1960
+ } else {
1961
+ console.log(`❌ CDP not running: ${st.reason || "unknown"}`);
1962
+ }
1963
+ if (st.binary) console.log(` Binary: ${st.binary}`);
1964
+ console.log(` Endpoint: ${st.endpoint}`);
1965
+ break;
1966
+ }
1967
+ case "doctor": {
1968
+ const rep = await dist.doctor();
1969
+ console.log("=== Browser Doctor ===\n");
1970
+ for (const c of rep.checks) {
1971
+ console.log(`${c.ok ? "βœ…" : "❌"} ${c.name}: ${c.detail}`);
1972
+ }
1973
+ console.log(rep.ok ? "\nAll checks passed." : "\nSome checks failed β€” see above.");
1974
+ process.exit(rep.ok ? 0 : 1);
1975
+ }
1976
+ case "goto":
1977
+ case "shot":
1978
+ case "screenshot":
1979
+ case "tabs":
1980
+ case "eval": {
1981
+ await dist.ensureRunning({ mode: "headless" });
1982
+ const { chromium } = await import("playwright").catch(() => ({ chromium: null }));
1983
+ if (!chromium) {
1984
+ console.error("❌ playwright not available. Run: npm install");
1985
+ process.exit(1);
1986
+ }
1987
+ const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
1988
+ try {
1989
+ if (sub === "tabs") {
1990
+ const tabs = [];
1991
+ for (const ctx of browser.contexts()) {
1992
+ for (const page of ctx.pages()) {
1993
+ tabs.push({ title: await page.title(), url: page.url() });
1994
+ }
1995
+ }
1996
+ console.log(JSON.stringify(tabs, null, 2));
1997
+ break;
1998
+ }
1999
+ const url = process.argv[4];
2000
+ if (!url) {
2001
+ console.error(`Usage: alvin-bot browser ${sub} <url> [args]`);
2002
+ process.exit(1);
2003
+ }
2004
+ const ctx = browser.contexts()[0] || (await browser.newContext());
2005
+ const page = await ctx.newPage();
2006
+ try {
2007
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
2008
+ if (sub === "goto") {
2009
+ console.log(JSON.stringify({ url: page.url(), title: await page.title() }, null, 2));
2010
+ } else if (sub === "shot" || sub === "screenshot") {
2011
+ const name = process.argv[5] || `shot_${Date.now()}.png`;
2012
+ const { CDP_SCREENSHOTS_DIR } = await import("../dist/paths.js");
2013
+ const out = name.startsWith("/") ? name : `${CDP_SCREENSHOTS_DIR}/${name}`;
2014
+ await page.screenshot({ path: out, fullPage: true });
2015
+ console.log(JSON.stringify({ url: page.url(), title: await page.title(), screenshot: out }, null, 2));
2016
+ } else if (sub === "eval") {
2017
+ const js = process.argv[5] || "document.title";
2018
+ const result = await page.evaluate(new Function(`return (${js})`));
2019
+ console.log(JSON.stringify({ url: page.url(), result }, null, 2));
2020
+ }
2021
+ } finally {
2022
+ await page.close();
2023
+ }
2024
+ } finally {
2025
+ await browser.close();
2026
+ }
2027
+ break;
2028
+ }
2029
+ default:
2030
+ console.log(`alvin-bot browser β€” bot-managed Chromium (CDP on port 9222)
2031
+
2032
+ start [headful|headless] Start Chromium with CDP (default: headless)
2033
+ stop Stop the bot-managed Chromium
2034
+ status Show PID + binary + endpoint
2035
+ doctor Diagnose common issues
2036
+ goto <url> Navigate and print page info as JSON
2037
+ shot <url> [filename] Screenshot to ~/.alvin-bot/browser/screenshots/
2038
+ eval <url> <js> Evaluate JS expression in page context
2039
+ tabs List all open tabs
2040
+
2041
+ Notes:
2042
+ β€’ Uses Playwright's bundled Chromium β€” no conflict with your normal Chrome.
2043
+ β€’ Profile persists at ~/.alvin-bot/browser/profile/ (cookies survive restarts).
2044
+ β€’ First run needs: npx playwright install chromium
2045
+ `);
2046
+ process.exit(sub ? 1 : 0);
2047
+ }
2048
+ } catch (err) {
2049
+ console.error(`❌ ${err.message || err}`);
2050
+ process.exit(1);
2051
+ }
2052
+ break;
2053
+ }
1931
2054
  default:
1932
2055
  console.log(`
1933
2056
  ${t("cli.title")}
@@ -1939,6 +2062,7 @@ ${t("cli.commands")}
1939
2062
  doctor ${t("cli.doctorDesc")}
1940
2063
  audit Security health check (permissions, secrets, config)
1941
2064
  search Search your assets, memories, and skills
2065
+ browser Manage bot-owned Chromium (start/stop/goto/shot/doctor)
1942
2066
  update ${t("cli.updateDesc")}
1943
2067
  start ${t("cli.startDesc")} (background via PM2)
1944
2068
  start -f Start in foreground (for debugging)
@@ -27,6 +27,7 @@ import { BOT_VERSION } from "../version.js";
27
27
  import { getWebPort } from "../web/server.js";
28
28
  import { getUsageSummary, getAllRateLimits, formatTokens } from "../services/usage-tracker.js";
29
29
  import { runUpdate, getAutoUpdate, setAutoUpdate, startAutoUpdateLoop } from "../services/updater.js";
30
+ import { getReleaseHighlights } from "../services/release-highlights.js";
30
31
  import { getHealthStatus, isFailedOver } from "../services/heartbeat.js";
31
32
  import { t, LOCALE_NAMES, LOCALE_FLAGS } from "../i18n.js";
32
33
  // Kick off auto-update loop on module load if the persistent flag is set.
@@ -1875,6 +1876,17 @@ export function registerCommands(bot) {
1875
1876
  const result = await runUpdate();
1876
1877
  if (result.ok) {
1877
1878
  await ctx.reply(`βœ… ${result.message}`);
1879
+ // Extract the installed version from the message (e.g. "Installed v4.16.1 ...")
1880
+ // so we can look up its CHANGELOG block. Falls silently if no match.
1881
+ const versionMatch = result.message.match(/v(\d+\.\d+\.\d+)/);
1882
+ if (versionMatch) {
1883
+ const highlights = getReleaseHighlights(versionMatch[1]);
1884
+ if (highlights) {
1885
+ await ctx.reply(`πŸ“ *What's new in v${versionMatch[1]}*\n\n${highlights}`, {
1886
+ parse_mode: "Markdown",
1887
+ });
1888
+ }
1889
+ }
1878
1890
  if (result.requiresRestart) {
1879
1891
  await ctx.reply(t("bot.update.restarting", lang));
1880
1892
  setTimeout(() => process.exit(0), 500);
@@ -100,8 +100,8 @@ export async function handlePlatformMessage(msg, adapter) {
100
100
  touchProfile(profileKey, msg.userName, msg.userHandle, msg.platform, text);
101
101
  // v4.12.0 β€” Workspace resolution: channel β†’ workspace β†’ persona + cwd.
102
102
  // P1 #2 β€” If the platform has a getChannelName helper (Slack does), use
103
- // it to enable channel-name-based workspace matching (e.g. #alev-b β†’
104
- // workspaces/alev-b.md). Cached in the adapter, so no extra API call
103
+ // it to enable channel-name-based workspace matching (e.g. #my-project β†’
104
+ // workspaces/my-project.md). Cached in the adapter, so no extra API call
105
105
  // after the first hit per channel.
106
106
  let channelName;
107
107
  const getChannelName = adapter.getChannelName;
package/dist/paths.js CHANGED
@@ -108,11 +108,21 @@ export const AGENTS_FILE = resolve(DATA_DIR, "AGENTS.md");
108
108
  export const HOOKS_DIR = resolve(DATA_DIR, "hooks");
109
109
  /** scripts/browse-server.cjs β€” HTTP gateway for persistent browser sessions */
110
110
  export const BROWSE_SERVER_SCRIPT = resolve(BOT_ROOT, "scripts", "browse-server.cjs");
111
- /** ~/.claude/hub/SCRIPTS/browser.sh β€” Hub 3-tier browser router (stealth, CDP, ext) */
111
+ /** ~/.claude/hub/SCRIPTS/browser.sh β€” Optional dev-only 3-tier browser router.
112
+ * Used ONLY if present (maintainer dev environment). Not required for normal operation β€”
113
+ * the bot has its own CDP bootstrap (see src/services/cdp-bootstrap.ts). */
112
114
  export const HUB_BROWSER_SH = resolve(os.homedir(), ".claude", "hub", "SCRIPTS", "browser.sh");
115
+ /** browser/profile/ β€” Persistent Chromium profile for CDP (cookies, login state) */
116
+ export const CDP_PROFILE_DIR = resolve(DATA_DIR, "browser", "profile");
117
+ /** browser/screenshots/ β€” CDP screenshot output directory */
118
+ export const CDP_SCREENSHOTS_DIR = resolve(DATA_DIR, "browser", "screenshots");
119
+ /** browser/chrome-cdp.pid β€” PID of Chromium started by cdp-bootstrap */
120
+ export const CDP_PID_FILE = resolve(DATA_DIR, "browser", "chrome-cdp.pid");
121
+ /** browser/chrome-cdp.log β€” Chromium stderr/stdout when started by cdp-bootstrap */
122
+ export const CDP_LOG_FILE = resolve(DATA_DIR, "browser", "chrome-cdp.log");
113
123
  /** data/exec-allowlist.json β€” User-defined exec allowlist */
114
124
  export const EXEC_ALLOWLIST_FILE = resolve(DATA_DIR, "exec-allowlist.json");
115
- /** assets/ β€” User asset files (CVs, cover letters, legal docs, photos) */
125
+ /** assets/ β€” User-supplied files organized in category subdirectories */
116
126
  export const ASSETS_DIR = resolve(DATA_DIR, "assets");
117
127
  /** assets/INDEX.json β€” Machine-readable asset registry */
118
128
  export const ASSETS_INDEX_JSON = resolve(DATA_DIR, "assets", "INDEX.json");
@@ -61,7 +61,7 @@ export function buildAlvinMcpServer(ctx) {
61
61
  .describe("The full prompt for the sub-agent. Be specific and self-contained β€” the sub-agent has no access to this conversation's context and will see only this prompt."),
62
62
  description: z
63
63
  .string()
64
- .describe("Short human-readable title (e.g. 'SEO audit alev-b.com', 'Research Higgsfield Seedance 2.0'). Shown to the user when the result arrives."),
64
+ .describe("Short human-readable title (e.g. 'SEO audit example.com', 'Research topic X'). Shown to the user when the result arrives."),
65
65
  }, async (args) => {
66
66
  try {
67
67
  const result = dispatchDetachedAgent({
@@ -50,22 +50,16 @@ function walkDir(dir) {
50
50
  }
51
51
  /**
52
52
  * Generate a human-readable description from a filename.
53
- * "acme-cover-letter.html" β†’ "Cover Letter: Acme"
54
53
  * "profile-photo.jpeg" β†’ "Profile Photo"
54
+ * "my-document.html" β†’ "My Document"
55
55
  */
56
56
  function descriptionFromFilename(filename, category) {
57
57
  const name = filename.replace(/\.[^.]+$/, ""); // strip extension
58
58
  const words = name.replace(/[-_]/g, " ").trim();
59
- // Special handling for known patterns
60
- if (category === "cover-letters") {
61
- const company = words.replace(/cover letter/i, "").replace(/^Cover_Letter_[A-Za-z_]+_/i, "").trim();
62
- return `Cover Letter: ${company || words}`;
63
- }
64
- if (category === "cv-templates") {
65
- return `CV Template: ${words}`;
66
- }
67
- // Default: capitalize words
68
- return words.replace(/\b\w/g, c => c.toUpperCase());
59
+ // Prefix with capitalized category for disambiguation when the filename alone is terse
60
+ const prefix = category ? category.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase()) + ": " : "";
61
+ const title = words.replace(/\b\w/g, c => c.toUpperCase());
62
+ return prefix ? `${prefix}${title}` : title;
69
63
  }
70
64
  /**
71
65
  * Determine category for a file.
@@ -17,6 +17,7 @@ import { config } from "../config.js";
17
17
  import { BROWSE_SERVER_SCRIPT, HUB_BROWSER_SH } from "../paths.js";
18
18
  import { screenshotUrl, extractText, generatePdf } from "./browser.js";
19
19
  import { webfetchNavigate, WebfetchFailed } from "./browser-webfetch.js";
20
+ import * as cdpBootstrap from "./cdp-bootstrap.js";
20
21
  const CDP_PORT = 9222;
21
22
  const EXEC_TIMEOUT = 60_000; // 60s for page loads via shell
22
23
  // ── Logging ──────────────────────────────────────────────────────────
@@ -125,23 +126,35 @@ export async function resolveStrategy(preferred) {
125
126
  case "cdp":
126
127
  if (await isCDPAvailable())
127
128
  return "cdp";
128
- // Try starting CDP via hub script
129
+ // Bot-owned bootstrap is the primary path β€” works for every install,
130
+ // no Hub dependency, no conflict with user's own Chrome.
131
+ try {
132
+ log("CDP not running β€” starting bot-managed Chromium via cdp-bootstrap...");
133
+ await cdpBootstrap.ensureRunning({ mode: "headless" });
134
+ if (await isCDPAvailable()) {
135
+ log("CDP bootstrap started successfully.");
136
+ return "cdp";
137
+ }
138
+ }
139
+ catch (err) {
140
+ log(`CDP bootstrap failed: ${err.message}`);
141
+ }
142
+ // Dev-only fallback: maintainer Hub script, if present
129
143
  if (isHubBrowserAvailable()) {
130
144
  try {
131
- log("CDP Chrome not running β€” attempting to start via hub browser.sh...");
145
+ log("Trying Hub script as fallback...");
132
146
  execSync(`"${HUB_BROWSER_SH}" cdp start headless`, {
133
147
  stdio: "pipe",
134
148
  timeout: 15_000,
135
149
  });
136
- // Give it a moment to spin up
137
150
  await new Promise((r) => setTimeout(r, 3000));
138
151
  if (await isCDPAvailable()) {
139
- log("CDP Chrome started successfully.");
152
+ log("CDP via Hub script.");
140
153
  return "cdp";
141
154
  }
142
155
  }
143
156
  catch (err) {
144
- log(`Failed to start CDP Chrome: ${err.message}`);
157
+ log(`Hub script fallback failed: ${err.message}`);
145
158
  }
146
159
  }
147
160
  log("CDP unavailable. Falling back.");
@@ -385,7 +398,7 @@ async function withCdpPage(fn) {
385
398
  await browser.close(); // Closes CDP connection, not Chrome itself
386
399
  }
387
400
  }
388
- const NEEDS_INTERACTIVE_HINT = "Start CDP Chrome: ~/.claude/hub/SCRIPTS/browser.sh cdp start headless";
401
+ const NEEDS_INTERACTIVE_HINT = "Start CDP: alvin-bot browser start (headless by default)";
389
402
  /**
390
403
  * Get accessibility tree (gateway preferred, CDP fallback returns outerHTML).
391
404
  * The @eN ref model only exists in the gateway; under CDP we return a