argsbarg 4.0.3 → 4.1.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 (99) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +6 -6
  3. package/docs/ai-skills.md +9 -4
  4. package/docs/bundled-docs.md +1 -0
  5. package/docs/cli-program.md +4 -4
  6. package/docs/config-schema.md +1 -4
  7. package/docs/developing.md +1 -1
  8. package/docs/install.md +160 -39
  9. package/docs/mcp.md +38 -11
  10. package/docs/templates/cursor/rules/cli-program.mdc +1 -1
  11. package/examples/config-app/program.ts +0 -3
  12. package/examples/consumer-app/README.md +1 -1
  13. package/examples/consumer-app/src/program.ts +5 -13
  14. package/examples/mcp-test.ts +14 -24
  15. package/index.d.ts +60 -5
  16. package/package.json +1 -1
  17. package/src/builtins/builtins.test.ts +20 -4
  18. package/src/builtins/config.test.ts +31 -25
  19. package/src/builtins/config.ts +4 -3
  20. package/src/builtins/install.ts +50 -57
  21. package/src/builtins/mcp.ts +1 -1
  22. package/src/capabilities.ts +4 -4
  23. package/src/cli.ts +2 -0
  24. package/src/config/bootstrap.ts +167 -66
  25. package/src/config/context.test.ts +22 -36
  26. package/src/config/context.ts +5 -4
  27. package/src/config/file.test.ts +66 -56
  28. package/src/config/file.ts +33 -25
  29. package/src/config/resolve.test.ts +25 -1
  30. package/src/config/resolve.ts +43 -8
  31. package/src/config.integration.test.ts +17 -10
  32. package/src/docs/api-guide.test.ts +1 -1
  33. package/src/docs/docs.test.ts +1 -1
  34. package/src/docs/mcp-guide.ts +15 -4
  35. package/src/docs/mcp-resources.test.ts +63 -0
  36. package/src/docs/mcp-resources.ts +68 -0
  37. package/src/hidden-mcpb.test.ts +41 -1
  38. package/src/index.ts +4 -0
  39. package/src/install/{binary.ts → app.ts} +13 -13
  40. package/src/install/bootstrap.ts +22 -0
  41. package/src/install/detect-installed.ts +2 -97
  42. package/src/install/index.ts +187 -110
  43. package/src/install/install-validate.test.ts +61 -0
  44. package/src/install/install.test.ts +138 -42
  45. package/src/install/mcp-openclaw.test.ts +40 -0
  46. package/src/install/mcp-openclaw.ts +106 -0
  47. package/src/install/normalize.ts +35 -0
  48. package/src/install/paths.ts +27 -13
  49. package/src/install/plan.ts +30 -259
  50. package/src/install/shell.ts +2 -2
  51. package/src/install/status.test.ts +85 -0
  52. package/src/install/status.ts +22 -9
  53. package/src/install/target-base.ts +93 -0
  54. package/src/install/target-detect.ts +20 -0
  55. package/src/install/target-effective.ts +131 -0
  56. package/src/install/target-mcp-cli.ts +149 -0
  57. package/src/install/target-mcp-json.ts +130 -0
  58. package/src/install/target-plan-build.ts +67 -0
  59. package/src/install/target-registry.ts +57 -0
  60. package/src/install/target-scope.ts +266 -0
  61. package/src/install/target-skill.ts +104 -0
  62. package/src/install/target-types.ts +145 -0
  63. package/src/install/targets/app.ts +69 -0
  64. package/src/install/targets/chatgpt-mcp.ts +12 -0
  65. package/src/install/targets/claude-code-mcp.ts +15 -0
  66. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  67. package/src/install/targets/claude-skill.ts +16 -0
  68. package/src/install/targets/codex-mcp.ts +25 -0
  69. package/src/install/targets/codex-skill.ts +14 -0
  70. package/src/install/targets/completions.ts +133 -0
  71. package/src/install/targets/configure.ts +59 -0
  72. package/src/install/targets/cursor-mcp.ts +15 -0
  73. package/src/install/targets/cursor-skill.ts +16 -0
  74. package/src/install/targets/index.ts +53 -0
  75. package/src/install/targets/openclaw-mcp.ts +25 -0
  76. package/src/install/targets/openclaw-skill.ts +17 -0
  77. package/src/install/targets/opencode-mcp.ts +101 -0
  78. package/src/install/targets/opencode-skill.ts +15 -0
  79. package/src/install/targets.test.ts +136 -0
  80. package/src/install/uninstall.ts +16 -152
  81. package/src/install/update.test.ts +17 -2
  82. package/src/install/update.ts +2 -5
  83. package/src/invoke.test.ts +7 -1
  84. package/src/mcp/bundle.ts +16 -4
  85. package/src/mcp/claude.test.ts +23 -4
  86. package/src/mcp/claude.ts +18 -13
  87. package/src/mcp/tools.ts +4 -1
  88. package/src/mcp/zip.test.ts +17 -0
  89. package/src/mcp/zip.ts +62 -9
  90. package/src/mcp.integration.test.ts +18 -1
  91. package/src/parse.test.ts +57 -4
  92. package/src/paths/host.ts +11 -11
  93. package/src/paths/remove-empty-dir.ts +13 -0
  94. package/src/skill/generate.ts +89 -5
  95. package/src/skill/hint.ts +5 -0
  96. package/src/skill/install.ts +33 -6
  97. package/src/skill/naming.ts +28 -0
  98. package/src/types.ts +65 -4
  99. package/src/validate.ts +79 -4
