argsbarg 3.0.0 → 3.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +6 -6
  3. package/bun.lock +21 -0
  4. package/docs/ai-skills.md +8 -4
  5. package/docs/bundled-docs.md +91 -0
  6. package/docs/install.md +16 -4
  7. package/docs/mcp.md +4 -6
  8. package/examples/minimal.ts +6 -0
  9. package/examples/nested.ts +6 -0
  10. package/index.d.ts +46 -1
  11. package/package.json +4 -1
  12. package/src/builtins/completion-bash.ts +1 -8
  13. package/src/builtins/completion-fish.ts +0 -5
  14. package/src/builtins/completion-zsh.ts +1 -9
  15. package/src/builtins/dispatch.ts +40 -0
  16. package/src/builtins/export.ts +9 -0
  17. package/src/builtins/install.ts +9 -3
  18. package/src/builtins/presentation.ts +9 -0
  19. package/src/builtins/shell-helpers.ts +0 -2
  20. package/src/builtins/update.ts +14 -0
  21. package/src/capabilities.ts +12 -1
  22. package/src/docs/api-guide.test.ts +55 -0
  23. package/src/docs/api-guide.ts +129 -0
  24. package/src/docs/builtin.ts +61 -0
  25. package/src/docs/docs.test.ts +167 -0
  26. package/src/docs/mcp-guide.ts +118 -0
  27. package/src/docs/resolve.ts +119 -0
  28. package/src/help.ts +3 -7
  29. package/src/index.test.ts +40 -66
  30. package/src/index.ts +4 -0
  31. package/src/install/binary.ts +8 -3
  32. package/src/install/index.ts +55 -30
  33. package/src/install/plan.ts +5 -3
  34. package/src/install/update.test.ts +106 -0
  35. package/src/install/update.ts +55 -0
  36. package/src/invoke.ts +17 -15
  37. package/src/mcp/tools.ts +1 -1
  38. package/src/parse.ts +0 -27
  39. package/src/runtime.ts +12 -6
  40. package/src/schema.ts +7 -2
  41. package/src/skill/generate.ts +6 -39
  42. package/src/types.ts +47 -0
  43. package/src/validate.ts +53 -6
@@ -5,7 +5,9 @@ import { isCliLeaf, isCliRouter } from "../types.ts";
5
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
6
6
  import { cliBuiltinInstallCommand } from "./install.ts";
7
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
8
+ import { cliBuiltinUpdateCommand } from "./update.ts";
8
9
  import { cliBuiltinVersionCommand } from "./version.ts";
10
+ import { cliBuiltinDocsGroupIfEnabled } from "../docs/builtin.ts";
9
11
 
10
12
  /** Built-in command nodes injected for help, schema, and completions. */
11
13
  export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
@@ -16,6 +18,13 @@ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities)
16
18
  if (caps.install) {
17
19
  builtins.push(cliBuiltinInstallCommand(program));
18
20
  }
21
+ if (caps.update) {
22
+ builtins.push(cliBuiltinUpdateCommand(program));
23
+ }
24
+ const docsGroup = cliBuiltinDocsGroupIfEnabled(program);
25
+ if (docsGroup) {
26
+ builtins.push(docsGroup);
27
+ }
19
28
  if (caps.mcp) {
20
29
  builtins.push(cliBuiltinMcpCommand());
21
30
  }
