@bike4mind/cli 3.3.0 → 4.0.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/README.md CHANGED
@@ -43,10 +43,12 @@ pnpm install
43
43
  The CLI uses native dependencies (`better-sqlite3` for image caching, `sharp` for image processing) that require compilation. These should build automatically during installation, but if you encounter errors:
44
44
 
45
45
  **Prerequisites:**
46
+
46
47
  - Python 3 (required by node-gyp)
47
48
  - C++ compiler (Xcode Command Line Tools on macOS, build-essential on Linux, Visual Studio on Windows)
48
49
 
49
50
  **Manual rebuild if needed:**
51
+
50
52
  ```bash
51
53
  # If you see "Could not locate the bindings file" errors
52
54
  pnpm rebuild better-sqlite3
@@ -77,42 +79,43 @@ A bare positional argument seeds **and** submits the first turn while keeping th
77
79
 
78
80
  **Environment / API** (see [API Configuration](#api-configuration)):
79
81
 
80
- | Flag | Effect |
81
- | --- | --- |
82
- | `--dev` | Point the CLI at the local dev server (`http://localhost:3001`) and remember it. Aliases: `--local`, single-dash forms. |
83
- | `--prod` | Point the CLI at Bike4Mind production and remember it. Alias: `--production`. |
82
+ | Flag | Effect |
83
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `--dev` | Point the CLI at the local dev server (`http://localhost:3001`) and remember it. Aliases: `--local`, single-dash forms. |
85
+ | `--prod` | Point the CLI at Bike4Mind production and remember it. Alias: `--production`. |
84
86
  | `--api-url <url>` | Point the CLI at **any** instance — a self-hosted stack, an AWS deployment, `http://localhost:3000`, etc. Clears cached auth (bound to the old origin) and exits; run `b4m` again to sign in. |
85
- | `--reset-api` | Reset the API URL to the built-in default and clear auth, then exit. |
87
+ | `--reset-api` | Reset the API URL to the built-in default and clear auth, then exit. |
86
88
 
87
89
  **Session / behavior:**
88
90
 