package/src/parse.test.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "./mcp/tools.ts";
21
21
  import { ParseKind, parse, postParseValidate } from "./parse.ts";
22
22
  import { cliSchemaJson } from "./schema.ts";
23
- import { generateSkillBundle } from "./skill/generate.ts";
23
+ import { generatePluginSkillBundle, generateSkillBundle } from "./skill/generate.ts";
24
24
  import { cliSkillInstall } from "./skill/install.ts";
25
25
  import {
26
26
  enumMcpFixture,
@@ -614,7 +614,7 @@ test("cliSchemaExport resolves program key in install notes", () => {
614
614
 
615
615
  const json = cliSchemaJson(root);
616
616
  expect(json).not.toContain("{argsbarg:program}");
617
- expect(json).toContain("myapp install --all --yes");
617
+ expect(json).toContain("myapp install --yes");
618
618
  });
619
619
 
620
620
  test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
@@ -835,7 +835,7 @@ test("cliValidateProgram rejects resource URI matching default schema URI", () =
835
835
  },
836
836
  commands: [{ key: "x", description: "", handler: () => {} }],
837
837
  });
838
- expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
838
+ expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
839
839
  });
840
840
 
841
841
  test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
@@ -849,7 +849,21 @@ test("cliValidateProgram rejects resource URI matching schemaResourceUri", () =>
849
849
  },
850
850
  commands: [{ key: "x", description: "", handler: () => {} }],
851
851
  });
852
- expect(() => cliValidateProgram(root)).toThrow(/conflicts with the built-in schema resource/);
852
+ expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
853
+ });
854
+
855
+ test("cliValidateProgram rejects resource URI matching auto docs topic", () => {
856
+ const root = testProgram({
857
+ key: "app",
858
+ description: "",
859
+ docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
860
+ mcpServer: {
861
+ enabled: true,
862
+ resources: [{ uri: "app://docs/readme", name: "dup", load: () => "" }],
863
+ },
864
+ commands: [{ key: "x", description: "", handler: () => {} }],
865
+ });
866
+ expect(() => cliValidateProgram(root)).toThrow(/conflicts with auto docs topic resource/);
853
867
  });
854
868
 
