argsbarg 1.4.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
  2. package/.private/scratch.md +1 -1
  3. package/CHANGELOG.md +39 -1
  4. package/README.md +29 -21
  5. package/docs/ai-skills.md +24 -52
  6. package/docs/install.md +84 -0
  7. package/docs/mcp.md +8 -8
  8. package/examples/mcp-test.ts +3 -3
  9. package/examples/minimal.ts +3 -3
  10. package/examples/nested.ts +3 -3
  11. package/examples/option-required.ts +3 -3
  12. package/index.d.ts +44 -50
  13. package/package.json +1 -1
  14. package/src/builtins/builtins.test.ts +101 -0
  15. package/src/builtins/completion-bash.ts +240 -0
  16. package/src/builtins/completion-fish.ts +73 -0
  17. package/src/builtins/completion-group.ts +50 -0
  18. package/src/builtins/completion-zsh.ts +244 -0
  19. package/src/builtins/dispatch.ts +138 -0
  20. package/src/builtins/export.ts +53 -0
  21. package/src/builtins/index.ts +10 -0
  22. package/src/builtins/install.ts +99 -0
  23. package/src/builtins/mcp.ts +13 -0
  24. package/src/builtins/presentation.ts +50 -0
  25. package/src/builtins/scopes.ts +46 -0
  26. package/src/builtins/shell-helpers.ts +24 -0
  27. package/src/capabilities.ts +32 -0
  28. package/src/completion.ts +10 -693
  29. package/src/context.ts +21 -6
  30. package/src/help.ts +21 -9
  31. package/src/index.test.ts +114 -118
  32. package/src/index.ts +2 -1
  33. package/src/install/binary.ts +82 -0
  34. package/src/install/compiled.ts +15 -0
  35. package/src/install/completions.ts +52 -0
  36. package/src/install/detect-installed.ts +67 -0
  37. package/src/install/index.ts +196 -0
  38. package/src/install/install.test.ts +124 -0
  39. package/src/install/mcp-config.ts +70 -0
  40. package/src/install/paths.ts +69 -0
  41. package/src/install/plan.ts +183 -0
  42. package/src/install/shell.ts +56 -0
  43. package/src/install/status.ts +63 -0
  44. package/src/install/uninstall.ts +111 -0
  45. package/src/invoke.ts +14 -5
  46. package/src/mcp/server.ts +3 -3
  47. package/src/mcp/tools.ts +17 -17
  48. package/src/mcp.ts +2 -2
  49. package/src/parse.ts +55 -27
  50. package/src/runtime.ts +47 -100
  51. package/src/schema.ts +10 -52
  52. package/src/skill/generate.ts +10 -10
  53. package/src/skill/install.ts +21 -19
  54. package/src/types.test.ts +40 -0
  55. package/src/types.ts +59 -49
  56. package/src/validate.ts +89 -83
  57. package/src/ai.ts +0 -7
