@feniix/bridgekit 0.11.0 → 0.13.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
@@ -4,6 +4,69 @@ All notable changes to `@feniix/bridgekit` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.13.0] - 2026-05-27
8
+
9
+ ### Added
10
+
11
+ - New `buildStdio` option on `BinWrapperOptions`
12
+ (`@feniix/bridgekit/bin-wrapper`). Default `"inherit"` preserves
13
+ existing behavior. **MCP stdio server bins should pass
14
+ `["ignore", "inherit", "inherit"]`** — the build subprocess's stdout
15
+ otherwise contaminates the parent's JSON-RPC framing on its own
16
+ stdout. The default-`"inherit"` behavior was a latent risk for the
17
+ two current consumers that adopted `runBinWrapper` (works today only
18
+ because `npm run <script> --silent` happens to be quiet under tsc) and
19
+ an active blocker for `pi-exa`, which could not migrate from its
20
+ hand-rolled wrapper. Resolves
21
+ [#59](https://github.com/feniix/bridgekit/issues/59).
22
+
23
+ ### Changed
24
+
25
+ - `PiToolDefinition.promptGuidelines` (the internal tool-shape declared
26
+ in `src/adapters/pi.ts` and exposed via
27
+ `PiToolRegistration["registerTool"]`'s parameter) widened from
28
+ `readonly string[]` to `string[]` to close a contravariance gap with
29
+ pi-coding-agent's `ExtensionAPI`. The runtime contract is unchanged —
30
+ `registerPiTools` has spread-copied the source array into a fresh
31
+ `string[]` before handing it to `pi.registerTool` since 0.9.1
32
+ (PR #48); only the type declaration catches up to reality. Consumers
33
+ (`pi-exa`, `pi-code-reasoning`, `pi-sequential-thinking`) can drop
34
+ the `pi as unknown as PiToolRegistration` cast at their
35
+ `registerPiTools` call sites.
36
+ `PortableTool.hostExtras.pi.promptGuidelines` (the consumer-facing
37
+ metadata type) stays `readonly string[]` — that's the right contract
38
+ for immutable consumer-owned data that bridgekit reads. Resolves
39
+ [#60](https://github.com/feniix/bridgekit/issues/60).
40
+
41
+ ## [0.12.0] - 2026-05-27
42
+
43
+ ### Documentation
44
+
45
+ - `README.md` gets a full `bin-wrapper` adapter section under "API
46
+ reference" — usage example, options breakdown, `runServer`
47
+ contract, trusted-literal security note, and the missing-entry
48
+ behavior matrix. Previously the subpath had a single line in the
49
+ entrypoint table and zero body documentation. Closes the largest
50
+ gap surfaced by the post-0.11.0 doc audit.
51
+ - `README.md` "Why" bullet and "Packaging" section updated for the
52
+ four-entrypoint shape and the `runBinWrapper` recommendation
53
+ (replaces the pre-0.11.0 manual checked-in-wrapper recipe).
54
+ - `llms.txt` rewrites the "Mixed source-loaded hosts and compiled
55
+ MCP bins" section to lead with `runBinWrapper`. The 8-bullet
56
+ manual-wrapper recipe is demoted to a fallback bullet for
57
+ authors writing their own wrapper.
58
+ - `dist/src/bin-wrapper.d.ts` added to the published-declarations
59
+ lists in `README.md` and `llms.txt`.
60
+ - `docs/README.md` lists `docs/rfc-host-extras.md` in the
61
+ maintainer-docs catalog. The RFC is JSDoc-linked from
62
+ `src/core/define-tool.ts` and was missing from the catalog.
63
+ - `src/adapters/mcp.typecheck.ts` fixes a wrong version stamp on
64
+ the `signalFromExtra` adversarial-pin comment (was `0.10.0 (#3)`,
65
+ should be `0.11.0 (#3)`). The pin's code is correct; only the
66
+ comment was wrong.
67
+
68
+ No API or behavior changes.
69
+
7
70
  ## [0.11.0] - 2026-05-27
8
71
 
9
72
  ### Added
package/README.md CHANGED
@@ -66,7 +66,7 @@ This package is ESM-only and supports Node.js 22.19.0 or newer. Published module
66
66
  - Define a tool once; ship it to pi and MCP without per-host forks.
67
67
  - Host-neutral tool files: no pi or MCP SDK imports in the tool definition itself.
68
68
  - TypeBox schemas pass through to MCP `inputSchema` directly — no JSON Schema conversion step.
69
- - Import-passive, `sideEffects: false`, three-entrypoint split (`.`, `./pi`, `./mcp`) so pi-only consumers do not pull the MCP SDK and vice versa.
69
+ - Import-passive, `sideEffects: false`, four-entrypoint split (`.`, `./pi`, `./mcp`, `./bin-wrapper`) so pi-only consumers do not pull the MCP SDK and vice versa.
70
70
  - Conformance-tested public surface: a packed-install smoke test enforces the runtime export set, deep-import rejection, and type-level strictness against the installed declarations.
71
71
 
72
72
  ## API reference
@@ -233,6 +233,42 @@ Portable validation failures and portable `isError: true` results return `CallTo
233
233
 
234
234
  The two adapters now read in parallel: invalid args and portable `isError` results return `{ isError: true }` from both hosts by default, and the same result-guard helpers (`isValidationFailure`, `isDomainFailure`) narrow them on either side.
235
235
 
236
+ ### bin-wrapper (since 0.11.0)
237
+
238
+ The `bin-wrapper` subpath ships a helper for npm `bin` scripts that load a compiled MCP entrypoint and build it on first invocation when the compiled output is missing. Replaces the ~25-line "resolve dist path → spawn build if missing → import and run" boilerplate that downstream consumers with mixed source-loaded pi + compiled MCP packages previously hand-rolled.
239
+
240
+ ```js
241
+ #!/usr/bin/env node
242
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
243
+
244
+ await runBinWrapper({
245
+ metaUrl: import.meta.url,
246
+ mcpEntry: "dist/extensions/mcp-server.js",
247
+ buildScript: "build:mcp",
248
+ });
249
+ ```
250
+
251
+ Options:
252
+
253
+ - `metaUrl` (required): `import.meta.url` of the bin script. Used to locate the package root (the bin's parent directory).
254
+ - `mcpEntry` (required): path to the compiled MCP entry, relative to the package root (e.g. `"dist/extensions/mcp-server.js"`).
255
+ - `buildScript` (required): npm script to invoke when the entry is missing (e.g. `"build:mcp"`).
256
+ - `buildTimeoutMs` (optional, default `60_000`): timeout for the build subprocess. Distinct "Build timed out…" diagnostic fires when exceeded.
257
+ - `logPrefix` (optional, default `"bridgekit-bin"`): prefix on the "Failed to build…" diagnostic.
258
+ - `buildStdio` (optional, default `"inherit"`): `stdio` mode passed to `spawnSync` when the build script runs. **MCP stdio server bins should pass `["ignore", "inherit", "inherit"]`** so the build subprocess's stdout cannot contaminate the parent's JSON-RPC framing channel (`process.stdout`). stderr stays inherited so build diagnostics remain visible.
259
+
260
+ The MCP entry module must export `async function runServer(): Promise<void>`. Consumers with a different export name can alias on export.
261
+
262
+ **Security: `mcpEntry` and `buildScript` must be literal strings in the caller's source.** The helper joins `mcpEntry` onto the resolved package root and dynamically `import()`s it, and it passes `buildScript` to `spawnSync` (with `shell: true` on Windows where `&`, `|`, and `^` are shell metacharacters). Sourcing either from CLI args or environment variables exposes arbitrary-file import and Windows command injection.
263
+
264
+ Behavior on missing entry:
265
+
266
+ - Build emits the entry → import and call `runServer()`.
267
+ - Build exits non-zero but the entry exists → recover (import and call `runServer()`).
268
+ - Build exits 0 but the entry is still missing → exit with code 1, generic "Failed to build…" diagnostic.
269
+ - Build exits non-zero with entry missing → exit with the build's status code, generic diagnostic.
270
+ - Build killed by timeout (SIGTERM) → exit 1, distinct timeout diagnostic naming `buildTimeoutMs`.
271
+
236
272
  ## Best practices
237
273
 
238
274
  Tool definition best practices:
@@ -258,7 +294,7 @@ Package and release checklist:
258
294
  - Avoid `workspace:` or `file:` dependency ranges in publishable packages.
259
295
  - Avoid dangling `sourceMappingURL` comments: publish maps and useful sources together, or disable source maps for package builds.
260
296
  - For MCP stdio bins, ensure the executable entrypoint starts with a Node shebang, has executable mode (`chmod +x` or equivalent), and is included by `npm pack --dry-run --json`.
261
- - If an npm-launched bin depends on generated output, prefer a checked-in wrapper under `bin/` over pointing `bin` directly at `dist/`; the wrapper should resolve the package-local generated file and may run the package-local build for workspace/local execution.
297
+ - If an npm-launched bin depends on generated output, use `runBinWrapper` from `@feniix/bridgekit/bin-wrapper` (since 0.11.0) — it resolves the package-local generated entry, runs the package-local build when output is missing in workspace/local execution, preserves build failures, and distinguishes timeout from build error in its diagnostic. The bin script becomes a three-line invocation; no hand-rolled wrapper needed.
262
298
  - If a package keeps a source-loaded host entrypoint (for example a pi extension source file), use a package-local MCP build behind that wrapper and narrow the build to the MCP entrypoint plus shared host-neutral modules.
263
299
  - Declare a compatible Node engine (`>=22.19.0`) in downstream packages that expose BridgeKit-powered MCP bins.
264
300
  - Run `npm run check`, `npm run test`, `npm run pack:dry-run`, and `npm run package-smoke` before publishing.
@@ -272,5 +308,5 @@ Read these files in order:
272
308
 
273
309
  1. `README.md` — public API, contracts, and best practices.
274
310
  2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
275
- 3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, and per-host metadata via `hostExtras`.
276
- 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, and `dist/src/mcp.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain the same implementation context.
311
+ 3. `examples/README.md` — copyable layouts for shared tools, pi extensions, MCP stdio servers, per-host metadata via `hostExtras`, and bin wrappers via `runBinWrapper`.
312
+ 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, `dist/src/mcp.d.ts`, and `dist/src/bin-wrapper.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain the same implementation context.
@@ -20,7 +20,7 @@ type PiToolDefinition = {
20
20
  parameters: TSchema;
21
21
  execute(toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: (update: PiToolUpdate) => void, ctx?: unknown): Promise<PiToolResult>;
22
22
  promptSnippet?: string;
23
- promptGuidelines?: readonly string[];
23
+ promptGuidelines?: string[];
24
24
  };
25
25
  export type PiToolRegistration = {
26
26
  registerTool(tool: PiToolDefinition): unknown;
@@ -30,6 +30,20 @@ export interface BinWrapperOptions {
30
30
  buildScript: string;
31
31
  buildTimeoutMs?: number;
32
32
  logPrefix?: string;
33
+ /**
34
+ * stdio mode passed to `spawnSync` when the build script runs. Defaults to
35
+ * `"inherit"`.
36
+ *
37
+ * **MCP stdio server bins must override this.** `process.stdout` IS the
38
+ * JSON-RPC channel for an MCP stdio server; any output the build subprocess
39
+ * emits to stdout (npm warnings, postinstall scripts, tsc diagnostics) flows
40
+ * directly into the parent's stdout via `stdio: "inherit"` and contaminates
41
+ * JSON-RPC framing. Pass `["ignore", "inherit", "inherit"]` to route the
42
+ * build's stdout to `/dev/null` while keeping stderr visible.
43
+ *
44
+ * @default "inherit"
45
+ */
46
+ buildStdio?: SpawnSyncOptions["stdio"];
33
47
  }
34
48
  export interface BinWrapperDeps {
35
49
  spawnSync: BinWrapperSpawnSync;
@@ -20,7 +20,7 @@ export async function runBinWrapperWithDeps(options, deps) {
20
20
  const timeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
21
21
  const build = deps.spawnSync("npm", ["run", options.buildScript, "--silent"], {
22
22
  cwd: packageRoot,
23
- stdio: "inherit",
23
+ stdio: options.buildStdio ?? "inherit",
24
24
  shell: process.platform === "win32",
25
25
  timeout: timeoutMs,
26
26
  });
package/llms.txt CHANGED
@@ -7,7 +7,7 @@ Use this package when you need to define a tool once and expose it through both
7
7
  1. `README.md` — public API, contracts, best practices, packaging notes.
8
8
  2. `llms.txt` — compact agent-facing usage rules and anti-patterns.
9
9
  3. `examples/README.md` — copyable end-to-end layouts for shared tools, pi extension wiring, MCP stdio server wiring, and per-host metadata via `hostExtras`.
10
- 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, and `dist/src/mcp.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain implementation context.
10
+ 4. Published declarations such as `dist/src/index.d.ts`, `dist/src/pi.d.ts`, `dist/src/mcp.d.ts`, and `dist/src/bin-wrapper.d.ts` — canonical installed-package type contracts. In a source checkout, the matching `src/` files contain implementation context.
11
11
 
12
12
  ## Import map
13
13
 
@@ -104,12 +104,27 @@ Some hosts, such as pi in source-extension packages, may intentionally load Type
104
104
 
105
105
  - Keep the host source entrypoint if that is the package convention, e.g. `pi.extensions: ["./extensions/index.ts"]`.
106
106
  - Add a package-local MCP build and point npm `bin` at a checked-in wrapper under `bin/`, not directly at generated `dist/` output.
107
- - The wrapper should resolve the package-local generated MCP server, run the package-local build when output is missing in workspace/local execution, and preserve non-zero build failures.
107
+ - **Use the built-in `runBinWrapper` helper** (since 0.11.0) instead of a hand-rolled wrapper. It resolves the package-local generated entry, runs the package-local build when output is missing in workspace/local execution, preserves non-zero build failures, recovers when the build emits the entry despite a non-zero exit, and emits a distinct timeout diagnostic when `spawnSync` is killed by `buildTimeoutMs`:
108
+
109
+ ```js
110
+ #!/usr/bin/env node
111
+ import { runBinWrapper } from "@feniix/bridgekit/bin-wrapper";
112
+ await runBinWrapper({
113
+ metaUrl: import.meta.url,
114
+ mcpEntry: "dist/extensions/mcp-server.js",
115
+ buildScript: "build:mcp",
116
+ // MCP stdio bins: route the build subprocess's stdout to /dev/null so it
117
+ // cannot contaminate the parent's JSON-RPC framing on process.stdout.
118
+ buildStdio: ["ignore", "inherit", "inherit"],
119
+ });
120
+ ```
121
+
122
+ `mcpEntry` and `buildScript` must be literal strings — sourcing them from CLI args or env vars opens arbitrary-file import and Windows command injection. The MCP entry module must export `async function runServer(): Promise<void>`. `buildStdio` defaults to `"inherit"`; MCP stdio server bins should override to `["ignore", "inherit", "inherit"]` (since 0.13.0) because `process.stdout` is the JSON-RPC channel and the build subprocess otherwise contaminates framing.
108
123
  - Narrow the MCP build to the MCP entrypoint and shared host-neutral modules; avoid compiling pi adapter entrypoints into the standalone MCP path.
109
124
  - Put runtime imports used by the compiled MCP bin in `dependencies`, not only peers or dev dependencies.
110
125
  - Declare BridgeKit's Node engine requirement in the downstream package (`>=22.19.0`).
111
- - Ensure the checked-in wrapper is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
112
- - Test wrapper behavior for existing output, missing output, failed builds, and builds that exit successfully without creating the expected file.
126
+ - Ensure the bin script is executable (`chmod +x` or equivalent) and verify the mode with `npm pack --dry-run --json`.
127
+ - If you write your own wrapper instead of using `runBinWrapper`, test the four behavior modes: existing output, missing output that the build creates, failed builds (non-zero exit), and builds that exit successfully without creating the expected file.
113
128
 
114
129
  ## Anti-patterns
115
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feniix/bridgekit",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "BridgeKit defines TypeBox-backed tools once and adapts them to pi, MCP, and other hosts.",
5
5
  "keywords": [
6
6
  "pi",