855
869
  test("allMcpResources includes custom resources", () => {
@@ -867,6 +881,20 @@ test("allMcpResources includes custom resources", () => {
867
881
  expect(resources.map((r) => r.uri)).toContain("test://x");
868
882
  });
869
883
 
884
+ test("allMcpResources includes docs topic resources", () => {
885
+ const root = testProgram({
886
+ key: "app",
887
+ description: "",
888
+ docs: { enabled: true, topics: { readme: { text: "# hi\n" } } },
889
+ mcpServer: { enabled: true },
890
+ commands: [{ key: "leaf", description: "", handler: () => {} }],
891
+ });
892
+ const resources = allMcpResources(root);
893
+ expect(resources.map((r) => r.uri)).toContain("app://docs/readme");
894
+ const readme = resources.find((r) => r.uri === "app://docs/readme");
895
+ expect(readme?.load()).toBe("# hi\n");
896
+ });
897
+
870
898
  test("applyShellEnv merges PATH and preserves host vars", () => {
871
899
  const origPath = process.env.PATH ?? "";
872
900
  const origHome = process.env.HOME;
@@ -1096,6 +1124,17 @@ test("install config on non-root node is rejected", () => {
1096
1124
  expect(() => cliValidateProgram(root)).toThrow(/install is only supported on the program root/);
1097
1125
  });
1098
1126
 
1127
+ test("install.prefix is rejected", () => {
1128
+ const root = {
1129
+ key: "app",
1130
+ version: "0.0.0",
1131
+ description: "",
1132
+ install: { prefix: "/opt/bin" },
1133
+ handler: () => {},
1134
+ } as unknown as CliProgram;
1135
+ expect(() => cliValidateProgram(root)).toThrow(/install\.prefix removed/);
1136
+ });
1137
+
1099
1138
  test("generateSkillBundle includes frontmatter and compact command index", () => {
1100
1139
  const bundle = generateSkillBundle(nestedMcpFixture, "cursor");
1101
1140
  expect(bundle.dirName).toBe("nested_ts");
@@ -1116,6 +1155,20 @@ test("generateSkillBundle includes frontmatter and compact command index", () =>
1116
1155
  expect(bundle.referenceMd).not.toContain("```json");
1117
1156
  });
1118
1157
 
1158
+ test("generatePluginSkillBundle is MCP routing stub without shell catalog", () => {
1159
+ const bundle = generatePluginSkillBundle(nestedMcpFixture);
1160
+ expect(bundle.dirName).toBe("nested_ts");
1161
+ expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
1162
+ expect(bundle.skillMd).toContain("MCP toolset");
1163
+ expect(bundle.skillMd).toContain("Server id: `nested_ts`");
1164
+ expect(bundle.skillMd).toContain("nested_ts://schema");
1165
+ expect(bundle.skillMd).toContain("tools/list");
1166
+ expect(bundle.skillMd).not.toContain("Invoke via shell");
1167
+ expect(bundle.skillMd).not.toContain("reference.md");
1168
+ expect(bundle.skillMd).not.toContain("`nested.ts stat owner lookup <path>`");
1169
+ expect(bundle.skillMd).not.toContain("## Commands");
1170
+ });
1171
+
1119
1172
  test("cliSkillInstall writes project Cursor skill files", () => {
1120
1173
  const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-"));
1121
1174
  const prev = process.cwd();
package/src/paths/host.ts CHANGED
@@ -21,20 +21,20 @@ export function expandTilde(path: string): string {
21
21
  return path;
22
22
  }
23
23
 
24
+ /** Display path using `~/` when under the user home directory. */
25
+ export function displayHomePath(absolutePath: string, home = userHome()): string {
26
+ if (home.length > 0 && absolutePath.startsWith(home)) {
27
+ return `~${absolutePath.slice(home.length)}`;
28
+ }
29
+ return absolutePath;
30
+ }
31
+
24
32
  /** XDG config base directory (`$XDG_CONFIG_HOME` or `~/.config`). */
25
33
  export function xdgConfigHome(home = userHome()): string {
26
34
  return process.env.XDG_CONFIG_HOME ?? join(home, ".config");
27
35
  }
28
36
 
29
- /** Windows `%APPDATA%` (or `~/AppData/Roaming`). */
30
- export function appDataHome(home = userHome()): string {
31
- return process.env.APPDATA ?? join(home, "AppData", "Roaming");
32
- }
33
-
34
- /** OS-appropriate base directory for app config files. */
35
- export function appConfigHome(home = userHome()): string {
36
- if (process.platform === "win32") {
37
- return appDataHome(home);
38
- }
39
- return xdgConfigHome(home);
37
+ /** App config library root (`~/.local/lib`). */
38
+ export function appConfigLibHome(home = userHome()): string {
39
+ return join(home, ".local", "lib");
40
40
  }
@@ -0,0 +1,13 @@
1
+ import { existsSync, readdirSync, rmdirSync } from "node:fs";
2
+
3
+ /** Removes a directory when it exists and is empty. Returns true when removed. */
4
+ export function removeEmptyDir(path: string): boolean {
5
+ if (!existsSync(path)) return false;
6
+ try {
7
+ if (readdirSync(path).length > 0) return false;
8
+ rmdirSync(path);
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
@@ -4,11 +4,19 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
4
4
 
5
5
  import { defaultConfigEntryTitle } from "../config/entry.ts";
6
6
  import { generateApiGuide } from "../docs/api-guide.ts";
7
- import { collectMcpTools, type McpToolDef, sanitizeToolSegment } from "../mcp/tools.ts";
7
+ import {
8
+ collectMcpTools,
9
+ type McpToolDef,
10
+ mcpServerId,
11
+ resolveMcpSchemaUri,
12
+ sanitizeToolSegment,
13
+ } from "../mcp/tools.ts";
8
14
  import { collectOptionDefs } from "../parse.ts";
9
15
  import { CliOptionKind, type CliProgram } from "../types.ts";
16
+ import type { SkillTarget } from "./naming.ts";
17
+ import { skillDirNameForTarget, skillFrontmatterName } from "./naming.ts";
10
18
 
11
- export type SkillTarget = "cursor" | "claude";
19
+ export type { SkillTarget } from "./naming.ts";
12
20
 
13
21
  export interface SkillBundle {
14
22
  dirName: string;
@@ -16,12 +24,28 @@ export interface SkillBundle {
16
24
  referenceMd: string;
17
25
  }
18
26
 
27
+ /** MCP routing skill for Claude Code plugin zips (SKILL.md only). */
28
+ export interface PluginSkillBundle {
29
+ dirName: string;
30
+ skillMd: string;
31
+ }
32
+
19
33
  /** Truncates text to maxLen with ellipsis. */
20
34
  function truncate(text: string, maxLen: number): string {
21
35
  if (text.length <= maxLen) return text;
22
36
  return `${text.slice(0, maxLen - 1)}…`;
23
37
  }
24
38
 
39
+ /** Builds MCP-oriented skill description for Claude plugin YAML frontmatter. */
40
+ function pluginSkillDescription(root: CliProgram): string {
41
+ const tools = collectMcpTools(root);
42
+ const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
43
+ const sample = paths.slice(0, 5).join(", ");
44
+ const more = paths.length > 5 ? `, and ${paths.length - 5} more` : "";
45
+ const desc = `Use the ${root.key} MCP toolset (${sample}${more}). Use when the user mentions ${root.key}${paths.length > 0 ? `, ${paths.slice(0, 3).join(", ")}` : ""}, or related tasks.`;
46
+ return truncate(desc, 1024);
47
+ }
48
+
25
49
  /** Builds third-person skill description for YAML frontmatter. */
26
50
  function skillDescription(root: CliProgram): string {
27
51
  const tools = collectMcpTools(root);
@@ -81,7 +105,7 @@ function buildConfigurationSection(root: CliProgram): string[] {
81
105
 
82
106
  /** Builds SKILL.md body for the given target. */
83
107
  function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): string {
84
- const name = sanitizeToolSegment(root.key);
108
+ const name = skillFrontmatterName(root.key, target);
85
109
  const description = skillDescription(root);
86
110
  const tools = collectMcpTools(root);
87
111
 
@@ -139,7 +163,7 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
139
163
  "Do not install under `~/.cursor/skills-cursor/` (reserved for Cursor built-ins).",
140
164
  "",
141
165
  );
142
- } else {
166
+ } else if (target === "claude") {
143
167
  lines.push(
144
168
  "## Claude Code",
145
169
  "",
@@ -149,6 +173,18 @@ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): s
149
173
  `- Bundled files in this directory are available via \`\${CLAUDE_SKILL_DIR}\` when the skill runs.`,
150
174
  "",
151
175
  );
176
+ } else if (target === "codex") {
177
+ lines.push(
178
+ "## Codex",
179
+ "",
180
+ `- Global: \`~/.codex/skills/${dirName}/\``,
181
+ "- Enable skills in `~/.codex/config.toml` if required.",
182
+ "",
183
+ );
184
+ } else if (target === "opencode") {
185
+ lines.push("## OpenCode", "", `- Global: \`~/.config/opencode/skills/${dirName}/\``, "");
186
+ } else {
187
+ lines.push("## OpenClaw", "", `- Global: \`~/.openclaw/skills/${dirName}/\``, "");
152
188
  }
153
189
 
154
190
  return lines.join("\n");
@@ -159,9 +195,57 @@ function buildReferenceMd(root: CliProgram): string {
159
195
  return generateApiGuide(root);
160
196
  }
161
197
 
198
+ /** Builds MCP routing SKILL.md for Claude Code plugin zips. */
199
+ function buildPluginSkillMd(root: CliProgram, dirName: string): string {
200
+ const name = sanitizeToolSegment(root.key);
201
+ const description = pluginSkillDescription(root);
202
+ const serverId = mcpServerId(root);
203
+ const schemaUri = resolveMcpSchemaUri(root);
204
+
205
+ const lines: string[] = [
206
+ "---",
207
+ `name: ${name}`,
208
+ `description: ${description}`,
209
+ "---",
210
+ "",
211
+ `# ${root.key}`,
212
+ "",
213
+ root.description,
214
+ "",
215
+ "## Execution",
216
+ "",
217
+ "This plugin bundles an MCP server. Use MCP tools to fulfill requests.",
218
+ "",
219
+ `- Server id: \`${serverId}\` (configured in plugin \`.mcp.json\`)`,
220
+ "- Tool names and argument shapes come from MCP `tools/list`",
221
+ `- Full schema: \`${schemaUri}\` (same as \`${root.key} docs schema\`)`,
222
+ "",
223
+ ];
224
+
225
+ lines.push(...buildConfigurationSection(root));
226
+
227
+ lines.push(
228
+ "## Claude Code plugin",
229
+ "",
230
+ `Invoke with \`/${dirName}\` or let Claude auto-match from the description.`,
231
+ "",
232
+ );
233
+
234
+ return lines.join("\n");
235
+ }
236
+
237
+ /** Generates MCP routing SKILL.md for Claude Code plugin zips. */
238
+ export function generatePluginSkillBundle(root: CliProgram): PluginSkillBundle {
239
+ const dirName = sanitizeToolSegment(root.key);
240
+ return {
241
+ dirName,
242
+ skillMd: buildPluginSkillMd(root, dirName),
243
+ };
244
+ }
245
+
162
246
  /** Generates SKILL.md and reference.md for Cursor or Claude Code. */
163
247
  export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
164
- const dirName = sanitizeToolSegment(root.key);
248
+ const dirName = skillDirNameForTarget(root.key, target);
165
249
  return {
166
250
  dirName,
167
251
  skillMd: buildSkillMd(root, target, dirName),
package/src/skill/hint.ts CHANGED
@@ -58,3 +58,8 @@ export function applySkillBundleHints(
58
58
  referenceMd: insertGeneratedHint(referenceMd, hint),
59
59
  };
60
60
  }
61
+
62
+ /** Applies bundle hint to plugin SKILL.md (after frontmatter). */
63
+ export function applyPluginSkillHint(program: CliProgram, skillMd: string): string {
64
+ return insertGeneratedHint(skillMd, skillBundleHint(program), { afterFrontmatter: true });
65
+ }
@@ -5,19 +5,28 @@ import type { CliProgram } from "../types.ts";
5
5
  import { generateSkillBundle, type SkillTarget } from "./generate.ts";
6
6
  import { applySkillInstallHints } from "./hint.ts";
7
7
 
8
+ export { skillDirNameForTarget, skillFrontmatterName, skillSlug } from "./naming.ts";
9
+
8
10
  export interface SkillInstallOpts {
9
11
  global?: boolean;
10
- /** When true, remove an existing skill directory before writing. */
11
12
  rimraf?: boolean;
12
- /** When true, skip writes but return paths that would change. */
13
13
  dry?: boolean;
14
14
  }
15
15
 
16
16
  function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean): string {
17
- const base = global
18
- ? join(userHome(), target === "cursor" ? ".cursor" : ".claude", "skills")
19
- : join(process.cwd(), target === "cursor" ? ".cursor" : ".claude", "skills");
20
- return join(base, dirName);
17
+ const home = userHome();
18
+ switch (target) {
19
+ case "cursor":
20
+ return join(global ? home : process.cwd(), ".cursor", "skills", dirName);
21
+ case "claude":
22
+ return join(global ? home : process.cwd(), ".claude", "skills", dirName);
23
+ case "codex":
24
+ return join(global ? home : process.cwd(), ".codex", "skills", dirName);
25
+ case "opencode":
26
+ return join(global ? home : process.cwd(), ".config", "opencode", "skills", dirName);
27
+ case "openclaw":
28
+ return join(global ? home : process.cwd(), ".openclaw", "skills", dirName);
29
+ }
21
30
  }
22
31
 
23
32
  /** Writes SKILL.md and reference.md; returns changed file paths. */
@@ -47,3 +56,21 @@ export function cliSkillInstall(
47
56
  changed.push(skillPath, refPath);
48
57
  return changed;
49
58
  }
59
+
60
+ /** Maps install action kind to skill target. */
61
+ export function skillTargetFromActionKind(kind: string): SkillTarget | undefined {
62
+ switch (kind) {
63
+ case "cursor-skill":
64
+ return "cursor";
65
+ case "claude-skill":
66
+ return "claude";
67
+ case "codex-skill":
68
+ return "codex";
69
+ case "opencode-skill":
70
+ return "opencode";
71
+ case "openclaw-skill":
72
+ return "openclaw";
73
+ default:
74
+ return undefined;
75
+ }
76
+ }
@@ -0,0 +1,28 @@
1
+ /** Agent skill install targets. */
2
+ export type SkillTarget = "cursor" | "claude" | "codex" | "opencode" | "openclaw";
3
+
4
+ import { sanitizeToolSegment } from "../mcp/tools.ts";
5
+
6
+ /** Kebab-case skill folder + frontmatter name for agents that require hyphen slugs. */
7
+ export function skillSlug(programKey: string): string {
8
+ return programKey
9
+ .replace(/[^a-zA-Z0-9]+/g, "-")
10
+ .replace(/^-+|-+$/g, "")
11
+ .toLowerCase();
12
+ }
13
+
14
+ /** Directory name for a skill target (underscore vs kebab conventions). */
15
+ export function skillDirNameForTarget(programKey: string, target: SkillTarget): string {
16
+ if (target === "cursor" || target === "claude") {
17
+ return sanitizeToolSegment(programKey);
18
+ }
19
+ return skillSlug(programKey);
20
+ }
21
+
22
+ /** Frontmatter `name` for SKILL.md by target. */
23
+ export function skillFrontmatterName(programKey: string, target: SkillTarget): string {
24
+ if (target === "cursor" || target === "claude") {
25
+ return sanitizeToolSegment(programKey);
26
+ }
27
+ return skillSlug(programKey);
28
+ }
package/src/types.ts CHANGED
@@ -133,6 +133,10 @@ export interface CliMcpBundleConfig {
133
133
  export interface CliMcpServerConfig {
134
134
  /** When `true`, enables the `mcp` built-in and MCP stdio server. */
135
135
  enabled: boolean;
136
+ /** When `true`, `mcp bundle` writes `dist/<key>.mcpb` for Claude Desktop. Default false. */
137
+ mcpd?: boolean;
138
+ /** When `true`, `mcp bundle` also writes `dist/claude-plugin/<name>.zip`. Default false. */
139
+ claudePlugin?: boolean;
136
140
  /** Resource URI for schema export (default: `<sanitized root key>://schema`). */
137
141
  schemaResourceUri?: string;
138
142
  /**
@@ -224,8 +228,6 @@ export interface CliAppConfigEntry {
224
228
  * App configuration block on the program root ({@link CliProgram.appConfig}).
225
229
  */
226
230
  export interface CliAppConfig {
227
- /** Default: `~/.config/<sanitized-key>/config` (or `%APPDATA%/<key>/config` on Windows). */
228
- path?: string;
229
231
  /** Built-in `config get` / `config set`. Default: enabled when `appConfig` is set. */
230
232
  commands?: boolean | { enabled?: boolean; mcpSet?: boolean };
231
233
  /** Block JSON Schema (draft-07). When omitted, synthesize all-string schema from `entries`. */
@@ -237,8 +239,15 @@ export interface CliAppConfig {
237
239
  export interface CliInstallConfig {
238
240
  /** When `false`, hide/disable `install` (default: enabled). */
239
241
  enabled?: boolean;
240
- /** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
241
- prefix?: string;
242
+ /**
243
+ * Default agent integration for full install (`install --all`).
244
+ * - `'mcp'` when `mcpServer.enabled` (default): MCP targets in `--all`; paired skills excluded.
245
+ * - `'skill'` when MCP is off (default): skill targets in `--all`; paired MCP excluded.
246
+ * - `'both'`: install MCP and skill for the same host when both are available.
247
+ */
248
+ agentIntegration?: InstallAgentIntegration;
249
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
250
+ targets?: CliInstallTargets;
242
251
  /**
243
252
  * When set, enables `install --update` on the program root.
244
253
  * Should download or locate the latest release binary and return its path.
@@ -246,6 +255,58 @@ export interface CliInstallConfig {
246
255
  updateGetLatest?: CliUpdateGetLatest;
247
256
  }
248
257
 
258
+ /** Agent integration mode for install — MCP vs shell skill per host. */
259
+ export type InstallAgentIntegration = "mcp" | "skill" | "both";
260
+
261
+ /** Boolean or structured gate for one install artifact. */
262
+ export type InstallTargetSpec =
263
+ | boolean
264
+ | {
265
+ /** When false, artifact is never installed (even with scoped CLI flags). Default true. */
266
+ enabled?: boolean;
267
+ /** When true, included in bare `install` / `install --all`. Default varies by key. */
268
+ includedInAll?: boolean;
269
+ };
270
+
271
+ export interface ResolvedInstallTarget {
272
+ enabled: boolean;
273
+ includedInAll: boolean;
274
+ }
275
+
276
+ /** Per-artifact gates for full install/uninstall. See {@link resolveEffectiveInstallTargets}. */
277
+ export interface CliInstallTargets {
278
+ /** Copy app to `~/.local/bin/<key>`. Default includedInAll true (opt-out). */
279
+ app?: InstallTargetSpec;
280
+ /** ChatGPT desktop MCP. Default false. */
281
+ chatgptMcp?: InstallTargetSpec;
282
+ /** Claude Code MCP (`~/.claude.json`). Default false. */
283
+ claudeCodeMcp?: InstallTargetSpec;
284
+ /** Claude Desktop MCP. Default false. */
285
+ claudeDesktopMcp?: InstallTargetSpec;
286
+ /** Claude Code skill. Default false. */
287
+ claudeSkill?: InstallTargetSpec;
288
+ /** Codex MCP (`codex mcp add`). Default false. */
289
+ codexMcp?: InstallTargetSpec;
290
+ /** Codex skill. Default false. */
291
+ codexSkill?: InstallTargetSpec;
292
+ /** Shell completions for detected shells. Default includedInAll true (opt-out). */
293
+ completions?: InstallTargetSpec;
294
+ /** App config: wizard on install, file removal on uninstall. Default includedInAll true. */
295
+ configure?: InstallTargetSpec;
296
+ /** Cursor MCP. Default false. */
297
+ cursorMcp?: InstallTargetSpec;
298
+ /** Cursor skill. Default false. */
299
+ cursorSkill?: InstallTargetSpec;
300
+ /** OpenClaw MCP. Default false. */
301
+ openclawMcp?: InstallTargetSpec;
302
+ /** OpenClaw skill. Default false. */
303
+ openclawSkill?: InstallTargetSpec;
304
+ /** OpenCode MCP. Default false. */
305
+ opencodeMcp?: InstallTargetSpec;
306
+ /** OpenCode skill. Default false. */
307
+ opencodeSkill?: InstallTargetSpec;
308
+ }
309
+
249
310
  /**
250
311
  * One bundled documentation topic for the `docs` built-in (program root only).
251
312
  */
package/src/validate.ts CHANGED
@@ -3,8 +3,10 @@ This module validates CLI schemas before execution.
3
3
  */
4
4
 
5
5
  import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
6
+ import { reservedDocsTopicResourceUris } from "./docs/mcp-resources.ts";
6
7
  import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
7
8
  import { validateFormatValue } from "./formats.ts";
9
+ import { AGENT_PAIRS } from "./install/target-registry.ts";
8
10
  import { resolveMcpSchemaUri } from "./mcp/tools.ts";
9
11
  import {
10
12
  type CliLeaf,
@@ -13,6 +15,8 @@ import {
13
15
  type CliProgram,
14
16
  CliSchemaValidationError,
15
17
  CliValueFormat,
18
+ type InstallAgentIntegration,
19
+ type InstallTargetSpec,
16
20
  isCliLeaf,
17
21
  isCliRouter,
18
22
  } from "./types.ts";
@@ -104,6 +108,69 @@ function validateConfigBlock(appConfigBlock: import("./types.ts").CliAppConfig):
104
108
  }
105
109
  }
106
110
 
111
+ const PAIR_HOST_LABELS: Record<string, string> = {
112
+ cursorMcp: "cursor",
113
+ claudeCodeMcp: "claudeCode",
114
+ codexMcp: "codex",
115
+ opencodeMcp: "opencode",
116
+ openclawMcp: "openclaw",
117
+ };
118
+
119
+ function installTargetExplicitTruthy(spec: InstallTargetSpec | undefined): boolean {
120
+ if (spec === undefined || spec === false) return false;
121
+ if (spec === true) return true;
122
+ return spec.enabled !== false;
123
+ }
124
+
125
+ /** Validates `program.install` targets and agentIntegration. */
126
+ function validateInstallConfig(program: CliProgram): void {
127
+ const install = program.install;
128
+ if (!install) return;
129
+
130
+ if ("prefix" in install) {
131
+ throw new CliSchemaValidationError(
132
+ "install.prefix removed; app installs to ~/.local/bin/<key>",
133
+ );
134
+ }
135
+
136
+ if (!install.targets) return;
137
+
138
+ const targets = install.targets;
139
+ if ("allSkills" in targets || "allMcps" in targets) {
140
+ throw new CliSchemaValidationError(
141
+ "install.targets.allSkills/allMcps removed; use agentIntegration and per-key targets",
142
+ );
143
+ }
144
+
145
+ const integration: InstallAgentIntegration =
146
+ install.agentIntegration ?? (program.mcpServer?.enabled === true ? "mcp" : "skill");
147
+
148
+ for (const [mcpKey, skillKey] of AGENT_PAIRS) {
149
+ const mcpSpec = targets[mcpKey];
150
+ const skillSpec = targets[skillKey];
151
+ const mcpOn = installTargetExplicitTruthy(mcpSpec);
152
+ const skillOn = installTargetExplicitTruthy(skillSpec);
153
+ const host = PAIR_HOST_LABELS[mcpKey] ?? mcpKey;
154
+
155
+ if (mcpOn && skillOn && integration !== "both") {
156
+ throw new CliSchemaValidationError(
157
+ `install.targets: ${host} has both MCP and skill configured; set agentIntegration: 'both' or disable one side`,
158
+ );
159
+ }
160
+
161
+ if (integration === "skill" && mcpOn) {
162
+ throw new CliSchemaValidationError(
163
+ `install.targets.${mcpKey} requires agentIntegration: 'both' when agentIntegration is 'skill'`,
164
+ );
165
+ }
166
+ if (integration === "mcp" && skillOn) {
167
+ throw new CliSchemaValidationError(
168
+ `install.targets.${skillKey} requires agentIntegration: 'both' when agentIntegration is 'mcp'`,
169
+ );
170
+ }
171
+ }
172
+ }
173
+
107
174
  /** Validates a program schema. */
108
175
  export function cliValidateProgram(program: CliProgram): void {
109
176
  if (!program.version || program.version.trim().length === 0) {
@@ -141,6 +208,10 @@ export function cliValidateProgram(program: CliProgram): void {
141
208
  }
142
209
  }
143
210
 
211
+ if (program.install !== undefined) {
212
+ validateInstallConfig(program);
213
+ }
214
+
144
215
  const caps = resolveCapabilities(program);
145
216
  const reserved = reservedCommandNames(caps);
146
217
 
@@ -209,11 +280,15 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
209
280
 
210
281
  if (isRoot && program.mcpServer?.enabled === true && program.mcpServer.resources) {
211
282
  const schemaUri = resolveMcpSchemaUri(program);
283
+ const reserved = new Set([schemaUri, ...reservedDocsTopicResourceUris(program)]);
212
284
  const uris = program.mcpServer.resources.map((r) => r.uri);
213
- if (uris.includes(schemaUri)) {
214
- throw new CliSchemaValidationError(
215
- `mcpServer.resources URI '${schemaUri}' conflicts with the built-in schema resource`,
216
- );
285
+ for (const uri of uris) {
286
+ if (reserved.has(uri)) {
287
+ const kind = uri === schemaUri ? "built-in schema resource" : "auto docs topic resource";
288
+ throw new CliSchemaValidationError(
289
+ `mcpServer.resources URI '${uri}' conflicts with ${kind}`,
290
+ );
291
+ }
217
292
  }
218
293
  if (new Set(uris).size !== uris.length) {
219
294
  throw new CliSchemaValidationError("mcpServer.resources URIs must be unique");