alvin-bot 4.15.2 β†’ 4.16.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 CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.16.0] β€” 2026-04-20
6
+
7
+ ### πŸš€ Feature: bot-owned CDP Chromium β€” no more hub dependency
8
+
9
+ **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.
10
+
11
+ **Fix β€” three additions:**
12
+
13
+ 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.
14
+
15
+ 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/`.
16
+
17
+ 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.
18
+
19
+ **Skills updated:**
20
+ - `skills/browse/SKILL.md` β€” rewritten to use `alvin-bot browser ...` commands; hub-script references removed (kept as "if present" note for dev environments).
21
+ - `skills/social-fetch/SKILL.md` β€” CDP fallback line uses `alvin-bot browser goto/shot`.
22
+
23
+ **Docs:**
24
+ - `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).
25
+ - `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/`.
26
+
27
+ **First-run setup (one-time):**
28
+ ```bash
29
+ npx playwright install chromium
30
+ ```
31
+
32
+ **Verified on 2026-04-20 with user's daily Chrome running:**
33
+ - `alvin-bot browser start` β†’ PID + endpoint, no LaunchServices hijack
34
+ - `alvin-bot browser stop` + immediate `alvin-bot browser shot <url>` β†’ CDP auto-starts, screenshot written (15 KB PNG in `~/.alvin-bot/browser/screenshots/`)
35
+ - `alvin-bot browser doctor` β†’ all 4 checks green (binary, endpoint, PID, profile lock)
36
+ - `npm test` β†’ 504/504 tests passing
37
+
5
38
  ## [4.15.2] β€” 2026-04-17
6
39
 
7
40
  ### πŸ› Fix: sleep-aware heartbeat prevents false failover after macOS wake
@@ -124,7 +157,7 @@ Four hardcoded Claude model IDs replaced with current strings: `claude-sonnet-4-
124
157
 
125
158
  ### πŸ› Patch: watcher zombie-entry fix (missing outputFile > 10 min = failed)
126
159
 
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.
160
+ **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
161
 
129
162
  **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
163
 
@@ -167,7 +200,7 @@ Four hardcoded Claude model IDs replaced with current strings: `claude-sonnet-4-
167
200
 
168
201
  ### πŸ› Patch: `/subagents list` now shows v4.13+ dispatch agents too
169
202
 
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.
203
+ **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
204
 
172
205
  **Root cause:** two separate registries for sub-agents:
173
206
  - `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 +455,7 @@ This matches the OpenClaw experience the user was asking about β€” except it's b
422
455
 
423
456
  ### πŸ› Patch: recover partial output from interrupted background sub-agents
424
457
 
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.
458
+ **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
459
 
427
460
  **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
461
 
@@ -471,7 +504,7 @@ Result: on the next `pollOnce()` after v4.12.4 ships, the three stuck agents get
471
504
 
472
505
  ### πŸ› Patch: Background sub-agent no longer blocks the main Telegram session
473
506
 
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.
507
+ **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
508
 
476
509
  **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
510
 
@@ -693,7 +726,7 @@ Both the platform handler (Slack/Discord/WhatsApp) and the Telegram main handler
693
726
 
694
727
  #### P0 #4 β€” Slack Setup Documentation (`docs/install/slack-setup.md`, `docs/install/slack-manifest.json`)
695
728
 
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.
729
+ 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
730
 
698
731
  #### P1 #1 β€” Slack Progress Ticker (`src/platforms/slack.ts`)
699
732
 
@@ -707,7 +740,7 @@ Step-by-step guide: create Slack App from manifest β†’ Socket Mode β†’ App-Level
707
740
 
708
741
  `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
742
 
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.
743
+ 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
744
 
712
745
  #### P1 #3 β€” Telegram `/workspace` + `/workspaces` Commands
713
746
 
@@ -823,7 +856,7 @@ Inspired by Mem0's auto-extraction. When `compactSession()` archives old message
823
856
 
824
857
  - **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
858
  - **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.
859
+ - **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
860
  - **Decay/aging deferred.** Daily logs grow monotonically. Will be addressed alongside SQLite migration.
828
861
 
829
862
  #### Testing
@@ -898,11 +931,11 @@ Live-verified via isolated SDK probe (`node sdk-probe.mjs` inside the repo) whic
898
931
 
