argsbarg 3.3.14 → 3.4.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.
@@ -1,2 +1,4 @@
1
1
  - [x] --schema feature for ai agents
2
- - [x] opt-out install feature?
2
+ - [x] opt-out install feature?
3
+ - [x] outputSchema
4
+ - [x] mcpb bundle
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.4.0] - 2026-06-22
11
+
12
+ ### Added
13
+
14
+ - **`hidden`** — boolean on commands and options; omitted from help listings, `docs schema` / `docs api`, shell completions, and MCP `tools/list`, but still parseable and invocable. Direct `-h` on a hidden command still works.
15
+ - **`mcp bundle`** — built-in subcommand when `mcpServer.enabled` (macOS-only v1). Runs `myapp mcp bundle` to pack `dist/<key>.mcpb` from `dist/<key>`. Bare `myapp mcp` still starts the stdio server.
16
+ - **`mcpServer.bundle`** — optional author, icon, and `longDescription` for MCP Bundle metadata.
17
+ - **Leaf `outputSchema`** — optional JSON Schema for structured stdout; exported in `docs schema`, `docs api`, skill `reference.md`, and MCP `tools/list` (stdout not validated at runtime yet). Legacy `mcpTool.outputSchema` still works.
18
+ - **MCP tool descriptions** — leaf `notes` are appended to `tools/list` descriptions (`{argsbarg:program}` resolved).
19
+
10
20
  ## [3.3.14] - 2026-06-21
11
21
 
12
22
  ### Changed
@@ -335,7 +345,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
335
345
  - 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`).
336
346
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
337
347
 
338
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.3.14...HEAD
348
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.4.0...HEAD
349
+ [3.4.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.4.0
339
350
  [3.3.14]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.14
340
351
  [3.3.13]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.13
341
352
  [3.3.12]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.3.12
@@ -113,6 +113,25 @@ Many "MCP problems" are schema or handler gaps. Prefer these over escape hatches
113
113
  | `requiresEnv: [...]` | Runtime secrets; appended to MCP description and enforced at `tools/call` |
114
114
  | `description: "..."` | **Irreducible** MCP limitation (e.g. live tail / `--watch` cannot be streamed on the MCP wire yet) |
115
115
 
116
+ ### Structured stdout
117
+
118
+ On **leaf commands**, set `outputSchema` to a JSON Schema describing stdout when the handler emits JSON (typically with `--json`, or via MCP on the headless path):
119
+
120
+ ```typescript
121
+ {
122
+ key: "lookup",
123
+ description: "Resolve owner info.",
124
+ outputSchema: {
125
+ type: "object",
126
+ properties: { user: { type: "string" }, path: { type: "string" } },
127
+ required: ["user", "path"],
128
+ },
129
+ handler: (ctx) => { /* ... */ },
130
+ }
131
+ ```
132
+
133
+ Exported in `docs schema`, `docs api`, skill `reference.md`, and MCP `tools/list`. Not validated at runtime yet. Pair with `notes` for prose examples; do not duplicate the full schema in `notes`.
134
+
116
135
  Do **not** use `mcpTool.description` to paper over missing `--yes`, non-standard flag names, or handlers that only work interactively — fix those instead.
117
136
 
118
137
  If help text and MCP behavior match after your fixes, **omit `mcpTool` entirely**.
package/docs/mcp.md CHANGED
@@ -103,7 +103,7 @@ Tool names are derived from the command path, with each segment sanitized (non-a
103
103
 
104
104
  ### Tool descriptions
105
105
 
106
- Each tool’s `description` includes the human CLI path and the leaf’s help text, separated by an em dash. Tool arguments are defined in `inputSchema` (options and positionals with their descriptions). `mcpTool.requiresEnv` is appended as `[requires env: …]`.
106
+ Each tool’s `description` includes the human CLI path and the leaf’s help text, separated by an em dash. Leaf **`notes`** are appended after a blank line (with `{argsbarg:program}` resolved). Tool arguments are defined in `inputSchema` (options and positionals with their descriptions). `mcpTool.requiresEnv` is appended as `[requires env: …]` on auto-generated descriptions.
107
107
 
108
108
  | CLI path | MCP `description` (example) |
109
109
  | --- | --- |
@@ -138,6 +138,8 @@ mcpTool: {
138
138
  }
139
139
  ```