89
- | Flag | Effect |
90
- | --- | --- |
91
- | `--verbose`, `-v` | Show debug logs in the console (also always written to file — see [Debug Logs](#debug-logs)). |
92
- | `--debug-stream` | Ultra-verbose: log every SSE event (stream-parser debugging). Implies `--verbose`. |
93
- | `--no-project-config` | Skip loading project-specific config (`.bike4mind/`). |
94
- | `--add-dir <dir>` | Grant file access to an additional directory. Repeatable. |
95
- | `--ollama-host <url>` | Add a local Ollama endpoint's models to the picker (e.g. `http://localhost:11434`). |
96
- | `--no-remote-skills` | Skip fetching remote B4M-web skills for this run (local files only). |
91
+ | Flag | Effect |
92
+ | --------------------- | --------------------------------------------------------------------------------------------- |
93
+ | `--verbose`, `-v` | Show debug logs in the console (also always written to file — see [Debug Logs](#debug-logs)). |
94
+ | `--debug-stream` | Ultra-verbose: log every SSE event (stream-parser debugging). Implies `--verbose`. |
95
+ | `--no-project-config` | Skip loading project-specific config (`.bike4mind/`). |
96
+ | `--add-dir <dir>` | Grant file access to an additional directory. Repeatable. |
97
+ | `--ollama-host <url>` | Add a local Ollama endpoint's models to the picker (e.g. `http://localhost:11434`). |
98
+ | `--no-remote-skills` | Skip fetching remote B4M-web skills for this run (local files only). |
97
99
 
98
100
  **Headless / non-interactive** (for scripts and CI):
99
101
 
100
- | Flag | Effect |
101
- | --- | --- |
102
- | `-p`, `--prompt <query>` | Run a single query non-interactively and exit. |
103
- | `--output-format <fmt>` | Output when using `-p`: `text` (default), `json`, or `stream-json` (NDJSON of thoughts/actions/observations). |
104
- | `--dangerously-skip-permissions` | With `-p`, auto-allow all tool permission prompts. Use with caution in CI/CD. |
102
+ | Flag | Effect |
103
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
104
+ | `-p`, `--prompt <query>` | Run a single query non-interactively and exit. |
105
+ | `--output-format <fmt>` | Output when using `-p`: `text` (default), `json`, or `stream-json` (NDJSON of thoughts/actions/observations). |
106
+ | `--dangerously-skip-permissions` | With `-p`, auto-allow all tool permission prompts. Use with caution in CI/CD. |
105
107
 
106
108
  **Info:**
107
109
 
108
- | Flag | Effect |
109
- | --- | --- |
110
- | `--help`, `-h` | Show help. |
110
+ | Flag | Effect |
111
+ | ----------------- | ----------------- |
112
+ | `--help`, `-h` | Show help. |
111
113
  | `--version`, `-V` | Show CLI version. |
112
114
 
113
115
  > **Host / `claude`-drop-in flags** (`--mcp-config`, `--strict-mcp-config`, `--append-system-prompt`, `--allowedTools`, `--settings`, `--session-id`, `--resume`) are documented under [Running as a `claude` engine inside a host app](#running-as-a-claude-engine-inside-a-host-app).
114
116
 
115
117
  **Examples:**
118
+
116
119
  ```bash
117
120
  # Point at a self-hosted stack, then sign in
118
121
  b4m --api-url http://localhost:3000
@@ -138,8 +141,31 @@ b4m mcp enable <name> | disable <name> # Toggle a server on/off
138
141
  b4m mcp serve [--http] [--port <n>] # Expose Bike4Mind itself as an MCP server
139
142
  b4m update # Check for and install CLI updates
140
143
  b4m doctor # Diagnose the CLI installation (Node, registry, ripgrep, native modules)
144
+ b4m skills push <path> # Create a skill from a folder, SKILL.md, or commands/*.md
145
+ b4m skills push <path> --update # Update your skill with the same name without a prompt
141
146
  ```
142
147
 
148
+ ### Pushing a local skill
149
+
150
+ Log in with `b4m /login`, then run `b4m skills push ~/.claude/skills/my-skill`.
151
+ Only the Markdown instructions are uploaded; supporting files and unsupported
152
+ frontmatter (including `allowed-tools`) are reported and skipped. If no description
153
+ is present, the first non-empty body line supplies it.
154
+
155
+ An existing skill prompts before updating. For scripts without a terminal, pass
156
+ `--update` explicitly. Removing `argument-hint` from the file clears the stored hint
157
+ on update. A Skill Market import cannot be overwritten; rename the local skill or
158
+ delete the import first.
159
+
160
+ An export from another Skill Market author requires separate consent because the
161
+ copy becomes your own trusted skill. Prefer importing it from the market to retain
162
+ attribution and updates; `--yes` acknowledges this warning when pushing a copy.
163
+
164
+ If push reports missing or expired authentication, run `b4m /login` again. If a
165
+ non-interactive push stops at a name conflict, use `--update` only when you intend
166
+ to replace that skill. Server validation errors are printed without creating or
167
+ updating the skill.
168
+
143
169
  ### Switching environments (`--dev` / `--prod`)
144
170
 
145
171
  Flip which backend the CLI talks to without editing config by hand:
@@ -162,16 +188,28 @@ shown in the startup banner (`🌍 API Environment: …`).
162
188
  > For a one-off custom/self-hosted URL, use the in-session `/set-api <url>` command
163
189
  > (see [API Configuration](#api-configuration) below).
164
190
 
191
+ ### Typing while the agent works
192
+
193
+ You can keep typing while a turn is running. Nothing you send is dropped, and a second turn never starts in parallel with the first.
194
+
195
+ - **Enter** steers the running turn. The message is added to the agent's next model request, which is sent after the current batch of tool calls finishes. If the agent has just answered and has iterations left, it reopens to take the message into account. A message the turn never reaches runs as the next turn.
196
+ - **Tab** queues the message as a follow-up. It runs as a new turn once the current one ends. Everything still queued at that point runs together as a single turn, in the order you sent it.
197
+ - **Esc** cancels the turn and moves any queued messages back into the input, where you can edit and resend them or clear them with Ctrl+C. Any message you send while the cancelled turn is still shutting down runs once the turn has stopped.
198
+
199
+ Queued messages appear above the input. Follow-ups are labelled `[next turn]`.
200
+
165
201
  ## Commands
166
202
 
167
203
  While in interactive mode. This mirrors the registry in `src/config/commands.ts` — run `/help` in a session for the live list (which also includes any custom and project commands).
168
204
 
169
205
  **Authentication:**
206
+
170
207
  - `/login` - Authenticate with your B4M account
171
208
  - `/logout` - Clear authentication and sign out
172
209
  - `/whoami` - Show current authenticated user
173
210
 
174
211
  **Session management:**
212
+
175
213
  - `/save <name>` - Save current session
176
214
  - `/resume` (alias `/sessions`) - List and resume saved sessions
177
215
  - `/clear` (alias `/new`) - Start a new session
@@ -180,11 +218,13 @@ While in interactive mode. This mirrors the registry in `src/config/commands.ts`
180
218
  - `/usage` - Show credit usage and balance
181
219
 
182
220
  **API configuration:**
221
+
183
222
  - `/set-api <url>` - Connect to a self-hosted / custom Bike4Mind instance
184
223
  - `/reset-api` - Reset to the Bike4Mind main service
185
224
  - `/api-info` - Show current API configuration
186
225
 
187
226
  **Files & checkpoints:**
227
+
188
228
  - `/add-dir <path>` / `/remove-dir <path>` / `/dirs` - Manage accessible directories
189
229
  - `/undo` - Undo the last file change
190
230
  - `/checkpoints` - List file restore points
@@ -193,9 +233,11 @@ While in interactive mode. This mirrors the registry in `src/config/commands.ts`
193
233
  - `/rewind` - Rewind the conversation to a previous point
194
234
 
195
235
  **Tool permissions:**
236
+
196
237
  - `/trust <tool-name>` / `/untrust <tool-name>` / `/trusted` - Manage auto-approved tools
197
238
 
198
239
  **Sandbox** (OS-level isolation for `bash`):
240
+
199
241
  - `/sandbox` - Show sandbox status and configuration
200
242
  - `/sandbox:enable` / `/sandbox:disable` - Toggle the sandbox
201
243
  - `/sandbox:mode <auto-allow|permissions>` - Set enforcement mode
@@ -204,20 +246,24 @@ While in interactive mode. This mirrors the registry in `src/config/commands.ts`
204
246
  - `/sandbox:violations [count]` / `/sandbox:violations:clear` - Inspect / clear violations
205
247
 
206
248
  **MCP & agents:**
249
+
207
250
  - `/mcp` (alias `/mcp:list`) - Show MCP server status and connected tools
208
251
  - `/agents` (alias `/agents:list`) - List available agents
209
252
  - `/agents:new <name>` / `/agents:reload` - Create / reload agent definitions
210
253
 
211
254
  **Custom commands:**
255
+
212
256
  - `/commands` - List all custom commands
213
257
  - `/commands:new <name>` / `/commands:reload` - Create / reload custom commands
214
258
 
215
259
  **Durable workflow** (cross-session continuity):
260
+
216
261
  - `/workflow [decisions|blockers|handoff|review-gates]` - Workflow overview or a section
217
262
  - `/decisions` / `/blockers` / `/review-gates` - Shortcuts for the sections above
218
263
  - `/handoff [generate|--local]` - Show or generate a session handoff (`--local` = LLM-free snapshot)
219
264
 
220
265
  **General:**
266
+
221
267
  - `/help` - Show help
222
268
  - `/config` - Open the interactive configuration editor
223
269
  - `/project-config` - Show merged project configuration
@@ -304,6 +350,7 @@ export SERPER_API_KEY="your-key-here"
304
350
  ```
305
351
 
306
352
  **Available Tools:**
353
+
307
354
  - ✅ `dice_roll` - No API key needed
308
355
  - ✅ `math_evaluate` - No API key needed
309
356
  - ✅ `current_datetime` - No API key needed
@@ -315,6 +362,7 @@ export SERPER_API_KEY="your-key-here"
315
362
  - 🔑 `deep_research` - Requires `toolApiKeys.serper`
316
363
 
317
364
  **Get API Keys:**
365
+
318
366
  - OpenWeather API: https://openweathermap.org/api
319
367
  - Serper API: https://serper.dev/
320
368
 
@@ -361,6 +409,7 @@ You can run MCP servers in Docker containers for isolation and portability:
361
409
  ```
362
410
 
363
411
  **Docker Configuration Notes:**
412
+
364
413
  - The `-i` flag is required for stdin communication with the MCP server
365
414
  - Use `-e VAR_NAME` (without value) to pass a variable through to the container. It must be listed in this server's `env` block: the MCP child is started with those variables plus the MCP SDK's own defaults (`PATH`, `HOME`, `SHELL`, `TERM`, `USER`), never with your whole shell environment
366
415
  - Keys that would have the runtime load code before the server starts - `NODE_*`, `LD_*`, `DYLD_*`, `ELECTRON_RUN_AS_NODE` - are refused and logged. Everything else in `env` is passed through, so `HTTPS_PROXY`, `NO_PROXY` and a `PATH` your wrapper needs all work here
@@ -387,6 +436,7 @@ You can also run any MCP server via npx or custom executables:
387
436
  ```
388
437
 
389
438
  **Available Built-in MCP Servers:**
439
+
390
440
  - 🔧 **GitHub** - Repository management, issues, PRs (requires `GITHUB_ACCESS_TOKEN`)
391
441
  - 🔧 **LinkedIn** - Post management, analytics (requires `LINKEDIN_ACCESS_TOKEN`, `COMPANY_NAME`)
392
442
  - 🔧 **Atlassian** - Jira/Confluence integration (requires `ATLASSIAN_ACCESS_TOKEN`, `ATLASSIAN_CLOUD_ID`, `ATLASSIAN_SITE_URL`)
@@ -395,16 +445,32 @@ You can also run any MCP server via npx or custom executables:
395
445
 
396
446
  #### Serve Bike4Mind itself as an MCP server (`b4m mcp serve`)
397
447
 
398
- The reverse of adding a server: `b4m mcp serve` exposes your Bike4Mind backend to any MCP client (Claude Desktop, editors, other agents). It advertises nineteen tools (`list_notebooks`, `get_notebook`, `create_notebook`, `rename_notebook`, `clone_notebook`, `delete_notebook`, `list_projects`, `get_project`, `create_project`, `send_message`, `search_knowledge_base`, `list_lakes`, `list_files`, `get_file`, `generate_sound_effect`, `text_to_speech`, `generate_image`, `create_artifact`, `publish_artifact`) plus four resource templates (`b4m://notebook/{id}`, `b4m://file/{id}`, `b4m://project/{id}`, `b4m://artifact/{id}`), each listable and readable as JSON, and `b4m://agent-quest` - the credential-free manifest for The Open Door, the onboarding quest for agents.
448
+ The reverse of adding a server: `b4m mcp serve` exposes your Bike4Mind backend to any MCP client (Claude Desktop, editors, other agents). It advertises twenty-six tools (`list_notebooks`, `get_notebook`, `create_notebook`, `rename_notebook`, `clone_notebook`, `delete_notebook`, `list_projects`, `get_project`, `create_project`, `send_message`, `search_knowledge_base`, `list_lakes`, `list_files`, `get_file`, `generate_sound_effect`, `text_to_speech`, `generate_image`, `create_artifact`, `publish_artifact`, `bob_read_site`, `bob_compare_sites`, `bob_recheck`, `get_bob_report`, `get_bob_comparison`, `list_bob_reports`, `bob_runs_left`) plus four resource templates (`b4m://notebook/{id}`, `b4m://file/{id}`, `b4m://project/{id}`, `b4m://artifact/{id}`), each listable and readable as JSON, and `b4m://agent-quest` - the credential-free manifest for The Open Door, the onboarding quest for agents.
399
449
 
400
450
  To get answers grounded in a data lake, call `list_lakes`, then `create_notebook { dataLakeId }` with a lake's id, then `send_message { notebookId }` on that notebook: the result carries the `reply` plus the `citables` it was grounded in. `list_lakes` needs a key with `datalake:read` (and data lakes enabled on the instance).
401
451
 
452
+ The seven Bob tools read websites with Bob on an instance that serves it. `bob_read_site` and `bob_compare_sites` spend the key owner's credits and publish each report, as a run started in the app does; they wait up to `waitSeconds` (default 600, at most 900) with progress, then return the report as Markdown with its in-app and published links, or the id to read back with `get_bob_report` / `get_bob_comparison` when Bob is still reading. `bob_recheck` re-scans one finding against the live site; it costs no credits and adds the reading to the existing report. Bob's routes enforce their scopes: starting work needs `bob:run`, which can also read back, and reading needs `bob:read`.
453
+
402
454
  ```bash
403
455
  b4m mcp serve # stdio (default) - for local clients that spawn the process
404
456
  b4m mcp serve --http --port 7000 # stateless streamable HTTP at http://127.0.0.1:7000/mcp
405
457
  ```
406
458
 
407
- Auth precedence is `--api-key` > `B4M_API_KEY` > the stored login; the endpoint is `B4M_API_URL` > `--api-url` > the configured backend. Tool *listing* needs no credentials; tool *calls* authenticate. Prefer `B4M_API_KEY` over `--api-key` (a flag value shows up in process listings). HTTP mode binds loopback only (`127.0.0.1`, `/mcp` path) and has no per-request auth, so do not expose it off-host. The advertised scopes are the recommended key configuration, not per-route hard gates. See [Serve Bike4Mind as an MCP server](../../BIKE4MIND_CLI.md#serve-bike4mind-as-an-mcp-server-b4m-mcp-serve) for the Claude Desktop config and details.
459
+ Over stdio only, when the caller is entitled to OptiHashi and the key carries `optihashi:read` and `optihashi:compute`, the server also offers `optihashi_solvers` and `optihashi_solve` (a free, bounded classical solver race). With an API key, the CLI first checks the key's own scopes and offers them only when it holds both. The solve input schema comes from the server at startup, with a 5 second discovery budget; restart the session to pick up changed family ids, solver ids or limits. `optihashi_solvers` fetches the current roster on every call. With no entitlement, scopes or feature, they are simply not listed. No paid compute is exposed, and HTTP mode never offers them.
460
+
461
+ Auth precedence is `--api-key` > `B4M_API_KEY` > the stored login; the endpoint is `B4M_API_URL` > `--api-url` > the configured backend. Tool _listing_ needs no credentials, apart from the optional stdio-only tools above, which are fetched once at startup; tool _calls_ authenticate. Prefer `B4M_API_KEY` over `--api-key` (a flag value shows up in process listings). HTTP mode binds loopback only (`127.0.0.1`, `/mcp` path) and requires an `Authorization: Bearer <token>` header on every request. The token is `--token`, else `B4M_MCP_TOKEN`, else a random one generated at startup and printed to stderr (a token you set yourself is printed masked). Prefer `B4M_MCP_TOKEN` over `--token`, which shows up in process listings. The advertised scopes are the recommended key configuration, not per-route hard gates, apart from Bob's, which its routes enforce. See [Serve Bike4Mind as an MCP server](../../BIKE4MIND_CLI.md#serve-bike4mind-as-an-mcp-server-b4m-mcp-serve) for the Claude Desktop config and details. Point an HTTP client at it like this:
462
+
463
+ ```json
464
+ {
465
+ "mcpServers": {
466
+ "bike4mind": {
467
+ "type": "http",
468
+ "url": "http://127.0.0.1:7000/mcp",
469
+ "headers": { "Authorization": "Bearer <token>" }
470
+ }
471
+ }
472
+ }
473
+ ```
408
474
 
409
475
  ## Plugins
410
476
 
@@ -438,22 +504,22 @@ The entry default-exports a factory. It receives a context (currently `{ logger
438
504
 
439
505
  ```js
440
506
  export default ctx => ({
441
- name: 'foo', // unique module name
507
+ name: 'foo', // unique module name
442
508
  description: 'What this plugin does',
443
509
  getTools: () => [
444
510
  {
445
- toolFn: async args => 'result', // invoked when the agent calls the tool
511
+ toolFn: async args => 'result', // invoked when the agent calls the tool
446
512
  toolSchema: {
447
513
  name: 'foo_tool',
448
514
  description: 'What the tool does',
449
- parameters: { type: 'object', properties: {} }, // JSON Schema
515
+ parameters: { type: 'object', properties: {} }, // JSON Schema
450
516
  },
451
517
  },
452
518
  ],
453
519
  getSystemPromptSection: () => 'Extra system-prompt text (or an empty string)',
454
- getCommands: () => [{ name: 'foo', description: '...', execute: args => {} }], // optional
455
- registerWsHandlers: wsManager => {}, // optional
456
- dispose: () => {}, // optional, runs on exit/disable
520
+ getCommands: () => [{ name: 'foo', description: '...', execute: args => {} }], // optional
521
+ registerWsHandlers: wsManager => {}, // optional
522
+ dispose: () => {}, // optional, runs on exit/disable
457
523
  });
458
524
  ```
459
525
 
@@ -489,6 +555,7 @@ The CLI includes a `recent_changes` tool that uses git history to find recently
489
555
  ### How It Works
490
556
 
491
557
  The tool uses `git log` to track file modifications and returns:
558
+
492
559
  - Files sorted by activity (most commits first)
493
560
  - Commit messages for context
494
561
  - Optional statistics showing lines changed
@@ -503,6 +570,7 @@ The CLI supports loading project-specific instructions from context files, simil
503
570
  ### Supported Files
504
571
 
505
572
  **Project-level** (in your project directory, checked in priority order):
573
+
506
574
  1. `CLAUDE.local.md` - Personal/gitignored instructions (highest priority)
507
575
  2. `CLAUDE.md` - Project instructions
508
576
  3. `AGENTS.md` - Cross-tool standard
@@ -511,6 +579,7 @@ The CLI supports loading project-specific instructions from context files, simil
511
579
  6. `INSTRUCTIONS.md` - Project instructions (lowest priority)
512
580
 
513
581
  **Global** (in `~/.bike4mind/`):
582
+
514
583
  1. `AI.local.md` - Personal global instructions
515
584
  2. `AI.md` - Global instructions
516
585
 
@@ -535,6 +604,7 @@ Create a `CLAUDE.md` in your project:
535
604
  ```
536
605
 
537
606
  On startup, you'll see:
607
+
538
608
  ```
539
609
  📄 Project context: CLAUDE.md
540
610
  ```
@@ -546,6 +616,7 @@ The agent will follow these instructions for all interactions in that project.
546
616
  Sessions are saved to `~/.bike4mind/sessions/`
547
617
 
548
618
  Each session includes:
619
+
549
620
  - Full conversation history
550
621
  - Token usage tracking
551
622
  - Agent reasoning steps
@@ -556,6 +627,7 @@ Each session includes:
556
627
  Debug logs are automatically written to `~/.bike4mind/debug/[session-id].txt` for every session, regardless of the `--verbose` flag.
557
628
 
558
629
  **What's logged:**
630
+
559
631
  - Session configuration (API endpoint, environment, model)
560
632
  - HTTP requests and responses (with size and preview)
561
633
  - SSE streaming events from LLM
@@ -564,6 +636,7 @@ Debug logs are automatically written to `~/.bike4mind/debug/[session-id].txt` fo
564
636
  - Authentication flow events
565
637
 
566
638
  **Accessing logs:**
639
+
567
640
  ```bash
568
641
  # List all debug logs
569
642
  ls -lh ~/.bike4mind/debug/
@@ -576,13 +649,41 @@ tail -f ~/.bike4mind/debug/[session-id].txt
576
649
  ```
577
650
 
578
651
  **Log retention:**
652
+
579
653
  - Debug logs older than 30 days are automatically cleaned up on CLI startup
580
654
  - Each log file is named by session ID for easy correlation
581
655
 
582
656
  **Verbose mode:**
657
+
583
658
  - Without `--verbose`: Debug logs only written to file (clean console output)
584
659
  - With `--verbose`: Debug logs shown in console AND written to file
585
660
 
661
+ ### Hearth hook troubleshooting
662
+
663
+ If the standalone `bin/hearth-hook.mjs` reporter is not reaching Hearth, enable its diagnostic:
664
+
665
+ ```bash
666
+ export B4M_HEARTH_DEBUG=1
667
+ echo '{}' | node packages/cli/bin/hearth-hook.mjs
668
+ ```
669
+
670
+ Set `B4M_API_URL` and `B4M_API_KEY` first. With missing credentials the hook makes no request.
671
+ Diagnostics are off by default; an empty `B4M_HEARTH_DEBUG` or exactly `0` also disables them.
672
+ Other nonempty values enable one stderr line on failure:
673
+
674
+ - `POST /api/hearth/events failed: HTTP 401`: check whether the API key is valid.
675
+ - `POST /api/hearth/events error: ECONNREFUSED`: check the endpoint and server availability.
676
+ - `POST /api/hearth/events error: TimeoutError`: check connectivity and server responsiveness.
677
+ - `stdin payload error: SyntaxError`: the hook received malformed JSON.
678
+ - `configuration error: ERR_INVALID_URL`: check that `B4M_API_URL` is a valid absolute URL.
679
+ - `configuration error: missing B4M_API_URL or B4M_API_KEY`: set both variables before running the hook.
680
+
681
+ Each line starts with `hearth-hook:`. Errors expose a native error code or name, never the error
682
+ message, API key, URL, or hook payload. The hook still exits `0` on reporting failures. With debugging
683
+ enabled, the request gets 2.75 seconds and the diagnostic flush gets up to 250 ms within the 3-second
684
+ request/diagnostic budget. A closed or stalled stderr can lose the diagnostic; reporting remains
685
+ best-effort. Unset `B4M_HEARTH_DEBUG` after troubleshooting to keep routine hook events quiet.
686
+
586
687
  ## Development
587
688
 
588
689
  This section covers the contributor workflow for hacking on the CLI from a checkout of the monorepo. For end-user installation see [Installation](#installation) above.
@@ -679,6 +780,7 @@ pnpm --filter @bike4mind/agents dev
679
780
  `dist/` rebuilds on save, so the next `b4m` invocation sees changes without a manual `build`.
680
781
 
681
782
  Caveats:
783
+
682
784
  - This does **not** hot-reload an already-running interactive `b4m` session — you still exit and re-run.
683
785
  - Running multiple watchers in parallel (one per package you're touching) hasn't been load-tested in this repo. If watch seems to miss changes or feels unreliable, fall back to manual `pnpm --filter <pkg> build`.
684
786
 
@@ -786,15 +888,15 @@ runtime. Unknown flags are tolerated (never hard-error) and the interactive TUI
786
888
  preserved (a positional prompt seeds **and** submits turn 1 without switching to
787
889
  headless `-p`):
788
890
 
789
- | Flag | Effect |
790
- | --- | --- |
791
- | `--mcp-config <file>` | Inject MCP servers from a claude-shape JSON (`{ "mcpServers": {...} }`). Supports HTTP transport (`type: "http"`, `url`, `headers`) with per-launch Bearer auth. |
792
- | `--strict-mcp-config` | Use ONLY `--mcp-config` servers; ignore file-config and `.mcp.json` (board YAML pane). |
793
- | `--append-system-prompt <text>` | Append text verbatim to the end of the system prompt (the 3-layer brief). |
794
- | `--allowedTools <patterns>` | Auto-approve matching tools without a permission prompt (glob `mcp__manifold__*` or a space-separated explicit list). |
795
- | `--settings <json>` | Inline JSON; the `hooks` subset drives lifecycle hooks (see below). Malformed JSON is ignored, never fatal. |
891
+ | Flag | Effect |
892
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
893
+ | `--mcp-config <file>` | Inject MCP servers from a claude-shape JSON (`{ "mcpServers": {...} }`). Supports HTTP transport (`type: "http"`, `url`, `headers`) with per-launch Bearer auth. |
894
+ | `--strict-mcp-config` | Use ONLY `--mcp-config` servers; ignore file-config and `.mcp.json` (board YAML pane). |
895
+ | `--append-system-prompt <text>` | Append text verbatim to the end of the system prompt (the 3-layer brief). |
896
+ | `--allowedTools <patterns>` | Auto-approve matching tools without a permission prompt (glob `mcp__manifold__*` or a space-separated explicit list). |
897
+ | `--settings <json>` | Inline JSON; the `hooks` subset drives lifecycle hooks (see below). Malformed JSON is ignored, never fatal. |
796
898
  | `--session-id <uuid>` / `--resume <uuid>` | Pin / resume a session (board pane). `/clear` and `/compact` keep the pinned uuid; a bad `--resume` prints `No conversation found with session ID <uuid>` and exits non-zero so the host can self-heal the pane. |
797
- | `<positional prompt>` | Seeds and auto-fires turn 1 while staying interactive. |
899
+ | `<positional prompt>` | Seeds and auto-fires turn 1 while staying interactive. |
798
900
 
799
901
  ### Lifecycle hooks (`--settings.hooks`)
800
902
 
@@ -217,6 +217,10 @@ const argv = await yargs(hideBin(process.argv))
217
217
  type: 'string',
218
218
  description: 'Bike4Mind API URL (overrides B4M_API_URL and the configured backend)',
219
219
  })
220
+ .option('token', {
221
+ type: 'string',
222
+ description: 'Bearer token required by --http mode (overrides B4M_MCP_TOKEN; generated and printed when unset). Visible in process listings; prefer B4M_MCP_TOKEN',
223
+ })
220
224
  .example('b4m mcp serve', 'Serve over stdio for Claude Desktop and other local clients')
221
225
  .example('b4m mcp serve --http --port 7000', 'Serve over streamable HTTP on port 7000');
222
226
  }, async () => {
@@ -250,6 +254,31 @@ const argv = await yargs(hideBin(process.argv))
250
254
  })
251
255
  .demandCommand(1, 'You must provide a subcommand (list, add, remove)');
252
256
  })
257
+ .command('skills', 'Manage your Bike4Mind skills', (yargs) => {
258
+ return yargs
259
+ .command('push <path>', 'Create or update a skill from a Claude Code SKILL.md', (yargs) => {
260
+ return yargs
261
+ .positional('path', {
262
+ type: 'string',
263
+ describe: 'Skill folder, its SKILL.md, or a commands/*.md file',
264
+ })
265
+ .option('update', {
266
+ type: 'boolean',
267
+ description: 'Overwrite your existing skill with the same name without asking',
268
+ default: false,
269
+ })
270
+ .option('yes', {
271
+ type: 'boolean',
272
+ description: "Push another author's Skill Market skill as your own without asking",
273
+ default: false,
274
+ })
275
+ .example('b4m skills push ~/.claude/skills/my-skill', 'Create a skill from a Claude Code skill folder')
276
+ .example('b4m skills push ./SKILL.md --update', 'Update the existing skill of the same name');
277
+ }, async () => {
278
+ // Handled by external command handler
279
+ })
280
+ .demandCommand(1, 'You must provide a subcommand (push)');
281
+ })
253
282
  .command('update', 'Check for and install CLI updates')
254
283
  .command('doctor', 'Run diagnostic checks on CLI installation')
255
284
  .command('acp', 'Run as an Agent Client Protocol (ACP) stdio server for editors like Zed')
@@ -312,7 +341,7 @@ if (argv.resume) {
312
341
  }
313
342
  // Positional task (claude `<prompt>` form): seeds AND submits turn 1, stays interactive.
314
343
  // Only when it's not a known subcommand and headless -p wasn't used.
315
- const KNOWN_SUBCOMMANDS = new Set(['mcp', 'plugin', 'update', 'doctor', 'acp']);
344
+ const KNOWN_SUBCOMMANDS = new Set(['mcp', 'plugin', 'skills', 'update', 'doctor', 'acp']);
316
345
  if (argv.prompt === undefined && argv._.length > 0 && !KNOWN_SUBCOMMANDS.has(String(argv._[0]))) {
317
346
  process.env.B4M_INITIAL_PROMPT = String(argv._[0]);
318
347
  }
@@ -480,6 +509,31 @@ if (argv._[0] === 'plugin') {
480
509
  }
481
510
  }
482
511
 
512
+ // Handle skills subcommands (external commands)
513
+ if (argv._[0] === 'skills') {
514
+ const skillsSubcommand = argv._[1];
515
+
516
+ try {
517
+ let handleSkillsCommand;
518
+
519
+ if (isDev) {
520
+ const { register } = require('tsx/esm/api');
521
+ register();
522
+ const module = await import('../src/commands/skillsCommand.ts');
523
+ handleSkillsCommand = module.handleSkillsCommand;
524
+ } else {
525
+ const module = await import('../dist/commands/skillsCommand.mjs');
526
+ handleSkillsCommand = module.handleSkillsCommand;
527
+ }
528
+
529
+ await handleSkillsCommand(skillsSubcommand, argv);
530
+ process.exit(0);
531
+ } catch (error) {
532
+ console.error('Error:', error.message);
533
+ process.exit(1);
534
+ }
535
+ }
536
+
483
537
  // Handle update command (external command)
484
538
  if (argv._[0] === 'update') {
485
539
  try {
@@ -9,6 +9,9 @@
9
9
  // shared default channel by NAME and the server find-or-creates it, so a fresh
10
10
  // install needs no per-user setup and lands in the same channel the cc-bridge
11
11
  // reports into (one roster, not two half-rosters).
12
+ // Optional: B4M_HEARTH_DEBUG - any value but empty or 0 writes one stderr line
13
+ // when reporting fails or stdin errors. Off by default: hook stderr can reach the
14
+ // user's terminal on every event, so a stale key must not nag.
12
15
  // Always exits 0 - a reporting hook must never block the session.
13
16
  //
14
17
  // DISCLOSURE. Everything sent here lands in an append-only log that other
@@ -39,7 +42,36 @@
39
42
  import { basename, resolve } from 'node:path';
40
43
  import { homedir } from 'node:os';
41
44
 
42
- const { B4M_API_URL, B4M_API_KEY, B4M_HEARTH_CHANNEL, B4M_HEARTH_DISCLOSURE } = process.env;
45
+ const { B4M_API_URL, B4M_API_KEY, B4M_HEARTH_CHANNEL, B4M_HEARTH_DISCLOSURE, B4M_HEARTH_DEBUG } = process.env;
46
+
47
+ const DEBUG = Boolean(B4M_HEARTH_DEBUG) && B4M_HEARTH_DEBUG !== '0';
48
+
49
+ // The diagnostic is best-effort: with the reader gone, an unhandled EPIPE here
50
+ // would kill the process and decide the exit code, which this hook must never do.
51
+ process.stderr.on('error', () => {});
52
+
53
+ // Resolves once the line is flushed: stderr to a pipe is async on macOS, so
54
+ // exiting straight after a bare write can drop it. Bounded, because a full pipe
55
+ // with a stalled reader never calls back - and a diagnostic must not be the
56
+ // thing that holds the session open.
57
+ const FLUSH_DEADLINE_MS = 250;
58
+ const REQUEST_DEADLINE_MS = 3000;
59
+ const debug = line =>
60
+ DEBUG
61
+ ? new Promise(done => {
62
+ const deadline = setTimeout(done, FLUSH_DEADLINE_MS).unref();
63
+ process.stderr.write(`hearth-hook: ${line}\n`, () => {
64
+ clearTimeout(deadline);
65
+ done();
66
+ });
67
+ })
68
+ : Promise.resolve();
69
+
70
+ function errorCode(error) {
71
+ const code = error?.cause?.code ?? error?.code;
72
+ if (typeof code === 'string') return code;
73
+ return typeof error?.name === 'string' ? error.name : 'unknown';
74
+ }
43
75
 
44
76
  const DEFAULT_DISCLOSURE = 2;
45
77
  const MAX_DISCLOSURE = 2;
@@ -238,10 +270,18 @@ function describe({ label, tier, workspace, activity }) {
238
270
  }
239
271
 
240
272
  const chunks = [];
273
+ process.stdin.on('error', err => {
274
+ debug(`stdin error: ${errorCode(err)}`).finally(() => process.exit(0));
275
+ });
241
276
  process.stdin.on('data', c => chunks.push(c));
242
277
  process.stdin.on('end', async () => {
278
+ let pending = Promise.resolve();
279
+ let operation = 'stdin payload';
243
280
  try {
244
- if (!B4M_API_URL || !B4M_API_KEY) return;
281
+ if (!B4M_API_URL || !B4M_API_KEY) {
282
+ pending = debug('configuration error: missing B4M_API_URL or B4M_API_KEY');
283
+ return;
284
+ }
245
285
  const hook = JSON.parse(Buffer.concat(chunks).toString() || '{}');
246
286
 
247
287
  const tier = disclosureTier();
@@ -267,7 +307,10 @@ process.stdin.on('end', async () => {
267
307
  const activity = activityOf(hook);
268
308
  if (tier >= 2) payload.activity = activity;
269
309
 
270
- await fetch(new URL('/api/hearth/events', B4M_API_URL), {
310
+ operation = 'configuration';
311
+ const endpoint = new URL('/api/hearth/events', B4M_API_URL);
312
+ operation = 'POST /api/hearth/events';
313
+ const res = await fetch(endpoint, {
271
314
  method: 'POST',
272
315
  headers: { 'content-type': 'application/json', 'x-api-key': B4M_API_KEY },
273
316
  body: JSON.stringify({
@@ -281,12 +324,15 @@ process.stdin.on('end', async () => {
281
324
  // had posted it - the log could not distinguish operator from tooling.
282
325
  actor: { kind: 'agent', displayName: label },
283
326
  }),
284
- // Bounded so a hung request can never stall the session past 3s.
285
- signal: AbortSignal.timeout(3000),
327
+ // Reserve the debug flush inside the same 3s request/diagnostic budget.
328
+ signal: AbortSignal.timeout(DEBUG ? REQUEST_DEADLINE_MS - FLUSH_DEADLINE_MS : REQUEST_DEADLINE_MS),
286
329
  });
287
- } catch {
288
- // Swallow everything: reporting must never fail the hook.
330
+ if (!res.ok) pending = debug(`POST /api/hearth/events failed: HTTP ${res.status}`);
331
+ } catch (err) {
332
+ // Never print error messages: they can contain URLs or input content.
333
+ pending = debug(`${operation} error: ${errorCode(err)}`);
289
334
  } finally {
335
+ await pending;
290
336
  process.exit(0);
291
337
  }
292
338
  });