899
932
  #### What you'll see as a user
900
933
 
901
- Send: *"Make a SEO audit of gethomes.io and alev-b.com in parallel"*
934
+ Send: *"Make a SEO audit of example.com and example.com in parallel"*
902
935
 
903
936
  - **0 s** β€” Claude responds: *"Starting both audits in the background β€” I'll send the reports when done."* Main session **unlocks**.
904
937
  - **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.
938
+ - **~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
939
 
907
940
  #### Non-goals
908
941
 
@@ -961,7 +994,7 @@ He was right. My v4.9.0 `stopWebServer()` fix was *prevention* β€” it stopped th
961
994
 
962
995
  ### πŸ›  Two UX bugs found in production after v4.9.2 β€” now closed
963
996
 
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:
997
+ 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
998
 
966
999
  **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
1000
 
@@ -1090,12 +1123,12 @@ The `browse` skill used to instruct the agent to start `node scripts/browse-serv
1090
1123
  - **Tier 1** β€” `browser.sh stealth <url>` (Playwright + stealth plugin, headless, Cloudflare-masking)
1091
1124
  - **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
1125
  - **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.
1126
+ - Explicit escalation ladder (WebFetch β†’ stealth β†’ CDP β†’ ask the maintainer to log in) and a `NIEMALS browse-server.cjs nutzen` anti-rule.
1094
1127
  - Concrete working targets (StepStone βœ…, Michael Page βœ…, LinkedIn βœ… with login, Indeed ❌) so the agent knows what to try where.
1095
1128
 
1096
1129
  - **`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
1130
  - **`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.
1131
+ - **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
1132
  - **Explicit PNG extension** on auto-generated screenshot filenames (`shot_<ts>.png`) so Playwright's format inference is unambiguous.
1100
1133
  - **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
1134
 
@@ -1124,7 +1157,7 @@ Sub-agents and `ai-query` cron jobs used to hard-cap at 5 minutes (`SUBAGENT_TIM
1124
1157
 
1125
1158
  ### πŸ› Silenced harmless `message is not modified` Telegram errors
1126
1159
 
1127
- Occasionally Ali would see a red banner at the bottom of an Alvin message:
1160
+ Occasionally the maintainer would see a red banner at the bottom of an Alvin message:
1128
1161
 
1129
1162
  > 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
1163
 
@@ -1159,7 +1192,7 @@ After 4.8.7, running `/update` after a manual rebuild will correctly say *"Disk
1159
1192
 
1160
1193
  ### ✨ Internal watchdog with crash-loop brake (`src/services/watchdog.ts`)
1161
1194
 
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).
1195
+ 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
1196
 
1164
1197
  **New module**: `src/services/watchdog.ts`. Two responsibilities:
1165
1198
 
@@ -1274,7 +1307,7 @@ After 4.8.5, `/update` on the test MacBook will correctly detect the npm install
1274
1307
 
1275
1308
  ### πŸ› WhatsApp self-chat detection for the new `@lid` identity format
1276
1309
 
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`.
1310
+ 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
1311
 
1279
1312
  **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
1313
 
@@ -1346,7 +1379,7 @@ Offline-friendly status command β€” no running bot required. Prints:
1346
1379
  - On Linux/Windows: `pm2 jlist` check for the `alvin-bot` process
1347
1380
  - **Live info** (when the bot is running with the web UI on :3100): Uptime, active model
1348
1381
 
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.
1382
+ 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
1383
 
1351
1384
  Example:
1352
1385
 
@@ -1449,7 +1482,7 @@ New behavior in `bin/cli.js`:
1449
1482
  - **pm2 now empty** β†’ *"pm2 now has zero managed processes. Remove it with: `npm uninstall -g pm2`"*
1450
1483
  - **pm2 still has other projects** β†’ *"pm2 still has other projects running β€” leaving it installed."*
1451
1484
 
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.
1485
+ 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
1486
 
1454
1487
  ## [4.7.0] β€” 2026-04-11
1455
1488
 
@@ -1829,7 +1862,7 @@ Remaining unaddressed (by design, require breaking upgrades or overrides):
1829
1862
 
1830
1863
  ### ✨ Stability Improvements
1831
1864
 
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.
1865
+ **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
1866
 
1834
1867
  New behavior:
1835
1868
  - **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)
@@ -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