argsbarg 3.4.2 → 3.6.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 (73) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +24 -8
  3. package/biome.json +29 -6
  4. package/bun.lock +22 -0
  5. package/docs/cli-program.md +75 -1
  6. package/docs/install.md +1 -1
  7. package/docs/mcp.md +45 -1
  8. package/docs/templates/cursor/rules/cli-program.mdc +15 -21
  9. package/index.d.ts +95 -50
  10. package/justfile +24 -6
  11. package/package.json +4 -2
  12. package/scripts/release.ts +26 -9
  13. package/src/builtins/builtins.test.ts +9 -4
  14. package/src/builtins/completion-bash.ts +74 -50
  15. package/src/builtins/completion-fish.ts +3 -8
  16. package/src/builtins/completion-group.ts +1 -1
  17. package/src/builtins/completion-zsh.ts +80 -42
  18. package/src/builtins/dispatch.ts +20 -16
  19. package/src/builtins/export.ts +19 -10
  20. package/src/builtins/index.ts +9 -4
  21. package/src/builtins/install.ts +10 -10
  22. package/src/builtins/mcp.ts +1 -1
  23. package/src/builtins/presentation.ts +8 -8
  24. package/src/builtins/scopes.ts +1 -1
  25. package/src/builtins/version.ts +1 -1
  26. package/src/completion.ts +4 -4
  27. package/src/context.ts +91 -15
  28. package/src/docs/api-guide.test.ts +2 -2
  29. package/src/docs/api-guide.ts +19 -5
  30. package/src/docs/builtin.ts +27 -8
  31. package/src/docs/docs.test.ts +18 -11
  32. package/src/docs/mcp-guide.ts +108 -25
  33. package/src/docs/resolve.ts +10 -3
  34. package/src/docs/save.ts +11 -3
  35. package/src/formats.test.ts +35 -0
  36. package/src/formats.ts +135 -0
  37. package/src/headless.test.ts +8 -16
  38. package/src/help.ts +73 -43
  39. package/src/hidden-mcpb.test.ts +7 -6
  40. package/src/hidden.ts +2 -2
  41. package/src/index.test.ts +120 -96
  42. package/src/index.ts +36 -24
  43. package/src/install/binary.ts +12 -5
  44. package/src/install/completions.ts +7 -3
  45. package/src/install/detect-installed.ts +29 -4
  46. package/src/install/gh-release-update.ts +31 -23
  47. package/src/install/index.ts +69 -19
  48. package/src/install/install.test.ts +31 -8
  49. package/src/install/mcp-codex.test.ts +57 -0
  50. package/src/install/mcp-codex.ts +125 -0
  51. package/src/install/mcp-config.ts +12 -5
  52. package/src/install/mcp-opencode.test.ts +98 -0
  53. package/src/install/mcp-opencode.ts +149 -0
  54. package/src/install/paths.ts +29 -3
  55. package/src/install/plan.ts +73 -6
  56. package/src/install/shell.ts +1 -4
  57. package/src/install/status.ts +12 -6
  58. package/src/install/uninstall.ts +38 -4
  59. package/src/install/update.test.ts +2 -2
  60. package/src/install/update.ts +3 -1
  61. package/src/invoke.ts +12 -9
  62. package/src/mcp/bundle.ts +36 -8
  63. package/src/mcp/env.ts +7 -13
  64. package/src/mcp/server.ts +12 -6
  65. package/src/mcp/tools.ts +83 -18
  66. package/src/mcp.ts +3 -3
  67. package/src/parse.ts +129 -27
  68. package/src/runtime.ts +22 -12
  69. package/src/schema.ts +11 -5
  70. package/src/skill/generate.ts +4 -4
  71. package/src/skill/install.ts +6 -2
  72. package/src/types.ts +24 -0
  73. package/src/validate.ts +75 -16
package/src/context.ts CHANGED
@@ -7,10 +7,15 @@ It keeps handlers small with a typed read API for flags, strings, numbers, and c
7
7
  parsed values.
8
8
  */