@@ -0,0 +1,183 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliProgram } from "../types.ts";
4
+ import { installBinary } from "./binary.ts";
5
+ import { installCompletions } from "./completions.ts";
6
+ import { detectInstalledArtifacts } from "./detect-installed.ts";
7
+ import { expectedMcpEntry, mergeMcpConfig } from "./mcp-config.ts";
8
+ import { InstallPaths, userHome } from "./paths.ts";
9
+ import { detectShells } from "./shell.ts";
10
+
11
+ export interface InstallOpts {
12
+ all?: boolean;
13
+ bin?: boolean;
14
+ completions?: boolean;
15
+ skill?: boolean;
16
+ mcp?: boolean;
17
+ update?: boolean;
18
+ status?: boolean;
19
+ uninstall?: boolean;
20
+ yes?: boolean;
21
+ dry?: boolean;
22
+ json?: boolean;
23
+ quiet?: boolean;
24
+ prefix?: string;
25
+ }
26
+
27
+ export type InstallActionKind = "binary" | "completions" | "cursor-skill" | "claude-skill" | "cursor-mcp" | "claude-mcp";
28
+
29
+ export interface InstallAction {
30
+ kind: InstallActionKind;
31
+ summary: string;
32
+ message: string;
33
+ run: () => string[];
34
+ }
35
+
36
+ function wantsBin(opts: InstallOpts): boolean {
37
+ return !!(opts.all || opts.bin || opts.update);
38
+ }
39
+
40
+ function wantsCompletions(opts: InstallOpts): boolean {
41
+ return !!(opts.all || opts.completions);
42
+ }
43
+
44
+ function wantsSkill(opts: InstallOpts): boolean {
45
+ return !!(opts.all || opts.skill);
46
+ }
47
+
48
+ function wantsMcp(opts: InstallOpts, root: CliProgram): boolean {
49
+ return !!(opts.all || opts.mcp) && root.mcpServer !== undefined;
50
+ }
51
+
52
+ /** Builds install actions for normal mode (--all / scoped targets). */
53
+ export function buildInstallPlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
54
+ const actions: InstallAction[] = [];
55
+ const dry = !!opts.dry;
56
+
57
+ if (wantsBin(opts)) {
58
+ actions.push({
59
+ kind: "binary",
60
+ summary: `binary: ${paths.binaryPath}`,
61
+ message: `Installing binary to ${paths.binaryPath}`,
62
+ run: () => installBinary(root, paths, dry).changedFiles,
63
+ });
64
+ }
65
+
66
+ if (wantsCompletions(opts)) {
67
+ const shells = detectShells();
68
+ if (shells.bash) {
69
+ actions.push({
70
+ kind: "completions",
71
+ summary: `bash completion: ${paths.bashCompletion}`,
72
+ message: `Writing bash completion to ${paths.bashCompletion}`,
73
+ run: () => {
74
+ const all = installCompletions(root, paths, dry);
75
+ return all.filter((p) => p === paths.bashCompletion);
76
+ },
77
+ });
78
+ }
79
+ if (shells.zsh) {
80
+ actions.push({
81
+ kind: "completions",
82
+ summary: `zsh completion: ${paths.zshCompletion}`,
83
+ message: `Writing zsh completion to ${paths.zshCompletion}`,
84
+ run: () => {
85
+ const all = installCompletions(root, paths, dry);
86
+ return all.filter((p) => p === paths.zshCompletion);
87
+ },
88
+ });
89
+ }
90
+ if (shells.fish) {
91
+ actions.push({
92
+ kind: "completions",
93
+ summary: `fish completion: ${paths.fishCompletion}`,
94
+ message: `Writing fish completion to ${paths.fishCompletion}`,
95
+ run: () => {
96
+ const all = installCompletions(root, paths, dry);
97
+ return all.filter((p) => p === paths.fishCompletion);
98
+ },
99
+ });
100
+ }
101
+ }
102
+
103
+ if (wantsSkill(opts)) {
104
+ const home = userHome();
105
+ if (existsSync(join(home, ".cursor"))) {
106
+ actions.push({
107
+ kind: "cursor-skill",
108
+ summary: `cursor skill: ${paths.cursorSkillDir}/`,
109
+ message: `Installing Cursor skill to ${paths.cursorSkillDir}/`,
110
+ run: () => [],
111
+ });
112
+ }
113
+ if (existsSync(join(home, ".claude"))) {
114
+ actions.push({
115
+ kind: "claude-skill",
116
+ summary: `claude skill: ${paths.claudeSkillDir}/`,
117
+ message: `Installing Claude Code skill to ${paths.claudeSkillDir}/`,
118
+ run: () => [],
119
+ });
120
+ }
121
+ }
122
+
123
+ if (wantsMcp(opts, root)) {
124
+ const entry = expectedMcpEntry(root);
125
+ if (existsSync(join(userHome(), ".cursor"))) {
126
+ actions.push({
127
+ kind: "cursor-mcp",
128
+ summary: `cursor mcp: ${paths.cursorMcpPath} (server "${paths.mcpName}")`,
129
+ message: `Merging MCP server "${paths.mcpName}" into ${paths.cursorMcpPath}`,
130
+ run: () => {
131
+ mergeMcpConfig(paths.cursorMcpPath, paths.mcpName, entry, dry);
132
+ return [paths.cursorMcpPath];
133
+ },
134
+ });
135
+ }
136
+ actions.push({
137
+ kind: "claude-mcp",
138
+ summary: `claude mcp: ${paths.claudeMcpPath} (server "${paths.mcpName}")`,
139
+ message: `Merging MCP server "${paths.mcpName}" into ${paths.claudeMcpPath}`,
140
+ run: () => {
141
+ mergeMcpConfig(paths.claudeMcpPath, paths.mcpName, entry, dry);
142
+ return [paths.claudeMcpPath];
143
+ },
144
+ });
145
+ }
146
+
147
+ return actions;
148
+ }
149
+
150
+ /** Builds update actions for artifacts already installed. */
151
+ export function buildUpdatePlan(root: CliProgram, paths: InstallPaths, opts: InstallOpts): InstallAction[] {
152
+ const detected = detectInstalledArtifacts(paths);
153
+ const scoped: InstallOpts = {
154
+ bin: true,
155
+ completions: detected.bashCompletion || detected.zshCompletion || detected.fishCompletion,
156
+ skill: detected.cursorSkill || detected.claudeSkill,
157
+ mcp: (detected.cursorMcp || detected.claudeMcp) && root.mcpServer !== undefined,
158
+ dry: opts.dry,
159
+ };
160
+ const plan = buildInstallPlan(root, paths, scoped);
161
+
162
+ return plan.filter((action) => {
163
+ switch (action.kind) {
164
+ case "binary":
165
+ return true;
166
+ case "completions":
167
+ if (action.summary.startsWith("bash")) return detected.bashCompletion;
168
+ if (action.summary.startsWith("zsh")) return detected.zshCompletion;
169
+ if (action.summary.startsWith("fish")) return detected.fishCompletion;
170
+ return false;
171
+ case "cursor-skill":
172
+ return detected.cursorSkill;
173
+ case "claude-skill":
174
+ return detected.claudeSkill;
175
+ case "cursor-mcp":
176
+ return detected.cursorMcp;
177
+ case "claude-mcp":
178
+ return detected.claudeMcp;
179
+ default:
180
+ return false;
181
+ }
182
+ });
183
+ }
@@ -0,0 +1,56 @@
1
+ export interface ShellDetection {
2
+ bash: boolean;
3
+ zsh: boolean;
4
+ fish: boolean;
5
+ }
6
+
7
+ /** Detects which shells are available on PATH. */
8
+ export function detectShells(): ShellDetection {
9
+ return {
10
+ bash: Bun.which("bash") !== null,
11
+ zsh: Bun.which("zsh") !== null,
12
+ fish: Bun.which("fish") !== null,
13
+ };
14
+ }
15
+
16
+ export function rcMarkerStart(appKey: string, tag: string): string {
17
+ return `# ${appKey}:${tag}`;
18
+ }
19
+
20
+ export function rcMarkerEnd(appKey: string, tag: string): string {
21
+ return `# end ${appKey}:${tag}`;
22
+ }
23
+
24
+ /** Returns rc snippet block for PATH, or null if already present. */
25
+ export function buildPathRcBlock(appKey: string, bindir: string): string {
26
+ const start = rcMarkerStart(appKey, "path");
27
+ const end = rcMarkerEnd(appKey, "path");
28
+ return [start, `export PATH="${bindir}:$PATH"`, end].join("\n");
29
+ }
30
+
31
+ /** Returns rc snippet block for zsh fpath, or null if already present. */
32
+ export function buildZshFpathRcBlock(appKey: string, completionsDir: string): string {
33
+ const start = rcMarkerStart(appKey, "fpath");
34
+ const end = rcMarkerEnd(appKey, "fpath");
35
+ return [start, `fpath=(${completionsDir} $fpath)`, end].join("\n");
36
+ }
37
+
38
+ /** Removes a marker-delimited block from rc file content. */
39
+ export function removeRcBlock(content: string, appKey: string, tag: string): string {
40
+ const start = rcMarkerStart(appKey, tag);
41
+ const end = rcMarkerEnd(appKey, tag);
42
+ const re = new RegExp(
43
+ `${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`,
44
+ "g",
45
+ );
46
+ return content.replace(re, "");
47
+ }
48
+
49
+ /** Returns true when the marker block already exists in content. */
50
+ export function hasRcBlock(content: string, appKey: string, tag: string): boolean {
51
+ return content.includes(rcMarkerStart(appKey, tag));
52
+ }
53
+
54
+ function escapeRegExp(s: string): string {
55
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ }
@@ -0,0 +1,63 @@
1
+ import { CliProgram } from "../types.ts";
2
+ import { buildInstallStatus, detectInstalledArtifacts } from "./detect-installed.ts";
3
+ import type { InstallOpts } from "./plan.ts";
4
+ import { resolveInstallPaths } from "./paths.ts";
5
+
6
+ export function installOut(msg: string, opts: InstallOpts): void {
7
+ if (opts.quiet || opts.json) return;
8
+ process.stdout.write(msg + "\n");
9
+ }
10
+
11
+ export function installInfo(msg: string, opts: InstallOpts): void {
12
+ if (opts.quiet) return;
13
+ if (opts.json && !opts.dry) return;
14
+ const prefix = opts.dry ? "[dry run] " : "";
15
+ process.stderr.write(prefix + msg + "\n");
16
+ }
17
+
18
+ export function installErr(msg: string): void {
19
+ process.stderr.write(msg + "\n");
20
+ }
21
+
22
+ /** Prints install status to stdout (human or JSON). */
23
+ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
24
+ const paths = resolveInstallPaths(root, opts);
25
+ const detected = detectInstalledArtifacts(paths);
26
+ const status = buildInstallStatus(paths, detected);
27
+
28
+ if (opts.json) {
29
+ const json: Record<string, string> = {};
30
+ if (status.binary) json.binary = status.binary;
31
+ if (status.bashCompletion) json.bashCompletion = status.bashCompletion;
32
+ if (status.zshCompletion) json.zshCompletion = status.zshCompletion;
33
+ if (status.fishCompletion) json.fishCompletion = status.fishCompletion;
34
+ if (status.cursorSkill) json.cursorSkill = status.cursorSkill;
35
+ if (status.claudeSkill) json.claudeSkill = status.claudeSkill;
36
+ if (status.cursorMcp) json.cursorMcp = status.cursorMcp;
37
+ if (status.claudeMcp) json.claudeMcp = status.claudeMcp;
38
+ process.stdout.write(JSON.stringify(json, null, 2) + "\n");
39
+ return;
40
+ }
41
+
42
+ installOut(`Installed artifacts for ${root.key}:`, opts);
43
+ const lines: [string, string | undefined][] = [
44
+ ["binary", status.binary],
45
+ ["bash completion", status.bashCompletion],
46
+ ["zsh completion", status.zshCompletion],
47
+ ["fish completion", status.fishCompletion],
48
+ ["cursor skill", status.cursorSkill],
49
+ ["claude skill", status.claudeSkill],
50
+ ["cursor mcp", status.cursorMcp],
51
+ ["claude mcp", status.claudeMcp],
52
+ ];
53
+ let any = false;
54
+ for (const [label, value] of lines) {
55
+ if (value) {
56
+ installOut(` ${label}: ${value}`, opts);
57
+ any = true;
58
+ }
59
+ }
60
+ if (!any) {
61
+ installOut(" (none detected)", opts);
62
+ }
63
+ }
@@ -0,0 +1,111 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CliProgram } from "../types.ts";
4
+ import { uninstallBinary } from "./binary.ts";
5
+ import { uninstallCompletions } from "./completions.ts";
6
+ import { detectInstalledArtifacts } from "./detect-installed.ts";
7
+ import { removeMcpConfig } from "./mcp-config.ts";
8
+ import { InstallPaths, userHome } from "./paths.ts";
9
+ import type { InstallOpts } from "./plan.ts";
10
+
11
+ export interface UninstallAction {
12
+ summary: string;
13
+ message: string;
14
+ run: () => string[];
15
+ }
16
+
17
+ function scopeAll(opts: InstallOpts): boolean {
18
+ return !opts.bin && !opts.completions && !opts.skill && !opts.mcp;
19
+ }
20
+
21
+ /** Builds uninstall actions from detected artifacts. */
22
+ export function buildUninstallPlan(
23
+ root: CliProgram,
24
+ paths: InstallPaths,
25
+ opts: InstallOpts,
26
+ ): UninstallAction[] {
27
+ const detected = detectInstalledArtifacts(paths);
28
+ const all = scopeAll(opts);
29
+ const dry = !!opts.dry;
30
+ const actions: UninstallAction[] = [];
31
+
32
+ if ((all || opts.bin) && detected.binary) {
33
+ actions.push({
34
+ summary: `binary: ${paths.binaryPath}`,
35
+ message: `Removing binary ${paths.binaryPath}`,
36
+ run: () => uninstallBinary(root, paths, dry),
37
+ });
38
+ }
39
+
40
+ if ((all || opts.completions) && (detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)) {
41
+ if (detected.bashCompletion) {
42
+ actions.push({
43
+ summary: `bash completion: ${paths.bashCompletion}`,
44
+ message: `Removing bash completion ${paths.bashCompletion}`,
45
+ run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.bashCompletion),
46
+ });
47
+ }
48
+ if (detected.zshCompletion) {
49
+ actions.push({
50
+ summary: `zsh completion: ${paths.zshCompletion}`,
51
+ message: `Removing zsh completion ${paths.zshCompletion}`,
52
+ run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.zshCompletion),
53
+ });
54
+ }
55
+ if (detected.fishCompletion) {
56
+ actions.push({
57
+ summary: `fish completion: ${paths.fishCompletion}`,
58
+ message: `Removing fish completion ${paths.fishCompletion}`,
59
+ run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.fishCompletion),
60
+ });
61
+ }
62
+ }
63
+
64
+ if ((all || opts.skill) && detected.cursorSkill) {
65
+ actions.push({
66
+ summary: `cursor skill: ${paths.cursorSkillDir}/`,
67
+ message: `Removing Cursor skill ${paths.cursorSkillDir}/`,
68
+ run: () => [],
69
+ });
70
+ }
71
+
72
+ if ((all || opts.skill) && detected.claudeSkill) {
73
+ actions.push({
74
+ summary: `claude skill: ${paths.claudeSkillDir}/`,
75
+ message: `Removing Claude Code skill ${paths.claudeSkillDir}/`,
76
+ run: () => [],
77
+ });
78
+ }
79
+
80
+ if ((all || opts.mcp) && root.mcpServer !== undefined) {
81
+ if (detected.cursorMcp) {
82
+ actions.push({
83
+ summary: `cursor mcp: ${paths.cursorMcpPath}`,
84
+ message: `Removing MCP server "${paths.mcpName}" from ${paths.cursorMcpPath}`,
85
+ run: () => {
86
+ removeMcpConfig(paths.cursorMcpPath, paths.mcpName, dry);
87
+ return [paths.cursorMcpPath];
88
+ },
89
+ });
90
+ }
91
+ if (detected.claudeMcp) {
92
+ actions.push({
93
+ summary: `claude mcp: ${paths.claudeMcpPath}`,
94
+ message: `Removing MCP server "${paths.mcpName}" from ${paths.claudeMcpPath}`,
95
+ run: () => {
96
+ removeMcpConfig(paths.claudeMcpPath, paths.mcpName, dry);
97
+ return [paths.claudeMcpPath];
98
+ },
99
+ });
100
+ }
101
+ }
102
+
103
+ return actions;
104
+ }
105
+
106
+ /** Rimraf skill directories during uninstall. */
107
+ export function uninstallSkillDir(dir: string, dry: boolean): string[] {
108
+ if (!existsSync(dir)) return [];
109
+ if (!dry) rmSync(dir, { recursive: true, force: true });
110
+ return [dir + "/"];
111
+ }
package/src/invoke.ts CHANGED
@@ -6,7 +6,7 @@ process.exit so MCP tool calls can run handlers repeatedly.
6
6
 
