argsbarg 1.4.1 → 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/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
@@ -28,21 +28,20 @@ export enum CliOptionKind {
28
28
  }
29
29
 
30
30
  /**
31
- * When fallbackCommand is used for missing or unknown top-level tokens.
32
- * Only the program root may set a non-default mode or a non-nil fallbackCommand.
31
+ * When `fallbackCommand` is used for missing or unknown subcommand tokens at a routing node.
33
32
  */
34
33
  export enum CliFallbackMode {
35
34
  /**
36
- * If argv has no first subcommand, route to `fallbackCommand`; if the first token is unknown, error.
35
+ * If argv has no next subcommand, route to `fallbackCommand`; if the token is unknown, error.
37
36
  */
38
37
  MissingOnly = "missingOnly",
39
38
  /**
40
- * If argv has no first subcommand or the first token is not a known child, route to `fallbackCommand`.
39
+ * If argv has no next subcommand or the token is not a known child, route to `fallbackCommand`.
41
40
  */
42
41
  MissingOrUnknown = "missingOrUnknown",
43
42
  /**
44
- * If the first token is present but not a known child, route to `fallbackCommand`.
45
- * When the first subcommand token is missing (empty argv), do not use fallback (implicit root help).
43
+ * If the next token is present but not a known child, route to `fallbackCommand`.
44
+ * When the subcommand token is missing (exhausted argv), do not use fallback (implicit scoped help).
46
45
  */
47
46
  UnknownOnly = "unknownOnly",
48
47
  }
@@ -91,7 +90,7 @@ export interface CliPositional {
91
90
  }
92
91
 
93
92
  /**
94
- * Root-only. Enables `myapp mcp` and MCP stdio server metadata.
93
+ * Root-only. Enables `myapp ai mcp` and MCP stdio server metadata.
95
94
  */
96
95
  export interface CliMcpServerConfig {
97
96
  /** `initialize` serverInfo.name (default: root `key`). */
@@ -154,6 +153,16 @@ export interface CliMcpToolConfig {
154
153
  requiresEnv?: string[];
155
154
  }
156
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
+
157
166
  /**
158
167
  * Base properties shared by all command nodes.
159
168
  */
@@ -166,8 +175,10 @@ export interface CliCommandBase {
166
175
  notes?: string;
167
176
  /** Global or command-level flags/options. */
168
177
  options?: CliOption[];
169
- /** Root-only. When set, enables the `mcp` built-in subcommand. */
178
+ /** Root-only. When set, enables the `ai mcp` built-in subcommand. */
170
179
  mcpServer?: CliMcpServerConfig;
180
+ /** Root-only. Opt out of `ai skill` install with `{ enabled: false }`. */
181
+ aiSkill?: CliAiSkillConfig;
171
182
  /** Leaf-only. Per-tool MCP exposure and metadata. */
172
183
  mcpTool?: CliMcpToolConfig;
173
184
  }
@@ -186,17 +197,17 @@ export type CliCommand =
186
197
  positionals?: CliPositional[];
187
198
  /** Nested subcommands (empty for leaf commands). */
188
199
  commands?: never;
189
- /** Default top-level subcommand (routing commands only). */
200
+ /** Default subcommand (routing commands only). */
190
201
  fallbackCommand?: never;
191
- /** How fallbackCommand is applied (routing commands only). */
202
+ /** How fallbackCommand is applied at this routing node (routing commands only). */
192
203
  fallbackMode?: never;
193
204
  })
194
205
  | (CliCommandBase & {
195
206
  /** Nested subcommands. */
196
207
  commands: CliCommand[];
197
- /** Default top-level subcommand when argv omits a command or uses an unknown first token. */
208
+ /** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
198
209
  fallbackCommand?: string;
199
- /** How fallbackCommand is applied. */
210
+ /** How fallbackCommand is applied at this routing node (not root-only). */
200
211
  fallbackMode?: CliFallbackMode;
201
212
  /** Handler function (leaf commands only). */
202
213
  handler?: never;
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.
@@ -35,25 +35,15 @@ export function cliValidateRoot(root: CliCommand): void {
35
35
 
36
36
  /** Recursively validates a command node: handlers vs children, options, and positionals. */
37
37
  function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
38
- // Fallback only on root
39
- if (!isRoot && cmd.fallbackCommand !== undefined) {
40
- throw new CliSchemaValidationError(
41
- "Fallback is only supported on the program root (not on " + cmd.key + ")",
42
- );
43
- }
44
- if (
45
- !isRoot &&
46
- cmd.fallbackMode !== undefined &&
47
- cmd.fallbackMode !== CliFallbackMode.MissingOnly
48
- ) {
38
+ if (!isRoot && cmd.mcpServer !== undefined) {
49
39
  throw new CliSchemaValidationError(
50
- "fallbackMode may only be set on the program root (not on " + cmd.key + ")",
40
+ "mcpServer is only supported on the program root (not on " + cmd.key + ")",
51
41
  );
52
42
  }
53
43
 
54
- if (!isRoot && cmd.mcpServer !== undefined) {
44
+ if (!isRoot && cmd.aiSkill !== undefined) {
55
45
  throw new CliSchemaValidationError(
56
- "mcpServer is only supported on the program root (not on " + cmd.key + ")",
46
+ "aiSkill is only supported on the program root (not on " + cmd.key + ")",
57
47
  );
58
48
  }
59
49
 
@@ -89,6 +79,22 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
89
79
  seenNames.add(child.key);
90
80
  }
91
81
 
82
+ if (cmd.fallbackMode !== undefined && cmd.fallbackCommand === undefined) {
83
+ throw new CliSchemaValidationError(
84
+ `fallbackMode requires fallbackCommand on '${cmd.key}'`,
85
+ );
86
+ }
87
+
88
+ if (cmd.fallbackCommand !== undefined) {
89
+ const children = cmd.commands ?? [];
90
+ const valid = children.find((c) => c.key === cmd.fallbackCommand);
91
+ if (!valid) {
92
+ throw new CliSchemaValidationError(
93
+ `fallbackCommand '${cmd.fallbackCommand}' is not a child of '${cmd.key}'`,
94
+ );
95
+ }
96
+ }
97
+
92
98
  // Validate options (short name uniqueness, reserved -h, required presence)
93
99
  const seenShorts = new Set<string>();
94
100
  for (const opt of cmd.options ?? []) {