argsbarg 4.0.2 → 4.0.4
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 +17 -1
- package/README.md +1 -1
- package/docs/ai-skills.md +2 -0
- package/docs/bundled-docs.md +1 -0
- package/docs/cli-program.md +1 -1
- package/docs/config-schema.md +1 -1
- package/docs/mcp.md +17 -2
- package/examples/consumer-app/README.md +1 -0
- package/examples/consumer-app/src/program.ts +2 -10
- package/examples/mcp-test.ts +6 -0
- package/index.d.ts +4 -0
- package/package.json +1 -1
- package/src/config/context.test.ts +11 -1
- package/src/config/context.ts +11 -1
- package/src/config/file.ts +5 -0
- package/src/docs/mcp-guide.ts +14 -3
- package/src/docs/mcp-resources.test.ts +63 -0
- package/src/docs/mcp-resources.ts +68 -0
- package/src/mcp/claude.test.ts +9 -3
- package/src/mcp/claude.ts +7 -9
- package/src/mcp/tools.ts +4 -1
- package/src/mcp.integration.test.ts +17 -0
- package/src/parse.test.ts +45 -3
- package/src/skill/generate.ts +71 -1
- package/src/skill/hint.ts +5 -0
- package/src/validate.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [4.0.4] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **MCP docs topic resources** — when `docs.enabled` and `mcpServer.enabled`, each user `docs.topics` key is auto-exposed as `<mcpId>://docs/<topicKey>` (`text/markdown`, same body as `myapp docs <topic>`). Built-in `docs schema` / `api` / `skill` / `mcp` are not auto-exposed.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Claude Code plugin skill** — `mcp bundle` plugin zip includes an MCP routing `SKILL.md` only (no shell catalog, no `reference.md`). `install --skill` unchanged.
|
|
19
|
+
- **Validation** — `mcpServer.resources` URIs that collide with auto docs topic resources are rejected at schema validation time.
|
|
20
|
+
|
|
21
|
+
## [4.0.3] - 2026-06-24
|
|
22
|
+
|
|
23
|
+
|
|
10
24
|
## [4.0.2] - 2026-06-24
|
|
11
25
|
|
|
12
26
|
|
|
@@ -455,7 +469,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
455
469
|
- 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`).
|
|
456
470
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
457
471
|
|
|
458
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.
|
|
472
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.4...HEAD
|
|
473
|
+
[4.0.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.4
|
|
474
|
+
[4.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.3
|
|
459
475
|
[4.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.2
|
|
460
476
|
[4.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.1
|
|
461
477
|
[4.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.0
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/argsbarg)
|
|
7
7
|
[](https://bun.sh)
|
|
8
8
|
|
|
9
|
-
Build beautiful, well-behaved CLI apps with Bun — **no third-party runtime dependencies**.
|
|
9
|
+
Build beautiful, well-behaved CLI+MCP apps with Bun — **no third-party runtime dependencies**.
|
|
10
10
|
|
|
11
11
|
Why another CLI parser?
|
|
12
12
|
|
package/docs/ai-skills.md
CHANGED
|
@@ -48,6 +48,8 @@ Skills describe **shell invocation only** — no MCP setup, `mcp.json`, or `tool
|
|
|
48
48
|
|
|
49
49
|
`SKILL.md` is the routing index; `reference.md` matches `docs api`. Prefer `install --skill` over `docs skill` for agents. See [cli-program.md](cli-program.md).
|
|
50
50
|
|
|
51
|
+
**Claude Code plugin** (`mcp bundle` → `dist/claude-plugin/<name>.zip`) ships a separate MCP pointer skill (`SKILL.md` only) that routes agents to the bundled MCP server. `install --skill` remains shell-only.
|
|
52
|
+
|
|
51
53
|
See also:
|
|
52
54
|
|
|
53
55
|
- [Bundled docs](bundled-docs.md) — `docs` config and compile-time imports
|
package/docs/bundled-docs.md
CHANGED
|
@@ -104,6 +104,7 @@ All `docs` subcommands are hidden from MCP `tools/list` (`mcpTool: { enabled: fa
|
|
|
104
104
|
| `docs api` | Print command tree markdown to stdout |
|
|
105
105
|
| `docs schema` | Print command tree JSON to stdout |
|
|
106
106
|
| `docs` | Bundled markdown topics on stdout |
|
|
107
|
+
| MCP docs topic resources | User `docs.topics` on the MCP wire (`<mcpId>://docs/<topic>`) when docs + MCP enabled |
|
|
107
108
|
| `mcp` | Callable tools + schema resource |
|
|
108
109
|
|
|
109
110
|
Do not declare a top-level command named **`docs`** when `docs.enabled` is `true` — it is reserved.
|
package/docs/cli-program.md
CHANGED
|
@@ -431,7 +431,7 @@ await cli.run();
|
|
|
431
431
|
|
|
432
432
|
See [config-schema.md](config-schema.md) for codegen, [install.md](install.md), and [mcp.md](mcp.md).
|
|
433
433
|
|
|
434
|
-
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read`, `path` — prefer over `process.env` in handlers; env export remains for subprocess inheritance. `path` is the resolved absolute config file path (`program.appConfig.path` when set, otherwise the OS default from `program.key`).
|
|
434
|
+
**Handler access (`ctx.appConfig`):** `get`, `require`, `set`, `read`, `path`, `dir` — prefer over `process.env` in handlers; env export remains for subprocess inheritance. `path` is the resolved absolute config file path; `dir` is its parent directory (both honor `program.appConfig.path` when set, otherwise the OS default from `program.key`).
|
|
435
435
|
|
|
436
436
|
## Reserved names
|
|
437
437
|
|
package/docs/config-schema.md
CHANGED
|
@@ -42,7 +42,7 @@ await cli.run();
|
|
|
42
42
|
| `install --configure` / `--status` | Interactive setup and status |
|
|
43
43
|
| Built-in `config get` / `config set` | Read/write resolved values (opt-out via `commands: false`) |
|
|
44
44
|
| MCP bundle / Claude plugin | `userConfig` for entries with `env` set |
|
|
45
|
-
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `path` — prefer over `process.env` |
|
|
45
|
+
| `ctx.appConfig` in handlers | `get`, `require`, `set`, `read`, `path`, `dir` — prefer over `process.env` |
|
|
46
46
|
|
|
47
47
|
**Validation at runtime** — argsbarg validates the config file and `config set` / `ctx.appConfig.set` against the effective JSON Schema (block `jsonSchema` or synthesized all-string schema).
|
|
48
48
|
|
package/docs/mcp.md
CHANGED
|
@@ -244,6 +244,20 @@ The built-in schema resource (default URI `<sanitized-key>://schema`, e.g. `nest
|
|
|
244
244
|
| MIME type | `application/json` |
|
|
245
245
|
| Contents | `cliSchemaJson(root)` — handlers omitted, built-ins excluded |
|
|
246
246
|
|
|
247
|
+
### Auto docs topic resources
|
|
248
|
+
|
|
249
|
+
When both **`docs.enabled`** and **`mcpServer.enabled`** are true, each user key in **`docs.topics`** is also exposed as an MCP resource:
|
|
250
|
+
|
|
251
|
+
| Property | Value |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| URI | `<sanitized root key>://docs/<topicKey>` (e.g. `myapp://docs/readme`) |
|
|
254
|
+
| MIME type | `text/markdown` |
|
|
255
|
+
| Contents | Same body as `myapp docs <topicKey>` |
|
|
256
|
+
|
|
257
|
+
Built-in docs subcommands (`schema`, `api`, `skill`, `mcp`) are **not** auto-exposed — use the schema resource, `install --skill`, or CLI `docs` instead. `docs` subcommands remain hidden from MCP `tools/list`.
|
|
258
|
+
|
|
259
|
+
Custom `mcpServer.resources` URIs must not collide with the schema URI or any auto docs topic URI (validated at program compile time).
|
|
260
|
+
|
|
247
261
|
Add custom resources on the program root:
|
|
248
262
|
|
|
249
263
|
```typescript
|
|
@@ -261,7 +275,7 @@ mcpServer: {
|
|
|
261
275
|
},
|
|
262
276
|
```
|
|
263
277
|
|
|
264
|
-
URIs must be unique and must not equal `schemaResourceUri
|
|
278
|
+
URIs must be unique and must not equal `schemaResourceUri` or any auto docs topic URI (`<mcpId>://docs/<topicKey>`). `load()` runs synchronously at `resources/read` time.
|
|
265
279
|
|
|
266
280
|
## Invocation context
|
|
267
281
|
|
|
@@ -384,9 +398,10 @@ Manifest metadata is generated from your schema (`mcpServerId`, tools, `program.
|
|
|
384
398
|
.mcp.json
|
|
385
399
|
bin/myapp
|
|
386
400
|
skills/<dirName>/SKILL.md
|
|
387
|
-
skills/<dirName>/reference.md
|
|
388
401
|
```
|
|
389
402
|
|
|
403
|
+
The bundled `SKILL.md` is an **MCP routing stub** — it tells Claude to use the plugin’s MCP toolset (server id, `tools/list`, schema resource). It is not a shell CLI catalog and does not include `reference.md`. Use **`install --skill`** for a persisted shell-oriented skill bundle.
|
|
404
|
+
|
|
390
405
|
Load locally with `claude --plugin-dir ./dist/claude-plugin/myapp.zip`.
|
|
391
406
|
|
|
392
407
|
Bare **`myapp mcp`** still runs the stdio MCP server (unchanged for `install --mcp` and MCP hosts). Use **`install --mcp`** for Cursor, Claude Code, Claude Desktop, and OpenCode JSON config.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| `outputSchema` | `src/commands/status/types.ts` (`StatusJsonOutput`) → `schemas/outputSchemas.ts` |
|
|
12
12
|
| Schemagen | `scripts/schemagen.ts` + `scripts/schemagen/discover-schema-roots.ts` |
|
|
13
13
|
| Handler access | `ctx.appConfig` in `src/program.ts` |
|
|
14
|
+
| MCP doc topics | `docs.topics` auto-exposed as `<key>://docs/<topic>` resources when docs + MCP enabled |
|
|
14
15
|
| Package import | `from "argsbarg"` (not relative to argsbarg `src/`) |
|
|
15
16
|
|
|
16
17
|
## Quick start (in this repo)
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CliOptionKind,
|
|
9
9
|
type CliProgram,
|
|
10
10
|
} from "argsbarg";
|
|
11
|
+
import readmeText from "../README.md" with { type: "text" };
|
|
11
12
|
import { APP_CONFIG_JSON_SCHEMA } from "../schemas/configSchemas.ts";
|
|
12
13
|
import { STATUS_JSON_OUTPUT_SCHEMA } from "../schemas/outputSchemas.ts";
|
|
13
14
|
import type { StatusJsonOutput } from "./commands/status/types.ts";
|
|
@@ -46,21 +47,12 @@ export const program = {
|
|
|
46
47
|
enabled: true,
|
|
47
48
|
topics: {
|
|
48
49
|
readme: {
|
|
49
|
-
text:
|
|
50
|
+
text: readmeText,
|
|
50
51
|
},
|
|
51
52
|
},
|
|
52
53
|
},
|
|
53
54
|
mcpServer: {
|
|
54
55
|
enabled: true,
|
|
55
|
-
resources: [
|
|
56
|
-
{
|
|
57
|
-
uri: "consumer-app://readme",
|
|
58
|
-
name: "readme",
|
|
59
|
-
description: "Bundled readme topic.",
|
|
60
|
-
mimeType: "text/plain",
|
|
61
|
-
load: () => "# consumer-app\n\nKitchen-sink reference.\n",
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
56
|
},
|
|
65
57
|
install: {
|
|
66
58
|
updateGetLatest: async () => ({
|
package/examples/mcp-test.ts
CHANGED
package/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ declare class EmptyAppConfigSnapshot {
|
|
|
10
10
|
read(): ResolvedConfig;
|
|
11
11
|
/** Resolved absolute path to the app JSON config file (OS default from `program.key`). */
|
|
12
12
|
get path(): string;
|
|
13
|
+
/** Resolved absolute directory containing the config file. */
|
|
14
|
+
get dir(): string;
|
|
13
15
|
}
|
|
14
16
|
declare class AppConfigSnapshot {
|
|
15
17
|
private readonly program;
|
|
@@ -22,6 +24,8 @@ declare class AppConfigSnapshot {
|
|
|
22
24
|
read(): ResolvedConfig;
|
|
23
25
|
/** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
|
|
24
26
|
get path(): string;
|
|
27
|
+
/** Resolved absolute directory containing the config file. */
|
|
28
|
+
get dir(): string;
|
|
25
29
|
/** Replace snapshot after external bootstrap (internal). */
|
|
26
30
|
refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void;
|
|
27
31
|
private assertEntryKey;
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import type { CliProgram } from "../types.ts";
|
|
6
6
|
import { createAppConfigSnapshot } from "./context.ts";
|
|
7
|
+
import { resolveAppConfigDir } from "./file.ts";
|
|
7
8
|
import { resolveAppConfig } from "./resolve.ts";
|
|
8
9
|
|
|
9
10
|
function configProgram(configPath: string): CliProgram {
|
|
@@ -37,6 +38,7 @@ describe("config/context", () => {
|
|
|
37
38
|
expect(ctx.get("note")).toBe("hello");
|
|
38
39
|
expect(ctx.require("apiToken")).toBe("tok");
|
|
39
40
|
expect(ctx.path).toBe(path);
|
|
41
|
+
expect(ctx.dir).toBe(dirname(path));
|
|
40
42
|
|
|
41
43
|
ctx.set("note", "updated");
|
|
42
44
|
expect(ctx.get("note")).toBe("updated");
|
|
@@ -60,6 +62,7 @@ describe("config/context", () => {
|
|
|
60
62
|
expect(() => empty.set("any", "v")).toThrow(/program.appConfig is not set/);
|
|
61
63
|
expect(empty.path).toContain("x");
|
|
62
64
|
expect(empty.path.endsWith("/config") || empty.path.endsWith("\\config")).toBe(true);
|
|
65
|
+
expect(empty.dir).toBe(dirname(empty.path));
|
|
63
66
|
});
|
|
64
67
|
|
|
65
68
|
test("AppConfigSnapshot path uses OS default when program.appConfig.path omitted", () => {
|
|
@@ -75,5 +78,12 @@ describe("config/context", () => {
|
|
|
75
78
|
const ctx = createAppConfigSnapshot(program, {}, {});
|
|
76
79
|
expect(ctx.path).toContain("ctx_test");
|
|
77
80
|
expect(ctx.path.endsWith("/config") || ctx.path.endsWith("\\config")).toBe(true);
|
|
81
|
+
expect(ctx.dir).toBe(resolveAppConfigDir(program));
|
|
82
|
+
expect(ctx.dir).toBe(dirname(ctx.path));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("resolveAppConfigDir honors custom program.appConfig.path", () => {
|
|
86
|
+
const program = configProgram("/tmp/custom/settings.json");
|
|
87
|
+
expect(resolveAppConfigDir(program)).toBe("/tmp/custom");
|
|
78
88
|
});
|
|
79
89
|
});
|
package/src/config/context.ts
CHANGED
|
@@ -3,7 +3,7 @@ Handler-facing resolved app config snapshot (ctx.appConfig).
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { CliProgram } from "../types.ts";
|
|
6
|
-
import { resolveAppConfigPath, writeAppConfigFile } from "./file.ts";
|
|
6
|
+
import { resolveAppConfigDir, resolveAppConfigPath, writeAppConfigFile } from "./file.ts";
|
|
7
7
|
import type { ResolvedConfig } from "./resolve.ts";
|
|
8
8
|
import { exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
|
|
9
9
|
|
|
@@ -31,6 +31,11 @@ export class EmptyAppConfigSnapshot {
|
|
|
31
31
|
get path(): string {
|
|
32
32
|
return resolveAppConfigPath(this.program);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Resolved absolute directory containing the config file. */
|
|
36
|
+
get dir(): string {
|
|
37
|
+
return resolveAppConfigDir(this.program);
|
|
38
|
+
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** Resolved config for handlers with program.appConfig set. */
|
|
@@ -82,6 +87,11 @@ export class AppConfigSnapshot {
|
|
|
82
87
|
return resolveAppConfigPath(this.program);
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/** Resolved absolute directory containing the config file. */
|
|
91
|
+
get dir(): string {
|
|
92
|
+
return resolveAppConfigDir(this.program);
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
/** Replace snapshot after external bootstrap (internal). */
|
|
86
96
|
refresh(fileData: Record<string, unknown>, resolved: ResolvedConfig): void {
|
|
87
97
|
this.fileData = { ...fileData };
|
package/src/config/file.ts
CHANGED
|
@@ -22,6 +22,11 @@ export function resolveAppConfigPath(program: CliProgram): string {
|
|
|
22
22
|
return join(appConfigHome(), dirName, "config");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Resolved absolute directory containing the app JSON config file. */
|
|
26
|
+
export function resolveAppConfigDir(program: CliProgram): string {
|
|
27
|
+
return dirname(resolveAppConfigPath(program));
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/** Human-readable config path for error messages (`~` when under home). */
|
|
26
31
|
export function displayAppConfigPath(program: CliProgram): string {
|
|
27
32
|
const resolved = resolveAppConfigPath(program);
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../mcp/tools.ts";
|
|
11
11
|
import { collectOptionDefs } from "../parse.ts";
|
|
12
12
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
13
|
+
import { resolveDocsTopicResourceUri } from "./mcp-resources.ts";
|
|
14
|
+
import { docsEnabled, docsUserTopicKeys } from "./resolve.ts";
|
|
13
15
|
|
|
14
16
|
/** Extra host notes for generated `docs mcp` (manual fallbacks and ChatGPT Connectors). */
|
|
15
17
|
function appendManualHostSetup(lines: string[], root: CliProgram, serverId: string): void {
|
|
@@ -210,10 +212,19 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
210
212
|
"| `tools/list` | Callable tools for exposed leaf commands |",
|
|
211
213
|
"| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
|
|
212
214
|
`| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} docs schema\` |`,
|
|
213
|
-
"",
|
|
214
|
-
"## Exposed tools",
|
|
215
|
-
"",
|
|
216
215
|
);
|
|
216
|
+
if (docsEnabled(root)) {
|
|
217
|
+
const docs = root.docs;
|
|
218
|
+
if (docs) {
|
|
219
|
+
for (const key of docsUserTopicKeys(docs)) {
|
|
220
|
+
const uri = resolveDocsTopicResourceUri(root, key);
|
|
221
|
+
lines.push(
|
|
222
|
+
`| Docs topic \`${key}\` | \`${uri}\` — same markdown as \`${root.key} docs ${key}\` |`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
lines.push("", "## Exposed tools", "");
|
|
217
228
|
|
|
218
229
|
if (tools.length === 0) {
|
|
219
230
|
lines.push("(No MCP tools exposed.)", "");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { CliProgram } from "../types.ts";
|
|
3
|
+
import {
|
|
4
|
+
defaultDocsTopicResourceUri,
|
|
5
|
+
docsMcpResources,
|
|
6
|
+
reservedDocsTopicResourceUris,
|
|
7
|
+
resolveDocsTopicResourceUri,
|
|
8
|
+
} from "./mcp-resources.ts";
|
|
9
|
+
|
|
10
|
+
function fixture(opts?: { docs?: boolean; mcp?: boolean }): CliProgram {
|
|
11
|
+
const docs = opts?.docs !== false;
|
|
12
|
+
const mcp = opts?.mcp !== false;
|
|
13
|
+
return {
|
|
14
|
+
key: "my-app",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
description: "Test.",
|
|
17
|
+
...(docs
|
|
18
|
+
? {
|
|
19
|
+
docs: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
topics: {
|
|
22
|
+
readme: { text: "# Readme\n", description: "User guide." },
|
|
23
|
+
arch: { text: "# Arch\n" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
...(mcp ? { mcpServer: { enabled: true } } : {}),
|
|
29
|
+
handler: () => {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("defaultDocsTopicResourceUri", () => {
|
|
34
|
+
expect(defaultDocsTopicResourceUri("my_app", "readme")).toBe("my_app://docs/readme");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveDocsTopicResourceUri sanitizes program key", () => {
|
|
38
|
+
expect(resolveDocsTopicResourceUri(fixture(), "readme")).toBe("my_app://docs/readme");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("docsMcpResources when docs and MCP enabled", () => {
|
|
42
|
+
const resources = docsMcpResources(fixture());
|
|
43
|
+
expect(resources.map((r) => r.uri)).toEqual(["my_app://docs/readme", "my_app://docs/arch"]);
|
|
44
|
+
expect(resources[0]?.name).toBe("readme");
|
|
45
|
+
expect(resources[0]?.mimeType).toBe("text/markdown");
|
|
46
|
+
expect(resources[0]?.description).toBe("User guide.");
|
|
47
|
+
expect(resources[0]?.load()).toBe("# Readme\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("docsMcpResources empty when docs disabled", () => {
|
|
51
|
+
expect(docsMcpResources(fixture({ docs: false }))).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("docsMcpResources empty when MCP disabled", () => {
|
|
55
|
+
expect(docsMcpResources(fixture({ mcp: false }))).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
|
|
59
|
+
const program = fixture();
|
|
60
|
+
expect(reservedDocsTopicResourceUris(program)).toEqual(
|
|
61
|
+
docsMcpResources(program).map((r) => r.uri),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Auto MCP resources for user docs.topics when docs and MCP are both enabled.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliProgram } from "../types.ts";
|
|
6
|
+
import {
|
|
7
|
+
docsEnabled,
|
|
8
|
+
docsTopicContent,
|
|
9
|
+
docsTopicDescription,
|
|
10
|
+
docsUserTopicKeys,
|
|
11
|
+
} from "./resolve.ts";
|
|
12
|
+
|
|
13
|
+
/** Default URI pattern for a docs topic MCP resource (`<mcpId>://docs/<topicKey>`). */
|
|
14
|
+
export function defaultDocsTopicResourceUri(mcpId: string, topicKey: string): string {
|
|
15
|
+
return `${mcpId}://docs/${topicKey}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Sanitized MCP server id from program key (matches mcpServerId in mcp/tools.ts). */
|
|
19
|
+
function mcpIdFromProgram(program: CliProgram): string {
|
|
20
|
+
return program.key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolved URI for one user docs topic resource. */
|
|
24
|
+
export function resolveDocsTopicResourceUri(program: CliProgram, topicKey: string): string {
|
|
25
|
+
return defaultDocsTopicResourceUri(mcpIdFromProgram(program), topicKey);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** All auto-generated docs topic resources (empty when docs or MCP disabled). */
|
|
29
|
+
export function docsMcpResources(program: CliProgram): {
|
|
30
|
+
uri: string;
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
load: () => string;
|
|
35
|
+
}[] {
|
|
36
|
+
if (!docsEnabled(program) || program.mcpServer?.enabled !== true) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const docs = program.docs;
|
|
40
|
+
if (!docs) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return docsUserTopicKeys(docs).map((key) => {
|
|
44
|
+
const topic = docs.topics[key];
|
|
45
|
+
if (!topic) {
|
|
46
|
+
throw new Error(`docs topic missing: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
uri: resolveDocsTopicResourceUri(program, key),
|
|
50
|
+
name: key,
|
|
51
|
+
description: docsTopicDescription(key, topic.description),
|
|
52
|
+
mimeType: "text/markdown",
|
|
53
|
+
load: () => docsTopicContent(program, key),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reserved MCP resource URIs from auto docs topics (for validation). */
|
|
59
|
+
export function reservedDocsTopicResourceUris(program: CliProgram): string[] {
|
|
60
|
+
if (!docsEnabled(program) || program.mcpServer?.enabled !== true) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const docs = program.docs;
|
|
64
|
+
if (!docs) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return docsUserTopicKeys(docs).map((key) => resolveDocsTopicResourceUri(program, key));
|
|
68
|
+
}
|
package/src/mcp/claude.test.ts
CHANGED
|
@@ -55,7 +55,7 @@ describe("claude plugin", () => {
|
|
|
55
55
|
expect(paths.pluginZipPath).toBe(join(cwd, "dist", "claude-plugin", "myapp.zip"));
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("packClaudePlugin writes zip with
|
|
58
|
+
test("packClaudePlugin writes zip with MCP pointer skill only", () => {
|
|
59
59
|
const work = mkdtempSync(join(tmpdir(), "claude-plugin-test-"));
|
|
60
60
|
try {
|
|
61
61
|
const dist = join(work, "dist");
|
|
@@ -64,8 +64,14 @@ describe("claude plugin", () => {
|
|
|
64
64
|
writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 });
|
|
65
65
|
const paths = defaultClaudePluginPaths(configFixture, work);
|
|
66
66
|
packClaudePlugin(configFixture, { cwd: work, binaryPath });
|
|
67
|
-
const
|
|
68
|
-
expect(
|
|
67
|
+
const zip = readFileSync(paths.pluginZipPath);
|
|
68
|
+
expect(zip.length).toBeGreaterThan(0);
|
|
69
|
+
const zipText = zip.toString("utf8");
|
|
70
|
+
expect(zipText).toContain("skills/myapp/SKILL.md");
|
|
71
|
+
expect(zipText).toContain("MCP toolset");
|
|
72
|
+
expect(zipText).toContain("Server id: `myapp`");
|
|
73
|
+
expect(zipText).not.toContain("skills/myapp/reference.md");
|
|
74
|
+
expect(zipText).not.toContain("Invoke via shell");
|
|
69
75
|
} finally {
|
|
70
76
|
rmSync(work, { recursive: true, force: true });
|
|
71
77
|
}
|
package/src/mcp/claude.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { basename, join, relative, resolve } from "node:path";
|
|
19
19
|
import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { generatePluginSkillBundle } from "../skill/generate.ts";
|
|
21
|
+
import { applyPluginSkillHint } from "../skill/hint.ts";
|
|
22
22
|
import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
|
|
23
23
|
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
24
|
-
import { mcpServerId
|
|
24
|
+
import { mcpServerId } from "./tools.ts";
|
|
25
25
|
import { zipStore } from "./zip.ts";
|
|
26
26
|
|
|
27
27
|
const DIST_DIR = "dist";
|
|
@@ -116,13 +116,12 @@ function writePluginTree(
|
|
|
116
116
|
binaryPath: string,
|
|
117
117
|
binaryName: string,
|
|
118
118
|
): void {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const hinted = applySkillBundleHints(program, bundle.skillMd, bundle.referenceMd);
|
|
119
|
+
const bundle = generatePluginSkillBundle(program);
|
|
120
|
+
const skillMd = applyPluginSkillHint(program, bundle.skillMd);
|
|
122
121
|
|
|
123
122
|
mkdirSync(join(pluginRoot, ".claude-plugin"), { recursive: true });
|
|
124
123
|
mkdirSync(join(pluginRoot, "bin"), { recursive: true });
|
|
125
|
-
mkdirSync(join(pluginRoot, "skills",
|
|
124
|
+
mkdirSync(join(pluginRoot, "skills", bundle.dirName), { recursive: true });
|
|
126
125
|
|
|
127
126
|
writeFileSync(
|
|
128
127
|
join(pluginRoot, ".claude-plugin", "plugin.json"),
|
|
@@ -133,8 +132,7 @@ function writePluginTree(
|
|
|
133
132
|
`${JSON.stringify(generatePluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
134
133
|
);
|
|
135
134
|
cpSync(binaryPath, join(pluginRoot, "bin", binaryName), { mode: 0o755 });
|
|
136
|
-
writeFileSync(join(pluginRoot, "skills",
|
|
137
|
-
writeFileSync(join(pluginRoot, "skills", skillDirName, "reference.md"), hinted.referenceMd);
|
|
135
|
+
writeFileSync(join(pluginRoot, "skills", bundle.dirName, "SKILL.md"), skillMd);
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
/**
|
package/src/mcp/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
|
|
|
3
3
|
flat JSON tool arguments into argv for Cli.invoke.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { docsMcpResources } from "../docs/mcp-resources.ts";
|
|
6
7
|
import { cliResolveNotes } from "../help.ts";
|
|
7
8
|
import { visibleOptions } from "../hidden.ts";
|
|
8
9
|
import { collectOptionDefs } from "../parse.ts";
|
|
@@ -26,6 +27,8 @@ export function defaultMcpSchemaUri(mcpId: string): string {
|
|
|
26
27
|
return `${mcpId}://schema`;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
|
|
31
|
+
|
|
29
32
|
/** Sanitizes a command key segment for MCP tool names and server identity. */
|
|
30
33
|
export function sanitizeToolSegment(key: string): string {
|
|
31
34
|
return key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
@@ -209,7 +212,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
|
|
|
209
212
|
mimeType: r.mimeType ?? "text/plain",
|
|
210
213
|
load: r.load,
|
|
211
214
|
}));
|
|
212
|
-
return [builtIn, ...user];
|
|
215
|
+
return [builtIn, ...docsMcpResources(root), ...user];
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/** Recursively collects MCP tool definitions from user leaf commands. */
|
|
@@ -480,9 +480,26 @@ test("MCP resources/list includes custom resource", async () => {
|
|
|
480
480
|
const res = responses.get(10) as { result: { resources: { uri: string }[] } };
|
|
481
481
|
const uris = res.result.resources.map((r) => r.uri);
|
|
482
482
|
expect(uris).toContain("mcp_test://schema");
|
|
483
|
+
expect(uris).toContain("mcp_test://docs/readme");
|
|
483
484
|
expect(uris).toContain("test://hello");
|
|
484
485
|
});
|
|
485
486
|
|
|
487
|
+
test("MCP resources/read returns docs topic resource body", async () => {
|
|
488
|
+
const responses = await mcpRequest(
|
|
489
|
+
[
|
|
490
|
+
{
|
|
491
|
+
jsonrpc: "2.0",
|
|
492
|
+
id: 13,
|
|
493
|
+
method: "resources/read",
|
|
494
|
+
params: { uri: "mcp_test://docs/readme" },
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
{ script: "examples/mcp-test.ts" },
|
|
498
|
+
);
|
|
499
|
+
const res = responses.get(13) as { result: { contents: { text: string }[] } };
|
|
500
|
+
expect(res.result.contents[0]?.text).toBe("# MCP test readme\n");
|
|
501
|
+
});
|
|
502
|
+
|
|
486
503
|
test("MCP resources/read returns custom resource body", async () => {
|
|
487
504
|
const responses = await mcpRequest(
|
|
488
505
|
[{ jsonrpc: "2.0", id: 11, method: "resources/read", params: { uri: "test://hello" } }],
|
package/src/parse.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./mcp/tools.ts";
|
|
21
21
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
22
22
|
import { cliSchemaJson } from "./schema.ts";
|
|
23
|
-
import { generateSkillBundle } from "./skill/generate.ts";
|
|
23
|
+
import { generatePluginSkillBundle, generateSkillBundle } from "./skill/generate.ts";
|
|
24
24
|
import { cliSkillInstall } from "./skill/install.ts";
|
|
25
25
|
import {
|
|
26
26
|
enumMcpFixture,
|
|
@@ -835,7 +835,7 @@ test("cliValidateProgram rejects resource URI matching default schema URI", () =
|
|
|
835
835
|
},
|
|
836
836
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
837
837
|
});
|
|
838
|
-
expect(() => cliValidateProgram(root)).toThrow(/conflicts with
|
|
838
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
|
|
839
839
|
});
|
|
840
840
|
|
|
841
841
|
test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
|
|
@@ -849,7 +849,21 @@ test("cliValidateProgram rejects resource URI matching schemaResourceUri", () =>
|
|
|
849
849
|
},
|
|
850
850
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
851
851
|
});
|
|
852
|
-
expect(() => cliValidateProgram(root)).toThrow(/conflicts with
|
|
852
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("cliValidateProgram rejects resource URI matching auto docs topic", () => {
|
|
856
|
+
const root = testProgram({
|
|
857
|
+
key: "app",
|
|
858
|
+
description: "",
|
|
859
|
+
docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
|
|
860
|
+
mcpServer: {
|
|
861
|
+
enabled: true,
|
|
862
|
+
resources: [{ uri: "app://docs/readme", name: "dup", load: () => "" }],
|
|
863
|
+
},
|
|
864
|
+
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
865
|
+
});
|
|
866
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with auto docs topic resource/);
|
|
853
867
|
});
|
|
854
868
|
|
|
855
869
|
test("allMcpResources includes custom resources", () => {
|
|
@@ -867,6 +881,20 @@ test("allMcpResources includes custom resources", () => {
|
|
|
867
881
|
expect(resources.map((r) => r.uri)).toContain("test://x");
|
|
868
882
|
});
|
|
869
883
|
|
|
884
|
+
test("allMcpResources includes docs topic resources", () => {
|
|
885
|
+
const root = testProgram({
|
|
886
|
+
key: "app",
|
|
887
|
+
description: "",
|
|
888
|
+
docs: { enabled: true, topics: { readme: { text: "# hi\n" } } },
|
|
889
|
+
mcpServer: { enabled: true },
|
|
890
|
+
commands: [{ key: "leaf", description: "", handler: () => {} }],
|
|
891
|
+
});
|
|
892
|
+
const resources = allMcpResources(root);
|
|
893
|
+
expect(resources.map((r) => r.uri)).toContain("app://docs/readme");
|
|
894
|
+
const readme = resources.find((r) => r.uri === "app://docs/readme");
|
|
895
|
+
expect(readme?.load()).toBe("# hi\n");
|
|
896
|
+
});
|
|
897
|
+
|
|
870
898
|
test("applyShellEnv merges PATH and preserves host vars", () => {
|
|
871
899
|
const origPath = process.env.PATH ?? "";
|
|
872
900
|
const origHome = process.env.HOME;
|
|
@@ -1116,6 +1144,20 @@ test("generateSkillBundle includes frontmatter and compact command index", () =>
|
|
|
1116
1144
|
expect(bundle.referenceMd).not.toContain("```json");
|
|
1117
1145
|
});
|
|
1118
1146
|
|
|
1147
|
+
test("generatePluginSkillBundle is MCP routing stub without shell catalog", () => {
|
|
1148
|
+
const bundle = generatePluginSkillBundle(nestedMcpFixture);
|
|
1149
|
+
expect(bundle.dirName).toBe("nested_ts");
|
|
1150
|
+
expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
|
|
1151
|
+
expect(bundle.skillMd).toContain("MCP toolset");
|
|
1152
|
+
expect(bundle.skillMd).toContain("Server id: `nested_ts`");
|
|
1153
|
+
expect(bundle.skillMd).toContain("nested_ts://schema");
|
|
1154
|
+
expect(bundle.skillMd).toContain("tools/list");
|
|
1155
|
+
expect(bundle.skillMd).not.toContain("Invoke via shell");
|
|
1156
|
+
expect(bundle.skillMd).not.toContain("reference.md");
|
|
1157
|
+
expect(bundle.skillMd).not.toContain("`nested.ts stat owner lookup <path>`");
|
|
1158
|
+
expect(bundle.skillMd).not.toContain("## Commands");
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1119
1161
|
test("cliSkillInstall writes project Cursor skill files", () => {
|
|
1120
1162
|
const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-"));
|
|
1121
1163
|
const prev = process.cwd();
|
package/src/skill/generate.ts
CHANGED
|
@@ -4,7 +4,13 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
|
|
|
4
4
|
|
|
5
5
|
import { defaultConfigEntryTitle } from "../config/entry.ts";
|
|
6
6
|
import { generateApiGuide } from "../docs/api-guide.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
collectMcpTools,
|
|
9
|
+
type McpToolDef,
|
|
10
|
+
mcpServerId,
|
|
11
|
+
resolveMcpSchemaUri,
|
|
12
|
+
sanitizeToolSegment,
|
|
13
|
+
} from "../mcp/tools.ts";
|
|
8
14
|
import { collectOptionDefs } from "../parse.ts";
|
|
9
15
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
10
16
|
|
|
@@ -16,12 +22,28 @@ export interface SkillBundle {
|
|
|
16
22
|
referenceMd: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/** MCP routing skill for Claude Code plugin zips (SKILL.md only). */
|
|
26
|
+
export interface PluginSkillBundle {
|
|
27
|
+
dirName: string;
|
|
28
|
+
skillMd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/** Truncates text to maxLen with ellipsis. */
|
|
20
32
|
function truncate(text: string, maxLen: number): string {
|
|
21
33
|
if (text.length <= maxLen) return text;
|
|
22
34
|
return `${text.slice(0, maxLen - 1)}…`;
|
|
23
35
|
}
|
|
24
36
|
|
|
37
|
+
/** Builds MCP-oriented skill description for Claude plugin YAML frontmatter. */
|
|
38
|
+
function pluginSkillDescription(root: CliProgram): string {
|
|
39
|
+
const tools = collectMcpTools(root);
|
|
40
|
+
const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
|
|
41
|
+
const sample = paths.slice(0, 5).join(", ");
|
|
42
|
+
const more = paths.length > 5 ? `, and ${paths.length - 5} more` : "";
|
|
43
|
+
const desc = `Use the ${root.key} MCP toolset (${sample}${more}). Use when the user mentions ${root.key}${paths.length > 0 ? `, ${paths.slice(0, 3).join(", ")}` : ""}, or related tasks.`;
|
|
44
|
+
return truncate(desc, 1024);
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
/** Builds third-person skill description for YAML frontmatter. */
|
|
26
48
|
function skillDescription(root: CliProgram): string {
|
|
27
49
|
const tools = collectMcpTools(root);
|
|
@@ -159,6 +181,54 @@ function buildReferenceMd(root: CliProgram): string {
|
|
|
159
181
|
return generateApiGuide(root);
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
/** Builds MCP routing SKILL.md for Claude Code plugin zips. */
|
|
185
|
+
function buildPluginSkillMd(root: CliProgram, dirName: string): string {
|
|
186
|
+
const name = sanitizeToolSegment(root.key);
|
|
187
|
+
const description = pluginSkillDescription(root);
|
|
188
|
+
const serverId = mcpServerId(root);
|
|
189
|
+
const schemaUri = resolveMcpSchemaUri(root);
|
|
190
|
+
|
|
191
|
+
const lines: string[] = [
|
|
192
|
+
"---",
|
|
193
|
+
`name: ${name}`,
|
|
194
|
+
`description: ${description}`,
|
|
195
|
+
"---",
|
|
196
|
+
"",
|
|
197
|
+
`# ${root.key}`,
|
|
198
|
+
"",
|
|
199
|
+
root.description,
|
|
200
|
+
"",
|
|
201
|
+
"## Execution",
|
|
202
|
+
"",
|
|
203
|
+
"This plugin bundles an MCP server. Use MCP tools to fulfill requests.",
|
|
204
|
+
"",
|
|
205
|
+
`- Server id: \`${serverId}\` (configured in plugin \`.mcp.json\`)`,
|
|
206
|
+
"- Tool names and argument shapes come from MCP `tools/list`",
|
|
207
|
+
`- Full schema: \`${schemaUri}\` (same as \`${root.key} docs schema\`)`,
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
lines.push(...buildConfigurationSection(root));
|
|
212
|
+
|
|
213
|
+
lines.push(
|
|
214
|
+
"## Claude Code plugin",
|
|
215
|
+
"",
|
|
216
|
+
`Invoke with \`/${dirName}\` or let Claude auto-match from the description.`,
|
|
217
|
+
"",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Generates MCP routing SKILL.md for Claude Code plugin zips. */
|
|
224
|
+
export function generatePluginSkillBundle(root: CliProgram): PluginSkillBundle {
|
|
225
|
+
const dirName = sanitizeToolSegment(root.key);
|
|
226
|
+
return {
|
|
227
|
+
dirName,
|
|
228
|
+
skillMd: buildPluginSkillMd(root, dirName),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
162
232
|
/** Generates SKILL.md and reference.md for Cursor or Claude Code. */
|
|
163
233
|
export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
|
|
164
234
|
const dirName = sanitizeToolSegment(root.key);
|
package/src/skill/hint.ts
CHANGED
|
@@ -58,3 +58,8 @@ export function applySkillBundleHints(
|
|
|
58
58
|
referenceMd: insertGeneratedHint(referenceMd, hint),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/** Applies bundle hint to plugin SKILL.md (after frontmatter). */
|
|
63
|
+
export function applyPluginSkillHint(program: CliProgram, skillMd: string): string {
|
|
64
|
+
return insertGeneratedHint(skillMd, skillBundleHint(program), { afterFrontmatter: true });
|
|
65
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module validates CLI schemas before execution.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
|
|
6
|
+
import { reservedDocsTopicResourceUris } from "./docs/mcp-resources.ts";
|
|
6
7
|
import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
|
|
7
8
|
import { validateFormatValue } from "./formats.ts";
|
|
8
9
|
import { resolveMcpSchemaUri } from "./mcp/tools.ts";
|
|
@@ -209,11 +210,15 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
209
210
|
|
|
210
211
|
if (isRoot && program.mcpServer?.enabled === true && program.mcpServer.resources) {
|
|
211
212
|
const schemaUri = resolveMcpSchemaUri(program);
|
|
213
|
+
const reserved = new Set([schemaUri, ...reservedDocsTopicResourceUris(program)]);
|
|
212
214
|
const uris = program.mcpServer.resources.map((r) => r.uri);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
for (const uri of uris) {
|
|
216
|
+
if (reserved.has(uri)) {
|
|
217
|
+
const kind = uri === schemaUri ? "built-in schema resource" : "auto docs topic resource";
|
|
218
|
+
throw new CliSchemaValidationError(
|
|
219
|
+
`mcpServer.resources URI '${uri}' conflicts with ${kind}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
217
222
|
}
|
|
218
223
|
if (new Set(uris).size !== uris.length) {
|
|
219
224
|
throw new CliSchemaValidationError("mcpServer.resources URIs must be unique");
|