140
140
 
141
+ Set **`outputSchema` on the leaf** (not under `mcpTool`) — see [cli-program.md — Structured stdout](cli-program.md#structured-stdout).
142
+
141
143
  - **`description`** — when set, replaces the auto-generated `path — help` description entirely (no automatic `requiresEnv` suffix; mention vars in your text if needed).
142
144
  - **`requiresEnv`** — on auto-generated descriptions, appended as `[requires env: …]`. Enforced at `tools/call` time before the handler runs. Empty or unset env values count as missing.
143
145
 
@@ -276,7 +278,7 @@ mcpServer: {
276
278
  | `initialize` | Returns capabilities (`tools`, `resources`) and `serverInfo`. |
277
279
  | `notifications/initialized` | Acknowledged; no response (notification). |
278
280
  | `ping` | Returns `{}`. |
279
- | `tools/list` | Lists all tools with `name`, `description`, `inputSchema`. |
281
+ | `tools/list` | Lists all tools with `name`, `description`, `inputSchema`, and optional `outputSchema`. |
280
282
  | `tools/call` | Runs a leaf handler; params: `name`, `arguments` (object). |
281
283
  | `resources/list` | Lists schema + custom resources. |
282
284
  | `resources/read` | Returns resource body; params: `uri`. |
@@ -291,11 +293,29 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun
291
293
 
292
294
  You should get one JSON line on stdout with `result.capabilities` and `result.serverInfo`.
293
295
 
296
+ ## MCP Bundle (`mcp bundle`)
297
+
298
+ When `mcpServer.enabled` is true, **`mcp bundle`** packs a Claude Desktop **`.mcpb`** bundle (macOS-only v1):
299
+
300
+ ```bash
301
+ just build
302
+ ./dist/myapp mcp bundle
303
+ # → dist/myapp.mcpb
304
+ ```
305
+
306
+ Expects the compiled binary at **`dist/<program.key>`** and writes **`dist/<program.key>.mcpb`**. Manifest metadata is generated from your schema (`mcpServerId`, tools, `requiresEnv`). Optional pack-time fields live under **`mcpServer.bundle`** (`author`, `icon`, `longDescription`).
307
+
308
+ Bare **`myapp mcp`** still runs the stdio MCP server (unchanged for `install --mcp` and MCP hosts). Use **`install --mcp`** for Cursor / Claude Code JSON config.
309
+
310
+ ## Hidden commands and options
311
+
312
+ Set **`hidden: true`** on a command or option to omit it from help listings, `docs schema` / `docs api`, shell completions, and MCP `tools/list` / tool `inputSchema`. Hidden commands remain invocable; **`myapp hidden-cmd -h`** still works.
313
+
294
314
  ## Reserved names
295
315
 
296
316
  When MCP is enabled:
297
317
 
298
- - Do not declare a top-level command named **`completion`** — reserved for shell completions.
318
+ - Do not declare top-level commands named **`completion`** or **`mcp`** — reserved for platform builtins.
299
319
 
300
320
  Running `myapp mcp` without `mcpServer` on the root fails with an error (exit 1).
301
321
 
package/index.d.ts CHANGED
@@ -69,6 +69,8 @@ export declare enum CliFallbackMode {
69
69
  export interface CliOption {
70
70
  /** Option name (e.g., "name", "verbose"). */
71
71
  name: string;
72
+ /** When `true`, omit from help, schema, completions, and MCP tool inputSchema (still parseable). */
73
+ hidden?: boolean;
72
74
  /** Description shown in help. */
73
75
  description: string;
74
76
  /** Option kind: presence flag, string value, or number value. */
@@ -104,6 +106,18 @@ export interface CliPositional {
104
106
  */
105
107
  argMax?: number;
106
108
  }
109
+ /** Optional metadata for `mcp bundle` MCP Bundle output (program root `mcpServer.bundle` only). */
110
+ export interface CliMcpBundleConfig {
111
+ author?: {
112
+ name: string;
113
+ email?: string;
114
+ url?: string;
115
+ };
116
+ /** Repo-relative path to a PNG icon copied into the bundle. */
117
+ icon?: string;
118
+ /** Manifest `long_description` (defaults to program description). */
119
+ longDescription?: string;
120
+ }
107
121
  /**
108
122
  * Enables `myapp mcp` and MCP stdio server metadata (program root only).
109
123
  * Must include `enabled: true`; omit `mcpServer` entirely to disable MCP.
@@ -130,6 +144,8 @@ export interface CliMcpServerConfig {
130
144
  * URIs must be unique and must not equal schemaResourceUri.
131
145
  */
132
146
  resources?: CliMcpResource[];
147
+ /** Optional MCP Bundle (`.mcpb`) metadata for `mcp bundle`. */
148
+ bundle?: CliMcpBundleConfig;
133
149
  }
134
150
  /**
135
151
  * A custom MCP resource exposed under resources/list and resources/read.
@@ -163,6 +179,10 @@ export interface CliMcpToolConfig {
163
179
  * Empty string counts as absent.
164
180
  */
165
181
  requiresEnv?: string[];
182
+ /**
183
+ * @deprecated Set `outputSchema` on the leaf command instead.
184
+ */
185
+ outputSchema?: Record<string, unknown>;
166
186
  }
167
187
  /**
168
188
  * Opt-out and defaults for the `install` built-in (program root only).
@@ -222,6 +242,8 @@ export interface CliDocsConfig {
222
242
  export interface CliNodeBase {
223
243
  /** Program or command key (e.g., "myapp", "stat", "owner"). */
224
244
  key: string;
245
+ /** When `true`, omit from help listings, schema, completions, and MCP tools (still invocable). */
246
+ hidden?: boolean;
225
247
  /** Short description shown in help. */
226
248
  description: string;
227
249
  /** Additional notes shown in help (`{argsbarg:program}` → program key). */
@@ -237,6 +259,11 @@ export type CliLeaf = CliNodeBase & {
237
259
  handler: CliHandler;
238
260
  /** Positional argument definitions. */
239
261
  positionals?: CliPositional[];
262
+ /**
263
+ * JSON Schema for structured stdout (e.g. with `--json` or MCP when the handler emits JSON).
264
+ * Exported in `docs schema`, `docs api`, and MCP `tools/list`; not validated at runtime yet.
265
+ */
266
+ outputSchema?: Record<string, unknown>;
240
267
  /** Per-tool MCP exposure and metadata. */
241
268
  mcpTool?: CliMcpToolConfig;
242
269
  };
@@ -378,5 +405,25 @@ export declare function createGhVersionCheck(config: GhVersionCheckConfig): {
378
405
  };
379
406
  /** Shared `gh release view` fetcher for hooks and version-check refresh. */
380
407
  export declare function createGhFetchLatest(config: Pick<GhReleaseUpdateConfig, "repo" | "repoEnvHint">): () => Promise<string>;
408
+ /** Resolved paths for `mcp bundle`. */
409
+ export interface McpBundlePaths {
410
+ binaryPath: string;
411
+ outPath: string;
412
+ binaryName: string;
413
+ }
414
+ /** Default `dist/<key>` binary and `dist/<key>.mcpb` output under cwd. */
415
+ export declare function defaultMcpBundlePaths(program: CliProgram, cwd?: string): McpBundlePaths;
416
+ /** Generates MCPB `manifest.json` object from program schema and MCP tools. */
417
+ export declare function generateMcpManifest(program: CliProgram, binaryName: string): Record<string, unknown>;
418
+ export interface PackMcpBundleOpts {
419
+ cwd?: string;
420
+ binaryPath?: string;
421
+ outPath?: string;
422
+ }
423
+ /**
424
+ * Stages manifest + binary (+ optional icon) and writes a `.mcpb` ZIP.
425
+ * macOS-only v1; requires the compiled binary to exist.
426
+ */
427
+ export declare function packMcpBundle(program: CliProgram, opts?: PackMcpBundleOpts): string;
381
428
 
382
429
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.3.14",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -9,6 +9,7 @@ import { cliBuiltinMcpCommand } from "./mcp.ts";
9
9
  import { cliBuiltinVersionCommand } from "./version.ts";
10
10
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
11
11
  import { cliPresentationRoot } from "./presentation.ts";
12
+ import { runMcpBundle } from "../mcp/bundle.ts";
12
13
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
13
14
  import { cliMcpServeStdio } from "../mcp.ts";
14
15
  import { cliInstall } from "../install/index.ts";
@@ -72,12 +73,22 @@ export async function dispatchBuiltin(
72
73
  process.stderr.write("MCP is not enabled. Set mcpServer: { enabled: true } on the program root.\n");
73
74
  process.exit(1);
74
75
  }
75
- if (pr.path.length !== 1) {
76
- process.stderr.write("Unknown subcommand: mcp " + pr.path.slice(1).join(" ") + "\n");
77
- process.exit(1);
76
+ const sub = pr.path[1];
77
+ if (pr.path.length === 1 || sub === "serve") {
78
+ await cliMcpServeStdio(program);
79
+ process.exit(0);
78
80
  }
79
- await cliMcpServeStdio(program);
80
- process.exit(0);
81
+ if (pr.path.length === 2 && sub === "bundle") {
82
+ try {
83
+ runMcpBundle(program);
84
+ } catch (err) {
85
+ process.stderr.write(err instanceof Error ? err.message + "\n" : "mcp bundle failed.\n");
86
+ process.exit(1);
87
+ }
88
+ process.exit(0);
89
+ }
90
+ process.stderr.write("Unknown subcommand: mcp " + pr.path.slice(1).join(" ") + "\n");
91
+ process.exit(1);
81
92
  }
82
93
 
83
94
  if (pr.path[0] === "install") {
@@ -1,5 +1,7 @@
1
1
  import { type CliCapabilities, resolveCapabilities } from "../capabilities.ts";
2
- import type { CliFallbackMode, CliOption, CliPositional, CliProgram } from "../types.ts";
2
+ import type { CliFallbackMode, CliNode, CliOption, CliPositional, CliProgram } from "../types.ts";
3
+ import { isCliRouter } from "../types.ts";
4
+ import { visibleOptions } from "../hidden.ts";
3
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
6
  import { cliBuiltinInstallCommand } from "./install.ts";
5
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
@@ -11,6 +13,8 @@ export interface CliSchemaExport {
11
13
  key: string;
12
14
  description: string;
13
15
  notes?: string;
16
+ /** JSON Schema for structured stdout when set on the leaf `mcpTool`. */
17
+ outputSchema?: Record<string, unknown>;
14
18
  options?: CliOption[];
15
19
  fallbackCommand?: string;
16
20
  fallbackMode?: CliFallbackMode;
@@ -18,13 +22,11 @@ export interface CliSchemaExport {
18
22
  positionals?: CliPositional[];
19
23
  }
20
24
 
21
- function exportBuiltinNode(cmd: {
22
- key: string;
23
- description: string;
24
- notes?: string;
25
- options?: CliOption[];
26
- commands?: CliSchemaExport[];
27
- }): CliSchemaExport {
25
+ function exportBuiltinNode(cmd: CliNode): CliSchemaExport | null {
26
+ if (cmd.hidden) {
27
+ return null;
28
+ }
29
+
28
30
  const out: CliSchemaExport = {
29
31
  key: cmd.key,
30
32
  description: cmd.description,
@@ -32,11 +34,23 @@ function exportBuiltinNode(cmd: {
32
34
  if ((cmd.notes ?? "").length > 0) {
33
35
  out.notes = cmd.notes;
34
36
  }
35
- if ((cmd.options ?? []).length > 0) {
36
- out.options = cmd.options;
37
+ const options = visibleOptions(cmd.options);
38
+ if (options.length > 0) {
39
+ out.options = options;
37
40
  }
38
- if ((cmd.commands ?? []).length > 0) {
39
- out.commands = (cmd.commands ?? []).map((ch) => exportBuiltinNode(ch));
41
+ if (isCliRouter(cmd)) {
42
+ if (cmd.fallbackCommand !== undefined) {
43
+ out.fallbackCommand = cmd.fallbackCommand;
44
+ }
45
+ if (cmd.fallbackMode !== undefined) {
46
+ out.fallbackMode = cmd.fallbackMode;
47
+ }
48
+ const children = cmd.commands
49
+ .map((ch) => exportBuiltinNode(ch))
50
+ .filter((ch): ch is CliSchemaExport => ch !== null);
51
+ if (children.length > 0) {
52
+ out.commands = children;
53
+ }
40
54
  }
41
55
  return out;
42
56
  }
@@ -45,18 +59,18 @@ function exportBuiltinNode(cmd: {
45
59
  export function exportPresentationBuiltins(program: CliProgram, caps?: CliCapabilities): CliSchemaExport[] {
46
60
  const resolved = caps ?? resolveCapabilities(program);
47
61
  const builtins: CliSchemaExport[] = [
48
- exportBuiltinNode(cliBuiltinCompletionGroup(program)),
49
- exportBuiltinNode(cliBuiltinVersionCommand()),
62
+ exportBuiltinNode(cliBuiltinCompletionGroup(program))!,
63
+ exportBuiltinNode(cliBuiltinVersionCommand())!,
50
64
  ];
51
65
  if (resolved.install) {
52
- builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program)));
66
+ builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program))!);
53
67
  }
54
68
  const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
55
69
  if (docsGroup) {
56
- builtins.push(exportBuiltinNode(docsGroup));
70
+ builtins.push(exportBuiltinNode(docsGroup)!);
57
71
  }
58
72
  if (resolved.mcp) {
59
- builtins.push(exportBuiltinNode(cliBuiltinMcpCommand(program)));
73
+ builtins.push(exportBuiltinNode(cliBuiltinMcpCommand(program))!);
60
74
  }
61
75
  return builtins;
62
76
  }
@@ -4,7 +4,7 @@ export { completionFishScript } from "./completion-fish.ts";
4
4
  export { cliBuiltinCompletionGroup } from "./completion-group.ts";
5
5
  export { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
6
6
  export { cliBuiltinMcpCommand } from "./mcp.ts";
7
- export { cliPresentationRoot, presentationBuiltins } from "./presentation.ts";
7
+ export { cliParseRoot, cliPresentationRoot, parseBuiltins, presentationBuiltins } from "./presentation.ts";
8
8
  export { exportPresentationBuiltins, type CliSchemaExport } from "./export.ts";
9
9
  export { dispatchBuiltin, builtinInterceptRoot } from "./dispatch.ts";
10
10
  export { collectScopes, type ScopeRec } from "./scopes.ts";
@@ -1,9 +1,9 @@
1
1
  import { resolveCapabilities } from "../capabilities.ts";
2
2
  import { docsEnabled } from "../docs/resolve.ts";
3
- import { type CliLeaf, type CliProgram } from "../types.ts";
3
+ import { type CliLeaf, type CliProgram, type CliRouter, CliFallbackMode } from "../types.ts";
4
4
 
5
- /** Presence options for the top-level `mcp` built-in (leaf). */
6
- export function cliBuiltinMcpCommand(program: CliProgram): CliLeaf {
5
+ /** Built-in `mcp` router: bare `myapp mcp` runs stdio (via hidden `serve` fallback); `mcp bundle` packs `.mcpb`. */
6
+ export function cliBuiltinMcpCommand(program: CliProgram): CliRouter {
7
7
  const caps = resolveCapabilities(program);
8
8
  const lines = [
9
9
  "Stdio MCP server. Add to Cursor or Claude:",
@@ -18,10 +18,26 @@ export function cliBuiltinMcpCommand(program: CliProgram): CliLeaf {
18
18
  if (docsEnabled(program)) {
19
19
  lines.push("Full setup guide: {argsbarg:program} docs mcp");
20
20
  }
21
+
22
+ const serve: CliLeaf = {
23
+ key: "serve",
24
+ hidden: true,
25
+ description: "Run as an MCP server over stdio for AI agents.",
26
+ handler: () => {},
27
+ };
28
+
29
+ const bundle: CliLeaf = {
30
+ key: "bundle",
31
+ description: "Pack a Claude Desktop MCP Bundle (.mcpb) from dist/<key> (macOS-only v1).",
32
+ handler: () => {},
33
+ };
34
+
21
35
  return {
22
36
  key: "mcp",
23
- description: "Run as an MCP server over stdio for AI agents.",
37
+ description: "MCP server and bundle tools.",
24
38
  notes: lines.join("\n"),
25
- handler: () => {},
39
+ fallbackCommand: "serve",
40
+ fallbackMode: CliFallbackMode.MissingOnly,
41
+ commands: [serve, bundle],
26
42
  };
27
43
  }
@@ -1,5 +1,6 @@
1
1
  import type { CliCapabilities } from "../capabilities.ts";
2
2
  import { resolveCapabilities } from "../capabilities.ts";
3
+ import { presentationNode, visibleOptions } from "../hidden.ts";
3
4
  import type { CliLeaf, CliNode, CliProgram, CliRouter } from "../types.ts";
4
5
  import { isCliLeaf, isCliRouter } from "../types.ts";
5
6
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
@@ -8,8 +9,8 @@ import { cliBuiltinMcpCommand } from "./mcp.ts";
8
9
  import { cliBuiltinVersionCommand } from "./version.ts";
9
10
  import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
10
11
 
11
- /** Built-in command nodes injected for help, schema, and completions. */
12
- export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
12
+ /** All built-in command nodes for argv parsing (includes hidden builtins). */
13
+ export function parseBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
13
14
  const builtins: CliNode[] = [
14
15
  cliBuiltinCompletionGroup(program),
15
16
  cliBuiltinVersionCommand(),
@@ -27,9 +28,44 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
27
28
  return builtins;
28
29
  }
29
30
 
31
+ /** Built-in subtrees visible in help, schema, and completions (hidden builtins omitted). */
32
+ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
33
+ return parseBuiltins(program, caps).filter((b) => !b.hidden);
34
+ }
35
+
36
+ /**
37
+ * Full command tree for argv parsing, including hidden commands and builtins.
38
+ * Routing programs merge user commands with builtins; leaf programs wrap builtins only.
39
+ */
40
+ export function cliParseRoot(program: CliProgram): CliRouter {
41
+ const caps = resolveCapabilities(program);
42
+ const builtins = parseBuiltins(program, caps);
43
+
44
+ if (isCliLeaf(program)) {
45
+ return {
46
+ key: program.key,
47
+ description: program.description,
48
+ notes: program.notes,
49
+ options: program.options,
50
+ commands: builtins,
51
+ };
52
+ }
53
+
54
+ return {
55
+ key: program.key,
56
+ description: program.description,
57
+ notes: program.notes,
58
+ options: program.options,
59
+ fallbackCommand: program.fallbackCommand,
60
+ fallbackMode: program.fallbackMode,
61
+ commands: [...program.commands, ...builtins],
62
+ };
63
+ }
64
+
30
65
  /**
31
66
  * Returns a schema suitable for help display, including capability-built-in subtrees.
32
- * Routing programs get builtins merged; leaf programs are wrapped as a tiny router.
67
+ * Hidden commands and options are omitted. Routing programs get builtins merged;
68
+ * leaf programs are wrapped as a tiny router.
33
69
  */
34
70
  export function cliPresentationRoot(program: CliProgram): CliRouter {
35
71
  const caps = resolveCapabilities(program);
@@ -41,19 +77,23 @@ export function cliPresentationRoot(program: CliProgram): CliRouter {
41
77
  key: program.key,
42
78
  description: program.description,
43
79
  notes,
44
- options: program.options,
80
+ options: visibleOptions(program.options),
45
81
  commands: builtins,
46
82
  };
47
83
  }
48
84
 
85
+ const userCommands = program.commands
86
+ .map((ch) => presentationNode(ch))
87
+ .filter((ch): ch is CliNode => ch !== null);
88
+
49
89
  return {
50
90
  key: program.key,
51
91
  description: program.description,
52
92
  notes,
53
- options: program.options,
93
+ options: visibleOptions(program.options),
54
94
  fallbackCommand: program.fallbackCommand,
55
95
  fallbackMode: program.fallbackMode,
56
- commands: [...program.commands, ...builtins],
96
+ commands: [...userCommands, ...builtins],
57
97
  };
58
98
  }
59
99
 
@@ -74,4 +114,3 @@ export function presentationRootNotes(program: CliProgram, caps: CliCapabilities
74
114
 
75
115
  /** Presentation tree may include builtin leaf stubs. */
76
116
  export type CliPresentationNode = CliNode | CliLeaf;
77
-
@@ -105,3 +105,33 @@ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
105
105
  const md = generateApiGuide(fixture);
106
106
  expect(md).toContain("Invoke `myapp run`.");
107
107
  });
108
+
109
+ test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
110
+ const fixture: CliProgram = {
111
+ key: "myapp",
112
+ version: "1.0.0",
113
+ description: "Demo app.",
114
+ commands: [
115
+ {
116
+ key: "run",
117
+ description: "Run.",
118
+ outputSchema: {
119
+ type: "object",
120
+ properties: { id: { type: "string" } },
121
+ required: ["id"],
122
+ },
123
+ handler: () => {},
124
+ },
125
+ ],
126
+ };
127
+ const schema = cliSchemaExport(fixture);
128
+ expect(schema.commands![0]!.outputSchema).toEqual({
129
+ type: "object",
130
+ properties: { id: { type: "string" } },
131
+ required: ["id"],
132
+ });
133
+ const md = generateApiGuide(fixture);
134
+ expect(md).toContain("#### Output");
135
+ expect(md).toContain('"id"');
136
+ expect(md).toContain('"type": "string"');
137
+ });
@@ -52,6 +52,20 @@ function formatNotesBlockquote(notes: string, appKey: string): string {
52
52
  .join("\n");
53
53
  }
54
54
 
55
+ /** Markdown section for leaf outputSchema (docs api / skill reference). */
56
+ function formatOutputSchemaSection(schema: Record<string, unknown>): string[] {
57
+ return [
58
+ "#### Output",
59
+ "",
60
+ "JSON Schema for structured stdout (typically with `--json`, or via MCP when the handler emits JSON):",
61
+ "",
62
+ "```json",
63
+ JSON.stringify(schema, null, 2),
64
+ "```",
65
+ "",
66
+ ];
67
+ }
68
+
55
69
  /** Fallback routing note when present on a router node. */
56
70
  function fallbackLine(node: CliSchemaExport): string | null {
57
71
  if (node.fallbackCommand === undefined) {
@@ -103,6 +117,10 @@ function renderCommandNode(
103
117
  lines.push("");
104
118
  }
105
119
 
120
+ if (node.outputSchema !== undefined) {
121
+ lines.push(...formatOutputSchemaSection(node.outputSchema));
122
+ }
123
+
106
124
  const children = node.commands ?? [];
107
125
  if (children.length > 0) {
108
126
  lines.push("#### Subcommands", "");
package/src/help.ts CHANGED
@@ -8,6 +8,7 @@ style no matter how help is reached.
8
8
  */
9
9
 
10
10
  import { CliNode, CliOption, CliOptionKind, CliPositional, CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
11
+ import { visibleOptions, visibleSubcommands } from "./hidden.ts";
11
12
 
12
13
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
13
14
 
@@ -372,9 +373,9 @@ function rowsForPositionals(defs: CliPositional[], color: boolean): HelpRow[] {
372
373
  return defs.map((p) => ({ label: cliPositionalLabel(p, color), description: p.description }));
373
374
  }
374
375
 
375
- /** Table rows for subcommands, sorted by key. */
376
+ /** Table rows for subcommands, sorted by key (hidden commands omitted). */
376
377
  function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
377
- return cmds
378
+ return visibleSubcommands(cmds)
378
379
  .sort((a, b) => a.key.localeCompare(b.key))
379
380
  .map((c) => ({ label: c.key, description: c.description }));
380
381
  }
@@ -420,7 +421,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
420
421
  ).join("\n"),
421
422
  );
422
423
 
423
- const optBox = renderTableBox("Options", rowsForOptions(schema.options ?? [], color), hw, color);
424
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(schema.options), color), hw, color);
424
425
  if (optBox.length > 0) {
425
426
  lines.push("");
426
427
  lines.push(optBox.join("\n"));
@@ -470,7 +471,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr:
470
471
  ).join("\n"),
471
472
  );
472
473
 
473
- const optBox = renderTableBox("Options", rowsForOptions(node.options ?? [], color), hw, color);
474
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
474
475
  if (optBox.length > 0) {
475
476
  lines.push("");
476
477
  lines.push(optBox.join("\n"));