9
9
 
10
- import type { CliInvocation, CliNode, CliProgram } from "./types.ts";
11
- import { isCliLeaf, isCliRouter } from "./types.ts";
10
+ import { parseCommaList, parseDate, parseDateTime, parseDurationMs } from "./formats.ts";
11
+ import { collectOptionDefs } from "./parse.ts";
12
+ import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
13
+ import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
12
14
  import { strictParseDouble } from "./utils.ts";
13
15
 
16
+ /** Coerced leaf inputs keyed by option and positional names. */
17
+ export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
18
+
14
19
  /**
15
20
  * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
16
21
  */
@@ -70,38 +75,109 @@ export class CliContext {
70
75
  }
71
76
  }
72
77
 
78
+ /** Duration option in milliseconds (post-parse validated). */
79
+ durationOpt(name: string): number | undefined {
80
+ const s = this.opts[name];
81
+ if (s === undefined) return undefined;
82
+ return parseDurationMs(s);
83
+ }
84
+
85
+ /** Comma-list option as a string array (post-parse validated). */
86
+ commaListOpt(name: string): string[] | undefined {
87
+ const s = this.opts[name];
88
+ if (s === undefined) return undefined;
89
+ return parseCommaList(s);
90
+ }
91
+
92
+ /** Date option as canonical YYYY-MM-DD (post-parse validated). */
93
+ dateOpt(name: string): string | undefined {
94
+ const s = this.opts[name];
95
+ if (s === undefined) return undefined;
96
+ return parseDate(s);
97
+ }
98
+
99
+ /** Date-time option as normalized ISO 8601 UTC (post-parse validated). */
100
+ dateTimeOpt(name: string): string | undefined {
101
+ const s = this.opts[name];
102
+ if (s === undefined) return undefined;
103
+ return parseDateTime(s);
104
+ }
105
+
73
106
  /** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
74
107
  positional(name: string): string | string[] | undefined {
75
108
  return this._positionalMap()[name];
76
109
  }
77
110
 
78
- private _posMap: Record<string, string | string[]> | undefined;
111
+ /** Reads coerced option and positional values for the current leaf from schema metadata. */
112
+ readLeafInputs(): CliLeafInputs {
113
+ const leaf = this._leafNode();
114
+ if (!leaf) return {};
79
115
 
80
- private _positionalMap(): Record<string, string | string[]> {
81
- if (this._posMap) return this._posMap;
116
+ const out: CliLeafInputs = {};
117
+ for (const opt of collectOptionDefs(this.program, this.commandPath)) {
118
+ out[opt.name] = this._readOptionValue(opt);
119
+ }
120
+ for (const p of leaf.positionals ?? []) {
121
+ const val = this.positional(p.name);
122
+ if (val === undefined) {
123
+ out[p.name] = undefined;
124
+ } else if (Array.isArray(val)) {
125
+ out[p.name] = val;
126
+ } else {
127
+ out[p.name] = val;
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+
133
+ private _readOptionValue(opt: CliOption): boolean | number | string | string[] | undefined {
134
+ if (opt.kind === CliOptionKind.Presence) {
135
+ return this.hasFlag(opt.name);
136
+ }
137
+ if (opt.kind === CliOptionKind.Number) {
138
+ const n = this.numberOpt(opt.name);
139
+ return n === null ? undefined : n;
140
+ }
141
+ if (opt.format === CliValueFormat.Duration) {
142
+ return this.durationOpt(opt.name);
143
+ }
144
+ if (opt.format === CliValueFormat.CommaList) {
145
+ return this.commaListOpt(opt.name);
146
+ }
147
+ if (opt.format === CliValueFormat.Date) {
148
+ return this.dateOpt(opt.name);
149
+ }
150
+ if (opt.format === CliValueFormat.DateTime) {
151
+ return this.dateTimeOpt(opt.name);
152
+ }
153
+ return this.stringOpt(opt.name);
154
+ }
82
155
 
156
+ private _leafNode(): CliLeaf | undefined {
83
157
  let node: CliNode = this.program;
84
158
  for (const seg of this.commandPath) {
85
- if (!isCliRouter(node)) {
86
- this._posMap = {};
87
- return {};
88
- }
159
+ if (!isCliRouter(node)) return undefined;
89
160
  const child = node.commands.find((c) => c.key === seg);
90
- if (!child) {
91
- this._posMap = {};
92
- return {};
93
- }
161
+ if (!child) return undefined;
94
162
  node = child;
95
163
  }
164
+ return isCliLeaf(node) ? node : undefined;
165
+ }
166
+
167
+ private _posMap: Record<string, string | string[]> | undefined;
168
+
169
+ private _positionalMap(): Record<string, string | string[]> {
170
+ if (this._posMap) return this._posMap;
96
171
 
97
- if (!isCliLeaf(node)) {
172
+ const leaf = this._leafNode();
173
+ if (!leaf) {
98
174
  this._posMap = {};
99
175
  return {};
100
176
  }
101
177
 
102
178
  const map: Record<string, string | string[]> = {};
103
179
  let argIdx = 0;
104
- for (const p of node.positionals ?? []) {
180
+ for (const p of leaf.positionals ?? []) {
105
181
  const { argMax = 1 } = p;
106
182
  if (argMax === 0) {
107
183
  map[p.name] = this.args.slice(argIdx);
@@ -1,8 +1,8 @@
1
1
  import { expect, test } from "bun:test";
2
+ import { cliSchemaExport } from "../schema.ts";
2
3
  import type { CliProgram } from "../types.ts";
3
4
  import { CliOptionKind } from "../types.ts";
4
5
  import { generateApiGuide, generateApiGuideBody } from "./api-guide.ts";
5
- import { cliSchemaExport } from "../schema.ts";
6
6
 
7
7
  const nestedFixture: CliProgram = {
8
8
  key: "nested.ts",
@@ -125,7 +125,7 @@ test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
125
125
  ],
126
126
  };
127
127
  const schema = cliSchemaExport(fixture);
128
- expect(schema.commands![0]!.outputSchema).toEqual({
128
+ expect(schema.commands?.[0]?.outputSchema).toEqual({
129
129
  type: "object",
130
130
  properties: { id: { type: "string" } },
131
131
  required: ["id"],
@@ -23,6 +23,20 @@ function optionType(opt: CliOption): string {
23
23
  return opt.kind;
24
24
  }
25
25
 
26
+ function optionFormatDefault(opt: CliOption): string {
27
+ const parts: string[] = [];
28
+ if (opt.format !== undefined) {
29
+ parts.push(opt.format);
30
+ }
31
+ if (opt.default !== undefined) {
32
+ parts.push(`default \`${opt.default}\``);
33
+ }
34
+ if (opt.pattern !== undefined) {
35
+ parts.push(`pattern \`${opt.pattern}\``);
36
+ }
37
+ return parts.length > 0 ? parts.join("; ") : "—";
38
+ }
39
+
26
40
  /** Markdown table cell for one option flag. */
27
41
  function optionLabel(opt: CliOption): string {
28
42
  const long = `\`--${opt.name}\``;
@@ -33,7 +47,7 @@ function optionLabel(opt: CliOption): string {
33
47
  /** One options table row. */
34
48
  function formatOptionRow(opt: CliOption): string {
35
49
  const req = opt.required ? "required" : "optional";
36
- return `| ${optionLabel(opt)} | ${optionType(opt)} | ${req} | ${opt.description} |`;
50
+ return `| ${optionLabel(opt)} | ${optionType(opt)} | ${req} | ${optionFormatDefault(opt)} | ${opt.description} |`;
37
51
  }
38
52
 
39
53
  /** One positionals table row. */
@@ -99,9 +113,9 @@ function renderCommandNode(
99
113
 
100
114
  if ((node.options ?? []).length > 0) {
101
115
  lines.push("#### Options", "");
102
- lines.push("| Option | Type | Required | Description |");
103
- lines.push("| --- | --- | --- | --- |");
104
- for (const opt of node.options!) {
116
+ lines.push("| Option | Type | Required | Format / default | Description |");
117
+ lines.push("| --- | --- | --- | --- | --- |");
118
+ for (const opt of node.options ?? []) {
105
119
  lines.push(formatOptionRow(opt));
106
120
  }
107
121
  lines.push("");
@@ -111,7 +125,7 @@ function renderCommandNode(
111
125
  lines.push("#### Positionals", "");
112
126
  lines.push("| Argument | Type | Required | Description |");
113
127
  lines.push("| --- | --- | --- | --- |");
114
- for (const p of node.positionals!) {
128
+ for (const p of node.positionals ?? []) {
115
129
  lines.push(formatPositionalRow(p));
116
130
  }
117
131
  lines.push("");
@@ -1,4 +1,11 @@
1
- import { CliFallbackMode, CliOptionKind, type CliLeaf, type CliOption, type CliProgram, type CliRouter } from "../types.ts";
1
+ import {
2
+ CliFallbackMode,
3
+ type CliLeaf,
4
+ type CliOption,
5
+ CliOptionKind,
6
+ type CliProgram,
7
+ type CliRouter,
8
+ } from "../types.ts";
2
9
  import {
3
10
  DOCS_ROUTER_DESCRIPTION,
4
11
  docsEffectiveDefaultTopic,
@@ -16,7 +23,11 @@ const DOCS_SAVE_OPTION: CliOption = {
16
23
  kind: CliOptionKind.Presence,
17
24
  };
18
25
 
19
- function runDocsTopic(program: CliProgram, topic: string, ctx: { hasFlag(name: string): boolean }): void {
26
+ function runDocsTopic(
27
+ program: CliProgram,
28
+ topic: string,
29
+ ctx: { hasFlag(name: string): boolean },
30
+ ): void {
20
31
  if (ctx.hasFlag("save")) {
21
32
  process.stdout.write(`${saveDocsTopic(program, topic)}\n`);
22
33
  return;
@@ -43,24 +54,32 @@ function docsRouterNotes(): string {
43
54
 
44
55
  /** Built-in `docs` router with bundled topic subcommands. */
45
56
  export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
46
- const docs = program.docs!;
57
+ const docs = program.docs;
58
+ if (!docs) {
59
+ throw new Error("docs not enabled");
60
+ }
47
61
  const leaves: CliLeaf[] = [];
48
62
 
49
63
  for (const key of docsUserTopicKeys(docs)) {
50
- const topic = docs.topics[key]!;
64
+ const topic = docs.topics[key];
65
+ if (!topic) {
66
+ throw new Error(`docs topic missing: ${key}`);
67
+ }
51
68
  leaves.push(docsLeaf(program, key, docsTopicDescription(key, topic.description)));
52
69
  }
53
70
 
54
71
  if (docsIncludesMcpTopic(program)) {
55
- leaves.push(
56
- docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."),
57
- );
72
+ leaves.push(docsLeaf(program, "mcp", "Print MCP server setup and tool guidance."));
58
73
  }
59
74
 
60
75
  leaves.push(
61
76
  docsLeaf(program, "schema", "Print the full command tree as JSON."),
62
77
  docsLeaf(program, "api", "Print the full command reference as markdown."),
63
- docsLeaf(program, "skill", "Print a reference agent SKILL, use `install --skill` for optimized."),
78
+ docsLeaf(
79
+ program,
80
+ "skill",
81
+ "Print a reference agent SKILL, use `install --skill` for optimized.",
82
+ ),
64
83
  );
65
84
 
66
85
  return {
@@ -1,14 +1,14 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test";
1
+ import { afterEach, beforeEach, expect, test } from "bun:test";
2
2
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
3
  import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
5
  import { cliPresentationRoot } from "../builtins/presentation.ts";
6
6
  import { completionBashScript } from "../completion.ts";
7
7
  import { cliInvoke } from "../index.ts";
8
8
  import type { CliProgram } from "../types.ts";
9
9
  import { cliValidateProgram } from "../validate.ts";
10
- import { docsEffectiveDefaultTopic } from "./resolve.ts";
11
10
  import { generateMcpGuide } from "./mcp-guide.ts";
11
+ import { docsEffectiveDefaultTopic } from "./resolve.ts";
12
12
  import { saveDocsTopic } from "./save.ts";
13
13
 
14
14
  let workDir: string;
@@ -64,13 +64,15 @@ test("docs reserved when enabled", () => {
64
64
 
65
65
  test("docs rejects reserved topic keys", () => {
66
66
  const root = docsFixture();
67
- root.docs!.topics.schema = { text: "nope" };
67
+ const docs = root.docs;
68
+ if (!docs) throw new Error("expected docs fixture");
69
+ docs.topics.schema = { text: "nope" };
68
70
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
69
- delete root.docs!.topics.schema;
70
- root.docs!.topics.skill = { text: "nope" };
71
+ delete docs.topics.schema;
72
+ docs.topics.skill = { text: "nope" };
71
73
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
72
- delete root.docs!.topics.skill;
73
- root.docs!.topics.api = { text: "nope" };
74
+ delete docs.topics.skill;
75
+ docs.topics.api = { text: "nope" };
74
76
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
75
77
  });
76
78
 
@@ -127,9 +129,9 @@ test("presentation includes docs subtree", () => {
127
129
  const presentation = cliPresentationRoot(docsFixture());
128
130
  const docsNode = presentation.commands.find((c) => c.key === "docs");
129
131
  expect(docsNode).toBeDefined();
130
- expect(docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme")).toBe(
131
- true,
132
- );
132
+ expect(
133
+ docsNode && "commands" in docsNode && docsNode.commands.some((c) => c.key === "readme"),
134
+ ).toBe(true);
133
135
  });
134
136
 
135
137
  test("docs schema prints JSON", async () => {
@@ -199,6 +201,11 @@ test("generateMcpGuide includes schema URI and install targets", () => {
199
201
  expect(guide).toContain("myapp://schema");
200
202
  expect(guide).toContain("~/.cursor/mcp.json");
201
203
  expect(guide).toContain("claude_desktop_config.json");
204
+ expect(guide).toContain("## Installation");
205
+ expect(guide).toContain("## Running directly");
206
+ expect(guide).toContain("install --bin");
207
+ expect(guide).toContain("OpenAI Codex");
208
+ expect(guide).toContain("ChatGPT");
202
209
  });
203
210
 
204
211
  test("docs --save writes topic file", async () => {
@@ -1,11 +1,75 @@
1
- import { collectOptionDefs } from "../parse.ts";
1
+ import { resolveCapabilities } from "../capabilities.ts";
2
+ import { expectedOpenCodeMcpEntry, OPENCODE_CONFIG_SCHEMA } from "../install/mcp-opencode.ts";
2
3
  import {
3
4
  collectMcpTools,
5
+ type McpToolDef,
4
6
  mcpServerId,
5
7
  resolveMcpSchemaUri,
6
- type McpToolDef,
7
8
  } from "../mcp/tools.ts";
8
- import { type CliProgram, CliOptionKind } from "../types.ts";
9
+ import { collectOptionDefs } from "../parse.ts";
10
+ import { CliOptionKind, type CliProgram } from "../types.ts";
11
+
12
+ /** Extra host notes for generated `docs mcp` (manual fallbacks and ChatGPT Connectors). */
13
+ function appendManualHostSetup(lines: string[], root: CliProgram, serverId: string): void {
14
+ const openCodeEntry = expectedOpenCodeMcpEntry(root);
15
+
16
+ lines.push(
17
+ "| OpenCode | `~/.config/opencode/*` (when `~/.config/opencode` exists) |",
18
+ "| OpenAI Codex | `~/.codex/config.toml` via `codex mcp add` (when `codex` is on PATH) |",
19
+ "| ChatGPT desktop | `chatgpt_mcp_config.json` (when ChatGPT app data exists) |",
20
+ "",
21
+ "Claude Desktop paths by platform:",
22
+ "",
23
+ "- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`",
24
+ "- **Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`",
25
+ "- **Linux:** `~/.config/Claude/claude_desktop_config.json`",
26
+ "",
27
+ "ChatGPT desktop JSON (when auto-installed):",
28
+ "",
29
+ "- **macOS:** `~/Library/Application Support/ChatGPT/chatgpt_mcp_config.json`",
30
+ "- **Windows:** `%APPDATA%\\OpenAI\\ChatGPT\\chatgpt_mcp_config.json`",
31
+ "",
32
+ "Restart Claude Desktop and ChatGPT desktop after changing their config files.",
33
+ "",
34
+ "### Manual fallbacks",
35
+ "",
36
+ "**OpenCode** (no `~/.config/opencode` yet):",
37
+ "",
38
+ "```json",
39
+ JSON.stringify(
40
+ {
41
+ $schema: OPENCODE_CONFIG_SCHEMA,
42
+ mcp: { [serverId]: openCodeEntry },
43
+ },
44
+ null,
45
+ 2,
46
+ ),
47
+ "```",
48
+ "",
49
+ "**Codex** (`codex` not on PATH):",
50
+ "",
51
+ "```toml",
52
+ `[mcp_servers.${serverId}]`,
53
+ `command = "${root.key}"`,
54
+ 'args = ["mcp"]',
55
+ "```",
56
+ "",
57
+ `Or after installing Codex CLI: \`codex mcp add ${serverId} -- ${root.key} mcp\`.`,
58
+ "",
59
+ "### ChatGPT web (Connectors)",
60
+ "",
61
+ `OpenAI's documented path for **ChatGPT web/desktop** is **Settings → Connectors → Developer mode** with a **remote HTTPS MCP URL** — not local stdio. ChatGPT does not spawn \`${root.key} mcp\` directly.`,
62
+ "",
63
+ "For local stdio, bridge and tunnel, then register the HTTPS URL in Connectors:",
64
+ "",
65
+ `1. Expose \`${root.key} mcp\` over HTTP (e.g. \`mcp-remote\`).`,
66
+ "2. Tunnel if needed (ngrok, Cloudflare Tunnel).",
67
+ "3. Add the public URL as a custom connector.",
68
+ "",
69
+ "Desktop `chatgpt_mcp_config.json` is merged when the ChatGPT app is installed; support varies by build. Use Connectors when local JSON is absent or tools do not appear.",
70
+ "",
71
+ );
72
+ }
9
73
 
10
74
  /** Formats one exposed MCP tool for the auto-generated MCP guide. */
11
75
  function formatToolLine(root: CliProgram, tool: McpToolDef): string {
@@ -24,23 +88,36 @@ export function generateMcpGuide(root: CliProgram): string {
24
88
  const tools = collectMcpTools(root);
25
89
  const schemaUri = resolveMcpSchemaUri(root);
26
90
  const serverId = mcpServerId(root);
27
- const mcp = root.mcpServer!;
91
+ const mcp = root.mcpServer;
92
+ if (!mcp) {
93
+ throw new Error("MCP server not enabled");
94
+ }
95
+ const caps = resolveCapabilities(root);
28
96
 
29
97
  const lines: string[] = [
30
98
  `# MCP server (${root.key})`,
31
99
  "",
32
100
  `${root.key} exposes an MCP server with features similar to the CLI.`,
33
101
  "",
34
- "## Quick start",
35
- "",
36
- "```bash",
37
- `${root.key} mcp`,
38
- "```",
39
- "",
40
- "## Client setup",
102
+ "## Installation",
41
103
  "",
42
104
  "### `install --mcp`",
43
105
  "",
106
+ ];
107
+
108
+ if (caps.install) {
109
+ lines.push(
110
+ `Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`${root.key} install --bin --yes\` or \`install --all --yes\`). Host configs reference the binary by name.`,
111
+ "",
112
+ );
113
+ } else {
114
+ lines.push(
115
+ `The CLI binary \`${root.key}\` must already be on your PATH. Host configs reference it by name.`,
116
+ "",
117
+ );
118
+ }
119
+
120
+ lines.push(
44
121
  "```bash",
45
122
  `${root.key} install --mcp --yes`,
46
123
  "```",
@@ -52,18 +129,14 @@ export function generateMcpGuide(root: CliProgram): string {
52
129
  "| Cursor | `~/.cursor/mcp.json` (when `~/.cursor` exists) |",
53
130
  "| Claude Code | `~/.claude.json` |",
54
131
  "| Claude Desktop | `claude_desktop_config.json` (when Claude Desktop app data exists) |",
132
+ );
133
+
134
+ appendManualHostSetup(lines, root, serverId);
135
+
136
+ lines.push(
137
+ "### Manual `mcpServers` entry",
55
138
  "",
56
- "Claude Desktop paths by platform:",
57
- "",
58
- "- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`",
59
- "- **Windows:** `%APPDATA%\\Claude\\claude_desktop_config.json`",
60
- "- **Linux:** `~/.config/Claude/claude_desktop_config.json`",
61
- "",
62
- "Restart Claude Desktop after changing its config.",
63
- "",
64
- "### Manual entry",
65
- "",
66
- "Add under `mcpServers` in the host config:",
139
+ "For Cursor, Claude, and ChatGPT desktop JSON configs, add under `mcpServers`:",
67
140
  "",
68
141
  "```json",
69
142
  JSON.stringify(
@@ -80,7 +153,15 @@ export function generateMcpGuide(root: CliProgram): string {
80
153
  ),
81
154
  "```",
82
155
  "",
83
- ];
156
+ "## Running directly",
157
+ "",
158
+ "Start the stdio MCP server without editing host config:",
159
+ "",
160
+ "```bash",
161
+ `${root.key} mcp`,
162
+ "```",
163
+ "",
164
+ );
84
165
 
85
166
  if (mcp.shellEnv || mcp.envFile) {
86
167
  lines.push("## Environment", "");
@@ -91,7 +172,7 @@ export function generateMcpGuide(root: CliProgram): string {
91
172
  }
92
173
  if (mcp.envFile) {
93
174
  lines.push(
94
- "- **`envFile`** — loads `" + mcp.envFile + "` after shell env (overrides for its keys).",
175
+ `- **\`envFile\`** — loads \`${mcp.envFile}\` after shell env (overrides for its keys).`,
95
176
  );
96
177
  }
97
178
  lines.push("");
@@ -125,7 +206,9 @@ export function generateMcpGuide(root: CliProgram): string {
125
206
  "Arguments are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
126
207
  `See \`${root.key} docs schema\` or the schema resource for per-tool shapes.`,
127
208
  "",
128
- "Varargs positionals accept a JSON array or a comma-separated string.",
209
+ "Varargs positionals accept a JSON array of strings (not a comma-separated string).",
210
+ "Options with `format: comma-list` accept a comma-separated string or JSON array.",
211
+ "Options with a schema `default` are applied when omitted.",
129
212
  "",
130
213
  "## Protocol",
131
214
  "",
@@ -1,6 +1,6 @@
1
- import type { CliDocsConfig, CliProgram } from "../types.ts";
2
1
  import { cliSchemaJson } from "../schema.ts";
3
2
  import { generateSkillBundle } from "../skill/generate.ts";
3
+ import type { CliDocsConfig, CliProgram } from "../types.ts";
4
4
  import { generateApiGuide } from "./api-guide.ts";
5
5
  import { generateMcpGuide } from "./mcp-guide.ts";
6
6
 
@@ -31,7 +31,11 @@ export function docsEffectiveDefaultTopic(docs: CliDocsConfig): string {
31
31
  if (keys.length === 0) {
32
32
  throw new Error("docs.topics must be non-empty");
33
33
  }
34
- return keys[0]!;
34
+ const first = keys[0];
35
+ if (first === undefined) {
36
+ throw new Error("docs.topics must be non-empty");
37
+ }
38
+ return first;
35
39
  }
36
40
 
37
41
  /** Whether MCP auto-guide topic is included. */
@@ -53,7 +57,10 @@ export function docsTopicDescription(key: string, custom?: string): string {
53
57
 
54
58
  /** Markdown body for one docs topic key. */
55
59
  export function docsTopicText(program: CliProgram, topic: string): string {
56
- const docs = program.docs!;
60
+ const docs = program.docs;
61
+ if (!docs) {
62
+ throw new Error("docs not enabled");
63
+ }
57
64
  if (topic === "mcp") {
58
65
  if (!docsIncludesMcpTopic(program)) {
59
66
  throw new Error("Unknown docs topic 'mcp'.");
package/src/docs/save.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { generatedFileHtmlComment, insertGeneratedHint } from "../skill/hint.ts";
3
4
  import type { CliProgram } from "../types.ts";
4
5
  import { docsTopicContent } from "./resolve.ts";
5
- import { generatedFileHtmlComment, insertGeneratedHint } from "../skill/hint.ts";
6
6
 
7
7
  /** Relative output directory for `docs --save`. */
8
8
  export const DOCS_SAVE_DIR = "docs";
@@ -21,12 +21,20 @@ export function docsSaveGeneratedHint(program: CliProgram, topic: string): strin
21
21
  }
22
22
 
23
23
  /** Inserts save hint without breaking YAML frontmatter (`docs skill`). */
24
- export function applySaveGeneratedHint(program: CliProgram, topic: string, content: string): string {
24
+ export function applySaveGeneratedHint(
25
+ program: CliProgram,
26
+ topic: string,
27
+ content: string,
28
+ ): string {
25
29
  if (!docsTopicIsGeneratedByArgsbarg(topic)) {
26
30
  return content;
27
31
  }
28
32
  const hint = docsSaveGeneratedHint(program, topic);
29
- return insertGeneratedHint(content, hint, topic === "skill" ? { afterFrontmatter: true } : undefined);
33
+ return insertGeneratedHint(
34
+ content,
35
+ hint,
36
+ topic === "skill" ? { afterFrontmatter: true } : undefined,
37
+ );
30
38
  }
31
39
 
32
40
  /** File body for `--save` (hint on argsbarg-generated markdown only). */
@@ -0,0 +1,35 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ parseCommaList,
4
+ parseDate,
5
+ parseDateTime,
6
+ parseDurationMs,
7
+ validateFormatValue,
8
+ } from "./formats.ts";
9
+ import { CliValueFormat } from "./types.ts";
10
+
11
+ test("parseDurationMs parses minutes and hours", () => {
12
+ expect(parseDurationMs("30s")).toBe(30_000);
13
+ expect(parseDurationMs("5m")).toBe(5 * 60 * 1000);
14
+ expect(parseDurationMs("2h")).toBe(2 * 60 * 60 * 1000);
15
+ expect(parseDurationMs("1d")).toBe(24 * 60 * 60 * 1000);
16
+ });
17
+
18
+ test("parseCommaList splits and trims", () => {
19
+ expect(parseCommaList("a,b")).toEqual(["a", "b"]);
20
+ expect(parseCommaList(" a , b , ")).toEqual(["a", "b"]);
21
+ });
22
+
23
+ test("parseDate validates calendar dates", () => {
24
+ expect(parseDate("2026-06-22")).toBe("2026-06-22");
25
+ expect(() => parseDate("2026-02-30")).toThrow();
26
+ });
27
+
28
+ test("parseDateTime normalizes to UTC ISO", () => {
29
+ expect(parseDateTime("2026-06-22T15:00:00Z")).toBe("2026-06-22T15:00:00.000Z");
30
+ expect(() => parseDateTime("2026-06-22")).toThrow();
31
+ });
32
+
33
+ test("validateFormatValue rejects invalid duration", () => {
34
+ expect(() => validateFormatValue("nope", CliValueFormat.Duration)).toThrow();
35
+ });