@@ -20,5 +20,3 @@ export function mainName(schemaName: string): string {
20
20
 
21
21
  export const kHelpLong = "--help";
22
22
  export const kHelpShort = "-h";
23
- export const kSchemaLong = "--schema";
24
- export const kSchemaDesc = "Print the full command tree as JSON.";
@@ -0,0 +1,14 @@
1
+ import type { CliLeaf, CliProgram } from "../types.ts";
2
+ import { cliUpdate } from "../install/update.ts";
3
+
4
+ /** Built-in `update` command (enabled when `install.updateGetLatest` is set). */
5
+ export function cliBuiltinUpdateCommand(program: CliProgram): CliLeaf {
6
+ return {
7
+ key: "update",
8
+ description: "Download and install the latest release.",
9
+ mcpTool: { enabled: false },
10
+ handler: async () => {
11
+ await cliUpdate(program);
12
+ },
13
+ };
14
+ }
@@ -10,14 +10,19 @@ export interface CliCapabilities {
10
10
  completion: true;
11
11
  mcp: boolean;
12
12
  install: boolean;
13
+ docs: boolean;
14
+ update: boolean;
13
15
  }
14
16
 
15
17
  /** Resolves which capabilities are enabled for a program. */
16
18
  export function resolveCapabilities(program: CliProgram): CliCapabilities {
19
+ const install = program.install?.enabled !== false;
17
20
  return {
18
21
  completion: true,
19
22
  mcp: program.mcpServer?.enabled === true,
20
- install: program.install?.enabled !== false,
23
+ install,
24
+ docs: program.docs?.enabled === true,
25
+ update: install && typeof program.install?.updateGetLatest === "function",
21
26
  };
22
27
  }
23
28
 
@@ -27,6 +32,12 @@ export function reservedCommandNames(caps: CliCapabilities): string[] {
27
32
  if (caps.install) {
28
33
  names.push("install");
29
34
  }
35
+ if (caps.update) {
36
+ names.push("update");
37
+ }
38
+ if (caps.docs) {
39
+ names.push("docs");
40
+ }
30
41
  if (caps.mcp) {
31
42
  names.push("mcp");
32
43
  }
@@ -0,0 +1,55 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { CliProgram } from "../types.ts";
3
+ import { CliOptionKind } from "../types.ts";
4
+ import { generateApiGuide } from "./api-guide.ts";
5
+ import { cliSchemaExport } from "../schema.ts";
6
+
7
+ const nestedFixture: CliProgram = {
8
+ key: "nested.ts",
9
+ version: "1.0.0",
10
+ description: "Nested groups demo.",
11
+ docs: { enabled: true, topics: { readme: { text: "# readme\n" } } },
12
+ commands: [
13
+ {
14
+ key: "stat",
15
+ description: "File metadata.",
16
+ commands: [
17
+ {
18
+ key: "owner",
19
+ description: "Ownership helpers.",
20
+ commands: [
21
+ {
22
+ key: "lookup",
23
+ description: "Resolve owner info.",
24
+ options: [
25
+ {
26
+ name: "user-name",
27
+ description: "User to look up.",
28
+ kind: CliOptionKind.String,
29
+ shortName: "u",
30
+ },
31
+ ],
32
+ positionals: [
33
+ {
34
+ name: "path",
35
+ description: "File or directory.",
36
+ kind: CliOptionKind.String,
37
+ },
38
+ ],
39
+ handler: () => {},
40
+ },
41
+ ],
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ };
47
+
48
+ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
49
+ const md = generateApiGuide(nestedFixture);
50
+ const schema = cliSchemaExport(nestedFixture);
51
+ expect(md).toContain("`nested.ts stat owner lookup`");
52
+ expect(md).toContain("`--user-name` (`-u`)");
53
+ expect(md).toContain("`<path>`");
54
+ expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
55
+ });
@@ -0,0 +1,129 @@
1
+ import type { CliSchemaExport } from "../builtins/export.ts";
2
+ import { cliPositionalLabel } from "../help.ts";
3
+ import { cliSchemaExport } from "../schema.ts";
4
+ import type { CliOption, CliPositional, CliProgram } from "../types.ts";
5
+ import { CliFallbackMode, CliOptionKind } from "../types.ts";
6
+
7
+ /** CLI invocation path as a single string (`myapp stat owner lookup`). */
8
+ function commandPath(rootKey: string, path: string[]): string {
9
+ if (path.length === 0) {
10
+ return rootKey;
11
+ }
12
+ return [rootKey, ...path].join(" ");
13
+ }
14
+
15
+ /** Human-readable option type for API tables. */
16
+ function optionType(opt: CliOption): string {
17
+ if (opt.kind === CliOptionKind.Presence) {
18
+ return "flag";
19
+ }
20
+ if (opt.kind === CliOptionKind.Enum) {
21
+ return `enum (\`${(opt.choices ?? []).join("`, `")}\`)`;
22
+ }
23
+ return opt.kind;
24
+ }
25
+
26
+ /** Markdown table cell for one option flag. */
27
+ function optionLabel(opt: CliOption): string {
28
+ const long = `\`--${opt.name}\``;
29
+ const short = opt.shortName ? ` (\`-${opt.shortName}\`)` : "";
30
+ return `${long}${short}`;
31
+ }
32
+
33
+ /** One options table row. */
34
+ function formatOptionRow(opt: CliOption): string {
35
+ const req = opt.required ? "required" : "optional";
36
+ return `| ${optionLabel(opt)} | ${optionType(opt)} | ${req} | ${opt.description} |`;
37
+ }
38
+
39
+ /** One positionals table row. */
40
+ function formatPositionalRow(p: CliPositional): string {
41
+ const label = cliPositionalLabel(p, false);
42
+ const req = (p.argMin ?? 1) > 0 ? "required" : "optional";
43
+ return `| \`${label}\` | ${p.kind} | ${req} | ${p.description} |`;
44
+ }
45
+
46
+ /** Fallback routing note when present on a router node. */
47
+ function fallbackLine(node: CliSchemaExport): string | null {
48
+ if (node.fallbackCommand === undefined) {
49
+ return null;
50
+ }
51
+ const mode = node.fallbackMode ?? CliFallbackMode.MissingOnly;
52
+ return `**Default subcommand:** \`${node.fallbackCommand}\` (\`${mode}\`)`;
53
+ }
54
+
55
+ /** Renders one command node and recurses into subcommands. */
56
+ function renderCommandNode(
57
+ rootKey: string,
58
+ path: string[],
59
+ node: CliSchemaExport,
60
+ lines: string[],
61
+ ): void {
62
+ const level = Math.min(path.length + 2, 6);
63
+ const heading = "#".repeat(level);
64
+ const cmd = commandPath(rootKey, path);
65
+
66
+ lines.push(`${heading} \`${cmd}\``, "", node.description, "");
67
+
68
+ if (node.notes) {
69
+ lines.push(`> ${node.notes}`, "");
70
+ }
71
+
72
+ const fb = fallbackLine(node);
73
+ if (fb) {
74
+ lines.push(fb, "");
75
+ }
76
+
77
+ if ((node.options ?? []).length > 0) {
78
+ lines.push("#### Options", "");
79
+ lines.push("| Option | Type | Required | Description |");
80
+ lines.push("| --- | --- | --- | --- |");
81
+ for (const opt of node.options!) {
82
+ lines.push(formatOptionRow(opt));
83
+ }
84
+ lines.push("");
85
+ }
86
+
87
+ if ((node.positionals ?? []).length > 0) {
88
+ lines.push("#### Positionals", "");
89
+ lines.push("| Argument | Type | Required | Description |");
90
+ lines.push("| --- | --- | --- | --- |");
91
+ for (const p of node.positionals!) {
92
+ lines.push(formatPositionalRow(p));
93
+ }
94
+ lines.push("");
95
+ }
96
+
97
+ const children = node.commands ?? [];
98
+ if (children.length > 0) {
99
+ lines.push("#### Subcommands", "");
100
+ for (const child of children) {
101
+ lines.push(`- \`${child.key}\` — ${child.description}`);
102
+ }
103
+ lines.push("");
104
+ }
105
+
106
+ for (const child of children) {
107
+ renderCommandNode(rootKey, [...path, child.key], child, lines);
108
+ }
109
+ }
110
+
111
+ /** Generates markdown API reference from the same export as `docs schema`. */
112
+ export function generateApiGuide(program: CliProgram): string {
113
+ const schema = cliSchemaExport(program);
114
+ const lines: string[] = [
115
+ `# ${program.key} — CLI API reference`,
116
+ "",
117
+ schema.description,
118
+ "",
119
+ `Machine-readable export: \`${program.key} docs schema\``,
120
+ "",
121
+ ];
122
+
123
+ if (schema.notes) {
124
+ lines.push(`> ${schema.notes}`, "");
125
+ }
126
+
127
+ renderCommandNode(program.key, [], schema, lines);
128
+ return `${lines.join("\n").trimEnd()}\n`;
129
+ }
@@ -0,0 +1,61 @@
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, "schema", "Print the full command tree as JSON."),
41
+ docsLeaf(program, "api", "Print the command tree as markdown."),
42
+ docsLeaf(program, "skill", "Print generated Cursor SKILL.md content."),
43
+ docsLeaf(program, "all", "Print all bundled documentation combined."),
44
+ );
45
+
46
+ return {
47
+ key: "docs",
48
+ description: docs.description ?? DOCS_ROUTER_DESCRIPTION,
49
+ fallbackCommand: docsEffectiveDefaultTopic(docs),
50
+ fallbackMode: CliFallbackMode.MissingOnly,
51
+ commands: leaves,
52
+ };
53
+ }
54
+
55
+ /** Returns the docs built-in when enabled. */
56
+ export function cliBuiltinDocsGroupIfEnabled(program: CliProgram): CliRouter | null {
57
+ if (!docsEnabled(program)) {
58
+ return null;
59
+ }
60
+ return cliBuiltinDocsGroup(program);
61
+ }
@@ -0,0 +1,167 @@
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.schema = { text: "nope" };
50
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
51
+ delete root.docs!.topics.schema;
52
+ root.docs!.topics.skill = { text: "nope" };
53
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
54
+ delete root.docs!.topics.skill;
55
+ root.docs!.topics.api = { text: "nope" };
56
+ expect(() => cliValidateProgram(root)).toThrow(/reserved/);
57
+ });
58
+
59
+ test("docsEffectiveDefaultTopic uses first topic key", () => {
60
+ expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
61
+ });
62
+
63
+ test("bare docs prints first topic via cliInvoke", async () => {
64
+ const result = await cliInvoke(docsFixture(), ["docs"]);
65
+ expect(result.exitCode).toBe(0);
66
+ expect(result.stdout).toContain("Hello README");
67
+ });
68
+
69
+ test("docs readme prints bundled text", async () => {
70
+ const result = await cliInvoke(docsFixture(), ["docs", "readme"]);
71
+ expect(result.exitCode).toBe(0);
72
+ expect(result.stdout).toContain("Hello README");
73
+ });
74
+
75
+ test("docs defaultTopic override", async () => {
76
+ const root = docsFixture();
77
+ root.docs!.defaultTopic = "arch";
78
+ const result = await cliInvoke(root, ["docs"]);
79
+ expect(result.stdout).toContain("Architecture");
80
+ });
81
+
82
+ test("docs mcp when MCP enabled", async () => {
83
+ const result = await cliInvoke(docsFixture(true), ["docs", "mcp"]);
84
+ expect(result.exitCode).toBe(0);
85
+ expect(result.stdout).toContain("MCP server (myapp)");
86
+ expect(result.stdout).toContain("myapp mcp");
87
+ });
88
+
89
+ test("docs mcp absent from router when MCP disabled", async () => {
90
+ const root = docsFixture(false);
91
+ const presentation = cliPresentationRoot(root);
92
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
93
+ expect(docsNode && "commands" in docsNode).toBe(true);
94
+ if (docsNode && "commands" in docsNode) {
95
+ expect(docsNode.commands.some((c) => c.key === "mcp")).toBe(false);
96
+ }
97
+ const result = await cliInvoke(root, ["docs", "mcp"]);
98
+ expect(result.exitCode).not.toBe(0);
99
+ });
100
+
101
+ test("docs all concatenates user topics and mcp", () => {
102
+ const program = docsFixture(true);
103
+ const combined = combineAllDocs(program);
104
+ expect(combined).toContain("Hello README");
105
+ expect(combined).toContain("Architecture");
106
+ expect(combined).toContain("MCP server (myapp)");
107
+ });
108
+
109
+ test("presentation includes docs subtree", () => {
110
+ const presentation = cliPresentationRoot(docsFixture());
111
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
112
+ expect(docsNode).toBeDefined();
113
+ expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(
114
+ true,
115
+ );
116
+ });
117
+
118
+ test("docs schema prints JSON", async () => {
119
+ const result = await cliInvoke(docsFixture(), ["docs", "schema"]);
120
+ expect(result.exitCode).toBe(0);
121
+ const schema = JSON.parse(result.stdout);
122
+ expect(schema.key).toBe("myapp");
123
+ expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
124
+ });
125
+
126
+ test("docs api prints markdown reference", async () => {
127
+ const result = await cliInvoke(docsFixture(), ["docs", "api"]);
128
+ expect(result.exitCode).toBe(0);
129
+ expect(result.stdout).toContain("# myapp — CLI API reference");
130
+ expect(result.stdout).toContain("## `myapp run`");
131
+ expect(result.stdout).toContain("Run something.");
132
+ expect(result.stdout).toContain("myapp docs schema");
133
+ });
134
+
135
+ test("docs skill prints Cursor SKILL.md", async () => {
136
+ const result = await cliInvoke(docsFixture(), ["docs", "skill"]);
137
+ expect(result.exitCode).toBe(0);
138
+ expect(result.stdout).toContain("---");
139
+ expect(result.stdout).toContain("name: myapp");
140
+ expect(result.stdout).toContain("## Commands");
141
+ expect(result.stdout).not.toContain("mcp.json");
142
+ });
143
+
144
+ test("presentation includes docs schema and skill", () => {
145
+ const presentation = cliPresentationRoot(docsFixture());
146
+ const docsNode = presentation.commands.find((c) => c.key === "docs");
147
+ expect(docsNode && "commands" in docsNode).toBe(true);
148
+ if (docsNode && "commands" in docsNode) {
149
+ expect(docsNode.commands.some((c) => c.key === "schema")).toBe(true);
150
+ expect(docsNode.commands.some((c) => c.key === "api")).toBe(true);
151
+ expect(docsNode.commands.some((c) => c.key === "skill")).toBe(true);
152
+ }
153
+ });
154
+
155
+ test("completions offer docs subcommands", () => {
156
+ const bash = completionBashScript(cliPresentationRoot(docsFixture()));
157
+ expect(bash).toContain("docs) echo");
158
+ expect(bash).toContain("readme) echo");
159
+ expect(bash).toContain("schema) echo");
160
+ expect(bash).toContain("api) echo");
161
+ expect(bash).toContain("skill) echo");
162
+ });
163
+
164
+ test("generateMcpGuide includes schema URI", () => {
165
+ const guide = generateMcpGuide(docsFixture(true));
166
+ expect(guide).toContain("myapp://schema");
167
+ });
@@ -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} docs 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} docs 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 `docs 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
+ }