alvin-bot 4.5.1 → 4.7.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +278 -0
  2. package/README.md +25 -2
  3. package/bin/cli.js +325 -26
  4. package/dist/handlers/commands.js +505 -63
  5. package/dist/handlers/message.js +209 -14
  6. package/dist/i18n.js +470 -13
  7. package/dist/index.js +45 -5
  8. package/dist/providers/claude-sdk-provider.js +106 -14
  9. package/dist/providers/ollama-provider.js +32 -0
  10. package/dist/providers/openai-compatible.js +10 -1
  11. package/dist/providers/registry.js +112 -17
  12. package/dist/providers/types.js +25 -3
  13. package/dist/services/compaction.js +2 -0
  14. package/dist/services/cron.js +53 -42
  15. package/dist/services/heartbeat.js +41 -7
  16. package/dist/services/language-detect.js +12 -2
  17. package/dist/services/ollama-manager.js +339 -0
  18. package/dist/services/personality.js +20 -14
  19. package/dist/services/session.js +21 -3
  20. package/dist/services/subagent-delivery.js +266 -0
  21. package/dist/services/subagent-stats.js +123 -0
  22. package/dist/services/subagents.js +509 -42
  23. package/dist/services/telegram.js +28 -1
  24. package/dist/services/updater.js +158 -0
  25. package/dist/services/usage-tracker.js +11 -4
  26. package/dist/services/users.js +2 -1
  27. package/docs/HANDBOOK.md +856 -0
  28. package/package.json +7 -2
  29. package/test/claude-sdk-provider.test.ts +69 -0
  30. package/test/i18n.test.ts +108 -0
  31. package/test/registry.test.ts +201 -0
  32. package/test/subagent-delivery.test.ts +273 -0
  33. package/test/subagent-stats.test.ts +119 -0
  34. package/test/subagents-commands.test.ts +64 -0
  35. package/test/subagents-config.test.ts +114 -0
  36. package/test/subagents-depth.test.ts +58 -0
  37. package/test/subagents-inheritance.test.ts +67 -0
  38. package/test/subagents-name-resolver.test.ts +122 -0
  39. package/test/subagents-priority-reject.test.ts +88 -0
  40. package/test/subagents-queue.test.ts +127 -0
  41. package/test/subagents-shutdown.test.ts +126 -0
  42. package/test/subagents-toolset.test.ts +51 -0
  43. package/vitest.config.ts +17 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,284 @@
2
2
 
3
3
  All notable changes to Alvin Bot are documented here.
4
4
 
