argsbarg 3.6.0 → 3.6.2

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
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.6.2] - 2026-06-23
11
+
12
+ ### Added
13
+
14
+ - **`cli-program.md`** — “Read flags once, resolve once” pattern for multi-surface leaves (`read*Flags` + `resolve*Input`).
15
+
16
+ ## [3.6.1] - 2026-06-23
17
+
18
+ ### Fixed
19
+
20
+ - **npm package** — `package.json` `files` whitelist so publish no longer ships `.cursor/`, `.private/`, `.github/`, or other dev-only paths (npm does not honor `.gitignore`).
21
+
10
22
  ## [3.6.0] - 2026-06-23
11
23
 
12
24
  ### Added
@@ -387,7 +399,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
387
399
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
388
400
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
389
401
 
390
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.6.0...HEAD
402
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.6.2...HEAD
403
+ [3.6.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.6.2
404
+ [3.6.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.6.1
391
405
  [3.6.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.6.0
392
406
  [3.5.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.5.0
393
407
  [3.4.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.4.2
@@ -195,6 +195,59 @@ const { limit, "skip-readiness": skipReadiness, timeout } = ctx.readLeafInputs()
195
195
 
196
196
  Cross-field rules (e.g. `--match-remote` requires `--branch`) stay in consumer `resolve*` layers — argsbarg does not validate those.
197
197
 
198
+ ## Read flags once, resolve once
199
+
200
+ For apps with **Ink + headless + MCP** (multiple surfaces per leaf), avoid scattering `ctx.hasFlag` / `ctx.stringOpt` through the handler. Use two layers:
201
+
202
+ | Layer | Responsibility |
203
+ | --- | --- |
204
+ | **`read*Flags(ctx)`** | Read coerced values from `ctx` (`readLeafInputs()`, `durationOpt`, `commaListOpt`, shared mutator flags) into one typed struct |
205
+ | **`resolve*Input(flags)`** | Cross-field validation and defaults; returns `{ ok, input }` or `{ ok: false, error }` |
206
+
207
+ The handler calls **`read*Flags` once**, passes the struct to **`resolve*Input`**, then branches to Ink, headless, or MCP with the same resolved input.
208
+
209
+ **Shared reads** — when many leaves share options (`yes`, `dry-run`, `json`), one app-level helper (e.g. `readMutatingFlags(ctx)`) plus per-command extensions:
210
+
211
+ ```typescript
212
+ // cli/flags.ts
213
+ export function readMutatingFlags(ctx: CliContext) {
214
+ const dryRun = ctx.hasFlag("dry-run");
215
+ return {
216
+ dryRun,
217
+ yes: ctx.hasFlag("yes"),
218
+ explicitJson: wantsExplicitJson(ctx, ctx.hasFlag("json")),
219
+ };
220
+ }
221
+
222
+ // commands/reset/resolve.ts
223
+ export function readResetFlags(ctx: CliContext) {
224
+ return {
225
+ ...readMutatingFlags(ctx),
226
+ env: ctx.args[0],
227
+ force: ctx.hasFlag("force"),
228
+ services: ctx.commaListOpt("services"),
229
+ };
230
+ }
231
+
232
+ export function resolveResetInput(flags: ReturnType<typeof readResetFlags>) {
233
+ if (!flags.env) return { ok: false, error: "…" };
234
+ return { ok: true, input: { env: flags.env, force: flags.force, services: flags.services } };
235
+ }
236
+
237
+ // command handler
238
+ handler: async (ctx) => {
239
+ const flags = readResetFlags(ctx);
240
+ await dispatchMutatingCommand({
241
+ dryRun: flags.dryRun,
242
+ headless: shouldRunHeadlessWithYes(ctx, { yes: flags.yes, hasRequiredArgs: !!flags.env, dryRun: flags.dryRun }),
243
+ resolve: () => resolveResetInput(flags),
244
+ /* … */
245
+ });
246
+ };
247
+ ```
248
+
249
+ **JSON-only CLIs** — a single `readCommandOptions(ctx)` wrapping `readLeafInputs()` per shared option set is usually enough; full `resolve*` layering is optional.
250
+
198
251
  ## Headless-capable handlers
199
252
 
200
253
  Simple leaves (read args, print stdout) are already headless — no extra work. **Any handler that might mount Ink, prompt, or open a browser should also implement a scriptable fast path** for:
@@ -19,5 +19,6 @@ When adding or changing argsbarg schema, leaf handlers, or MCP exposure:
19
19
  - Interactive leaves: one headless path for MCP, non-TTY CLI, and `--yes` / `--dry-run` / `--json` (`shouldRunHeadless*`, `requireYesInNonTty`); not raw `isTTY`.
20
20
  - String options: `format` / `default` / `pattern` per `cli-program.md`; `readLeafInputs()` for multi-flag leaves.
21
21
  - Varargs (`argMax: 0`): CLI space-separated; MCP JSON array only — no comma-splitting positionals.
22
+ - Multi-surface leaves (Ink + headless + MCP): one **`read*Flags(ctx)`** per command (or shared family helper + extensions); one **`resolve*Input(flags)`** for cross-field rules — handler reads ctx once, all paths share the struct.
22
23
 
23
24
  **App-specific conventions:** add below or in a separate `.cursor/rules/` file (shared flags path, Ink vs JSON-only, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.6.0",
3
+ "version": "3.6.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -20,6 +20,15 @@
20
20
  "default": "./src/index.ts"
21
21
  }
22
22
  },
23
+ "files": [
24
+ "src",
25
+ "index.d.ts",
26
+ "docs",
27
+ "examples",
28
+ "README.md",
29
+ "LICENSE",
30
+ "CHANGELOG.md"
31
+ ],
23
32
  "devDependencies": {
24
33
  "@biomejs/biome": "^2.5.0",
25
34
  "@types/bun": "^1.3.12",
@@ -1,224 +0,0 @@
1
- ---
2
- name: CliProgram capabilities refactor
3
- overview: Introduce `CliNode` / `CliProgram` types and an internal capabilities resolver, replacing `CliCommand` in the public API as a **2.0.0 breaking change**. Keep capability machinery private to avoid locking in unstable internals.
4
- todos:
5
- - id: types-split
6
- content: "Refactor src/types.ts: CliNodeBase, CliLeaf, CliRouter, CliNode, CliProgram; remove CliCommand"
7
- status: completed
8
- - id: capabilities-module
9
- content: Add internal src/capabilities.ts (resolveCapabilities, reservedCommandNames)
10
- status: completed
11
- - id: wire-validate-presentation
12
- content: Update validate.ts, presentation.ts, export.ts, dispatch.ts, runtime.ts, invoke.ts to use CliProgram + caps
13
- status: completed
14
- - id: internal-walkers
15
- content: Update parse, context, mcp/tools, completion scopes, install paths to CliNode/CliProgram as appropriate
16
- status: completed
17
- - id: public-api-2
18
- content: Update index.ts exports (CliProgram only), run typegen, bump 2.0.0 CHANGELOG; verify qa-cli + idp-trees compile against local checkout
19
- status: completed
20
- - id: examples-docs
21
- content: Migrate examples + README to CliProgram / satisfies pattern
22
- status: completed
23
- - id: tests-migrate
24
- content: Migrate tests; keep runtime negative tests with cast helpers
25
- status: completed
26
- - id: nice-to-haves
27
- content: "Optional: ctx.program getter, cliValidateProgram rename, satisfies examples, @ts-expect-error type tests"
28
- status: completed
29
- isProject: false
30
- ---
31
-
32
- # CliProgram + internal capabilities (2.0)
33
-
34
- ## Goals
35
-
36
- - **Types**: `CliNode` (user tree) + `CliProgram` (what `cliRun` accepts) with root-only `mcpServer` / `install` only on `CliProgram`.
37
- - **Capabilities**: One internal resolver drives reserved names, help/schema/completion visibility, and dispatch guards.
38
- - **Exports**: Minimal public surface — no `resolveCapabilities`, no presentation helpers, no builtin command builders.
39
- - **Breaking**: Drop `CliCommand` from the public API — ship **2.0.0 directly** (Option B; no deprecated alias release).
40
-
41
- ## Type model
42
-
43
- Add to [`src/types.ts`](src/types.ts):
44
-
45
- ```typescript
46
- interface CliNodeBase { key; description; notes?; options? }
47
-
48
- type CliLeaf = CliNodeBase & { handler; positionals?; mcpTool? }
49
- type CliRouter = CliNodeBase & { commands: CliNode[]; fallbackCommand?; fallbackMode? }
50
- type CliNode = CliLeaf | CliRouter
51
-
52
- type CliProgram = CliNode & { mcpServer?; install? }
53
- ```
54
-
55
- Remove `mcpServer`, `install`, `mcpTool` from a shared `CliCommandBase`. Remove the old `CliCommand` union entirely **inside the repo**.
56
-
57
- **Leaf program roots** (`examples/minimal.ts`) remain valid: `CliProgram` = leaf + root config.
58
-
59
- ```mermaid
60
- flowchart TB
61
- subgraph public [Public API]
62
- CliProgram
63
- cliRun["cliRun(program)"]
64
- cliInvoke["cliInvoke(program, argv)"]
65
- end
66
- subgraph internal [Internal only]
67
- resolveCaps["resolveCapabilities(program)"]
68
- presentation["presentationRoot(program)"]
69
- dispatch["dispatchBuiltin(...)"]
70
- end
71
- CliProgram --> cliRun
72
- CliProgram --> resolveCaps
73
- resolveCaps --> presentation
74
- resolveCaps --> dispatch
75
- presentation --> help["help / schema / completions"]
76
- ```
77
-
78
- ## Export policy (intentionally narrow)
79
-
80
- ### Export from [`src/index.ts`](src/index.ts) only
81
-
82
- | Symbol | Action |
83
- |--------|--------|
84
- | `CliProgram` | **Add** — primary schema type |
85
- | `CliCommand` | **Remove** — breaking |
86
- | `CliNode`, `CliLeaf`, `CliRouter` | **Do not export** — authors use `satisfies CliProgram` or `typeof program.commands[n]` |
87
- | `CliOption`, `CliPositional`, config types | unchanged |
88
- | `cliRun`, `cliInvoke`, `CliContext`, enums | unchanged signatures, `CliProgram` param |
89
-
90
- Run `just typegen` so [`index.d.ts`](index.d.ts) reflects only the barrel.
91
-
92
- ### Keep internal (not in barrel, no deep `package.json` exports)
93
-
94
- - [`src/capabilities.ts`](src/capabilities.ts) (new): `resolveCapabilities`, `reservedCommandNames`
95
- - [`src/builtins/presentation.ts`](src/builtins/presentation.ts), [`export.ts`](src/builtins/export.ts), [`dispatch.ts`](src/builtins/dispatch.ts)
96
- - Completion emitters, install module, `setCompiledExecutableOverride`
97
- - [`src/completion.ts`](src/completion.ts) shim — trim re-exports if any leak toward public paths; tests import from `builtins/` or `completion.ts` directly in-repo only
98
-
99
- **Rule**: If it decides *when* a builtin appears, it stays internal. Consumers only see the resulting CLI behavior.
100
-
101
- ## Internal capabilities module
102
-
103
- New [`src/capabilities.ts`](src/capabilities.ts):
104
-
105
- ```typescript
106
- interface CliCapabilities {
107
- completion: true;
108
- mcp: boolean; // !!program.mcpServer
109
- install: boolean; // isCompiledExecutable() && program.install?.enabled !== false
110
- }
111
-
112
- function resolveCapabilities(program: CliProgram): CliCapabilities
113
- function reservedCommandNames(caps: CliCapabilities): string[]
114
- ```
115
-
116
- Wire callers to use this instead of re-deriving:
117
-
118
- | File | Change |
119
- |------|--------|
120
- | [`src/validate.ts`](src/validate.ts) | `cliValidateProgram(program)`; reserved names from `caps`; **keep** runtime root-only checks for untyped/JS abuse |
121
- | [`src/builtins/presentation.ts`](src/builtins/presentation.ts) | `presentationBuiltins(program, caps)` |
122
- | [`src/builtins/export.ts`](src/builtins/export.ts) | same |
123
- | [`src/builtins/dispatch.ts`](src/builtins/dispatch.ts) | derive `caps` once from `program` |
124
- | [`src/runtime.ts`](src/runtime.ts) | `cliRun(program: CliProgram)` |
125
-
126
- Walkers (`parse`, `mcp/tools`, completion scopes) take `CliNode` where they recurse; entrypoints take `CliProgram`.
127
-
128
- **Presentation vs user tree**: Help, `--schema`, and completion emitters consume `cliPresentationRoot(program)` (synthetic router with builtin stubs), not raw `CliProgram`. Capability logic decides what gets injected; emitters keep walking the same presentation shape.
129
-
130
- **Validate edge cases** (keep at runtime even when TS catches most mistakes):
131
-
132
- - `mcpServer` / `install` on inner nodes — reject (untyped/JS abuse)
133
- - `mcpTool` on program root (leaf-shaped `CliProgram`) — reject (same as today)
134
- - Reserved command names from `reservedCommandNames(caps)` — drop the old `cliPresentationRoot` escape hatch that skips injection when user already declared `completion`
135
-
136
- ## Context typing
137
-
138
- [`src/context.ts`](src/context.ts):
139
-
140
- - Change `ctx.schema` type to `CliProgram` (field name unchanged — avoids extra public surface).
141
- - **Nice-to-have**: add `get program(): CliProgram` alias returning `this.schema` with JSDoc pointing to `schema` for familiarity. Do **not** export a new type for this.
142
-
143
- ## Consumer migration (2.0)
144
-
145
- ### Before/after (representative of qa-cli, idp-trees, and all `: CliCommand`-typed consumers)
146
-
147
- ```typescript
148
- // Before (1.x)
149
- import { cliRun, type CliCommand, CliOptionKind, CliFallbackMode } from "argsbarg";
150
- const cli: CliCommand = { key: 'myapp', commands: [...], mcpServer: {...} };
151
- await cliRun(cli);
152
-
153
- // After (2.0)
154
- import { cliRun, type CliProgram, CliOptionKind, CliFallbackMode } from "argsbarg";
155
- const cli = { key: 'myapp', commands: [...], mcpServer: {...} } satisfies CliProgram;
156
- await cliRun(cli);
157
- ```
158
-
159
- No structural change for well-formed apps — only import + type annotation. `: CliProgram` also works if the consumer prefers explicit annotation over `satisfies`. Both patterns are supported and type-check identically.
160
-
161
- ### Consumer impact (verified)
162
-
163
- | Consumer | Files importing argsbarg | Uses `CliCommand`? | Uses `mcpServer`/`install`? | Migration cost |
164
- |---|---|---|---|---|
165
- | qa-cli | 3 (`index.tsx`, `mcpConfig.ts`, `mcp.ts`) | `const cli: CliCommand` | `mcpServer` on root | Replace `CliCommand` → `CliProgram` in 1 import, 1 annotation |
166
- | idp-trees | 3 (`index.tsx`, `mcp/config.ts`, `headless/mode.ts`) | `const cli: CliCommand` | `mcpServer` on root | Replace `CliCommand` → `CliProgram` in 1 import, 1 annotation |
167
-
168
- Both consumers only use `CliCommand` for the root schema annotation. Neither imports `CliNode`/`CliLeaf`/`CliRouter` (they can't — they don't exist yet). Neither has deeply nested command trees (max depth 2). All other imported types (`CliOptionKind`, `CliFallbackMode`, `CliMcpServerConfig`, `CliMcpToolConfig`, `CliInvocation`, `CliContext`, `isInteractiveTty`) remain unchanged — those files need zero changes.
169
-
170
- ### `install` builtin default behavior
171
-
172
- Both qa-cli and idp-trees rely on the `install` builtin being **enabled by default** (they do not set `install` on the root). The capabilities resolver must preserve this: `install: isCompiledExecutable() && program.install?.enabled !== false` — which defaults to enabled when `install` is absent.
173
-
174
- ## Tests and negative cases
175
-
176
- - Update fixtures in [`src/index.test.ts`](src/index.test.ts), [`src/builtins/builtins.test.ts`](src/builtins/builtins.test.ts), [`src/install/install.test.ts`](src/install/install.test.ts): `CliProgram` / `CliNode` internally.
177
- - Runtime rejection tests (`mcpServer` on nested node) use `as unknown as CliProgram` or a small `invalidProgram()` helper — proves validate still catches misuse without TS.
178
-
179
- ## Docs and changelog
180
-
181
- - [`README.md`](README.md): `CliProgram`, capabilities mental model (1 short paragraph), reserved names derived from config.
182
- - [`CHANGELOG.md`](CHANGELOG.md): **2.0.0** — `CliCommand` removed; `CliProgram` added; show before/after migration snippet. Include a note that structural schema shape is unchanged — only the type name and annotation pattern differ.
183
- - Optional short **Architecture** note in README (not a new doc file unless you want one).
184
-
185
- ### 2.0.0 migration snippet (for CHANGELOG)
186
-
187
- ```typescript
188
- // 1.x
189
- import { type CliCommand } from "argsbarg";
190
- const cli: CliCommand = { ... };
191
- // 2.0
192
- import { type CliProgram } from "argsbarg";
193
- const cli = { ... } satisfies CliProgram; // or : CliProgram
194
- ```
195
-
196
- ## Version and release
197
-
198
- **2.0.0** — rename-only breaking change for typed consumers; runtime behavior unchanged.
199
-
200
- **Release path (chosen): Option B** — jump straight to 2.0.0. No `CliCommand` deprecated alias in 1.6. Breaking changes are acceptable; known consumers (qa-cli, idp-trees) migrate in one import + one annotation each.
201
-
202
- ### Pre-release checklist (required before tag)
203
-
204
- 1. `just test` green in argsbarg
205
- 2. `just typegen` — `index.d.ts` exports `CliProgram` only (no `CliCommand`)
206
- 3. Point qa-cli and idp-trees `package.json` at local argsbarg checkout; confirm `tsc` / build passes with `CliProgram` migration applied in those repos
207
- 4. CHANGELOG 2.0.0 entry with migration snippet
208
-
209
- ---
210
-
211
- ## Nice-to-haves (defer if time-boxed)
212
-
213
- 1. **`satisfies CliProgram`** in all examples (better DX than `: CliProgram` annotation). Verify that `satisfies` still infers precise sub-command types (not widening to `object`), particularly in nested structures like `examples/nested.ts`.
214
- 2. **`ctx.program` getter** — alias for `ctx.schema`; document `schema` as legacy name in JSDoc only (no removal in 2.0).
215
- 3. **Internal rename** `cliValidateRoot` → `cliValidateProgram` (not exported today; safe).
216
- 4. **Type tests** in `src/types.test.ts` — compile-only assertions that invalid shapes fail (e.g. `mcpServer` on `CliNode`) using `@ts-expect-error` snippets.
217
- 5. **README diagram** — small mermaid of user tree vs injected capabilities (documentation only).
218
-
219
- ## Explicitly out of scope (avoid future breaks)
220
-
221
- - Exporting `CliCapabilities`, `resolveCapabilities`, `presentationRoot`, builtin command builders
222
- - Exporting `CliNode` / `CliLeaf` / `CliRouter` (can add in 2.x if demand appears; not needed for `nested.ts`-style apps)
223
- - `package.json` subpath exports (`argsbarg/builtins`, etc.)
224
- - Renaming `ctx.schema` in 2.0 (would break handlers that read `.schema`)
@@ -1,260 +0,0 @@
1
- ---
2
- name: MCP v1.1 polish
3
- overview: "Ship four agent-focused MCP improvements: richer tool descriptions with CLI paths, per-leaf `mcpEnabled` opt-out, stderr surfaced on successful tool calls, and `structuredContent` when handler stdout is valid JSON."
4
- todos:
5
- - id: types-mcpEnabled
6
- content: "Add mcpEnabled?: boolean to CliCommandBase; validate leaf-only in validate.ts"
7
- status: completed
8
- - id: tool-descriptions
9
- content: Add mcpToolDescription(path, rootKey, description); enrich descriptions; filter mcpEnabled === false
10
- status: completed
11
- - id: result-builder
12
- content: Create src/mcp/result.ts with buildToolCallSuccess (stderr blocks + JSON structuredContent)
13
- status: completed
14
- - id: server-wire
15
- content: Wire buildToolCallSuccess into tools/call success path in server.ts
16
- status: completed
17
- - id: tests
18
- content: Unit tests for description, mcpEnabled, result builder; subprocess tests for JSON structuredContent and stderr-on-success
19
- status: completed
20
- - id: docs-changelog
21
- content: Update docs/mcp.md and CHANGELOG.md; run just typegen and just test
22
- status: completed
23
- isProject: false
24
- ---
25
-
26
- # MCP follow-up improvements plan
27
-
28
- Four targeted enhancements to the existing opt-in MCP server. No new dependencies, no protocol transport changes, no public export of internals beyond the new `mcpEnabled` schema field.
29
-
30
- ```mermaid
31
- flowchart LR
32
- subgraph toolsList [tools/list]
33
- Walk[collectMcpTools]
34
- Desc["path + description"]
35
- Filter[mcpEnabled filter]
36
- end
37
- subgraph toolCall [tools/call]
38
- Invoke[cliInvoke]
39
- Build[buildToolCallResult]
40
- Resp["content + structuredContent?"]
41
- end
42
- Walk --> Filter --> Desc
43
- Invoke --> Build --> Resp
44
- ```
45
-
46
- ---
47
-
48
- ## 1. CLI path in tool descriptions
49
-
50
- **Goal:** Agents see the human invocation path without reading the schema resource first.
51
-
52
- **Format:**
53
-
54
- | Path | Leaf description | MCP `description` |
55
- | --- | --- | --- |
56
- | `["stat","owner","lookup"]` | `Resolve owner info.` | `stat owner lookup — Resolve owner info.` |
57
- | `["read"]` | `Print the first line…` | `read — Print the first line…` |
58
- | `[]` (root leaf app) | `Tiny demo.` | `{root.key} — Tiny demo.` |
59
-
60
- Use em dash (`—`) between path and description. Path segments are raw `key` values (not sanitized tool names). Every tool description uses the `prefix — description` form — there is no bare-description fallback.
61
-
62
- **Implementation:**
63
-
64
- - Add helper in [`src/mcp/tools.ts`](src/mcp/tools.ts). The helper **must** receive `rootKey` so root-leaf apps (empty path) can use the program name as the prefix:
65
-
66
- ```typescript
67
- /** Builds MCP tool description: "{cli path} — {description}". */
68
- function mcpToolDescription(path: string[], rootKey: string, description: string): string {
69
- const prefix = path.length > 0 ? path.join(" ") : rootKey;
70
- return `${prefix} — ${description}`;
71
- }
72
- ```
73
-
74
- - Call from `collectMcpTools` when building each `McpToolDef.description`:
75
-
76
- ```typescript
77
- description: mcpToolDescription(path, root.key, cmd.description),
78
- ```
79
-
80
- - `tools/list` already maps `t.description` — no server change needed.
81
-
82
- **Tests:** Update [`src/index.test.ts`](src/index.test.ts):
83
-
84
- - `stat_owner_lookup` description is `stat owner lookup — Resolve owner info.`
85
- - If testing a root-leaf fixture, description is `{root.key} — {description}`.
86
-
87
- ---
88
-
89
- ## 2. Per-leaf opt-out via `mcpEnabled`
90
-
91
- **Goal:** Authors can keep CLI commands for humans while hiding them from MCP tool discovery.
92
-
93
- **Schema change** in [`src/types.ts`](src/types.ts):
94
-
95
- ```typescript
96
- export interface CliCommandBase {
97
- // ...existing fields...
98
- /** Leaf-only. When `false`, omit this command from MCP tools (default: exposed). */
99
- mcpEnabled?: boolean;
100
- }
101
- ```
102
-
103
- **Semantics:**
104
-
105
- - Omitted or `true` → leaf is exposed (current behavior).
106
- - `false` → skipped in `collectMcpTools` walk.
107
- - Root `mcp?: CliMcpConfig` unchanged — it enables the server; `mcpEnabled` is unrelated.
108
-
109
- **Validation** in [`src/validate.ts`](src/validate.ts) `walkCommand`:
110
-
111
- - If `isRoot && cmd.mcpEnabled !== undefined` → throw `mcpEnabled is only supported on leaf commands`.
112
- - If routing node (has `commands`, no `handler`) and `mcpEnabled !== undefined` → same error.
113
- - No validation needed for `false` vs `true` beyond that.
114
-
115
- **Tool collection** in [`src/mcp/tools.ts`](src/mcp/tools.ts) `walk`:
116
-
117
- ```typescript
118
- if ("handler" in cmd && cmd.handler) {
119
- if (cmd.key === "completion" || cmd.key === "mcp") return;
120
- if (cmd.mcpEnabled === false) return;
121
- // push tool...
122
- }
123
- ```
124
-
125
- **Schema export:** Leave [`src/schema.ts`](src/schema.ts) unchanged for v1 — `--schema` still lists all commands (CLI discovery). Only MCP `tools/list` respects `mcpEnabled`. Document this distinction in [`docs/mcp.md`](docs/mcp.md).
126
-
127
- **Example (optional):** Add a hidden leaf in `nestedMcpFixture` only (not `examples/nested.ts`) with `mcpEnabled: false` to prove filtering without cluttering the demo app.
128
-
129
- ---
130
-
131
- ## 3. stderr on successful tool calls
132
-
133
- **Goal:** Warnings and diagnostic output on stderr are not silently dropped when `exitCode === 0`.
134
-
135
- **Current behavior** ([`src/mcp/server.ts`](src/mcp/server.ts) ~L133–141): success returns only `invokeResult.stdout`.
136
-
137
- **New behavior:** Extract a small builder (new file [`src/mcp/result.ts`](src/mcp/result.ts) recommended to keep server readable):
138
-
139
- ```typescript
140
- export interface McpToolCallSuccess {
141
- content: { type: "text"; text: string }[];
142
- structuredContent?: unknown;
143
- isError: false;
144
- }
145
-
146
- export function buildToolCallSuccess(stdout: string, stderr: string): McpToolCallSuccess
147
- ```
148
-
149
- **stderr formatting rules:**
150
-
151
- - If `stderr.trim()` is empty → single `content` item with stdout (or empty string if stdout empty).
152
- - If stderr non-empty → **two** `content` items:
153
- 1. `{ type: "text", text: stdout }` (may be empty string)
154
- 2. `{ type: "text", text: stderr.trim() }` — **raw stderr text, no `stderr:` prefix**
155
-
156
- Use multiple content blocks rather than concatenating into one string so hosts can distinguish streams; the second block’s position in the array is the signal that it came from stderr. Some MCP hosts render content blocks with their own labeling — a literal `stderr:` prefix would leak into user-visible tool output. Existing failure path (~L144–151) already prefers stderr — leave as-is.
157
-
158
- **Tests:**
159
-
160
- - Unit test `buildToolCallSuccess` with stdout-only, stderr-only, both.
161
- - Subprocess test: add a fixture leaf (in test fixture or temporary nested command) that `console.warn`s on stderr but exits 0; assert two content blocks and `isError: false`.
162
-
163
- ---
164
-
165
- ## 4. `structuredContent` when stdout is valid JSON
166
-
167
- **Goal:** When handlers emit JSON (e.g. `nested.ts` with `--json`), MCP clients get machine-readable output per [MCP tools spec](https://modelcontextprotocol.io/specification/draft/server/tools).
168
-
169
- **Parsing rules** (in `buildToolCallSuccess`):
170
-
171
- 1. Let `trimmed = stdout.trim()`.
172
- 2. If `trimmed.length === 0` → no `structuredContent`.
173
- 3. Try `JSON.parse(trimmed)`.
174
- 4. On success → set `structuredContent` to the parsed value (object, array, or primitive — all valid per 2025 spec).
175
- 5. On `SyntaxError` → no `structuredContent` (plain text handlers unchanged).
176
- 6. **Always** keep `content[0].text` as the raw stdout string when stdout is non-empty (spec: structured tools SHOULD also return serialized JSON in `content` — we already have the raw stdout which satisfies this for JSON handlers).
177
-
178
- **Primitive footgun (document, do not guard):** `JSON.parse("true")` yields `structuredContent: true`. A handler that intentionally prints the string `true` as human-readable output would get machine-typed output. This is spec-correct and rare in practice. Document in [`docs/mcp.md`](docs/mcp.md) rather than limiting to `typeof parsed === "object"` — that would reject valid JSON arrays and primitives that the 2025 spec explicitly allows.
179
-
180
- **Do not** auto-generate `outputSchema` in this release — that requires schema author input or inference and is out of scope.
181
-
182
- **Server wiring** in [`src/mcp/server.ts`](src/mcp/server.ts):
183
-
184
- ```typescript
185
- const result = buildToolCallSuccess(invokeResult.stdout, invokeResult.stderr);
186
- writeResponse({ jsonrpc: "2.0", id, result });
187
- ```
188
-
189
- Spread `structuredContent` onto result only when defined.
190
-
191
- **Tests:**
192
-
193
- - Unit: `buildToolCallSuccess('{"a":1}\n', '')` → `structuredContent: { a: 1 }`, content text preserved.
194
- - Unit: `buildToolCallSuccess('lookup user=x\n', '')` → no `structuredContent`.
195
- - Unit: `buildToolCallSuccess('true\n', '')` → `structuredContent: true` (documents primitive behavior).
196
- - Subprocess: extend existing `stat_owner_lookup` test or add parallel test with `json: true`:
197
-
198
- ```typescript
199
- params: {
200
- name: "stat_owner_lookup",
201
- arguments: { path: readme, "user-name": "test", json: true },
202
- }
203
- ```
204
-
205
- Assert `result.structuredContent` deep-equals `{ user: "test", path: readme }` and `content[0].text` is valid JSON string.
206
-
207
- ---
208
-
209
- ## Documentation and changelog
210
-
211
- Update [`docs/mcp.md`](docs/mcp.md):
212
-
213
- - **Tool names** section: document new description format with example (including root-leaf `{root.key} — …` case).
214
- - **Per-leaf visibility**: `mcpEnabled: false` on leaves; note `--schema` still includes the command.
215
- - **Tool results**: stderr on success as a second raw-text content block (no prefix); `structuredContent` when stdout is valid JSON (including primitives — document footgun); link to MCP spec.
216
- - **Short flags**: one-line note that tool args use long option names only (unchanged, but good to document while editing).
217
-
218
- Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]`:
219
-
220
- - Richer MCP tool descriptions with CLI paths.
221
- - `mcpEnabled` leaf opt-out.
222
- - MCP tool success responses include stderr when present.
223
- - MCP tool success responses include `structuredContent` for JSON stdout.
224
-
225
- Trim [`README.md`](README.md) MCP blurb only if needed — one line pointing to docs is enough.
226
-
227
- ---
228
-
229
- ## Type declarations and release hygiene
230
-
231
- - Run `just typegen` after [`src/types.ts`](src/types.ts) change so [`index.d.ts`](index.d.ts) exports `mcpEnabled`.
232
- - Run `just test` (typecheck + lint + `bun test`).
233
-
234
- **Public API surface:** Only `mcpEnabled?: boolean` on `CliCommandBase` is new. `cliInvoke`, `buildToolCallSuccess`, and MCP runtime remain internal.
235
-
236
- ---
237
-
238
- ## File change summary
239
-
240
- | File | Changes |
241
- | --- | --- |
242
- | [`src/types.ts`](src/types.ts) | Add `mcpEnabled?: boolean` with JSDoc |
243
- | [`src/validate.ts`](src/validate.ts) | Reject `mcpEnabled` on root and routing nodes |
244
- | [`src/mcp/tools.ts`](src/mcp/tools.ts) | `mcpToolDescription`, filter `mcpEnabled === false` |
245
- | [`src/mcp/result.ts`](src/mcp/result.ts) | **New** — `buildToolCallSuccess` |
246
- | [`src/mcp/server.ts`](src/mcp/server.ts) | Use builder for success path |
247
- | [`src/index.test.ts`](src/index.test.ts) | Unit + subprocess tests for all four features |
248
- | [`docs/mcp.md`](docs/mcp.md) | Document behavior |
249
- | [`CHANGELOG.md`](CHANGELOG.md) | Unreleased entries |
250
- | [`index.d.ts`](index.d.ts) | Regenerated via typegen |
251
-
252
- ---
253
-
254
- ## Out of scope (explicitly deferred)
255
-
256
- - Tool list caching
257
- - `outputSchema` on tools
258
- - Hiding `mcpEnabled: false` commands from `--schema`
259
- - Group-level `mcpEnabled` inheritance
260
- - `mcp.json` / Cursor config changes