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.
@@ -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.2...HEAD
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
- - **`mcp`** — when `mcpServer: {}` is set on the program root, run an MCP server over stdio (`myapp mcp`).
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 **`mcp`** — it is reserved when MCP is enabled.
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"]` only.
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`** is the subcommand that starts the server.
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`, `mcp`) are not exposed as tools.
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 **`mcp`** — it is reserved for the built-in subcommand.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "//just": "echo this app uses justfile for development tasks"
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
- const cmds: CliCommand[] = [cliBuiltinCompletionGroup(root.key)];
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
- cmds.push(cliBuiltinMcpCommand());
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: "mcp",
613
- description: "Run as an MCP server over stdio (for AI agents).",
614
- handler: () => {},
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("mcp");
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 mcp is rejected", () => {
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: "mcp",
818
+ key: "ai",
817
819
  description: "bad",
818
820
  handler: () => {},
819
821
  },
820
822
  ],
821
823
  };
822
- expect(() => cliValidateRoot(root)).toThrow(/Reserved command name: mcp/);
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("mcp");
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 === "mcp") {
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 { cliBuiltinCompletionGroup, cliBuiltinMcpCommand, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
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
- let parseRoot = root;
48
- let isLeafCompletionIntercept = false;
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
- // Intercept completion for Leaf roots (since they can't natively have a completion subcommand)
56
- // but wrap them in a dummy router so that the parser handles `-h` and errors correctly.
57
- if (root.handler && argv.length >= 1 && argv[0] === "completion") {
58
- isLeafCompletionIntercept = true;
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: [cliBuiltinCompletionGroup(root.key)],
63
- } as any;
64
- } else if (root.handler && argv.length >= 1 && argv[0] === "mcp" && root.mcpServer) {
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: [cliBuiltinMcpCommand()],
69
- } as CliCommand;
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] === "mcp") {
113
- if (!root.mcpServer) {
114
- process.stderr.write("Internal error: mcp not enabled.\n");
115
- process.exit(1);
116
- }
117
- if (pr.path.length !== 1) {
118
- process.stderr.write("Unknown subcommand: mcp " + pr.path.slice(1).join(" ") + "\n");
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", "mcp"];
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(