5
+ ## [4.7.0] — 2026-04-11
6
+
7
+ ### ✨ Sub-Agents Stufe 2 — live-stream, bounded queue, 24h stats
8
+
9
+ Stufe 2 of the sub-agents refinement spec lands alongside the same-day 4.6.0 release. Everything here builds on the Stufe 1 foundation and is fully unit-tested (85 passing tests).
10
+
11
+ #### A4 Live-Stream for user-spawns
12
+
13
+ `/subagents visibility live` enables a new delivery mode where user-spawned sub-agents stream their text incrementally into a single Telegram message, then post a completion banner as a separate message.
14
+
15
+ Implementation in `src/services/subagent-delivery.ts`:
16
+
17
+ - `LiveStream` class with `start()` / `update()` / `finalize()`
18
+ - `start()` posts an initial `⏳ <name> thinking…` placeholder and records its `message_id`
19
+ - `update()` is called on every text chunk from the agent's generator; it coalesces rapid updates via a throttle window of **800 ms** so we never exceed Telegram's edit rate limit. Multiple `update()` calls within the window collapse into a single edit with the latest accumulated text.
20
+ - `finalize()` flushes any pending text, replaces the `thinking…` header with the final body, then sends a new banner message so the user gets a completion notification (edits don't trigger push notifications).
21
+ - The live-stream message uses **plain text** (no `parse_mode`) so half-formed markdown during streaming can never cause an edit to be rejected. The final banner does use markdown.
22
+
23
+ Wiring in `runSubAgent`:
24
+
25
+ - Detects `effectiveVisibility === "live"` AND `source === "user"` AND `parentChatId`. Cron and implicit spawns are never live-streamed — cron because there's no interactive watcher, implicit because the parent Claude stream already shows everything inline.
26
+ - Creates the `LiveStream` via `createLiveStream()` before the for-await loop.
27
+ - Calls `liveStream.update(chunk.text)` on every text chunk.
28
+ - Calls `liveStream.finalize(info, result)` after the loop and marks `entry.delivered = true` so `spawnSubAgent.finally()` skips the regular `deliverSubAgentResult` path. If finalize fails, the `delivered` flag stays false and the normal banner delivery fires as a fallback.
29
+ - Falls back to `"banner"` mode transparently if the bot API doesn't support `editMessageText` (e.g. during tests or if `attachBotApi` was never called).
30
+
31
+ Tests added in `test/subagent-delivery.test.ts`:
32
+
33
+ - `start` posts an initial placeholder and stores the message_id
34
+ - `update` coalesces rapid calls into a single throttled edit within the 800 ms window
35
+ - `finalize` posts a banner as a new message
36
+ - `createLiveStream` returns `null` when `editMessageText` is missing
37
+
38
+ #### D3 Bounded priority queue
39
+
40
+ Previously, hitting `maxParallel` returned a hard reject. Now spawn requests that don't fit run into a **bounded priority queue**:
41
+
42
+ - Default cap: **20** slots (configurable via `/subagents queue <n>`, clamped to 0–200)
43
+ - Setting cap to 0 disables the queue entirely and restores the old reject-on-full behavior
44
+ - Priority order on drain: **user > cron > implicit**
45
+ - FIFO within each priority class
46
+ - Drains automatically when a running agent finishes — the `runSubAgent.finally()` now calls `drainQueue()` after cleanup
47
+
48
+ New fields:
49
+
50
+ - `SubAgentsConfig.queueCap: number` — persisted in `~/.alvin-bot/sub-agents.json`
51
+ - `SubAgentInfo.status: "queued"` — new valid state
52
+ - `SubAgentInfo.queuePosition?: number` — 1-based position in the queue, shown in `/subagents list` as `#N`
53
+
54
+ Functions in `subagents.ts`:
55
+
56
+ - `getQueueCap()` / `setQueueCap(n)` — public config accessors
57
+ - `drainQueue()` — called from `runSubAgent.finally()`, pops in priority order and transitions entries from `queued` to `running`
58
+ - `popHighestPriorityQueued()` — internal FIFO-per-priority scan
59
+ - `reindexQueue()` — keeps `SubAgentInfo.queuePosition` in sync after pop/cancel
60
+ - `cancelSubAgent()` now handles queued entries by removing them from the queue without starting `runSubAgent` at all
61
+ - `cancelAllSubAgents()` clears the pending queue before cancelling running agents, so shutdown doesn't spawn anything new
62
+ - `spawnSubAgent()` is split: queue decision first (run immediately vs queue vs reject), then `startRun()` helper starts the background loop
63
+
64
+ Reject messages stay priority-aware (D4) but now mention queue saturation:
65
+
66
+ - `user` spawn + pool full + cron/implicit in pool + queue full → *"Alle Slots belegt (N/M), davon X cron/implicit im Hintergrund. Queue voll (Q/C). /subagents list für Details …"*
67
+ - `user` spawn + pool full + user in pool + queue full → *"Alle Slots belegt (N/M) mit eigenen user-Spawns. Queue voll (Q/C). /subagents cancel <name> oder warten."*
68
+ - Non-user spawns + pool + queue full → *"Sub-agent limit reached (N running, Q/C queued). Wait for a running agent to finish or cancel one."*
69
+
70
+ Tests added in `test/subagents-queue.test.ts`:
71
+
72
+ - Default cap is 20
73
+ - Clamping (negative → 0, above 200 → 200, fractional floors)
74
+ - Round-trip through disk
75
+ - Third spawn at full pool lands as `status: "queued"` with `queuePosition: 1`
76
+ - Queue drains automatically when a running agent finishes
77
+ - Priority order: user spawns drain before cron at the same moment
78
+ - `cancelSubAgent` removes a queued entry
79
+
80
+ The existing priority-reject tests now explicitly set `queueCap = 0` to test the old reject path, and a new "queue enabled" test fills both pool and queue before asserting the reject message.
81
+
82
+ #### H3 24-hour run stats
83
+
84
+ New module `src/services/subagent-stats.ts` — a simple append-only JSON ring buffer persisted to `~/.alvin-bot/subagent-stats.json`. Each completed sub-agent run appends one entry:
85
+
86
+ ```ts
87
+ {
88
+ completedAt: number;
89
+ name: string;
90
+ source: "user" | "cron" | "implicit";
91
+ status: "completed" | "timeout" | "error" | "cancelled";
92
+ durationMs: number;
93
+ inputTokens: number;
94
+ outputTokens: number;
95
+ }
96
+ ```
97
+
98
+ On every load or append, entries older than 24 hours are pruned. A hard cap of 5000 entries protects against unbounded growth on high-frequency bots.
99
+
100
+ Accessors:
101
+
102
+ - `recordSubAgentRun(info, result)` — called from `runSubAgent.finally()` as a non-blocking side effect. Errors are logged but don't affect delivery.
103
+ - `getSubAgentStats()` — returns a `StatsSummary` with totals, per-source breakdown, and per-status counts.
104
+
105
+ New Telegram command **`/subagents stats`** renders the summary:
106
+
107
+ ```
108
+ 📊 Sub-Agent Stats — last 24h
109
+
110
+ Total: 44 runs · 165k in / 89k out · 12m
111
+
112
+ By source:
113
+ 👤 user: 12 runs · 45k in / 22k out
114
+ ⏰ cron: 8 runs · 31k in / 15k out
115
+ 🔗 implicit: 24 runs · 89k in / 52k out
116
+
117
+ By status:
118
+ ✅ completed: 42
119
+ ⚠️ cancelled: 1
120
+ ⏱️ timeout: 0
121
+ ❌ error: 1
122
+ ```
123
+
124
+ The JSON backing file is a deliberate short-term choice. When the SQLite migration lands (already scoped in a separate memory entry as `project_alvinbot_sqlite_migration.md`), we swap the backend without touching `getSubAgentStats()` or `recordSubAgentRun()` — both are designed as a narrow interface.
125
+
126
+ Tests added in `test/subagent-stats.test.ts`:
127
+
128
+ - Fresh install returns zeros
129
+ - Recording 3 runs updates totals + per-source breakdown
130
+ - Persistence + reload round-trip
131
+ - Entries older than 24h are pruned on load
132
+ - `byStatus` tracks cancelled/error/timeout separately
133
+
134
+ ### 🖥 CLI: `alvin-bot start` / `stop` now auto-detect LaunchAgent
135
+
136
+ The `start` and `stop` commands previously always went through pm2. That created a conflict after `alvin-bot launchd install`: the LaunchAgent ran the bot, but `alvin-bot start` would happily spawn a second instance via pm2, and `alvin-bot stop` would try to stop a pm2 process that didn't exist.
137
+
138
+ Now both commands check for `~/Library/LaunchAgents/com.alvinbot.app.plist` on macOS and switch transparently:
139
+
140
+ - **`alvin-bot start`** with a LaunchAgent present → `launchctl kickstart -k gui/$UID/com.alvinbot.app` (or `launchctl load -w` if not loaded yet). No pm2 involvement.
141
+ - **`alvin-bot stop`** with a LaunchAgent present → `launchctl unload -w` (doesn't remove the plist, just stops the daemon).
142
+ - **`alvin-bot start`** on macOS without a LaunchAgent → pm2 path + a helpful tip: *"💡 Tip: on macOS with Claude Code, switch to launchd for automatic Keychain access: alvin-bot launchd install"*.
143
+
144
+ Linux and Windows users are unaffected — they always get the pm2 path.
145
+
146
+ ### 🐛 Other
147
+
148
+ - `/subagents queue` is registered in the usage string for en/de/es/fr.
149
+ - `/subagents stats` is registered in the usage string for en/de/es/fr.
150
+ - `/subagents visibility` usage now lists `live` as a valid mode.
151
+ - Removed the leftover `alvin-bot-4.5.1.tgz` from the repo root.
152
+
153
+ ## [4.6.0] — 2026-04-11
154
+
155
+ ### ✨ Sub-Agents Stufe 1 — context-aware delivery, name-first addressing, shutdown notifications
156
+
157
+ **The big one.** Stufe 1 of the SubAgents refinement spec (9 design axes, two-stage rollout) is complete. Everything here is live-validated on a remote test MacBook via `@Alvin_testbot_bot` over Telegram with Claude Agent SDK + Max OAuth.
158
+
159
+ #### A4 + I3 — Source-aware delivery router
160
+
161
+ New module `src/services/subagent-delivery.ts`. Every completed sub-agent routes through a single entry point that picks its delivery path based on `SubAgentInfo.source`:
162
+
163
+ - `implicit` (Main-Claude calling the SDK `Task` tool) → **no-op**, the parent stream already shows the result.
164
+ - `user` (explicit user spawn) → **banner + final** to `parentChatId` in the originating chat.
165
+ - `cron` (scheduled job) → **banner + final** to the `chatId` from the cron job's target.
166
+
167
+ The banner format is fixed: `{icon} *{name}* {status} · {duration} · {input_tokens} in / {output_tokens} out` followed by the agent output. Status icons: ✅ completed, ⚠️ cancelled, ⏱️ timeout, ❌ error. Duration is human-formatted (`42s`, `3m 12s`). Token counts collapse at 1000 (`4.2k`).
168
+
169
+ Output chunking:
170
+ - ≤3800 chars → single message `banner + body`
171
+ - 3800–20000 chars → banner alone, then body chunks of 3800 chars each
172
+ - \>20000 chars → banner + the body as a `.md` file upload (via `grammy`'s `InputFile`)
173
+
174
+ The bot API is attached lazily at startup via `attachBotApi()` so `subagent-delivery.ts` stays free of a circular import on `index.ts`. Test hook `__setBotApiForTest()` lets Vitest inject a fake.
175
+
176
+ #### New command: `/subagents visibility <auto|banner|silent>`
177
+
178
+ Per-install persistent visibility setting, written to `~/.alvin-bot/sub-agents.json`. `silent` suppresses the delivery entirely — the result is still stored in the `activeAgents` map and pullable via `/subagents result <name>`. `auto` is the default and falls through to the source-based routing described above.
179
+
180
+ #### B2 — Name-first addressing with automatic `#N` collision suffixes
181
+
182
+ `/subagents cancel <name|id>` and `/subagents result <name|id>` now accept names, not just UUIDs. When a new spawn collides with an existing name, the resolver appends `#2`, `#3`, … using the smallest free index. Example: three parallel `review` spawns appear as `review`, `review#2`, `review#3` in `/subagents list`.
183
+
184
+ Resolution order:
185
+ 1. Explicit `#N` suffix (e.g. `review#2`) → exact match wins, never falls through to ambiguity
186
+ 2. Base name with a single sibling → that sibling
187
+ 3. Base name with multiple siblings **and** `ambiguousAsList: true` opt-in → disambiguation reply listing all candidates
188
+ 4. Base name with multiple siblings, no opt-in → first sibling
189
+ 5. No name match → UUID prefix (back-compat)
190
+
191
+ #### C3 — Parent inheritance
192
+
193
+ Sub-agents now inherit `workingDir` (with `inheritCwd: false` opt-out), `CLAUDE.md` (via `settingSources: ["project"]`), and the registry's provider/model. Conversation history is **not** inherited — the sub-agent reads only its own prompt, which forces clean, self-describing spawn requests and keeps parallel agents from colliding on shared context.
194
+
195
+ #### D4 — Priority-aware reject messages
196
+
197
+ Pool is still strictly capped (no preemption), but the error message when it's full now depends on who holds the slots:
198
+ - User spawn + background (cron/implicit) hold slots → message points at `/subagents list` so the user knows the pool isn't stuck on another interactive task
199
+ - User spawn + other user spawns → suggests cancel-or-wait with command hints
200
+ - Cron/implicit rejects → generic "limit reached" (those callers handle retry themselves)
201
+
202
+ #### E2 — Shutdown notifications
203
+
204
+ `cancelAllSubAgents(notify: true)` is now async and fires a delivery to each still-running agent before the process exits. Each notification is a synth `cancelled` result with the body `⚠️ Agent wurde durch Bot-Restart unterbrochen. Bitte neu triggern.` and routes through the normal I3 delivery path. Total delivery phase is capped at 5s so a hanging Telegram send can't block shutdown.
205
+
206
+ The shutdown hook in `src/index.ts` now `await`s `cancelAllSubAgents(true)` before stopping the grammy bot and tearing down plugins.
207
+
208
+ #### F2 — Depth cap (hard limit = 2)
209
+
210
+ `SubAgentConfig.depth` is a new optional field (defaults to 0 = root). `spawnSubAgent` rejects any depth > 2 with a clear error. The depth shows in `/subagents list` as `d0` / `d1` / `d2` with 2-space indentation per level, so nested scatter-gather runs are visually nested.
211
+
212
+ #### G1 — Toolset preset infrastructure
213
+
214
+ New `SubAgentConfig.toolset` field with a single valid value `"full"`. Runtime validation rejects any other string. This is purely infrastructure for future `"readonly"` / `"research"` presets — no behavior change today, but adding a preset later is a one-line diff.
215
+
216
+ #### H2 — Per-run token accounting in the banner
217
+
218
+ Every completed sub-agent's banner carries the input/output token counts it actually consumed. No aggregation (H3) — that comes later with the SQLite migration. For now, you can see "this agent cost me 4.2k/2.1k" right next to the result.
219
+
220
+ #### Tests
221
+
222
+ 67 passing Vitest tests across 12 files. New test files added for this release:
223
+ - `test/claude-sdk-provider.test.ts` — auth probe + `isAuthErrorOutput` helper
224
+ - `test/subagents-depth.test.ts` — depth cap (F2)
225
+ - `test/subagents-inheritance.test.ts` — cwd inheritance (C3)
226
+ - `test/subagents-toolset.test.ts` — toolset literal (G1)
227
+ - `test/subagents-name-resolver.test.ts` — `findSubAgentByName` including regression for exact-match vs ambiguity
228
+ - `test/subagents-commands.test.ts` — `cancelSubAgentByName`/`getSubAgentResultByName` helpers
229
+ - `test/subagent-delivery.test.ts` — I3 delivery router (all 5 source/visibility paths)
230
+ - `test/subagents-shutdown.test.ts` — E2 notify=true / notify=false + regression for shutdown double-delivery
231
+ - `test/subagents-priority-reject.test.ts` — D4 priority-aware reject messages
232
+ - `test/subagents-config.test.ts` — expanded with visibility config round-trip
233
+
234
+ ### 🖥 New CLI: `alvin-bot launchd install|uninstall|status` (macOS only)
235
+
236
+ **Why this matters.** Claude Code 2.x stores the Max-subscription OAuth token in the macOS Keychain, service `"Claude Code-credentials"`. Accessing the token requires:
237
+ 1. A Keychain ACL that permits the `claude` binary (granted via the "Always Allow" dialog on first GUI invocation)
238
+ 2. An *unlocked* Keychain in the calling process's security context
239
+
240
+ Processes started via SSH, pm2, or `nohup` run in a detached launchd session that does **not** inherit the GUI user's unlocked-Keychain state. Even a manual `security unlock-keychain -p '...'` only unlocks the current SSH session — the pm2 daemon running in its own context stays locked out. Result: the Bot saw `Not logged in · Please run /login` on every sub-agent query, and the fix in 4.6.0's Phase 0 exposes that as a clean error instead of leaking it as chat text.
241
+
242
+ **The fix**: run the bot as a **launchd user agent**. LaunchAgents run inside the GUI login session and inherit the unlocked Keychain automatically. No SSH dance, no pm2 drama, no manual unlocks on every restart.
243
+
244
+ ```
245
+ alvin-bot launchd install — Write ~/Library/LaunchAgents/com.alvinbot.app.plist,
246
+ unload any existing instance, launchctl load -w.
247
+ alvin-bot launchd uninstall — Unload and rm the plist.
248
+ alvin-bot launchd status — Plist existence, PID from `launchctl list`,
249
+ tail of ~/.alvin-bot/logs/alvin-bot.{out,err}.log.
250
+ ```
251
+
252
+ Plist details:
253
+ - `KeepAlive` → auto-restart on crash, not on successful exit
254
+ - `RunAtLoad` → starts on login
255
+ - `ThrottleInterval 10` → prevents rapid restart loops
256
+ - `PATH` covers `~/.local/bin`, `/opt/homebrew/bin` (Apple Silicon), `/usr/local/bin` (Intel Homebrew)
257
+ - stdout → `~/.alvin-bot/logs/alvin-bot.out.log`
258
+ - stderr → `~/.alvin-bot/logs/alvin-bot.err.log`
259
+
260
+ macOS users should migrate from `alvin-bot start` (pm2) to `alvin-bot launchd install`. Pm2 still works and remains the Linux/Windows default.
261
+
262
+ ### 🐛 Bug fixes
263
+
264
+ - **`ClaudeSDKProvider.isAvailable()` now actually probes authentication.** The old check only ran `claude --version`, which succeeds whether or not the CLI has a valid OAuth token. A locked-out CLI would be reported as available, and the `Not logged in` response would leak into the chat as a normal assistant message. New behavior: `claude --version` for the binary check, then `claude -p "ping"` to verify auth. If the output matches the "Not logged in" pattern, the provider reports `false` and the registry falls through to the next provider.
265
+
266
+ - **`ClaudeSDKProvider.query()` surfaces `Not logged in` as an error chunk.** Even in code paths where `isAvailable()` returned stale cache, a runtime failure during the stream would emit `Not logged in · Please run /login` as text. The query loop now detects the auth pattern on the first text chunk and yields a typed `error` chunk with a clear "Run `claude login`" message, instead of pretending it's a normal response.
267
+
268
+ - **`/subagents cancel|result <name#N>` now hits the exact entry.** Regression caught during the remote test: asking for `test-ping#2` returned the "Mehrdeutig — welchen meinst du?" ambiguity reply instead of the specific `#2` entry, because `findSubAgentByName` checked base-name siblings before the exact-name match when `ambiguousAsList: true` was set. Explicit `#N` queries now always win.
269
+
270
+ - **Shutdown double-delivery race fixed.** If the bot received SIGTERM while a sub-agent was mid-stream, Telegram saw two messages: a "completed · (empty output)" banner from `runSubAgent.finally()` (because the test generator exited gracefully after the abort), followed by the "cancelled · Bot-Restart" banner from `cancelAllSubAgents`. Fixed with a `delivered: boolean` flag on each `activeAgents` entry — whoever posts first sets it, the other skips.
271
+
272
+ - **`providerKeyMap` alignment in `src/index.ts`.** The pre-flight provider-key warning used `gemini-2.5-flash` as the map key, but the registry registers Google Gemini under `google`. Users who set `PRIMARY_PROVIDER=google` never saw the "GOOGLE_API_KEY missing" warning. Fixed by canonical `google → GOOGLE_API_KEY`; legacy custom-model aliases stay for rollback safety.
273
+
274
+ - **`cron.ts` ai-query triple-notification cleanup.** A single failed ai-query cron job was sending three legacy error messages (`slow-fox: cancelled — cancelled`, `AI-Query Error (slow-fox)`, `Cron Error (slow-fox)`) because the failure path fired `notifyCallback` in the inner `if`, the inner `catch`, and the outer `catch`. The I3 delivery router already posts the cancellation banner for ai-query jobs, so all three legacy notify calls are now skipped and ai-query errors propagate via the outer catch for bookkeeping only. Other job types (reminder, shell, http, message) keep the legacy notify path.
275
+
276
+ - **`/subagents` now shows up in Telegram's command autocomplete.** The grammy handler was registered from v4.0.0 but `setMyCommands` never listed it, so users had to know the exact spelling. Added.
277
+
278
+ ### 📚 Documentation
279
+
280
+ - New English-language handbook at `docs/HANDBOOK.md` — covers installation, architecture, all providers, the sub-agents system, cron jobs, platform adapters, security audit, and the web UI. Written to be readable standalone without cross-referencing the README.
281
+ - README.md updated with a pointer to the handbook and the new `launchd` command.
282
+
5
283
  ## [4.5.1] — 2026-04-09
6
284
 
7
285
  ### 🐛 TUI Header Rendering Hotfix
package/README.md CHANGED
@@ -109,13 +109,29 @@ alvin-bot start
109
109
 
110
110
  That's it. The setup wizard validates everything:
111
111
  - ✅ Tests your AI provider key
112
- - ✅ Verifies your Telegram bot token
112
+ - ✅ Verifies your Telegram bot token
113
113
  - ✅ Confirms the setup works before you start
114
114
 
115
115
  **Requires:** Node.js 18+ ([nodejs.org](https://nodejs.org)) · Telegram bot token ([@BotFather](https://t.me/BotFather)) · Your Telegram user ID ([@userinfobot](https://t.me/userinfobot))
116
116
 
117
117
  Free AI providers available — no credit card needed.
118
118
 
119
+ ### macOS: use `launchd` instead of pm2 (recommended)
120
+
121
+ If you're on macOS and using Claude Code (Max subscription) as your provider, run the bot as a **LaunchAgent** — it inherits the GUI login session so the macOS Keychain stays unlocked and the Claude OAuth token just works without any manual `security unlock-keychain` dance:
122
+
123
+ ```bash
124
+ alvin-bot launchd install # writes ~/Library/LaunchAgents/com.alvinbot.app.plist and starts the agent
125
+ alvin-bot launchd status # show PID + recent stdout/stderr logs
126
+ alvin-bot launchd uninstall # unload + remove the plist
127
+ ```
128
+
129
+ Pm2 still works and remains the default on Linux/Windows — but on macOS with Claude Code, `launchd` is the only path that reliably keeps Keychain access over restarts.
130
+
131
+ ### 📖 Handbook
132
+
133
+ For a full walkthrough of everything Alvin Bot can do — providers, sub-agents, cron jobs, plugins, MCP, security audit, web UI — read **[`docs/HANDBOOK.md`](docs/HANDBOOK.md)**.
134
+
119
135
  ### AI Providers
120
136
 
121
137
  | Provider | Cost | Best for |
@@ -436,7 +452,14 @@ alvin-bot tui # Terminal chat UI ✨
436
452
  alvin-bot chat # Alias for tui
437
453
  alvin-bot doctor # Health check
438
454
  alvin-bot update # Pull latest & rebuild
439
- alvin-bot start # Start the bot
455
+ alvin-bot start # Start the bot (background via pm2)
456
+ alvin-bot start -f # Start in foreground
457
+ alvin-bot stop # Stop the bot
458
+ alvin-bot launchd install # macOS only: install as LaunchAgent
459
+ alvin-bot launchd status # macOS only: show LaunchAgent state
460
+ alvin-bot launchd uninstall # macOS only: remove LaunchAgent
461
+ alvin-bot audit # Security health check
462
+ alvin-bot search # Search assets/memories/skills
440
463
  alvin-bot version # Show version
441
464
  ```
442
465