7
7
  import { CliContext } from "./context.ts";
8
8
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
9
- import { CliCommand } from "./types.ts";
9
+ import { type CliNode, type CliProgram, isCliRouter } from "./types.ts";
10
10
  import { format } from "node:util";
11
11
 
12
12
  /** Outcome of a non-exiting CLI invocation. */
@@ -40,7 +40,7 @@ class CliInvokeExit extends Error {
40
40
  }
41
41
 
42
42
  /** Looks up a subcommand or routing node by `key`. */
43
- function findChild(cmds: CliCommand[], name: string): CliCommand | undefined {
43
+ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
44
44
  return cmds.find((c) => c.key === name);
45
45
  }
46
46
 
@@ -48,7 +48,7 @@ function findChild(cmds: CliCommand[], name: string): CliCommand | undefined {
48
48
  * Parses argv against the user root, runs the leaf handler, and returns captured output.
49
49
  * Never calls process.exit.
50
50
  */
51
- export async function cliInvoke(root: CliCommand, argv: string[]): Promise<CliInvokeResult> {
51
+ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult> {
52
52
  let pr = parse(root, argv);
53
53
  pr = postParseValidate(root, pr);
54
54
 
@@ -82,9 +82,18 @@ export async function cliInvoke(root: CliCommand, argv: string[]): Promise<CliIn
82
82
  };
83
83
  }
84
84
 
85
- let current: CliCommand = root;
85
+ let current: CliProgram = root;
86
86
  for (const seg of pr.path) {
87
- const ch = findChild(current.commands ?? [], seg);
87
+ if (!isCliRouter(current)) {
88
+ return {
89
+ kind: "error",
90
+ exitCode: 1,
91
+ stdout: "",
92
+ stderr: "Internal error: missing handler for path.",
93
+ errorMsg: "Internal error: missing handler for path.",
94
+ };
95
+ }
96
+ const ch = findChild(current.commands, seg);
88
97
  if (!ch) {
89
98
  return {
90
99
  kind: "error",
package/src/mcp/server.ts CHANGED
@@ -4,7 +4,7 @@ resources, and ping. Responses are newline-delimited JSON on stdout only.
4
4
  */
5
5
 
6
6
  import { cliInvoke } from "../invoke.ts";
7
- import { CliCommand } from "../types.ts";
7
+ import { CliProgram } from "../types.ts";
8
8
  import { buildToolCallSuccess } from "./result.ts";
9
9
  import {
10
10
  allMcpResources,
@@ -41,7 +41,7 @@ function writeError(id: string | number | null | undefined, code: number, messag
41
41
  }
42
42
 
43
43
  /** Handles one NDJSON request line. */
44
- async function handleRequestLine(root: CliCommand, line: string): Promise<void> {
44
+ async function handleRequestLine(root: CliProgram, line: string): Promise<void> {
45
45
  let req: JsonRpcRequest;
46
46
  try {
47
47
  req = JSON.parse(line) as JsonRpcRequest;
@@ -217,7 +217,7 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
217
217
  }
218
218
 
219
219
  /** Runs the MCP NDJSON read loop on stdin until EOF. */
220
- export async function mcpServeStdioLoop(root: CliCommand): Promise<void> {
220
+ export async function mcpServeStdioLoop(root: CliProgram): Promise<void> {
221
221
  let buffer = "";
222
222
  for await (const chunk of Bun.stdin.stream()) {
223
223
  buffer += new TextDecoder().decode(chunk);
package/src/mcp/tools.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- This module maps CliCommand leaf nodes to MCP tool definitions and converts
2
+ This module maps CliProgram leaf nodes to MCP tool definitions and converts
3
3
  flat JSON tool arguments into argv for cliInvoke.
4
4
  */
5
5
 
@@ -7,7 +7,7 @@ import { readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { collectOptionDefs } from "../parse.ts";
9
9
  import { cliSchemaJson } from "../schema.ts";
10
- import { CliCommand, CliOption, CliOptionKind, CliPositional } from "../types.ts";
10
+ import { CliProgram, CliLeaf, CliNode, CliOption, CliOptionKind, CliPositional, isCliLeaf, isCliRouter } from "../types.ts";
11
11
 
12
12
  /** Default URI for the CLI schema MCP resource. */
13
13
  export const MCP_SCHEMA_URI_DEFAULT = "argsbarg://schema";
@@ -21,7 +21,7 @@ export interface McpToolDef {
21
21
  /** Command path segments from the program root. */
22
22
  path: string[];
23
23
  /** Leaf command node. */
24
- leaf: CliCommand;
24
+ leaf: CliLeaf;
25
25
  /** JSON Schema for tools/call arguments. */
26
26
  inputSchema: Record<string, unknown>;
27
27
  }
@@ -38,7 +38,7 @@ export function sanitizeToolSegment(key: string): string {
38
38
  }
39
39
 
40
40
  /** Builds the MCP tool name for a leaf at the given path. */
41
- export function mcpToolName(root: CliCommand, path: string[]): string {
41
+ export function mcpToolName(root: CliProgram, path: string[]): string {
42
42
  if (path.length === 0) {
43
43
  return sanitizeToolSegment(root.key);
44
44
  }
@@ -71,7 +71,7 @@ function positionalProperty(p: CliPositional): Record<string, unknown> {
71
71
  }
72
72
 
73
73
  /** Builds inputSchema for a leaf command. */
74
- function buildInputSchema(root: CliCommand, path: string[], leaf: CliCommand): Record<string, unknown> {
74
+ function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Record<string, unknown> {
75
75
  const properties: Record<string, unknown> = {};
76
76
  const required: string[] = [];
77
77
 
@@ -102,7 +102,7 @@ function buildInputSchema(root: CliCommand, path: string[], leaf: CliCommand): R
102
102
  }
103
103
 
104
104
  /** Resolves MCP tool description with optional override and requiresEnv suffix. */
105
- function resolveToolDescription(root: CliCommand, path: string[], leaf: CliCommand): string {
105
+ function resolveToolDescription(root: CliProgram, path: string[], leaf: CliLeaf): string {
106
106
  if (leaf.mcpTool?.description) {
107
107
  return leaf.mcpTool.description;
108
108
  }
@@ -124,7 +124,7 @@ export interface McpResourceEntry {
124
124
  }
125
125
 
126
126
  /** Returns built-in schema resource plus user mcpServer.resources. */
127
- export function allMcpResources(root: CliCommand): McpResourceEntry[] {
127
+ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
128
128
  const schemaUri = resolveMcpSchemaUri(root);
129
129
  const builtIn: McpResourceEntry = {
130
130
  uri: schemaUri,
@@ -144,13 +144,13 @@ export function allMcpResources(root: CliCommand): McpResourceEntry[] {
144
144
  }
145
145
 
146
146
  /** Recursively collects MCP tool definitions from user leaf commands. */
147
- export function collectMcpTools(root: CliCommand): McpToolDef[] {
147
+ export function collectMcpTools(root: CliProgram): McpToolDef[] {
148
148
  const out: McpToolDef[] = [];
149
149
 
150
150
  /** Walks the command tree and appends leaf tools. */
151
- function walk(cmd: CliCommand, path: string[]): void {
152
- if ("handler" in cmd && cmd.handler) {
153
- if (cmd.key === "completion" || cmd.key === "ai") {
151
+ function walk(cmd: CliNode, path: string[]): void {
152
+ if (isCliLeaf(cmd)) {
153
+ if (cmd.key === "completion" || cmd.key === "install" || cmd.key === "mcp") {
154
154
  return;
155
155
  }
156
156
  if (cmd.mcpTool?.enabled === false) {
@@ -165,15 +165,15 @@ export function collectMcpTools(root: CliCommand): McpToolDef[] {
165
165
  });
166
166
  return;
167
167
  }
168
- for (const ch of cmd.commands ?? []) {
168
+ for (const ch of cmd.commands) {
169
169
  walk(ch, [...path, ch.key]);
170
170
  }
171
171
  }
172
172
 
173
- if ("handler" in root && root.handler) {
173
+ if (isCliLeaf(root)) {
174
174
  walk(root, []);
175
175
  } else {
176
- for (const ch of root.commands ?? []) {
176
+ for (const ch of root.commands) {
177
177
  walk(ch, [ch.key]);
178
178
  }
179
179
  }
@@ -193,7 +193,7 @@ function resolveMcpVersionFromPackageJson(): string | undefined {
193
193
  }
194
194
 
195
195
  /** Resolves MCP server name and version for initialize. */
196
- export function resolveMcpServerInfo(root: CliCommand): { name: string; version: string } {
196
+ export function resolveMcpServerInfo(root: CliProgram): { name: string; version: string } {
197
197
  return {
198
198
  name: root.mcpServer?.name ?? root.key,
199
199
  version: root.mcpServer?.version ?? resolveMcpVersionFromPackageJson() ?? "0.0.0",
@@ -201,13 +201,13 @@ export function resolveMcpServerInfo(root: CliCommand): { name: string; version:
201
201
  }
202
202
 
203
203
  /** Resolves the schema resource URI for this app. */
204
- export function resolveMcpSchemaUri(root: CliCommand): string {
204
+ export function resolveMcpSchemaUri(root: CliProgram): string {
205
205
  return root.mcpServer?.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
206
206
  }
207
207
 
208
208
  /** Converts flat MCP tool arguments to argv for cliInvoke. */
209
209
  export function mcpToolCallToArgv(
210
- root: CliCommand,
210
+ root: CliProgram,
211
211
  tool: McpToolDef,
212
212
  args: Record<string, unknown>,
213
213
  ): string[] | { error: string } {
package/src/mcp.ts CHANGED
@@ -4,13 +4,13 @@ This module starts the ArgsBarg MCP stdio server for opt-in program roots.
4
4
 
5
5
  import { mcpServeStdioLoop } from "./mcp/server.ts";
6
6
  import { bootstrapMcpEnv } from "./mcp/env.ts";
7
- import { CliCommand } from "./types.ts";
7
+ import { CliProgram } from "./types.ts";
8
8
 
9
9
  /**
10
10
  * Runs the MCP JSON-RPC server on stdin/stdout until stdin closes, then exits.
11
11
  * Caller must ensure `root.mcpServer` is set.
12
12
  */
13
- export async function cliMcpServeStdio(root: CliCommand): Promise<never> {
13
+ export async function cliMcpServeStdio(root: CliProgram): Promise<never> {
14
14
  try {
15
15
  if (root.mcpServer) {
16
16
  bootstrapMcpEnv(root.mcpServer);