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.
- package/CHANGELOG.md +31 -1
- package/README.md +24 -8
- package/biome.json +29 -6
- package/bun.lock +22 -0
- package/docs/cli-program.md +75 -1
- package/docs/install.md +1 -1
- package/docs/mcp.md +45 -1
- package/docs/templates/cursor/rules/cli-program.mdc +15 -21
- package/index.d.ts +95 -50
- package/justfile +24 -6
- package/package.json +4 -2
- package/scripts/release.ts +26 -9
- package/src/builtins/builtins.test.ts +9 -4
- package/src/builtins/completion-bash.ts +74 -50
- package/src/builtins/completion-fish.ts +3 -8
- package/src/builtins/completion-group.ts +1 -1
- package/src/builtins/completion-zsh.ts +80 -42
- package/src/builtins/dispatch.ts +20 -16
- package/src/builtins/export.ts +19 -10
- package/src/builtins/index.ts +9 -4
- package/src/builtins/install.ts +10 -10
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +8 -8
- package/src/builtins/scopes.ts +1 -1
- package/src/builtins/version.ts +1 -1
- package/src/completion.ts +4 -4
- package/src/context.ts +91 -15
- package/src/docs/api-guide.test.ts +2 -2
- package/src/docs/api-guide.ts +19 -5
- package/src/docs/builtin.ts +27 -8
- package/src/docs/docs.test.ts +18 -11
- package/src/docs/mcp-guide.ts +108 -25
- package/src/docs/resolve.ts +10 -3
- package/src/docs/save.ts +11 -3
- package/src/formats.test.ts +35 -0
- package/src/formats.ts +135 -0
- package/src/headless.test.ts +8 -16
- package/src/help.ts +73 -43
- package/src/hidden-mcpb.test.ts +7 -6
- package/src/hidden.ts +2 -2
- package/src/index.test.ts +120 -96
- package/src/index.ts +36 -24
- package/src/install/binary.ts +12 -5
- package/src/install/completions.ts +7 -3
- package/src/install/detect-installed.ts +29 -4
- package/src/install/gh-release-update.ts +31 -23
- package/src/install/index.ts +69 -19
- package/src/install/install.test.ts +31 -8
- package/src/install/mcp-codex.test.ts +57 -0
- package/src/install/mcp-codex.ts +125 -0
- package/src/install/mcp-config.ts +12 -5
- package/src/install/mcp-opencode.test.ts +98 -0
- package/src/install/mcp-opencode.ts +149 -0
- package/src/install/paths.ts +29 -3
- package/src/install/plan.ts +73 -6
- package/src/install/shell.ts +1 -4
- package/src/install/status.ts +12 -6
- package/src/install/uninstall.ts +38 -4
- package/src/install/update.test.ts +2 -2
- package/src/install/update.ts +3 -1
- package/src/invoke.ts +12 -9
- package/src/mcp/bundle.ts +36 -8
- package/src/mcp/env.ts +7 -13
- package/src/mcp/server.ts +12 -6
- package/src/mcp/tools.ts +83 -18
- package/src/mcp.ts +3 -3
- package/src/parse.ts +129 -27
- package/src/runtime.ts +22 -12
- package/src/schema.ts +11 -5
- package/src/skill/generate.ts +4 -4
- package/src/skill/install.ts +6 -2
- package/src/types.ts +24 -0
- 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
|
|
11
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
128
|
+
expect(schema.commands?.[0]?.outputSchema).toEqual({
|
|
129
129
|
type: "object",
|
|
130
130
|
properties: { id: { type: "string" } },
|
|
131
131
|
required: ["id"],
|
package/src/docs/api-guide.ts
CHANGED
|
@@ -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("");
|
package/src/docs/builtin.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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(
|
|
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 {
|
package/src/docs/docs.test.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
71
|
+
delete docs.topics.schema;
|
|
72
|
+
docs.topics.skill = { text: "nope" };
|
|
71
73
|
expect(() => cliValidateProgram(root)).toThrow(/reserved/);
|
|
72
|
-
delete
|
|
73
|
-
|
|
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(
|
|
131
|
-
|
|
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 () => {
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -1,11 +1,75 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
"##
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
"",
|
package/src/docs/resolve.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
});
|