argsbarg 3.0.0 → 3.1.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
@@ -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.1.0] - 2026-06-20
11
+
12
+ ### Added
13
+
14
+ - **`docs` built-in** — opt in with `docs: { enabled: true, topics: { ... } }` on the program root. Bundled markdown topics on stdout (`myapp docs`, `myapp docs readme`, `myapp docs all`). Auto **`docs mcp`** guide when `docs` and `mcpServer` are both enabled. See [docs/bundled-docs.md](docs/bundled-docs.md).
15
+
16
+ ### Changed
17
+
18
+ - **Agent skills** — `SKILL.md` is shell-only (removed MCP setup, `mcp.json`, and `tools/call` content). Use `docs mcp` or MCP tools for agent execution guidance.
19
+
10
20
  ## [3.0.0] - 2026-06-20
11
21
 
12
22
  ### Added
@@ -198,7 +208,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
198
208
  - 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`).
199
209
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
200
210
 
201
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.0.0...HEAD
211
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v3.1.0...HEAD
212
+ [3.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.1.0
202
213
  [3.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v3.0.0
203
214
  [2.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v2.1.1
204
215
  [2.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v2.1.0
package/README.md CHANGED
@@ -99,10 +99,12 @@ Every app gets:
99
99
  - **`completion bash` / `completion zsh` / `completion fish`** — print shell completion scripts to stdout (injected by `cliRun`).
100
100
  - **`version`** — print `CliProgram.version` (`myapp version`).
101
101
  - **`mcp`** — when `mcpServer.enabled` is `true`, run as an MCP stdio server (`myapp mcp`).
102
+ - **`docs`** — when `docs.enabled` is `true`, print bundled markdown topics (`myapp docs`, `myapp docs readme`, …). See [docs/bundled-docs.md](docs/bundled-docs.md).
102
103
  - **`install`** — install the binary, completions, skills, and MCP config to the user environment (`myapp install --all --yes`). See [docs/install.md](docs/install.md).
103
104
 
104
105
  Do not declare a top-level command named **`completion`**, **`version`**, or **`install`** — they are reserved.
105
106
  When **`mcpServer.enabled`** is `true`, do not declare a top-level command named **`mcp`** — it is reserved for the MCP built-in.
107
+ When **`docs.enabled`** is `true`, do not declare a top-level command named **`docs`** — it is reserved for the docs built-in.
106
108
  Do not declare an option named **`schema`** — it is reserved for `--schema`.
107
109
 
108
110
 
@@ -228,7 +230,7 @@ The package root (`argsbarg` / `src/index.ts`) exports the types and runtime you
228
230
  | `cliInvoke(root, argv)` | Parse and dispatch without exiting; returns captured stdout/stderr. |
229
231
  | `cliErrWithHelp(ctx, msg)` | Print error + scoped help on stderr, exit 1. |
230
232
 
231
- Reserved identifiers (validated at startup): root commands **`completion`**, **`version`**, **`install`**, and **`mcp`** (only when `mcpServer.enabled` is `true`).
233
+ Reserved identifiers (validated at startup): root commands **`completion`**, **`version`**, **`install`**, **`docs`** (when `docs.enabled` is `true`), and **`mcp`** (when `mcpServer.enabled` is `true`).
232
234
 
233
235
  ---
234
236
 
package/bun.lock ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "argsbarg",
7
+ "devDependencies": {
8
+ "@types/bun": "^1.3.12",
9
+ },
10
+ },
11
+ },
12
+ "packages": {
13
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
14
+
15
+ "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="],
16
+
17
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
18
+
19
+ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
20
+ }
21
+ }
package/docs/ai-skills.md CHANGED
@@ -31,17 +31,21 @@ For library use, call `cliSkillInstall(root, "cursor" | "claude", { global: true
31
31
 
32
32
  ## Generated content
33
33
 
34
- - **`SKILL.md`** — YAML frontmatter, when-to-use guidance, command catalog, MCP setup hints
34
+ - **`SKILL.md`** — YAML frontmatter, when-to-use guidance, shell command catalog, pitfalls
35
35
  - **`reference.md`** — full `--schema` JSON export
36
36
 
37
- ## MCP vs skills
37
+ Skills describe **shell invocation only** — no MCP setup, `mcp.json`, or `tools/call` guidance. Use **`myapp docs mcp`** (when `docs` and `mcpServer` are enabled) or connect the MCP server for agent execution.
38
+
39
+ ## MCP vs skills vs docs
38
40
 
39
41
  | Mechanism | Role |
40
42
  | --- | --- |
41
43
  | **`myapp mcp`** (requires `mcpServer`) | Runtime tool execution over MCP |
42
- | **`myapp install --skill`** | Static discovery files for agents |
44
+ | **`myapp install --skill`** | Static shell command catalog for agents |
45
+ | **`myapp docs`** (requires `docs`) | Bundled markdown on stdout (`docs mcp` when MCP enabled) |
43
46
 
44
47
  See also:
45
48
 
49
+ - [Bundled docs](bundled-docs.md) — `docs` config and compile-time imports
46
50
  - [MCP server](mcp.md) — `mcpServer` config and `mcp` protocol
47
51
  - [Install](install.md) — binary, completions, skills, and MCP config
@@ -0,0 +1,77 @@
1
+ # Bundled documentation (`docs`)
2
+
3
+ ArgsBarg can expose bundled markdown topics as the built-in `docs` command group. Opt in on the program root with `docs: { enabled: true, topics: { ... } }`.
4
+
5
+ ## Quick start
6
+
7
+ ```typescript
8
+ import readmeText from "../README.md" with { type: "text" };
9
+ import archText from "../docs/architecture.md" with { type: "text" };
10
+
11
+ const cli = {
12
+ key: "myapp",
13
+ version: "1.0.0",
14
+ description: "My app.",
15
+ docs: {
16
+ enabled: true,
17
+ topics: {
18
+ readme: { text: readmeText },
19
+ architecture: { text: archText, description: "Contributor architecture notes." },
20
+ },
21
+ },
22
+ commands: [/* ... */],
23
+ } satisfies CliProgram;
24
+ ```
25
+
26
+ ```bash
27
+ myapp docs # first topic (readme) via fallback
28
+ myapp docs readme
29
+ myapp docs architecture
30
+ myapp docs all # all user topics; includes auto mcp when MCP enabled
31
+ myapp docs mcp # auto-generated when mcpServer.enabled
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ | Field | Default | Purpose |
37
+ | --- | --- | --- |
38
+ | `enabled` | *(required)* | Must be `true` when `docs` is set |
39
+ | `description` | `"Print bundled CLI documentation."` | Router help for `myapp docs` |
40
+ | `defaultTopic` | first key in `topics` | `fallbackCommand` for bare `myapp docs` |
41
+ | `topics` | *(required)* | Topic key → `{ text, description? }` |
42
+
43
+ Reserved topic keys in `topics`: **`mcp`**, **`all`** (supplied by the built-in).
44
+
45
+ When `description` is omitted on a topic, ArgsBarg generates leaf help (`readme` → "Print README (user guide).").
46
+
47
+ ## Compile-time bundling
48
+
49
+ Topic `text` must be **bundled markdown strings**. Use Bun text imports in the consumer module graph:
50
+
51
+ ```typescript
52
+ import readmeText from "../README.md" with { type: "text" };
53
+ ```
54
+
55
+ Bun embeds the file when you `bun build --compile`. ArgsBarg does not read the filesystem at runtime.
56
+
57
+ For several topics, use a barrel file (e.g. `src/docs/topics.ts`) so `index.tsx` stays small.
58
+
59
+ ## MCP guide (`docs mcp`)
60
+
61
+ When both `docs.enabled` and `mcpServer.enabled` are `true`, ArgsBarg injects a **`docs mcp`** topic with an auto-generated guide: tool list, `requiresEnv`, schema resource URI, `install --mcp`, and protocol notes.
62
+
63
+ There is no override API in v1 — customize behavior via `mcpTool.description` on leaf commands.
64
+
65
+ ## MCP tools
66
+
67
+ All `docs` subcommands are hidden from MCP `tools/list` (`mcpTool: { enabled: false }`).
68
+
69
+ ## Skills vs docs vs MCP
70
+
71
+ | Channel | Role |
72
+ | --- | --- |
73
+ | `install --skill` | Shell command catalog + `--schema` JSON (no MCP setup) |
74
+ | `docs` | Bundled markdown on stdout |
75
+ | `mcp` | Callable tools + schema resource |
76
+
77
+ Do not declare a top-level command named **`docs`** when `docs.enabled` is `true` — it is reserved.
@@ -15,6 +15,12 @@ const cli = {
15
15
  version: pkg.version,
16
16
  description: "Nested groups demo.",
17
17
  mcpServer: { enabled: true },
18
+ docs: {
19
+ enabled: true,
20
+ topics: {
21
+ readme: { text: "# nested.ts\n\nNested groups demo.\n" },
22
+ },
23
+ },
18
24
  commands: [
19
25
  {
20
26
  key: "stat",
package/index.d.ts CHANGED
@@ -173,6 +173,32 @@ export interface CliInstallConfig {
173
173
  /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
174
174
  prefix?: string;
175
175
  }
176
+ /**
177
+ * One bundled documentation topic for the `docs` built-in (program root only).
178
+ */
179
+ export interface CliDocsTopic {
180
+ /** Bundled markdown (use compile-time text imports in the consumer). */
181
+ text: string;
182
+ /** Leaf help text for `myapp docs <key> -h`. Auto-generated from key when omitted. */
183
+ description?: string;
184
+ }
185
+ /**
186
+ * Enables `myapp docs` and bundled markdown topics (program root only).
187
+ * Must include `enabled: true`; omit `docs` entirely to disable.
188
+ */
189
+ export interface CliDocsConfig {
190
+ /** When `true`, enables the `docs` built-in command group. */
191
+ enabled: boolean;
192
+ /** Router description for `myapp docs` (default: "Print bundled CLI documentation."). */
193
+ description?: string;
194
+ /**
195
+ * Subcommand for bare `myapp docs` (maps to router `fallbackCommand`).
196
+ * When omitted, uses the first key in `topics` (insertion order).
197
+ */
198
+ defaultTopic?: string;
199
+ /** Topic key → bundled markdown. Reserved keys: `mcp`, `all` (supplied by the built-in). */
200
+ topics: Record<string, CliDocsTopic>;
201
+ }
176
202
  /**
177
203
  * Base properties shared by all nodes in the user command tree.
178
204
  */
@@ -223,6 +249,8 @@ export type CliProgram = CliNode & {
223
249
  mcpServer?: CliMcpServerConfig;
224
250
  /** Opt-out and defaults for `install`. */
225
251
  install?: CliInstallConfig;
252
+ /** When set with `enabled: true`, enables the `docs` built-in command group. */
253
+ docs?: CliDocsConfig;
226
254
  };
227
255
  /**
228
256
  * Handler closure type for leaf commands.
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
+ "engines": {
6
+ "bun": ">=1.3"
7
+ },
5
8
  "scripts": {
6
9
  "//just": "echo this app uses justfile for development tasks"
7
10
  },
@@ -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 { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
12
13
  import { cliMcpServeStdio } from "../mcp.ts";
13
14
  import { cliInstall } from "../install/index.ts";
14
15
  import type { ParseResult } from "../parse.ts";
@@ -148,5 +149,17 @@ export function builtinInterceptRoot(
148
149
  };
149
150
  }
150
151
 
152
+ const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
153
+ if (first === "docs" && docsGroup) {
154
+ return {
155
+ parseRoot: {
156
+ key: program.key,
157
+ description: program.description,
158
+ commands: [docsGroup],
159
+ },
160
+ isLeafCompletionIntercept: false,
161
+ };
162
+ }
163
+
151
164
  return { parseRoot: program, isLeafCompletionIntercept: false };
152
165
  }
@@ -4,6 +4,7 @@ import { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
4
  import { cliBuiltinInstallCommand } from "./install.ts";
5
5
  import { cliBuiltinMcpCommand } from "./mcp.ts";
6
6
  import { cliBuiltinVersionCommand } from "./version.ts";
7
+ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
7
8
 
8
9
  /** JSON-safe command node (no handlers). */
9
10
  export interface CliSchemaExport {
@@ -50,6 +51,10 @@ export function exportPresentationBuiltins(program: CliProgram, caps?: CliCapabi
50
51
  if (resolved.install) {
51
52
  builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program)));
52
53
  }
54
+ const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
55
+ if (docsGroup) {
56
+ builtins.push(exportBuiltinNode(docsGroup));
57
+ }
53
58
  if (resolved.mcp) {
54
59
  builtins.push(exportBuiltinNode(cliBuiltinMcpCommand()));
55
60
  }
@@ -6,6 +6,7 @@ import { cliBuiltinCompletionGroup } from "./completion-group.ts";
6
6
  import { cliBuiltinInstallCommand } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
8
  import { cliBuiltinVersionCommand } from "./version.ts";
9
+ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
9
10
 
10
11
  /** Built-in command nodes injected for help, schema, and completions. */
11
12
  export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
@@ -16,6 +17,10 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
16
17
  if (caps.install) {
17
18
  builtins.push(cliBuiltinInstallCommand(program));
18
19
  }
20
+ const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
21
+ if (docsGroup) {
22
+ builtins.push(docsGroup);
23
+ }
19
24
  if (caps.mcp) {
20
25
  builtins.push(cliBuiltinMcpCommand());
21
26
  }
@@ -10,6 +10,7 @@ export interface CliCapabilities {
10
10
  completion: true;
11
11
  mcp: boolean;
12
12
  install: boolean;
13
+ docs: boolean;
13
14
  }
14
15
 
15
16
  /** Resolves which capabilities are enabled for a program. */
@@ -18,6 +19,7 @@ export function resolveCapabilities(program: CliProgram): CliCapabilities {
18
19
  completion: true,
19
20
  mcp: program.mcpServer?.enabled === true,
20
21
  install: program.install?.enabled !== false,
22
+ docs: program.docs?.enabled === true,
21
23
  };
22
24
  }
23
25
 
@@ -27,6 +29,9 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
27
29
  if (caps.install) {
28
30
  names.push("install");
29
31
  }
32
+ if (caps.docs) {
33
+ names.push("docs");
34
+ }
30
35
  if (caps.mcp) {
31
36
  names.push("mcp");
32
37
  }
@@ -0,0 +1,58 @@
1
+ import { CliFallbackMode, type CliLeaf, type CliProgram, type CliRouter } from "../types.ts";
2
+ import {
3
+ DOCS_ROUTER_DESCRIPTION,
4
+ docsEffectiveDefaultTopic,
5
+ docsEnabled,
6
+ docsIncludesMcpTopic,
7
+ docsTopicDescription,
8
+ docsUserTopicKeys,
9
+ printDocsTopic,
10
+ } from "./resolve.ts";
11
+
12
+ function docsLeaf(program: CliProgram, key: string, description: string): CliLeaf {
13
+ return {
14
+ key,
15
+ description,
16
+ mcpTool: { enabled: false },
17
+ handler: () => {
18
+ printDocsTopic(program, key);
19
+ },
20
+ };
21
+ }
22
+
23
+ /** Built-in `docs` router with bundled topic subcommands. */
24
+ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
25
+ const docs = program.docs!;
26
+ const leaves: CliLeaf[] = [];
27
+
28
+ for (const key of docsUserTopicKeys(docs)) {
29
+ const topic = docs.topics[key]!;
30
+ leaves.push(docsLeaf(program, key, docsTopicDescription(key, topic.description)));
31
+ }
32
+
33
+ if (docsIncludesMcpTopic(program)) {
34
+ leaves.push(
35
+ docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."),
36
+ );
37
+ }
38
+
39
+ leaves.push(
40
+ docsLeaf(program, "all", "Print all bundled documentation combined."),
41
+ );
42
+
43
+ return {
44
+ key: "docs",
45
+ description: docs.description ?? DOCS_ROUTER_DESCRIPTION,
46
+ fallbackCommand: docsEffectiveDefaultTopic(docs),
47
+ fallbackMode: CliFallbackMode.MissingOnly,
48
+ commands: leaves,
49
+ };
50
+ }
51
+
52
+ /** Returns the docs built-in when enabled. */
53
+ export function cliBuiltinDocsGroupIfEnabled(program: CliProgram): CliRouter | null {
54
+ if (!docsEnabled(program)) {
55
+ return null;
56
+ }
57
+ return cliBuiltinDocsGroup(program);
58
+ }
@@ -0,0 +1,121 @@
1
+ import { expect, test } from "bun:test";
2
+ import { cliPresentationRoot } from "../builtins/presentation.ts";
3
+ import { completionBashScript } from "../completion.ts";
4
+ import { cliInvoke } from "../index.ts";
5
+ import type { CliProgram } from "../types.ts";
6
+ import { cliValidateProgram } from "../validate.ts";
7
+ import { combineAllDocs, docsEffectiveDefaultTopic } from "./resolve.ts";
8
+ import { generateMcpGuide } from "./mcp-guide.ts";
9
+
10
+ function docsFixture(mcp = true): CliProgram {
11
+ return {
12
+ key: "myapp",
13
+ version: "1.0.0",
14
+ description: "Demo app.",
15
+ mcpServer: mcp ? { enabled: true } : undefined,
16
+ docs: {
17
+ enabled: true,
18
+ topics: {
19
+ readme: { text: "# Hello README\n" },
20
+ arch: { text: "# Architecture\n", description: "Contributor notes." },
21
+ },
22
+ },
23
+ commands: [
24
+ {
25
+ key: "run",
26
+ description: "Run something.",
27
+ handler: () => {},
28
+ },
29
+ ],
30
+ };
31
+ }
32
+
33
+ test("docs reserved when enabled", () => {
34
+ const root: CliProgram = {
35
+ ...docsFixture(),
36
+ commands: [
37
+ {
38
+ key: "docs",
39
+ description: "conflict",
40
+ handler: () => {},
41
+ },
42
+ ],
43
+ };
44
+ expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: docs/);
45
+ });
46
+
47
+ test("docs rejects reserved topic keys", () => {
48
+ const root = docsFixture();
49
+ root.docs!.topics.mcp = { text: "nope" };
50
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
51
+ });
52
+
53
+ test("docsEffectiveDefaultTopic uses first topic key", () => {
54
+ expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
55
+ });
56
+
57
+ test("bare docs prints first topic via cliInvoke", async () => {
58
+ const result = await cliInvoke(docsFixture(), ["docs"]);
59
+ expect(result.exitCode).toBe(0);
60
+ expect(result.stdout).toContain("Hello README");
61
+ });
62
+
63
+ test("docs readme prints bundled text", async () => {
64
+ const result = await cliInvoke(docsFixture(), ["docs", "readme"]);
65
+ expect(result.exitCode).toBe(0);
66
+ expect(result.stdout).toContain("Hello README");
67
+ });
68
+
69
+ test("docs defaultTopic override", async () => {
70
+ const root = docsFixture();
71
+ root.docs!.defaultTopic = "arch";
72
+ const result = await cliInvoke(root, ["docs"]);
73
+ expect(result.stdout).toContain("Architecture");
74
+ });
75
+
76
+ test("docs mcp when MCP enabled", async () => {
77
+ const result = await cliInvoke(docsFixture(true), ["docs", "mcp"]);
78
+ expect(result.exitCode).toBe(0);
79
+ expect(result.stdout).toContain("MCP server (myapp)");
80
+ expect(result.stdout).toContain("myapp mcp");
81
+ });
82
+
83
+ test("docs mcp absent from router when MCP disabled", async () => {
84
+ const root = docsFixture(false);
85
+ const presentation = cliPresentationRoot(root);
86
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
87
+ expect(docsNode && "commands" in docsNode).toBe(true);
88
+ if (docsNode && "commands" in docsNode) {
89
+ expect(docsNode.commands.some((c) => c.key === "mcp")).toBe(false);
90
+ }
91
+ const result = await cliInvoke(root, ["docs", "mcp"]);
92
+ expect(result.exitCode).not.toBe(0);
93
+ });
94
+
95
+ test("docs all concatenates user topics and mcp", () => {
96
+ const program = docsFixture(true);
97
+ const combined = combineAllDocs(program);
98
+ expect(combined).toContain("Hello README");
99
+ expect(combined).toContain("Architecture");
100
+ expect(combined).toContain("MCP server (myapp)");
101
+ });
102
+
103
+ test("presentation includes docs subtree", () => {
104
+ const presentation = cliPresentationRoot(docsFixture());
105
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
106
+ expect(docsNode).toBeDefined();
107
+ expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(
108
+ true,
109
+ );
110
+ });
111
+
112
+ test("completions offer docs subcommands", () => {
113
+ const bash = completionBashScript(cliPresentationRoot(docsFixture()));
114
+ expect(bash).toContain("docs) echo");
115
+ expect(bash).toContain("readme) echo");
116
+ });
117
+
118
+ test("generateMcpGuide includes schema URI", () => {
119
+ const guide = generateMcpGuide(docsFixture(true));
120
+ expect(guide).toContain("myapp://schema");
121
+ });
@@ -0,0 +1,118 @@
1
+ import { collectOptionDefs } from "../parse.ts";
2
+ import {
3
+ collectMcpTools,
4
+ mcpServerId,
5
+ resolveMcpSchemaUri,
6
+ type McpToolDef,
7
+ } from "../mcp/tools.ts";
8
+ import { type CliProgram, CliOptionKind } from "../types.ts";
9
+
10
+ /** Formats one exposed MCP tool for the auto-generated MCP guide. */
11
+ function formatToolLine(root: CliProgram, tool: McpToolDef): string {
12
+ const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
13
+ let line = `- \`${cliPath}\` — ${tool.description}`;
14
+ const opts = collectOptionDefs(root, tool.path);
15
+ const flags = opts.filter((o) => o.kind === CliOptionKind.Presence).map((o) => `--${o.name}`);
16
+ if (flags.length > 0) {
17
+ line += ` (flags: ${flags.join(", ")})`;
18
+ }
19
+ return line;
20
+ }
21
+
22
+ /** Generates the auto `docs mcp` markdown guide from schema and MCP config. */
23
+ export function generateMcpGuide(root: CliProgram): string {
24
+ const tools = collectMcpTools(root);
25
+ const schemaUri = resolveMcpSchemaUri(root);
26
+ const serverId = mcpServerId(root);
27
+ const mcp = root.mcpServer!;
28
+
29
+ const lines: string[] = [
30
+ `# MCP server (${root.key})`,
31
+ "",
32
+ `${root.key} exposes an MCP server via argsbarg. Each exposed leaf command becomes an MCP tool.`,
33
+ "",
34
+ "## Quick start",
35
+ "",
36
+ "```bash",
37
+ `${root.key} mcp`,
38
+ "```",
39
+ "",
40
+ "Cursor / Claude `mcp.json` entry:",
41
+ "",
42
+ "```json",
43
+ JSON.stringify(
44
+ {
45
+ mcpServers: {
46
+ [serverId]: {
47
+ command: root.key,
48
+ args: ["mcp"],
49
+ },
50
+ },
51
+ },
52
+ null,
53
+ 2,
54
+ ),
55
+ "```",
56
+ "",
57
+ "Or run:",
58
+ "",
59
+ "```bash",
60
+ `${root.key} install --mcp --yes`,
61
+ "```",
62
+ "",
63
+ ];
64
+
65
+ if (mcp.shellEnv || mcp.envFile) {
66
+ lines.push("## Environment", "");
67
+ if (mcp.shellEnv) {
68
+ lines.push(
69
+ "- **`shellEnv`** — captures login-shell environment at MCP startup (PATH, toolchain shims, exports).",
70
+ );
71
+ }
72
+ if (mcp.envFile) {
73
+ lines.push(
74
+ "- **`envFile`** — loads `" + mcp.envFile + "` after shell env (overrides for its keys).",
75
+ );
76
+ }
77
+ lines.push("");
78
+ }
79
+
80
+ lines.push(
81
+ "## What agents get",
82
+ "",
83
+ "| Mechanism | Purpose |",
84
+ "|-----------|---------|",
85
+ "| `tools/list` | Callable tools for exposed leaf commands |",
86
+ "| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
87
+ `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} --schema\` |`,
88
+ "",
89
+ "## Exposed tools",
90
+ "",
91
+ );
92
+
93
+ if (tools.length === 0) {
94
+ lines.push("(No MCP tools exposed.)", "");
95
+ } else {
96
+ for (const tool of tools) {
97
+ lines.push(formatToolLine(root, tool));
98
+ }
99
+ lines.push("");
100
+ }
101
+
102
+ lines.push(
103
+ "## Tool arguments",
104
+ "",
105
+ "Arguments are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
106
+ `See \`${root.key} --schema\` or the schema resource for per-tool shapes.`,
107
+ "",
108
+ "Varargs positionals accept a JSON array or a comma-separated string.",
109
+ "",
110
+ "## Protocol",
111
+ "",
112
+ "Stdio NDJSON JSON-RPC. Help and `--schema` are not available through tool calls.",
113
+ `Run \`${root.key} docs\` for bundled user documentation.`,
114
+ "",
115
+ );
116
+
117
+ return lines.join("\n");
118
+ }
@@ -0,0 +1,88 @@
1
+ import type { CliDocsConfig, CliProgram } from "../types.ts";
2
+ import { generateMcpGuide } from "./mcp-guide.ts";
3
+
4
+ /** Built-in docs subcommand keys not allowed in `docs.topics`. */
5
+ export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all"] as const;
6
+
7
+ export type DocsBuiltinTopicKey = (typeof DOCS_BUILTIN_TOPIC_KEYS)[number];
8
+
9
+ /** Default router description for the `docs` built-in. */
10
+ export const DOCS_ROUTER_DESCRIPTION = "Print bundled CLI documentation.";
11
+
12
+ /** Returns whether bundled docs are enabled on the program root. */
13
+ export function docsEnabled(program: CliProgram): boolean {
14
+ return program.docs?.enabled === true;
15
+ }
16
+
17
+ /** User topic keys in declaration order. */
18
+ export function docsUserTopicKeys(docs: CliDocsConfig): string[] {
19
+ return Object.keys(docs.topics);
20
+ }
21
+
22
+ /** Subcommand used when argv is bare `myapp docs`. */
23
+ export function docsEffectiveDefaultTopic(docs: CliDocsConfig): string {
24
+ if (docs.defaultTopic !== undefined) {
25
+ return docs.defaultTopic;
26
+ }
27
+ const keys = docsUserTopicKeys(docs);
28
+ if (keys.length === 0) {
29
+ throw new Error("docs.topics must be non-empty");
30
+ }
31
+ return keys[0]!;
32
+ }
33
+
34
+ /** Whether MCP auto-guide topic is included. */
35
+ export function docsIncludesMcpTopic(program: CliProgram): boolean {
36
+ return docsEnabled(program) && program.mcpServer?.enabled === true;
37
+ }
38
+
39
+ /** Leaf help description for a user topic. */
40
+ export function docsTopicDescription(key: string, custom?: string): string {
41
+ if (custom) {
42
+ return custom;
43
+ }
44
+ if (key === "readme") {
45
+ return "Print README (user guide).";
46
+ }
47
+ const label = key.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
48
+ return `Print ${label} documentation.`;
49
+ }
50
+
51
+ /** Ordered keys for `docs all` (user topics, then auto `mcp` when enabled). */
52
+ export function docsPrintOrder(program: CliProgram): string[] {
53
+ const docs = program.docs!;
54
+ const order = docsUserTopicKeys(docs);
55
+ if (docsIncludesMcpTopic(program)) {
56
+ order.push("mcp");
57
+ }
58
+ return order;
59
+ }
60
+
61
+ /** Markdown body for one docs topic key. */
62
+ export function docsTopicText(program: CliProgram, topic: string): string {
63
+ const docs = program.docs!;
64
+ if (topic === "mcp") {
65
+ if (!docsIncludesMcpTopic(program)) {
66
+ throw new Error("Unknown docs topic 'mcp'.");
67
+ }
68
+ return generateMcpGuide(program);
69
+ }
70
+ const entry = docs.topics[topic];
71
+ if (!entry) {
72
+ throw new Error(`Unknown docs topic '${topic}'.`);
73
+ }
74
+ return entry.text;
75
+ }
76
+
77
+ /** All bundled docs concatenated with horizontal rules. */
78
+ export function combineAllDocs(program: CliProgram): string {
79
+ return docsPrintOrder(program)
80
+ .map((key) => docsTopicText(program, key).trim())
81
+ .join("\n\n---\n\n");
82
+ }
83
+
84
+ /** Writes one docs topic (or `all`) to stdout. */
85
+ export function printDocsTopic(program: CliProgram, topic: string): void {
86
+ const content = topic === "all" ? combineAllDocs(program) : docsTopicText(program, topic);
87
+ process.stdout.write(`${content}\n`);
88
+ }
package/src/index.test.ts CHANGED
@@ -1760,7 +1760,10 @@ test("generateSkillBundle includes frontmatter and command catalog", () => {
1760
1760
  expect(bundle.dirName).toBe("nested_ts");
1761
1761
  expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
1762
1762
  expect(bundle.skillMd).toContain("stat owner lookup");
1763
- expect(bundle.skillMd).toContain("nested.ts mcp");
1763
+ expect(bundle.skillMd).toContain("Invoke via shell:");
1764
+ expect(bundle.skillMd).not.toContain("mcp.json");
1765
+ expect(bundle.skillMd).not.toContain("Prefer MCP");
1766
+ expect(bundle.skillMd).not.toContain("tools/call");
1764
1767
  expect(bundle.referenceMd).toContain("```json");
1765
1768
  expect(() => JSON.parse(bundle.referenceMd.match(/```json\n([\s\S]*?)\n```/)![1]!)).not.toThrow();
1766
1769
  });
package/src/index.ts CHANGED
@@ -20,6 +20,8 @@ export type {
20
20
  CliMcpServerConfig,
21
21
  CliMcpToolConfig,
22
22
  CliInstallConfig,
23
+ CliDocsConfig,
24
+ CliDocsTopic,
23
25
  CliOption,
24
26
  CliPositional,
25
27
  } from "./types.ts";
package/src/invoke.ts CHANGED
@@ -5,8 +5,10 @@ process.exit so MCP tool calls can run handlers repeatedly.
5
5
  */
6
6
 
7
7
  import { CliContext } from "./context.ts";
8
+ import { builtinInterceptRoot } from "./builtins/dispatch.ts";
9
+ import { cliPresentationRoot } from "./builtins/presentation.ts";
8
10
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
9
- import { type CliNode, type CliProgram, isCliRouter } from "./types.ts";
11
+ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
10
12
  import { format } from "node:util";
11
13
 
12
14
  /** Outcome of a non-exiting CLI invocation. */
@@ -49,8 +51,18 @@ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
49
51
  * Never calls process.exit.
50
52
  */
51
53
  export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult> {
52
- let pr = parse(root, argv);
53
- pr = postParseValidate(root, pr);
54
+ let parseRoot: CliNode = root;
55
+ if (isCliLeaf(root)) {
56
+ const intercept = builtinInterceptRoot(root, argv);
57
+ if (intercept.parseRoot !== root) {
58
+ parseRoot = intercept.parseRoot;
59
+ }
60
+ } else {
61
+ parseRoot = cliPresentationRoot(root);
62
+ }
63
+
64
+ let pr = parse(parseRoot, argv);
65
+ pr = postParseValidate(parseRoot, pr);
54
66
 
55
67
  if (pr.kind === ParseKind.Help) {
56
68
  return {
@@ -82,7 +94,7 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
82
94
  };
83
95
  }
84
96
 
85
- let current: CliNode = root;
97
+ let current: CliNode = parseRoot;
86
98
  for (const seg of pr.path) {
87
99
  if (!isCliRouter(current)) {
88
100
  return {
package/src/runtime.ts CHANGED
@@ -41,6 +41,11 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
41
41
  process.exit(1);
42
42
  }
43
43
 
44
+ if (argv.length >= 1 && argv[0] === "docs" && !caps.docs) {
45
+ process.stderr.write("docs is not enabled. Set docs: { enabled: true } on the program root.\n");
46
+ process.exit(1);
47
+ }
48
+
44
49
  let parseRoot: CliNode;
45
50
  let completionParseRoot: CliRouter = cliRootMergedWithBuiltins(program);
46
51
  let isLeafCompletionIntercept = false;
package/src/schema.ts CHANGED
@@ -5,7 +5,7 @@ This module serializes the CLI schema tree to JSON for machine-readable introspe
5
5
  import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
6
6
  import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
7
7
 
8
- const RESERVED = new Set(["completion", "install", "mcp"]);
8
+ const RESERVED = new Set(["completion", "install", "docs", "mcp", "version"]);
9
9
 
10
10
  function exportCommand(cmd: CliNode): CliSchemaExport {
11
11
  const out: CliSchemaExport = {
@@ -4,7 +4,7 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
4
4
 
5
5
  import { collectOptionDefs } from "../parse.ts";
6
6
  import { cliSchemaJson } from "../schema.ts";
7
- import { collectMcpTools, mcpServerId, sanitizeToolSegment } from "../mcp/tools.ts";
7
+ import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
8
8
  import { CliProgram, CliOptionKind } from "../types.ts";
9
9
 
10
10
  export type SkillTarget = "cursor" | "claude";
@@ -69,46 +69,14 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
69
69
  "",
70
70
  "## When to use",
71
71
  "",
72
- `Use this skill when working with **${root.key}** — shell commands, automation, or agent tool calls for this application.`,
72
+ `Use this skill when working with **${root.key}** — shell commands and automation for this application.`,
73
73
  "",
74
74
  "## Execution",
75
75
  "",
76
+ "Invoke via shell:",
77
+ "",
76
78
  ];
77
79
 
78
- if (root.mcpServer?.enabled === true) {
79
- lines.push(
80
- "**Prefer MCP** when a host has the server connected:",
81
- "",
82
- "```bash",
83
- `${root.key} mcp`,
84
- "```",
85
- "",
86
- "Example Cursor `mcp.json` entry:",
87
- "",
88
- "```json",
89
- JSON.stringify(
90
- {
91
- mcpServers: {
92
- [mcpServerId(root)]: {
93
- command: root.key,
94
- args: ["mcp"],
95
- },
96
- },
97
- },
98
- null,
99
- 2,
100
- ),
101
- "```",
102
- "",
103
- "When MCP tools are available, use `tools/call` with flat JSON arguments. Read the schema resource for full shapes.",
104
- "",
105
- "Otherwise invoke via shell:",
106
- "",
107
- );
108
- } else {
109
- lines.push("Invoke via shell:", "");
110
- }
111
-
112
80
  lines.push("```bash", `${root.key} <subcommand> [options] [args]`, "```", "", "## Commands", "");
113
81
 
114
82
  if (tools.length === 0) {
@@ -124,7 +92,6 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
124
92
  "## Pitfalls",
125
93
  "",
126
94
  "- Use `--` before tokens that look like flags when they are positional arguments.",
127
- "- Under MCP (`ctx.invocation === \"mcp\"`), child processes must not inherit stdout — use piped stdout.",
128
95
  "- Required environment variables are listed per command in descriptions (`requires env`).",
129
96
  "",
130
97
  "## Reference",
package/src/types.ts CHANGED
@@ -160,6 +160,34 @@ export interface CliInstallConfig {
160
160
  prefix?: string;
161
161
  }
162
162
 
163
+ /**
164
+ * One bundled documentation topic for the `docs` built-in (program root only).
165
+ */
166
+ export interface CliDocsTopic {
167
+ /** Bundled markdown (use compile-time text imports in the consumer). */
168
+ text: string;
169
+ /** Leaf help text for `myapp docs <key> -h`. Auto-generated from key when omitted. */
170
+ description?: string;
171
+ }
172
+
173
+ /**
174
+ * Enables `myapp docs` and bundled markdown topics (program root only).
175
+ * Must include `enabled: true`; omit `docs` entirely to disable.
176
+ */
177
+ export interface CliDocsConfig {
178
+ /** When `true`, enables the `docs` built-in command group. */
179
+ enabled: boolean;
180
+ /** Router description for `myapp docs` (default: "Print bundled CLI documentation."). */
181
+ description?: string;
182
+ /**
183
+ * Subcommand for bare `myapp docs` (maps to router `fallbackCommand`).
184
+ * When omitted, uses the first key in `topics` (insertion order).
185
+ */
186
+ defaultTopic?: string;
187
+ /** Topic key → bundled markdown. Reserved keys: `mcp`, `all` (supplied by the built-in). */
188
+ topics: Record<string, CliDocsTopic>;
189
+ }
190
+
163
191
  /**
164
192
  * Base properties shared by all nodes in the user command tree.
165
193
  */
@@ -214,6 +242,8 @@ export type CliProgram = CliNode & {
214
242
  mcpServer?: CliMcpServerConfig;
215
243
  /** Opt-out and defaults for `install`. */
216
244
  install?: CliInstallConfig;
245
+ /** When set with `enabled: true`, enables the `docs` built-in command group. */
246
+ docs?: CliDocsConfig;
217
247
  };
218
248
 
219
249
  /** True when the node is a leaf (has a handler). */
package/src/validate.ts CHANGED
@@ -13,6 +13,33 @@ import {
13
13
  isCliRouter,
14
14
  } from "./types.ts";
15
15
  import { resolveMcpSchemaUri } from "./mcp/tools.ts";
16
+ import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
17
+
18
+ /** Validates `docs` configuration on the program root. */
19
+ function validateDocsConfig(docs: import("./types.ts").CliDocsConfig): void {
20
+ const keys = Object.keys(docs.topics);
21
+ if (keys.length === 0) {
22
+ throw new CliSchemaValidationError("docs.topics must be non-empty");
23
+ }
24
+ for (const reserved of DOCS_BUILTIN_TOPIC_KEYS) {
25
+ if (reserved in docs.topics) {
26
+ throw new CliSchemaValidationError(
27
+ `docs.topics key '${reserved}' is reserved for the docs built-in`,
28
+ );
29
+ }
30
+ }
31
+ if (docs.defaultTopic !== undefined && !(docs.defaultTopic in docs.topics)) {
32
+ throw new CliSchemaValidationError(
33
+ `docs.defaultTopic '${docs.defaultTopic}' is not a key in docs.topics`,
34
+ );
35
+ }
36
+ for (const key of keys) {
37
+ const text = docs.topics[key]?.text;
38
+ if (text === undefined || text.length === 0) {
39
+ throw new CliSchemaValidationError(`docs.topics['${key}'].text must be non-empty`);
40
+ }
41
+ }
42
+ }
16
43
 
17
44
  /** Validates a program schema. */
18
45
  export function cliValidateProgram(program: CliProgram): void {
@@ -26,6 +53,16 @@ export function cliValidateProgram(program: CliProgram): void {
26
53
  );
27
54
  }
28
55
 
56
+ if (program.docs !== undefined && program.docs.enabled !== true) {
57
+ throw new CliSchemaValidationError(
58
+ "docs requires enabled: true; omit docs to disable bundled documentation",
59
+ );
60
+ }
61
+
62
+ if (program.docs?.enabled === true) {
63
+ validateDocsConfig(program.docs);
64
+ }
65
+
29
66
  const caps = resolveCapabilities(program);
30
67
  const reserved = reservedCommandNames(caps);
31
68
 
@@ -53,6 +90,11 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
53
90
  "install is only supported on the program root (not on " + node.key + ")",
54
91
  );
55
92
  }
93
+ if (rogue.docs !== undefined) {
94
+ throw new CliSchemaValidationError(
95
+ "docs is only supported on the program root (not on " + node.key + ")",
96
+ );
97
+ }
56
98
  }
57
99
 
58
100
  if (isCliLeaf(node)) {