argsbarg 1.4.2 → 1.4.3
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/.private/scratch.md +2 -1
- package/CHANGELOG.md +14 -1
- package/README.md +15 -4
- package/docs/ai-skills.md +75 -0
- package/docs/mcp.md +11 -9
- package/index.d.ts +13 -2
- package/package.json +1 -1
- package/src/ai.ts +7 -0
- package/src/completion.ts +50 -9
- package/src/index.test.ts +151 -9
- package/src/mcp/tools.ts +1 -1
- package/src/runtime.ts +42 -23
- package/src/skill/generate.ts +183 -0
- package/src/skill/install.ts +45 -0
- package/src/types.ts +14 -2
- package/src/validate.ts +7 -1
package/.private/scratch.md
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
- [x] --schema feature for ai agents
|
|
1
|
+
- [x] --schema feature for ai agents
|
|
2
|
+
- [ ] auto-generate/install a cursor skill format for ~/.cursor/skills?
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.3] - 2026-06-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`ai` built-in group** — `myapp ai skill cursor` and `myapp ai skill claude` install Agent Skills (`SKILL.md` + `reference.md`) to project or global skill directories.
|
|
15
|
+
- **`aiSkill`** root config to opt out of skill install (`{ enabled: false }`).
|
|
16
|
+
|
|
17
|
+
### Changed (breaking)
|
|
18
|
+
|
|
19
|
+
- **`myapp mcp`** → **`myapp ai mcp`**
|
|
20
|
+
- Reserved top-level command **`mcp`** → **`ai`** (user commands may now be named `mcp`)
|
|
21
|
+
|
|
10
22
|
## [1.4.2] - 2026-06-19
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -123,7 +135,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
123
135
|
- 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`).
|
|
124
136
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
125
137
|
|
|
126
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.4.
|
|
138
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.4.3...HEAD
|
|
139
|
+
[1.4.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.3
|
|
127
140
|
[1.4.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.2
|
|
128
141
|
[1.4.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.1
|
|
129
142
|
[1.4.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.0
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Why another CLI parser?
|
|
|
16
16
|
|
|
17
17
|
*Shell completions* — `completion bash` and `completion zsh` built-ins generate installable scripts from your schema so users get tab completion for commands, flags, and positionals without extra tooling.
|
|
18
18
|
|
|
19
|
-
*Optional MCP server* — set `mcpServer: {}` on the program root to expose leaf commands as MCP tools and the full CLI tree as a schema resource (`myapp mcp` over stdio). See [docs/mcp.md](docs/mcp.md).
|
|
19
|
+
*Optional MCP server* — set `mcpServer: {}` on the program root to expose leaf commands as MCP tools and the full CLI tree as a schema resource (`myapp ai mcp` over stdio). See [docs/mcp.md](docs/mcp.md). Install Cursor/Claude skills with `myapp ai skill cursor|claude` — see [docs/ai-skills.md](docs/ai-skills.md).
|
|
20
20
|
|
|
21
21
|
*Bun-optimized* — built from the ground up for Bun and TypeScript, leveraging Bun’s performance and modern JavaScript features without any extra dependencies.
|
|
22
22
|
|
|
@@ -96,19 +96,30 @@ Every app gets:
|
|
|
96
96
|
- `-h` / `--help` at any routing depth (scoped help).
|
|
97
97
|
- **`--schema`** at the program root — print the full command tree as JSON (for tooling and agents).
|
|
98
98
|
- **`completion bash` / `completion zsh`** — print shell completion scripts to stdout (injected by `cliRun`).
|
|
99
|
-
- **`
|
|
99
|
+
- **`ai`** — AI agent integration: `ai mcp` (when `mcpServer` is set), `ai skill cursor`, `ai skill claude` (install agent skills; opt out with `aiSkill: { enabled: false }`).
|
|
100
100
|
|
|
101
101
|
Do not declare a top-level command named **`completion`** — it is reserved for this built-in.
|
|
102
|
-
Do not declare a top-level command named **`
|
|
102
|
+
Do not declare a top-level command named **`ai`** — it is reserved for this built-in.
|
|
103
103
|
Do not declare an option named **`schema`** — it is reserved for `--schema`.
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
### MCP (AI agents)
|
|
107
107
|
|
|
108
|
-
Opt in on the program root with `mcpServer: {}` (or `{ name, version, … }`), then run `myapp mcp` for a stdio MCP server. Each leaf command becomes a tool; the CLI tree is available as resource `argsbarg://schema`. Handlers can read `ctx.invocation` and use `cliInvoke` for headless testing.
|
|
108
|
+
Opt in on the program root with `mcpServer: {}` (or `{ name, version, … }`), then run `myapp ai mcp` for a stdio MCP server. Each leaf command becomes a tool; the CLI tree is available as resource `argsbarg://schema`. Handlers can read `ctx.invocation` and use `cliInvoke` for headless testing.
|
|
109
109
|
|
|
110
110
|
See **[docs/mcp.md](docs/mcp.md)** for configuration, env bootstrapping, custom resources, Cursor setup, and protocol details.
|
|
111
111
|
|
|
112
|
+
### Agent skills
|
|
113
|
+
|
|
114
|
+
Install Cursor or Claude Code skills (command catalog + schema reference) with:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
myapp ai skill cursor # .cursor/skills/<name>/
|
|
118
|
+
myapp ai skill claude --global # ~/.claude/skills/<name>/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See **[docs/ai-skills.md](docs/ai-skills.md)** for opt-out, flags, and file layout.
|
|
122
|
+
|
|
112
123
|
|
|
113
124
|
### Shell completions
|
|
114
125
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Agent skills install
|
|
2
|
+
|
|
3
|
+
ArgsBarg can install [Agent Skills](https://code.claude.com/docs/en/skills) content for **Cursor** and **Claude Code** from your CLI schema. Each install writes two files:
|
|
4
|
+
|
|
5
|
+
| File | Purpose |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `SKILL.md` | Frontmatter + command catalog, execution notes, pitfalls |
|
|
8
|
+
| `reference.md` | Full `--schema` JSON export |
|
|
9
|
+
|
|
10
|
+
Skills are **install-only** — there is no print-to-stdout mode, because the artifact is always a two-file directory.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Project skill (commit .cursor/skills/ with your repo)
|
|
16
|
+
myapp ai skill cursor
|
|
17
|
+
|
|
18
|
+
# User-wide Claude Code skill
|
|
19
|
+
myapp ai skill claude --global
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
On success, one line is printed to **stderr** with the install path (not the skill body).
|
|
23
|
+
|
|
24
|
+
## Opt-out
|
|
25
|
+
|
|
26
|
+
Skill install is **enabled by default**. Opt out on the program root:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const cli: CliCommand = {
|
|
30
|
+
key: "myapp",
|
|
31
|
+
description: "My app.",
|
|
32
|
+
aiSkill: { enabled: false },
|
|
33
|
+
commands: [/* ... */],
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Optional custom skill directory name:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
aiSkill: { name: "my-custom-skill" },
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Flags
|
|
44
|
+
|
|
45
|
+
Available on `ai skill cursor` and `ai skill claude`:
|
|
46
|
+
|
|
47
|
+
| Flag | Effect |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `--global` | Install under the user skills directory instead of the project |
|
|
50
|
+
| `--force` | Overwrite an existing skill directory |
|
|
51
|
+
|
|
52
|
+
## Install locations
|
|
53
|
+
|
|
54
|
+
| Target | Project (default) | `--global` |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| Cursor | `.cursor/skills/<name>/` | `~/.cursor/skills/<name>/` |
|
|
57
|
+
| Claude Code | `.claude/skills/<name>/` | `~/.claude/skills/<name>/` |
|
|
58
|
+
|
|
59
|
+
`<name>` defaults to the sanitized program root `key` (same rules as MCP tool name segments).
|
|
60
|
+
|
|
61
|
+
Do **not** install under `~/.cursor/skills-cursor/` — that path is reserved for Cursor built-ins.
|
|
62
|
+
|
|
63
|
+
## MCP vs skills
|
|
64
|
+
|
|
65
|
+
| Mechanism | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| **`myapp ai mcp`** (requires `mcpServer`) | Runtime tool execution over MCP |
|
|
68
|
+
| **`myapp ai skill cursor\|claude`** | Static discovery and conventions for agents |
|
|
69
|
+
|
|
70
|
+
Generated `SKILL.md` recommends MCP when `mcpServer` is configured, and documents shell invocation as a fallback.
|
|
71
|
+
|
|
72
|
+
## Related
|
|
73
|
+
|
|
74
|
+
- [MCP server](mcp.md) — `mcpServer` config and `ai mcp` protocol
|
|
75
|
+
- [README built-ins](../README.md#built-ins) — reserved command `ai`
|
package/docs/mcp.md
CHANGED
|
@@ -22,17 +22,19 @@ const cli: CliCommand = {
|
|
|
22
22
|
2. Run the MCP server:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
myapp mcp
|
|
25
|
+
myapp ai mcp
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
The process reads NDJSON requests from stdin and writes NDJSON responses to stdout. It stays alive until stdin closes.
|
|
29
29
|
|
|
30
30
|
3. Point your MCP client at that command. See [Client setup](#client-setup).
|
|
31
31
|
|
|
32
|
+
Optionally install an agent skill for discovery without MCP: see [docs/ai-skills.md](ai-skills.md).
|
|
33
|
+
|
|
32
34
|
The `examples/nested.ts` demo enables MCP — try:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
bun run examples/nested.ts mcp
|
|
37
|
+
bun run examples/nested.ts ai mcp
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
## Client setup
|
|
@@ -46,17 +48,17 @@ Add a server entry under `mcpServers` in your Cursor MCP config:
|
|
|
46
48
|
"mcpServers": {
|
|
47
49
|
"myapp": {
|
|
48
50
|
"command": "bun",
|
|
49
|
-
"args": ["run", "myapp.ts", "mcp"]
|
|
51
|
+
"args": ["run", "myapp.ts", "ai", "mcp"]
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Use your real binary or script path. For a compiled CLI, `command` can be the installed binary and `args` can be `["mcp"]
|
|
57
|
+
Use your real binary or script path. For a compiled CLI, `command` can be the installed binary and `args` can be `["ai", "mcp"]`.
|
|
56
58
|
|
|
57
59
|
### Other MCP hosts
|
|
58
60
|
|
|
59
|
-
Any host that spawns a subprocess and wires stdin/stdout works the same way: the **command** is your app, and **`mcp`**
|
|
61
|
+
Any host that spawns a subprocess and wires stdin/stdout works the same way: the **command** is your app, and **`ai mcp`** starts the server.
|
|
60
62
|
|
|
61
63
|
## Configuration
|
|
62
64
|
|
|
@@ -83,7 +85,7 @@ mcpServer: {
|
|
|
83
85
|
|
|
84
86
|
## Tools
|
|
85
87
|
|
|
86
|
-
Every **user-defined leaf command** in your schema becomes one MCP tool. Built-ins (`completion`, `
|
|
88
|
+
Every **user-defined leaf command** in your schema becomes one MCP tool. Built-ins (`completion`, `ai`) are not exposed as tools.
|
|
87
89
|
|
|
88
90
|
### Tool names
|
|
89
91
|
|
|
@@ -275,7 +277,7 @@ Requests without an `id` are treated as notifications and do not receive a respo
|
|
|
275
277
|
### Manual smoke test
|
|
276
278
|
|
|
277
279
|
```bash
|
|
278
|
-
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun run examples/nested.ts mcp
|
|
280
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun run examples/nested.ts ai mcp
|
|
279
281
|
```
|
|
280
282
|
|
|
281
283
|
You should get one JSON line on stdout with `result.capabilities` and `result.serverInfo`.
|
|
@@ -284,11 +286,11 @@ You should get one JSON line on stdout with `result.capabilities` and `result.se
|
|
|
284
286
|
|
|
285
287
|
When MCP is enabled:
|
|
286
288
|
|
|
287
|
-
- Do not declare a top-level command named **`
|
|
289
|
+
- Do not declare a top-level command named **`ai`** — it is reserved for the built-in AI integration group.
|
|
288
290
|
- Do not declare a top-level command named **`completion`** — reserved for shell completions.
|
|
289
291
|
- Do not declare an option named **`schema`** — reserved for `--schema`.
|
|
290
292
|
|
|
291
|
-
Running `myapp mcp` without `mcpServer` on the root fails with an error (exit 1).
|
|
293
|
+
Running `myapp ai mcp` without `mcpServer` on the root fails with an error (exit 1).
|
|
292
294
|
|
|
293
295
|
## Design notes
|
|
294
296
|
|
package/index.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export interface CliPositional {
|
|
|
105
105
|
argMax?: number;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Root-only. Enables `myapp mcp` and MCP stdio server metadata.
|
|
108
|
+
* Root-only. Enables `myapp ai mcp` and MCP stdio server metadata.
|
|
109
109
|
*/
|
|
110
110
|
export interface CliMcpServerConfig {
|
|
111
111
|
/** `initialize` serverInfo.name (default: root `key`). */
|
|
@@ -165,6 +165,15 @@ export interface CliMcpToolConfig {
|
|
|
165
165
|
*/
|
|
166
166
|
requiresEnv?: string[];
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Root-only. Opt out of `ai skill` install commands with `{ enabled: false }`.
|
|
170
|
+
*/
|
|
171
|
+
export interface CliAiSkillConfig {
|
|
172
|
+
/** When `false`, disable `ai skill *` install commands (default: enabled). */
|
|
173
|
+
enabled?: boolean;
|
|
174
|
+
/** Skill directory name (default: sanitized root `key`). */
|
|
175
|
+
name?: string;
|
|
176
|
+
}
|
|
168
177
|
/**
|
|
169
178
|
* Base properties shared by all command nodes.
|
|
170
179
|
*/
|
|
@@ -177,8 +186,10 @@ export interface CliCommandBase {
|
|
|
177
186
|
notes?: string;
|
|
178
187
|
/** Global or command-level flags/options. */
|
|
179
188
|
options?: CliOption[];
|
|
180
|
-
/** Root-only. When set, enables the `mcp` built-in subcommand. */
|
|
189
|
+
/** Root-only. When set, enables the `ai mcp` built-in subcommand. */
|
|
181
190
|
mcpServer?: CliMcpServerConfig;
|
|
191
|
+
/** Root-only. Opt out of `ai skill` install with `{ enabled: false }`. */
|
|
192
|
+
aiSkill?: CliAiSkillConfig;
|
|
182
193
|
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
183
194
|
mcpTool?: CliMcpToolConfig;
|
|
184
195
|
}
|
package/package.json
CHANGED
package/src/ai.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module re-exports AI agent integration entry points (skill install).
|
|
3
|
+
MCP stdio serving remains in mcp.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { cliSkillInstall, type SkillInstallOpts } from "./skill/install.ts";
|
|
7
|
+
export { generateSkillBundle, type SkillBundle, type SkillTarget } from "./skill/generate.ts";
|
package/src/completion.ts
CHANGED
|
@@ -597,21 +597,62 @@ export function cliPresentationRoot(root: CliCommand): CliCommand {
|
|
|
597
597
|
} as CliCommand;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/** Presence options for skill install leaves (`--global`, `--force`). */
|
|
601
|
+
function skillInstallOptions(): CliOption[] {
|
|
602
|
+
return [
|
|
603
|
+
{
|
|
604
|
+
name: "global",
|
|
605
|
+
description: "Install to the user skills directory (~/.cursor/skills or ~/.claude/skills).",
|
|
606
|
+
kind: CliOptionKind.Presence,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "force",
|
|
610
|
+
description: "Overwrite an existing skill directory.",
|
|
611
|
+
kind: CliOptionKind.Presence,
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
|
|
600
616
|
/** Built-in commands shown in help and merged for routing CLIs. */
|
|
601
617
|
function presentationBuiltins(root: CliCommand): CliCommand[] {
|
|
602
|
-
|
|
618
|
+
return [cliBuiltinCompletionGroup(root.key), cliBuiltinAiGroup(root)];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Builds the `ai` built-in command group (MCP + skill install). */
|
|
622
|
+
export function cliBuiltinAiGroup(root: CliCommand): CliCommand {
|
|
623
|
+
const aiChildren: CliCommand[] = [
|
|
624
|
+
{
|
|
625
|
+
key: "skill",
|
|
626
|
+
description: "Install agent skill files for Cursor or Claude Code.",
|
|
627
|
+
commands: [
|
|
628
|
+
{
|
|
629
|
+
key: "cursor",
|
|
630
|
+
description: "Install Cursor skill (SKILL.md + reference.md).",
|
|
631
|
+
options: skillInstallOptions(),
|
|
632
|
+
handler: () => {},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
key: "claude",
|
|
636
|
+
description: "Install Claude Code skill (SKILL.md + reference.md).",
|
|
637
|
+
options: skillInstallOptions(),
|
|
638
|
+
handler: () => {},
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
|
|
603
644
|
if (root.mcpServer !== undefined) {
|
|
604
|
-
|
|
645
|
+
aiChildren.unshift({
|
|
646
|
+
key: "mcp",
|
|
647
|
+
description: "Run as an MCP server over stdio (for AI agents).",
|
|
648
|
+
handler: () => {},
|
|
649
|
+
});
|
|
605
650
|
}
|
|
606
|
-
return cmds;
|
|
607
|
-
}
|
|
608
651
|
|
|
609
|
-
/** Builds the static `mcp` leaf command (merged when `root.mcpServer` is set). */
|
|
610
|
-
export function cliBuiltinMcpCommand(): CliCommand {
|
|
611
652
|
return {
|
|
612
|
-
key: "
|
|
613
|
-
description: "
|
|
614
|
-
|
|
653
|
+
key: "ai",
|
|
654
|
+
description: "AI agent integration (MCP server and skill install).",
|
|
655
|
+
commands: aiChildren,
|
|
615
656
|
};
|
|
616
657
|
}
|
|
617
658
|
|
package/src/index.test.ts
CHANGED
|
@@ -19,12 +19,14 @@ import {
|
|
|
19
19
|
} from "./mcp/tools.ts";
|
|
20
20
|
import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
|
|
21
21
|
import { buildToolCallSuccess } from "./mcp/result.ts";
|
|
22
|
+
import { generateSkillBundle } from "./skill/generate.ts";
|
|
23
|
+
import { cliSkillInstall } from "./skill/install.ts";
|
|
22
24
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
23
25
|
import { cliSchemaJson } from "./schema.ts";
|
|
24
26
|
import { cliValidateRoot } from "./validate.ts";
|
|
25
27
|
import { expect, test } from "bun:test";
|
|
26
28
|
import { $ } from "bun";
|
|
27
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
28
30
|
import { tmpdir } from "node:os";
|
|
29
31
|
import { join } from "node:path";
|
|
30
32
|
|
|
@@ -727,7 +729,7 @@ async function mcpRequest(
|
|
|
727
729
|
opts?: { script?: string; env?: Record<string, string> },
|
|
728
730
|
): Promise<Map<string | number, object>> {
|
|
729
731
|
const script = opts?.script ?? "examples/nested.ts";
|
|
730
|
-
const proc = Bun.spawn(["bun", "run", script, "mcp"], {
|
|
732
|
+
const proc = Bun.spawn(["bun", "run", script, "ai", "mcp"], {
|
|
731
733
|
stdin: "pipe",
|
|
732
734
|
stdout: "pipe",
|
|
733
735
|
stderr: "pipe",
|
|
@@ -775,7 +777,7 @@ test("collectMcpTools lists user leaf commands only", () => {
|
|
|
775
777
|
expect(names).toContain("stat_owner_lookup");
|
|
776
778
|
expect(names).toContain("read");
|
|
777
779
|
expect(names).not.toContain("hidden");
|
|
778
|
-
expect(names).not.toContain("
|
|
780
|
+
expect(names).not.toContain("ai");
|
|
779
781
|
expect(names).not.toContain("completion");
|
|
780
782
|
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
781
783
|
expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
|
|
@@ -807,19 +809,34 @@ test("mcpToolCallToArgv expands varargs positionals", () => {
|
|
|
807
809
|
expect(argv).toEqual(["read", "a", "b"]);
|
|
808
810
|
});
|
|
809
811
|
|
|
810
|
-
test("reserved command name
|
|
812
|
+
test("reserved command name ai is rejected", () => {
|
|
811
813
|
const root: CliCommand = {
|
|
812
814
|
key: "app",
|
|
813
815
|
description: "",
|
|
814
816
|
commands: [
|
|
815
817
|
{
|
|
816
|
-
key: "
|
|
818
|
+
key: "ai",
|
|
817
819
|
description: "bad",
|
|
818
820
|
handler: () => {},
|
|
819
821
|
},
|
|
820
822
|
],
|
|
821
823
|
};
|
|
822
|
-
expect(() => cliValidateRoot(root)).toThrow(/Reserved command name:
|
|
824
|
+
expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: ai/);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test("top-level command name mcp is allowed", () => {
|
|
828
|
+
const root: CliCommand = {
|
|
829
|
+
key: "app",
|
|
830
|
+
description: "",
|
|
831
|
+
commands: [
|
|
832
|
+
{
|
|
833
|
+
key: "mcp",
|
|
834
|
+
description: "user command",
|
|
835
|
+
handler: () => {},
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
};
|
|
839
|
+
expect(() => cliValidateRoot(root)).not.toThrow();
|
|
823
840
|
});
|
|
824
841
|
|
|
825
842
|
test("mcpServer on non-root node is rejected", () => {
|
|
@@ -997,10 +1014,10 @@ test("MCP ping returns empty result", async () => {
|
|
|
997
1014
|
expect(res.result).toEqual({});
|
|
998
1015
|
});
|
|
999
1016
|
|
|
1000
|
-
test("minimal.ts mcp without opt-in fails", async () => {
|
|
1001
|
-
const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
|
|
1017
|
+
test("minimal.ts ai mcp without opt-in fails", async () => {
|
|
1018
|
+
const { stderr, exitCode } = await $`bun run examples/minimal.ts ai mcp`.nothrow().quiet();
|
|
1002
1019
|
expect(exitCode).toBe(1);
|
|
1003
|
-
expect(stderr.toString()).toContain("
|
|
1020
|
+
expect(stderr.toString()).toContain("MCP is not enabled");
|
|
1004
1021
|
});
|
|
1005
1022
|
|
|
1006
1023
|
test("ctx.invocation is cli via cliRun", async () => {
|
|
@@ -1634,4 +1651,129 @@ test("mcpToolCallToArgv empty string varargs appends nothing", () => {
|
|
|
1634
1651
|
const read = tools.find((t) => t.name === "read")!;
|
|
1635
1652
|
const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: "" });
|
|
1636
1653
|
expect(argv).toEqual(["read"]);
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// ── AI builtins and skills ────────────────────────────────────────────────────
|
|
1657
|
+
|
|
1658
|
+
test("aiSkill on non-root node is rejected", () => {
|
|
1659
|
+
const root: CliCommand = {
|
|
1660
|
+
key: "app",
|
|
1661
|
+
description: "",
|
|
1662
|
+
commands: [
|
|
1663
|
+
{
|
|
1664
|
+
key: "x",
|
|
1665
|
+
description: "",
|
|
1666
|
+
aiSkill: { enabled: false },
|
|
1667
|
+
handler: () => {},
|
|
1668
|
+
},
|
|
1669
|
+
],
|
|
1670
|
+
};
|
|
1671
|
+
expect(() => cliValidateRoot(root)).toThrow(/aiSkill is only supported on the program root/);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
test("generateSkillBundle includes frontmatter and command catalog", () => {
|
|
1675
|
+
const bundle = generateSkillBundle(nestedMcpFixture, "cursor");
|
|
1676
|
+
expect(bundle.dirName).toBe("nested_ts");
|
|
1677
|
+
expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
|
|
1678
|
+
expect(bundle.skillMd).toContain("stat owner lookup");
|
|
1679
|
+
expect(bundle.skillMd).toContain("ai mcp");
|
|
1680
|
+
expect(bundle.referenceMd).toContain("```json");
|
|
1681
|
+
expect(() => JSON.parse(bundle.referenceMd.match(/```json\n([\s\S]*?)\n```/)![1]!)).not.toThrow();
|
|
1682
|
+
});
|
|
1683
|
+
|
|
1684
|
+
test("cliSkillInstall writes project Cursor skill files", () => {
|
|
1685
|
+
const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-"));
|
|
1686
|
+
const prev = process.cwd();
|
|
1687
|
+
process.chdir(cwd);
|
|
1688
|
+
try {
|
|
1689
|
+
const msg = cliSkillInstall(nestedMcpFixture, "cursor", { force: true });
|
|
1690
|
+
expect(msg).toContain(".cursor/skills/nested_ts/");
|
|
1691
|
+
const skillDir = join(cwd, ".cursor", "skills", "nested_ts");
|
|
1692
|
+
expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
|
|
1693
|
+
expect(existsSync(join(skillDir, "reference.md"))).toBe(true);
|
|
1694
|
+
expect(readFileSync(join(skillDir, "SKILL.md"), "utf8")).toContain("stat owner lookup");
|
|
1695
|
+
} finally {
|
|
1696
|
+
process.chdir(prev);
|
|
1697
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test("cliSkillInstall global uses HOME skills directory", () => {
|
|
1702
|
+
const home = mkdtempSync(join(tmpdir(), "argsbarg-home-"));
|
|
1703
|
+
const prevHome = process.env.HOME;
|
|
1704
|
+
process.env.HOME = home;
|
|
1705
|
+
try {
|
|
1706
|
+
const msg = cliSkillInstall(nestedMcpFixture, "cursor", { global: true, force: true });
|
|
1707
|
+
expect(msg).toContain(join(home, ".cursor", "skills", "nested_ts"));
|
|
1708
|
+
expect(existsSync(join(home, ".cursor", "skills", "nested_ts", "SKILL.md"))).toBe(true);
|
|
1709
|
+
} finally {
|
|
1710
|
+
if (prevHome === undefined) {
|
|
1711
|
+
delete process.env.HOME;
|
|
1712
|
+
} else {
|
|
1713
|
+
process.env.HOME = prevHome;
|
|
1714
|
+
}
|
|
1715
|
+
rmSync(home, { recursive: true, force: true });
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
test("cliSkillInstall fails when directory exists without force", () => {
|
|
1720
|
+
const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-dup-"));
|
|
1721
|
+
const prev = process.cwd();
|
|
1722
|
+
process.chdir(cwd);
|
|
1723
|
+
const prevExit = process.exit;
|
|
1724
|
+
let exitCode = 0;
|
|
1725
|
+
process.exit = ((code?: number) => {
|
|
1726
|
+
exitCode = code ?? 0;
|
|
1727
|
+
throw new Error("exit");
|
|
1728
|
+
}) as typeof process.exit;
|
|
1729
|
+
try {
|
|
1730
|
+
cliSkillInstall(nestedMcpFixture, "cursor", { force: true });
|
|
1731
|
+
try {
|
|
1732
|
+
cliSkillInstall(nestedMcpFixture, "cursor", {});
|
|
1733
|
+
} catch {
|
|
1734
|
+
// expected exit throw
|
|
1735
|
+
}
|
|
1736
|
+
expect(exitCode).toBe(1);
|
|
1737
|
+
} finally {
|
|
1738
|
+
process.exit = prevExit;
|
|
1739
|
+
process.chdir(prev);
|
|
1740
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
test("cliSkillInstall claude target uses .claude/skills", () => {
|
|
1745
|
+
const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-claude-"));
|
|
1746
|
+
const prev = process.cwd();
|
|
1747
|
+
process.chdir(cwd);
|
|
1748
|
+
try {
|
|
1749
|
+
const msg = cliSkillInstall(nestedMcpFixture, "claude", { force: true });
|
|
1750
|
+
expect(msg).toContain(".claude/skills/nested_ts/");
|
|
1751
|
+
expect(readFileSync(join(cwd, ".claude", "skills", "nested_ts", "SKILL.md"), "utf8")).toContain(
|
|
1752
|
+
"Claude Code",
|
|
1753
|
+
);
|
|
1754
|
+
} finally {
|
|
1755
|
+
process.chdir(prev);
|
|
1756
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
test("ai skill cursor fails when aiSkill disabled", async () => {
|
|
1761
|
+
const dir = mkdtempSync(join(tmpdir(), "argsbarg-skill-off-"));
|
|
1762
|
+
const script = join(dir, "skill-off.ts");
|
|
1763
|
+
writeFileSync(
|
|
1764
|
+
script,
|
|
1765
|
+
`import { cliRun, CliCommand } from ${JSON.stringify(join(import.meta.dir, "index.ts"))};
|
|
1766
|
+
const cli: CliCommand = {
|
|
1767
|
+
key: "offapp",
|
|
1768
|
+
description: "demo",
|
|
1769
|
+
aiSkill: { enabled: false },
|
|
1770
|
+
commands: [{ key: "x", description: "x", handler: () => {} }],
|
|
1771
|
+
};
|
|
1772
|
+
await cliRun(cli);
|
|
1773
|
+
`,
|
|
1774
|
+
"utf8",
|
|
1775
|
+
);
|
|
1776
|
+
const { stderr, exitCode } = await $`bun run ${script} ai skill cursor`.nothrow().quiet();
|
|
1777
|
+
expect(exitCode).toBe(1);
|
|
1778
|
+
expect(stderr.toString()).toContain("AI skills are disabled");
|
|
1637
1779
|
});
|
package/src/mcp/tools.ts
CHANGED
|
@@ -150,7 +150,7 @@ export function collectMcpTools(root: CliCommand): McpToolDef[] {
|
|
|
150
150
|
/** Walks the command tree and appends leaf tools. */
|
|
151
151
|
function walk(cmd: CliCommand, path: string[]): void {
|
|
152
152
|
if ("handler" in cmd && cmd.handler) {
|
|
153
|
-
if (cmd.key === "completion" || cmd.key === "
|
|
153
|
+
if (cmd.key === "completion" || cmd.key === "ai") {
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
if (cmd.mcpTool?.enabled === false) {
|
package/src/runtime.ts
CHANGED
|
@@ -7,9 +7,10 @@ It keeps execution flow out of the public barrel so the exported API stays small
|
|
|
7
7
|
the runtime responsibilities remain easy to reason about.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { cliBuiltinAiGroup, cliBuiltinCompletionGroup, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
11
|
import { CliContext } from "./context.ts";
|
|
12
12
|
import { cliHelpRender } from "./help.ts";
|
|
13
|
+
import { cliSkillInstall } from "./skill/install.ts";
|
|
13
14
|
import { cliMcpServeStdio } from "./mcp.ts";
|
|
14
15
|
import { parse, postParseValidate, ParseKind } from "./parse.ts";
|
|
15
16
|
import { cliSchemaJson } from "./schema.ts";
|
|
@@ -44,29 +45,27 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
44
45
|
process.exit(1);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (root.handler && argv.length >= 1 && argv[0] === "mcp" && !root.mcpServer) {
|
|
51
|
-
process.stderr.write("Unknown command: mcp\n");
|
|
48
|
+
if (argv.length >= 2 && argv[0] === "ai" && argv[1] === "mcp" && !root.mcpServer) {
|
|
49
|
+
process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
|
|
52
50
|
process.exit(1);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
let parseRoot = root;
|
|
54
|
+
let isLeafCompletionIntercept = false;
|
|
55
|
+
|
|
56
|
+
if (root.handler && argv.length >= 1 && argv[0] === "ai") {
|
|
59
57
|
parseRoot = {
|
|
60
58
|
key: root.key,
|
|
61
59
|
description: root.description,
|
|
62
|
-
commands: [
|
|
63
|
-
} as
|
|
64
|
-
} else if (root.handler && argv.length >= 1 && argv[0] === "
|
|
60
|
+
commands: [cliBuiltinAiGroup(root)],
|
|
61
|
+
} as CliCommand;
|
|
62
|
+
} else if (root.handler && argv.length >= 1 && argv[0] === "completion") {
|
|
63
|
+
isLeafCompletionIntercept = true;
|
|
65
64
|
parseRoot = {
|
|
66
65
|
key: root.key,
|
|
67
66
|
description: root.description,
|
|
68
|
-
commands: [
|
|
69
|
-
} as
|
|
67
|
+
commands: [cliBuiltinCompletionGroup(root.key)],
|
|
68
|
+
} as any;
|
|
70
69
|
} else {
|
|
71
70
|
parseRoot = cliRootMergedWithBuiltins(root);
|
|
72
71
|
}
|
|
@@ -109,16 +108,36 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
if (pr.path[0] === "
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
if (pr.path[0] === "ai") {
|
|
112
|
+
if (pr.path[1] === "mcp") {
|
|
113
|
+
if (!root.mcpServer) {
|
|
114
|
+
process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (pr.path.length !== 2) {
|
|
118
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
await cliMcpServeStdio(root);
|
|
122
|
+
} else if (pr.path[1] === "skill" && (pr.path[2] === "cursor" || pr.path[2] === "claude")) {
|
|
123
|
+
if (root.aiSkill?.enabled === false) {
|
|
124
|
+
process.stderr.write("AI skills are disabled. Remove aiSkill.enabled: false from the program root.\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
if (pr.path.length !== 3) {
|
|
128
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const msg = cliSkillInstall(root, pr.path[2], {
|
|
132
|
+
global: pr.opts.global === "1",
|
|
133
|
+
force: pr.opts.force === "1",
|
|
134
|
+
});
|
|
135
|
+
process.stderr.write(msg + "\n");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
} else {
|
|
138
|
+
process.stderr.write("Unknown subcommand: ai " + pr.path.slice(1).join(" ") + "\n");
|
|
119
139
|
process.exit(1);
|
|
120
140
|
}
|
|
121
|
-
await cliMcpServeStdio(root);
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
let current = parseRoot;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module generates Agent Skills content (SKILL.md + reference.md) from a CLI schema.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collectOptionDefs } from "../parse.ts";
|
|
6
|
+
import { cliSchemaJson } from "../schema.ts";
|
|
7
|
+
import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
|
|
8
|
+
import { CliCommand, CliOptionKind } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
export type SkillTarget = "cursor" | "claude";
|
|
11
|
+
|
|
12
|
+
export interface SkillBundle {
|
|
13
|
+
dirName: string;
|
|
14
|
+
skillMd: string;
|
|
15
|
+
referenceMd: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Truncates text to maxLen with ellipsis. */
|
|
19
|
+
function truncate(text: string, maxLen: number): string {
|
|
20
|
+
if (text.length <= maxLen) return text;
|
|
21
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Builds third-person skill description for YAML frontmatter. */
|
|
25
|
+
function skillDescription(root: CliCommand): string {
|
|
26
|
+
const tools = collectMcpTools(root);
|
|
27
|
+
const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
|
|
28
|
+
const sample = paths.slice(0, 5).join(", ");
|
|
29
|
+
const more = paths.length > 5 ? `, and ${paths.length - 5} more` : "";
|
|
30
|
+
const desc = `Operates the ${root.key} CLI (${sample}${more}). Use when the user mentions ${root.key}${paths.length > 0 ? `, ${paths.slice(0, 3).join(", ")}` : ""}, or related tasks.`;
|
|
31
|
+
return truncate(desc, 1024);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Formats one command line for the catalog section. */
|
|
35
|
+
function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcpTools>[number]): string {
|
|
36
|
+
const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
|
|
37
|
+
let line = `- **\`${cliPath}\`** — ${tool.description}`;
|
|
38
|
+
const opts = collectOptionDefs(root, tool.path);
|
|
39
|
+
const flags = opts.filter((o) => o.kind === CliOptionKind.Presence).map((o) => `--${o.name}`);
|
|
40
|
+
if (flags.length > 0) {
|
|
41
|
+
line += ` (flags: ${flags.join(", ")})`;
|
|
42
|
+
}
|
|
43
|
+
const enums = opts.filter((o) => o.kind === CliOptionKind.Enum && o.choices?.length);
|
|
44
|
+
for (const e of enums) {
|
|
45
|
+
line += ` (\`--${e.name}\`: ${e.choices!.join(" | ")})`;
|
|
46
|
+
}
|
|
47
|
+
const varargs = (tool.leaf.positionals ?? []).filter((p) => (p.argMax ?? 1) === 0);
|
|
48
|
+
if (varargs.length > 0) {
|
|
49
|
+
line += ` (varargs: ${varargs.map((p) => p.name).join(", ")})`;
|
|
50
|
+
}
|
|
51
|
+
return line;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Builds SKILL.md body for the given target. */
|
|
55
|
+
function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): string {
|
|
56
|
+
const name = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
|
|
57
|
+
const description = skillDescription(root);
|
|
58
|
+
const tools = collectMcpTools(root);
|
|
59
|
+
|
|
60
|
+
const lines: string[] = [
|
|
61
|
+
"---",
|
|
62
|
+
`name: ${name}`,
|
|
63
|
+
`description: ${description}`,
|
|
64
|
+
"---",
|
|
65
|
+
"",
|
|
66
|
+
`# ${root.key}`,
|
|
67
|
+
"",
|
|
68
|
+
root.description,
|
|
69
|
+
"",
|
|
70
|
+
"## When to use",
|
|
71
|
+
"",
|
|
72
|
+
`Use this skill when working with **${root.key}** — shell commands, automation, or agent tool calls for this application.`,
|
|
73
|
+
"",
|
|
74
|
+
"## Execution",
|
|
75
|
+
"",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (root.mcpServer !== undefined) {
|
|
79
|
+
lines.push(
|
|
80
|
+
"**Prefer MCP** when a host has the server connected:",
|
|
81
|
+
"",
|
|
82
|
+
"```bash",
|
|
83
|
+
`${root.key} ai mcp`,
|
|
84
|
+
"```",
|
|
85
|
+
"",
|
|
86
|
+
"Example Cursor `mcp.json` entry:",
|
|
87
|
+
"",
|
|
88
|
+
"```json",
|
|
89
|
+
JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
mcpServers: {
|
|
92
|
+
[root.mcpServer.name ?? root.key]: {
|
|
93
|
+
command: root.key,
|
|
94
|
+
args: ["ai", "mcp"],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
null,
|
|
99
|
+
2,
|
|
100
|
+
),
|
|
101
|
+
"```",
|
|
102
|
+
"",
|
|
103
|
+
"When MCP tools are available, use `tools/call` with flat JSON arguments. Read the schema resource for full shapes.",
|
|
104
|
+
"",
|
|
105
|
+
"Otherwise invoke via shell:",
|
|
106
|
+
"",
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push("Invoke via shell:", "");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("```bash", `${root.key} <subcommand> [options] [args]`, "```", "", "## Commands", "");
|
|
113
|
+
|
|
114
|
+
if (tools.length === 0) {
|
|
115
|
+
lines.push("(No leaf commands in schema.)", "");
|
|
116
|
+
} else {
|
|
117
|
+
for (const tool of tools) {
|
|
118
|
+
lines.push(formatCommandEntry(root, tool));
|
|
119
|
+
}
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lines.push(
|
|
124
|
+
"## Pitfalls",
|
|
125
|
+
"",
|
|
126
|
+
"- Use `--` before tokens that look like flags when they are positional arguments.",
|
|
127
|
+
"- Under MCP (`ctx.invocation === \"mcp\"`), child processes must not inherit stdout — use piped stdout.",
|
|
128
|
+
"- Required environment variables are listed per command in descriptions (`requires env`).",
|
|
129
|
+
"",
|
|
130
|
+
"## Reference",
|
|
131
|
+
"",
|
|
132
|
+
"See `reference.md` in this skill directory for the full `--schema` JSON export.",
|
|
133
|
+
"",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (target === "cursor") {
|
|
137
|
+
lines.push(
|
|
138
|
+
"## Cursor install location",
|
|
139
|
+
"",
|
|
140
|
+
`- Project: \`.cursor/skills/${dirName}/\``,
|
|
141
|
+
`- Global: \`~/.cursor/skills/${dirName}/\``,
|
|
142
|
+
"",
|
|
143
|
+
"Do not install under `~/.cursor/skills-cursor/` (reserved for Cursor built-ins).",
|
|
144
|
+
"",
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
lines.push(
|
|
148
|
+
"## Claude Code",
|
|
149
|
+
"",
|
|
150
|
+
`- Invoke with \`/${dirName}\` or let Claude auto-match from the description.`,
|
|
151
|
+
`- Project skills: \`.claude/skills/${dirName}/\``,
|
|
152
|
+
`- Global skills: \`~/.claude/skills/${dirName}/\``,
|
|
153
|
+
`- Bundled files in this directory are available via \`\${CLAUDE_SKILL_DIR}\` when the skill runs.`,
|
|
154
|
+
"",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Builds reference.md with pretty-printed schema JSON. */
|
|
162
|
+
function buildReferenceMd(root: CliCommand): string {
|
|
163
|
+
return [
|
|
164
|
+
`# ${root.key} — CLI reference`,
|
|
165
|
+
"",
|
|
166
|
+
"Generated from the program `--schema` export. Handlers and runtime-only nodes are omitted.",
|
|
167
|
+
"",
|
|
168
|
+
"```json",
|
|
169
|
+
cliSchemaJson(root).trimEnd(),
|
|
170
|
+
"```",
|
|
171
|
+
"",
|
|
172
|
+
].join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Generates SKILL.md and reference.md for Cursor or Claude Code. */
|
|
176
|
+
export function generateSkillBundle(root: CliCommand, target: SkillTarget): SkillBundle {
|
|
177
|
+
const dirName = root.aiSkill?.name ?? sanitizeToolSegment(root.key);
|
|
178
|
+
return {
|
|
179
|
+
dirName,
|
|
180
|
+
skillMd: buildSkillMd(root, target, dirName),
|
|
181
|
+
referenceMd: buildReferenceMd(root),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module installs generated Agent Skills to Cursor or Claude Code skill directories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { CliCommand } from "../types.ts";
|
|
9
|
+
import { generateSkillBundle, type SkillTarget } from "./generate.ts";
|
|
10
|
+
|
|
11
|
+
export interface SkillInstallOpts {
|
|
12
|
+
global?: boolean;
|
|
13
|
+
force?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Resolves the user home directory (`$HOME` when set). */
|
|
17
|
+
function userHome(): string {
|
|
18
|
+
return process.env.HOME ?? homedir();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolves the install directory for a skill target. */
|
|
22
|
+
function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean): string {
|
|
23
|
+
const base = global
|
|
24
|
+
? join(userHome(), target === "cursor" ? ".cursor" : ".claude", "skills")
|
|
25
|
+
: join(process.cwd(), target === "cursor" ? ".cursor" : ".claude", "skills");
|
|
26
|
+
return join(base, dirName);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Writes SKILL.md and reference.md to the target skills directory. */
|
|
30
|
+
export function cliSkillInstall(root: CliCommand, target: SkillTarget, opts: SkillInstallOpts): string {
|
|
31
|
+
const bundle = generateSkillBundle(root, target);
|
|
32
|
+
const dir = resolveSkillDir(target, bundle.dirName, opts.global ?? false);
|
|
33
|
+
|
|
34
|
+
if (existsSync(dir) && !opts.force) {
|
|
35
|
+
process.stderr.write(`Skill directory already exists: ${dir}\nUse --force to overwrite.\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
writeFileSync(join(dir, "SKILL.md"), bundle.skillMd, "utf8");
|
|
41
|
+
writeFileSync(join(dir, "reference.md"), bundle.referenceMd, "utf8");
|
|
42
|
+
|
|
43
|
+
const label = target === "cursor" ? "Cursor" : "Claude Code";
|
|
44
|
+
return `Installed ${label} skill to ${dir}/`;
|
|
45
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -90,7 +90,7 @@ export interface CliPositional {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
* Root-only. Enables `myapp mcp` and MCP stdio server metadata.
|
|
93
|
+
* Root-only. Enables `myapp ai mcp` and MCP stdio server metadata.
|
|
94
94
|
*/
|
|
95
95
|
export interface CliMcpServerConfig {
|
|
96
96
|
/** `initialize` serverInfo.name (default: root `key`). */
|
|
@@ -153,6 +153,16 @@ export interface CliMcpToolConfig {
|
|
|
153
153
|
requiresEnv?: string[];
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Root-only. Opt out of `ai skill` install commands with `{ enabled: false }`.
|
|
158
|
+
*/
|
|
159
|
+
export interface CliAiSkillConfig {
|
|
160
|
+
/** When `false`, disable `ai skill *` install commands (default: enabled). */
|
|
161
|
+
enabled?: boolean;
|
|
162
|
+
/** Skill directory name (default: sanitized root `key`). */
|
|
163
|
+
name?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
156
166
|
/**
|
|
157
167
|
* Base properties shared by all command nodes.
|
|
158
168
|
*/
|
|
@@ -165,8 +175,10 @@ export interface CliCommandBase {
|
|
|
165
175
|
notes?: string;
|
|
166
176
|
/** Global or command-level flags/options. */
|
|
167
177
|
options?: CliOption[];
|
|
168
|
-
/** Root-only. When set, enables the `mcp` built-in subcommand. */
|
|
178
|
+
/** Root-only. When set, enables the `ai mcp` built-in subcommand. */
|
|
169
179
|
mcpServer?: CliMcpServerConfig;
|
|
180
|
+
/** Root-only. Opt out of `ai skill` install with `{ enabled: false }`. */
|
|
181
|
+
aiSkill?: CliAiSkillConfig;
|
|
170
182
|
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
171
183
|
mcpTool?: CliMcpToolConfig;
|
|
172
184
|
}
|
package/src/validate.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./types.ts";
|
|
15
15
|
import { MCP_SCHEMA_URI_DEFAULT } from "./mcp/tools.ts";
|
|
16
16
|
|
|
17
|
-
const reservedCommandNames = ["completion", "
|
|
17
|
+
const reservedCommandNames = ["completion", "ai"];
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Validates the static CliCommand tree against ArgBarg rules.
|
|
@@ -41,6 +41,12 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
if (!isRoot && cmd.aiSkill !== undefined) {
|
|
45
|
+
throw new CliSchemaValidationError(
|
|
46
|
+
"aiSkill is only supported on the program root (not on " + cmd.key + ")",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
const isLeaf = "handler" in cmd && !!cmd.handler;
|
|
45
51
|
if (!isLeaf && cmd.mcpTool !== undefined) {
|
|
46
52
|
throw new CliSchemaValidationError(
|