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.
- package/CHANGELOG.md +42 -1
- package/README.md +6 -6
- package/docs/ai-skills.md +9 -4
- package/docs/bundled-docs.md +1 -0
- package/docs/cli-program.md +4 -4
- package/docs/config-schema.md +1 -4
- package/docs/developing.md +1 -1
- package/docs/install.md +160 -39
- package/docs/mcp.md +38 -11
- package/docs/templates/cursor/rules/cli-program.mdc +1 -1
- package/examples/config-app/program.ts +0 -3
- package/examples/consumer-app/README.md +1 -1
- package/examples/consumer-app/src/program.ts +5 -13
- package/examples/mcp-test.ts +14 -24
- package/index.d.ts +60 -5
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +20 -4
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/install.ts +50 -57
- package/src/builtins/mcp.ts +1 -1
- package/src/capabilities.ts +4 -4
- package/src/cli.ts +2 -0
- package/src/config/bootstrap.ts +167 -66
- package/src/config/context.test.ts +22 -36
- package/src/config/context.ts +5 -4
- package/src/config/file.test.ts +66 -56
- package/src/config/file.ts +33 -25
- package/src/config/resolve.test.ts +25 -1
- package/src/config/resolve.ts +43 -8
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +1 -1
- package/src/docs/docs.test.ts +1 -1
- package/src/docs/mcp-guide.ts +15 -4
- package/src/docs/mcp-resources.test.ts +63 -0
- package/src/docs/mcp-resources.ts +68 -0
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +4 -0
- package/src/install/{binary.ts → app.ts} +13 -13
- package/src/install/bootstrap.ts +22 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +187 -110
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +138 -42
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize.ts +35 -0
- package/src/install/paths.ts +27 -13
- package/src/install/plan.ts +30 -259
- package/src/install/shell.ts +2 -2
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -9
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +131 -0
- package/src/install/target-mcp-cli.ts +149 -0
- package/src/install/target-mcp-json.ts +130 -0
- package/src/install/target-plan-build.ts +67 -0
- package/src/install/target-registry.ts +57 -0
- package/src/install/target-scope.ts +266 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +145 -0
- package/src/install/targets/app.ts +69 -0
- package/src/install/targets/chatgpt-mcp.ts +12 -0
- package/src/install/targets/claude-code-mcp.ts +15 -0
- package/src/install/targets/claude-desktop-mcp.ts +12 -0
- package/src/install/targets/claude-skill.ts +16 -0
- package/src/install/targets/codex-mcp.ts +25 -0
- package/src/install/targets/codex-skill.ts +14 -0
- package/src/install/targets/completions.ts +133 -0
- package/src/install/targets/configure.ts +59 -0
- package/src/install/targets/cursor-mcp.ts +15 -0
- package/src/install/targets/cursor-skill.ts +16 -0
- package/src/install/targets/index.ts +53 -0
- package/src/install/targets/openclaw-mcp.ts +25 -0
- package/src/install/targets/openclaw-skill.ts +17 -0
- package/src/install/targets/opencode-mcp.ts +101 -0
- package/src/install/targets/opencode-skill.ts +15 -0
- package/src/install/targets.test.ts +136 -0
- package/src/install/uninstall.ts +16 -152
- package/src/install/update.test.ts +17 -2
- package/src/install/update.ts +2 -5
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +23 -4
- package/src/mcp/claude.ts +18 -13
- package/src/mcp/tools.ts +4 -1
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +18 -1
- package/src/parse.test.ts +57 -4
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/skill/generate.ts +89 -5
- package/src/skill/hint.ts +5 -0
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +65 -4
- package/src/validate.ts +79 -4
package/src/install/uninstall.ts
CHANGED
|
@@ -1,164 +1,19 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "node:fs";
|
|
2
|
-
import { resolveAppConfigPath, uninstallAppConfig } from "../config/file.ts";
|
|
3
2
|
import type { CliProgram } from "../types.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { removeMcpConfig } from "./mcp-config.ts";
|
|
9
|
-
import { detectOpenCodeMcpConfigPath, removeOpenCodeMcpConfig } from "./mcp-opencode.ts";
|
|
10
|
-
import { type InstallPaths, userHome } from "./paths.ts";
|
|
11
|
-
import {
|
|
12
|
-
type InstallOpts,
|
|
13
|
-
wantsInstallBin,
|
|
14
|
-
wantsInstallCompletions,
|
|
15
|
-
wantsInstallMcp,
|
|
16
|
-
wantsInstallSkill,
|
|
17
|
-
wantsUninstallConfig,
|
|
18
|
-
} from "./plan.ts";
|
|
3
|
+
import type { InstallPaths } from "./paths.ts";
|
|
4
|
+
import { buildUninstallPlanFromTargets } from "./target-plan-build.ts";
|
|
5
|
+
import { skillTargetForUninstallSummary } from "./target-registry.ts";
|
|
6
|
+
import type { InstallOpts, UninstallAction } from "./target-types.ts";
|
|
19
7
|
|
|
20
|
-
export
|
|
21
|
-
summary: string;
|
|
22
|
-
message: string;
|
|
23
|
-
run: () => string[];
|
|
24
|
-
}
|
|
8
|
+
export type { InstallActionKind, InstallOpts, UninstallAction } from "./target-types.ts";
|
|
25
9
|
|
|
26
|
-
/** Builds uninstall actions for scoped targets
|
|
10
|
+
/** Builds uninstall actions for scoped targets or --all (ignores install.targets on --all). */
|
|
27
11
|
export function buildUninstallPlan(
|
|
28
12
|
root: CliProgram,
|
|
29
13
|
paths: InstallPaths,
|
|
30
14
|
opts: InstallOpts,
|
|
31
15
|
): UninstallAction[] {
|
|
32
|
-
|
|
33
|
-
const dry = !!opts.dry;
|
|
34
|
-
const actions: UninstallAction[] = [];
|
|
35
|
-
|
|
36
|
-
if (wantsInstallBin(opts) && detected.binary) {
|
|
37
|
-
actions.push({
|
|
38
|
-
summary: `binary: ${paths.binaryPath}`,
|
|
39
|
-
message: `Removing binary ${paths.binaryPath}`,
|
|
40
|
-
run: () => uninstallBinary(root, paths, dry),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
wantsInstallCompletions(opts) &&
|
|
46
|
-
(detected.bashCompletion || detected.zshCompletion || detected.fishCompletion)
|
|
47
|
-
) {
|
|
48
|
-
if (detected.bashCompletion) {
|
|
49
|
-
actions.push({
|
|
50
|
-
summary: `bash completion: ${paths.bashCompletion}`,
|
|
51
|
-
message: `Removing bash completion ${paths.bashCompletion}`,
|
|
52
|
-
run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.bashCompletion),
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (detected.zshCompletion) {
|
|
56
|
-
actions.push({
|
|
57
|
-
summary: `zsh completion: ${paths.zshCompletion}`,
|
|
58
|
-
message: `Removing zsh completion ${paths.zshCompletion}`,
|
|
59
|
-
run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.zshCompletion),
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if (detected.fishCompletion) {
|
|
63
|
-
actions.push({
|
|
64
|
-
summary: `fish completion: ${paths.fishCompletion}`,
|
|
65
|
-
message: `Removing fish completion ${paths.fishCompletion}`,
|
|
66
|
-
run: () => uninstallCompletions(paths, dry).filter((p) => p === paths.fishCompletion),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (wantsInstallSkill(opts) && detected.cursorSkill) {
|
|
72
|
-
actions.push({
|
|
73
|
-
summary: `cursor skill: ${paths.cursorSkillDir}/`,
|
|
74
|
-
message: `Removing Cursor skill ${paths.cursorSkillDir}/`,
|
|
75
|
-
run: () => [],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (wantsInstallSkill(opts) && detected.claudeSkill) {
|
|
80
|
-
actions.push({
|
|
81
|
-
summary: `claude skill: ${paths.claudeSkillDir}/`,
|
|
82
|
-
message: `Removing Claude Code skill ${paths.claudeSkillDir}/`,
|
|
83
|
-
run: () => [],
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (wantsInstallMcp(opts, root)) {
|
|
88
|
-
if (detected.cursorMcp) {
|
|
89
|
-
actions.push({
|
|
90
|
-
summary: `cursor mcp: ${paths.cursorMcpPath}`,
|
|
91
|
-
message: `Removing MCP server "${paths.mcpName}" from ${paths.cursorMcpPath}`,
|
|
92
|
-
run: () => {
|
|
93
|
-
removeMcpConfig(paths.cursorMcpPath, paths.mcpName, dry);
|
|
94
|
-
return [paths.cursorMcpPath];
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
if (detected.claudeMcp) {
|
|
99
|
-
actions.push({
|
|
100
|
-
summary: `claude code mcp: ${paths.claudeMcpPath}`,
|
|
101
|
-
message: `Removing MCP server "${paths.mcpName}" from ${paths.claudeMcpPath}`,
|
|
102
|
-
run: () => {
|
|
103
|
-
removeMcpConfig(paths.claudeMcpPath, paths.mcpName, dry);
|
|
104
|
-
return [paths.claudeMcpPath];
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
if (detected.claudeDesktopMcp) {
|
|
109
|
-
actions.push({
|
|
110
|
-
summary: `claude desktop mcp: ${paths.claudeDesktopMcpPath}`,
|
|
111
|
-
message: `Removing MCP server "${paths.mcpName}" from ${paths.claudeDesktopMcpPath}`,
|
|
112
|
-
run: () => {
|
|
113
|
-
removeMcpConfig(paths.claudeDesktopMcpPath, paths.mcpName, dry);
|
|
114
|
-
return [paths.claudeDesktopMcpPath];
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (detected.opencodeMcp) {
|
|
119
|
-
const openCodePath =
|
|
120
|
-
detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) ?? paths.opencodeMcpPath;
|
|
121
|
-
actions.push({
|
|
122
|
-
summary: `opencode mcp: ${openCodePath}`,
|
|
123
|
-
message: `Removing MCP server "${paths.mcpName}" from ${openCodePath}`,
|
|
124
|
-
run: () => {
|
|
125
|
-
removeOpenCodeMcpConfig(openCodePath, paths.mcpName, dry);
|
|
126
|
-
return [openCodePath];
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
if (detected.codexMcp) {
|
|
131
|
-
actions.push({
|
|
132
|
-
summary: `codex mcp: ${paths.codexConfigPath}`,
|
|
133
|
-
message: `Removing MCP server "${paths.mcpName}" from Codex via codex mcp remove`,
|
|
134
|
-
run: () => {
|
|
135
|
-
removeCodexMcpConfig(userHome(), paths.mcpName, dry);
|
|
136
|
-
return [paths.codexConfigPath];
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
if (detected.chatGptMcp) {
|
|
141
|
-
actions.push({
|
|
142
|
-
summary: `chatgpt desktop mcp: ${paths.chatGptMcpPath}`,
|
|
143
|
-
message: `Removing MCP server "${paths.mcpName}" from ${paths.chatGptMcpPath}`,
|
|
144
|
-
run: () => {
|
|
145
|
-
removeMcpConfig(paths.chatGptMcpPath, paths.mcpName, dry);
|
|
146
|
-
return [paths.chatGptMcpPath];
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (wantsUninstallConfig(opts, root)) {
|
|
153
|
-
const configPath = resolveAppConfigPath(root);
|
|
154
|
-
actions.push({
|
|
155
|
-
summary: `app config: ${configPath}`,
|
|
156
|
-
message: `Removing app config ${configPath}`,
|
|
157
|
-
run: () => (uninstallAppConfig(root, dry) ? [configPath] : []),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return actions;
|
|
16
|
+
return buildUninstallPlanFromTargets(root, paths, opts);
|
|
162
17
|
}
|
|
163
18
|
|
|
164
19
|
/** Rimraf skill directories during uninstall. */
|
|
@@ -167,3 +22,12 @@ export function uninstallSkillDir(dir: string, dry: boolean): string[] {
|
|
|
167
22
|
if (!dry) rmSync(dir, { recursive: true, force: true });
|
|
168
23
|
return [`${dir}/`];
|
|
169
24
|
}
|
|
25
|
+
|
|
26
|
+
/** Skill directory for uninstall action summary prefix. */
|
|
27
|
+
export function skillDirFromUninstallSummary(
|
|
28
|
+
summary: string,
|
|
29
|
+
paths: InstallPaths,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
const target = skillTargetForUninstallSummary(summary);
|
|
32
|
+
return target?.skillDir(paths);
|
|
33
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { tmpdir } from "node:os";
|
|
4
12
|
import { join } from "node:path";
|
|
5
13
|
import { cliPresentationRoot } from "../builtins/presentation.ts";
|
|
@@ -68,10 +76,13 @@ test("runInstallMutation honors --from for binary copy", async () => {
|
|
|
68
76
|
writeFileSync(source, "#!/bin/sh\necho hi\n", "utf8");
|
|
69
77
|
chmodSync(source, 0o755);
|
|
70
78
|
|
|
79
|
+
const bindir = join(home, ".local", "bin");
|
|
80
|
+
mkdirSync(bindir, { recursive: true });
|
|
81
|
+
writeFileSync(join(bindir, "testapp"), "old\n", "utf8");
|
|
82
|
+
|
|
71
83
|
const { changed } = await runInstallMutation(root, {
|
|
72
84
|
reinstall: "1",
|
|
73
85
|
yes: "1",
|
|
74
|
-
quiet: "1",
|
|
75
86
|
from: source,
|
|
76
87
|
});
|
|
77
88
|
|
|
@@ -90,6 +101,10 @@ test("Cli.invoke install --update uses hook and reinstalls", async () => {
|
|
|
90
101
|
version: "2.0.0",
|
|
91
102
|
}));
|
|
92
103
|
|
|
104
|
+
const bindir = join(home, ".local", "bin");
|
|
105
|
+
mkdirSync(bindir, { recursive: true });
|
|
106
|
+
writeFileSync(join(bindir, "testapp"), "old\n", "utf8");
|
|
107
|
+
|
|
93
108
|
const result = await new Cli(root).invoke(["install", "--update"]);
|
|
94
109
|
expect(result.exitCode).toBe(0);
|
|
95
110
|
expect(result.stdout).toContain("Updated testapp 1.0.0 → 2.0.0");
|
package/src/install/update.ts
CHANGED
|
@@ -7,9 +7,7 @@ import { installErr } from "./status.ts";
|
|
|
7
7
|
export async function cliUpdate(root: CliProgram): Promise<never> {
|
|
8
8
|
const hook = root.install?.updateGetLatest;
|
|
9
9
|
if (!hook) {
|
|
10
|
-
installErr(
|
|
11
|
-
"install --update is not configured. Set install.updateGetLatest on the program root.",
|
|
12
|
-
);
|
|
10
|
+
installErr("Remote updates are not supported by this app.");
|
|
13
11
|
process.exit(1);
|
|
14
12
|
}
|
|
15
13
|
|
|
@@ -23,7 +21,7 @@ export async function cliUpdate(root: CliProgram): Promise<never> {
|
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
if (!artifact.path || !existsSync(artifact.path)) {
|
|
26
|
-
installErr(
|
|
24
|
+
installErr("Update download did not return a valid app binary.");
|
|
27
25
|
process.exit(1);
|
|
28
26
|
}
|
|
29
27
|
|
|
@@ -38,7 +36,6 @@ export async function cliUpdate(root: CliProgram): Promise<never> {
|
|
|
38
36
|
await runInstallMutation(root, {
|
|
39
37
|
reinstall: "1",
|
|
40
38
|
yes: "1",
|
|
41
|
-
quiet: "1",
|
|
42
39
|
from: artifact.path,
|
|
43
40
|
});
|
|
44
41
|
} catch (err) {
|
package/src/invoke.test.ts
CHANGED
|
@@ -16,7 +16,13 @@ test("ctx.invocation is cli via Cli.run", async () => {
|
|
|
16
16
|
const indexPath = join(import.meta.dir, "index.ts");
|
|
17
17
|
const { stdout } = await $`bun -e ${`
|
|
18
18
|
import { Cli, CliProgram } from ${JSON.stringify(indexPath)};
|
|
19
|
-
const program = {
|
|
19
|
+
const program = {
|
|
20
|
+
key: "t",
|
|
21
|
+
description: "d",
|
|
22
|
+
version: "0.0.0",
|
|
23
|
+
install: { enabled: false },
|
|
24
|
+
handler: (ctx) => console.log(ctx.invocation),
|
|
25
|
+
};
|
|
20
26
|
await new Cli(program).run([]);
|
|
21
27
|
`}`.quiet();
|
|
22
28
|
expect(stdout.toString().trim()).toBe("cli");
|
package/src/mcp/bundle.ts
CHANGED
|
@@ -160,9 +160,21 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
/** Runs `mcp bundle`: writes
|
|
163
|
+
/** Runs `mcp bundle`: writes enabled dist artifacts; prints one path per line. */
|
|
164
164
|
export function runMcpBundle(program: CliProgram): void {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const mcp = program.mcpServer;
|
|
166
|
+
if (!mcp?.mcpd && !mcp?.claudePlugin) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"mcp bundle: enable mcpServer.mcpd and/or mcpServer.claudePlugin on the program root.",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines: string[] = [];
|
|
173
|
+
if (mcp.mcpd) {
|
|
174
|
+
lines.push(packMcpBundle(program));
|
|
175
|
+
}
|
|
176
|
+
if (mcp.claudePlugin) {
|
|
177
|
+
lines.push(packClaudePlugin(program));
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
168
180
|
}
|
package/src/mcp/claude.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import type { CliProgram } from "../types.ts";
|
|
@@ -36,6 +37,7 @@ describe("claude plugin", () => {
|
|
|
36
37
|
test("generatePluginManifest includes userConfig from program.appConfig", () => {
|
|
37
38
|
const manifest = generatePluginManifest(configFixture, "myapp");
|
|
38
39
|
expect(manifest.name).toBe("myapp");
|
|
40
|
+
expect(manifest.mcpServers).toBe(".mcp.json");
|
|
39
41
|
const userConfig = manifest.userConfig as Record<string, Record<string, unknown>>;
|
|
40
42
|
expect(userConfig.api_token?.description).toBe("Token from settings.");
|
|
41
43
|
expect(userConfig.api_token?.sensitive).toBe(true);
|
|
@@ -55,7 +57,7 @@ describe("claude plugin", () => {
|
|
|
55
57
|
expect(paths.pluginZipPath).toBe(join(cwd, "dist", "claude-plugin", "myapp.zip"));
|
|
56
58
|
});
|
|
57
59
|
|
|
58
|
-
test("packClaudePlugin writes zip with
|
|
60
|
+
test("packClaudePlugin writes zip with MCP pointer skill only", () => {
|
|
59
61
|
const work = mkdtempSync(join(tmpdir(), "claude-plugin-test-"));
|
|
60
62
|
try {
|
|
61
63
|
const dist = join(work, "dist");
|
|
@@ -64,8 +66,25 @@ describe("claude plugin", () => {
|
|
|
64
66
|
writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 });
|
|
65
67
|
const paths = defaultClaudePluginPaths(configFixture, work);
|
|
66
68
|
packClaudePlugin(configFixture, { cwd: work, binaryPath });
|
|
67
|
-
const
|
|
68
|
-
expect(
|
|
69
|
+
const zip = readFileSync(paths.pluginZipPath);
|
|
70
|
+
expect(zip.length).toBeGreaterThan(0);
|
|
71
|
+
const zipText = zip.toString("utf8");
|
|
72
|
+
expect(zipText).toContain("skills/myapp/SKILL.md");
|
|
73
|
+
expect(zipText).toContain("MCP toolset");
|
|
74
|
+
expect(zipText).toContain("Server id: `myapp`");
|
|
75
|
+
expect(zipText).not.toContain("skills/myapp/reference.md");
|
|
76
|
+
expect(zipText).not.toContain("Invoke via shell");
|
|
77
|
+
|
|
78
|
+
writeFileSync(join(work, "out.zip"), zip);
|
|
79
|
+
const extract = join(work, "extract");
|
|
80
|
+
mkdirSync(extract, { recursive: true });
|
|
81
|
+
execSync("unzip -o -q ../out.zip", { cwd: extract });
|
|
82
|
+
const binMode = statSync(join(extract, "bin", "myapp")).mode & 0o777;
|
|
83
|
+
expect(binMode & 0o111).not.toBe(0);
|
|
84
|
+
const pluginJson = JSON.parse(
|
|
85
|
+
readFileSync(join(extract, ".claude-plugin", "plugin.json"), "utf8"),
|
|
86
|
+
) as { mcpServers: string };
|
|
87
|
+
expect(pluginJson.mcpServers).toBe(".mcp.json");
|
|
69
88
|
} finally {
|
|
70
89
|
rmSync(work, { recursive: true, force: true });
|
|
71
90
|
}
|
package/src/mcp/claude.ts
CHANGED
|
@@ -12,17 +12,18 @@ import {
|
|
|
12
12
|
readdirSync,
|
|
13
13
|
readFileSync,
|
|
14
14
|
rmSync,
|
|
15
|
+
statSync,
|
|
15
16
|
writeFileSync,
|
|
16
17
|
} from "node:fs";
|
|
17
18
|
import { tmpdir } from "node:os";
|
|
18
19
|
import { basename, join, relative, resolve } from "node:path";
|
|
19
20
|
import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
21
|
+
import { generatePluginSkillBundle } from "../skill/generate.ts";
|
|
22
|
+
import { applyPluginSkillHint } from "../skill/hint.ts";
|
|
22
23
|
import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
|
|
23
24
|
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
24
|
-
import { mcpServerId
|
|
25
|
-
import { zipStore } from "./zip.ts";
|
|
25
|
+
import { mcpServerId } from "./tools.ts";
|
|
26
|
+
import { type ZipFileEntry, zipStore } from "./zip.ts";
|
|
26
27
|
|
|
27
28
|
const DIST_DIR = "dist";
|
|
28
29
|
const CLAUDE_PLUGIN_DIR = "claude-plugin";
|
|
@@ -67,6 +68,7 @@ export function generatePluginManifest(
|
|
|
67
68
|
version: program.version,
|
|
68
69
|
description: program.description,
|
|
69
70
|
author: defaultAuthor(bundle),
|
|
71
|
+
mcpServers: ".mcp.json",
|
|
70
72
|
};
|
|
71
73
|
const userConfig = buildProgramUserConfig(program);
|
|
72
74
|
if (userConfig) {
|
|
@@ -93,8 +95,8 @@ export function generatePluginMcpJson(
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function collectZipEntries(rootDir: string, dir = rootDir):
|
|
97
|
-
const entries:
|
|
98
|
+
function collectZipEntries(rootDir: string, dir = rootDir): ZipFileEntry[] {
|
|
99
|
+
const entries: ZipFileEntry[] = [];
|
|
98
100
|
for (const ent of readdirSync(dir, { withFileTypes: true }) as Dirent[]) {
|
|
99
101
|
const full = join(dir, ent.name);
|
|
100
102
|
if (ent.isDirectory()) {
|
|
@@ -105,7 +107,12 @@ function collectZipEntries(rootDir: string, dir = rootDir): { name: string; data
|
|
|
105
107
|
continue;
|
|
106
108
|
}
|
|
107
109
|
const rel = relative(rootDir, full).split("\\").join("/");
|
|
108
|
-
|
|
110
|
+
const stMode = statSync(full).mode;
|
|
111
|
+
const entry: ZipFileEntry = { name: rel, data: readFileSync(full) };
|
|
112
|
+
if (stMode & 0o111) {
|
|
113
|
+
entry.unixMode = stMode;
|
|
114
|
+
}
|
|
115
|
+
entries.push(entry);
|
|
109
116
|
}
|
|
110
117
|
return entries;
|
|
111
118
|
}
|
|
@@ -116,13 +123,12 @@ function writePluginTree(
|
|
|
116
123
|
binaryPath: string,
|
|
117
124
|
binaryName: string,
|
|
118
125
|
): void {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const hinted = applySkillBundleHints(program, bundle.skillMd, bundle.referenceMd);
|
|
126
|
+
const bundle = generatePluginSkillBundle(program);
|
|
127
|
+
const skillMd = applyPluginSkillHint(program, bundle.skillMd);
|
|
122
128
|
|
|
123
129
|
mkdirSync(join(pluginRoot, ".claude-plugin"), { recursive: true });
|
|
124
130
|
mkdirSync(join(pluginRoot, "bin"), { recursive: true });
|
|
125
|
-
mkdirSync(join(pluginRoot, "skills",
|
|
131
|
+
mkdirSync(join(pluginRoot, "skills", bundle.dirName), { recursive: true });
|
|
126
132
|
|
|
127
133
|
writeFileSync(
|
|
128
134
|
join(pluginRoot, ".claude-plugin", "plugin.json"),
|
|
@@ -133,8 +139,7 @@ function writePluginTree(
|
|
|
133
139
|
`${JSON.stringify(generatePluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
134
140
|
);
|
|
135
141
|
cpSync(binaryPath, join(pluginRoot, "bin", binaryName), { mode: 0o755 });
|
|
136
|
-
writeFileSync(join(pluginRoot, "skills",
|
|
137
|
-
writeFileSync(join(pluginRoot, "skills", skillDirName, "reference.md"), hinted.referenceMd);
|
|
142
|
+
writeFileSync(join(pluginRoot, "skills", bundle.dirName, "SKILL.md"), skillMd);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
package/src/mcp/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
|
|
|
3
3
|
flat JSON tool arguments into argv for Cli.invoke.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { docsMcpResources } from "../docs/mcp-resources.ts";
|
|
6
7
|
import { cliResolveNotes } from "../help.ts";
|
|
7
8
|
import { visibleOptions } from "../hidden.ts";
|
|
8
9
|
import { collectOptionDefs } from "../parse.ts";
|
|
@@ -26,6 +27,8 @@ export function defaultMcpSchemaUri(mcpId: string): string {
|
|
|
26
27
|
return `${mcpId}://schema`;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
|
|
31
|
+
|
|
29
32
|
/** Sanitizes a command key segment for MCP tool names and server identity. */
|
|
30
33
|
export function sanitizeToolSegment(key: string): string {
|
|
31
34
|
return key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
@@ -209,7 +212,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
|
|
|
209
212
|
mimeType: r.mimeType ?? "text/plain",
|
|
210
213
|
load: r.load,
|
|
211
214
|
}));
|
|
212
|
-
return [builtIn, ...user];
|
|
215
|
+
return [builtIn, ...docsMcpResources(root), ...user];
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/** Recursively collects MCP tool definitions from user leaf commands. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { zipStore } from "./zip.ts";
|
|
7
|
+
|
|
8
|
+
test("zipStore preserves unix executable mode on extract", () => {
|
|
9
|
+
const work = mkdtempSync(join(tmpdir(), "zip-exec-"));
|
|
10
|
+
const data = Buffer.from("#!/bin/sh\necho hi\n");
|
|
11
|
+
const zip = zipStore([{ name: "bin/tool", data, unixMode: 0o100755 }]);
|
|
12
|
+
writeFileSync(join(work, "plugin.zip"), zip);
|
|
13
|
+
execSync("unzip -o -q plugin.zip", { cwd: work });
|
|
14
|
+
const mode = statSync(join(work, "bin", "tool")).mode & 0o777;
|
|
15
|
+
expect(mode & 0o111).not.toBe(0);
|
|
16
|
+
expect(readFileSync(join(work, "bin", "tool"), "utf8")).toBe("#!/bin/sh\necho hi\n");
|
|
17
|
+
});
|
package/src/mcp/zip.ts
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
Minimal ZIP writer (store method, no compression) for MCPB and Claude plugin bundles.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/** Info-ZIP Unix UID/GID extra field (`0x7875`) for permission restore on extract. */
|
|
6
|
+
function unixUxExtraField(uid: number, gid: number): Buffer {
|
|
7
|
+
const uidBuf = Buffer.alloc(4);
|
|
8
|
+
uidBuf.writeUInt32LE(uid >>> 0, 0);
|
|
9
|
+
const gidBuf = Buffer.alloc(4);
|
|
10
|
+
gidBuf.writeUInt32LE(gid >>> 0, 0);
|
|
11
|
+
const payload = Buffer.concat([Buffer.from([1, 4]), uidBuf, Buffer.from([4]), gidBuf]);
|
|
12
|
+
const header = Buffer.alloc(4);
|
|
13
|
+
header.writeUInt16LE(0x7875, 0);
|
|
14
|
+
header.writeUInt16LE(payload.length, 2);
|
|
15
|
+
return Buffer.concat([header, payload]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Info-ZIP Unix permission extra field (`0x7855`). */
|
|
19
|
+
function unixUpExtraField(unixMode: number): Buffer {
|
|
20
|
+
const payload = Buffer.alloc(5);
|
|
21
|
+
payload.writeUInt8(1, 0);
|
|
22
|
+
payload.writeUInt32LE(unixMode >>> 0, 1);
|
|
23
|
+
const header = Buffer.alloc(4);
|
|
24
|
+
header.writeUInt16LE(0x7855, 0);
|
|
25
|
+
header.writeUInt16LE(payload.length, 2);
|
|
26
|
+
return Buffer.concat([header, payload]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function unixExtraFields(unixMode: number): Buffer {
|
|
30
|
+
const { uid, gid } = defaultUnixIds();
|
|
31
|
+
return Buffer.concat([unixUpExtraField(unixMode), unixUxExtraField(uid, gid)]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function defaultUnixIds(): { uid: number; gid: number } {
|
|
35
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 501;
|
|
36
|
+
const gid = typeof process.getgid === "function" ? process.getgid() : 20;
|
|
37
|
+
return { uid, gid };
|
|
38
|
+
}
|
|
39
|
+
|
|
5
40
|
/** CRC-32 for ZIP local headers. */
|
|
6
41
|
function crc32(data: Buffer): number {
|
|
7
42
|
let crc = 0xffffffff;
|
|
@@ -18,8 +53,16 @@ function crc32(data: Buffer): number {
|
|
|
18
53
|
return (crc ^ 0xffffffff) >>> 0;
|
|
19
54
|
}
|
|
20
55
|
|
|
56
|
+
/** One file entry for {@link zipStore}. */
|
|
57
|
+
export interface ZipFileEntry {
|
|
58
|
+
name: string;
|
|
59
|
+
data: Buffer;
|
|
60
|
+
/** Unix permission bits (e.g. `0o755`). Encoded in ZIP external attributes when set. */
|
|
61
|
+
unixMode?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
/** Writes a minimal ZIP (store, no compression) with one or more files. */
|
|
22
|
-
export function zipStore(files:
|
|
65
|
+
export function zipStore(files: ZipFileEntry[]): Buffer {
|
|
23
66
|
const parts: Buffer[] = [];
|
|
24
67
|
const central: Buffer[] = [];
|
|
25
68
|
let offset = 0;
|
|
@@ -27,9 +70,11 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
27
70
|
for (const file of files) {
|
|
28
71
|
const nameBuf = Buffer.from(file.name, "utf8");
|
|
29
72
|
const crc = crc32(file.data);
|
|
30
|
-
const
|
|
73
|
+
const unixMode = file.unixMode;
|
|
74
|
+
const extra = unixMode !== undefined ? unixExtraFields(unixMode) : Buffer.alloc(0);
|
|
75
|
+
const local = Buffer.alloc(30 + nameBuf.length + extra.length);
|
|
31
76
|
local.writeUInt32LE(0x04034b50, 0);
|
|
32
|
-
local.writeUInt16LE(20, 4);
|
|
77
|
+
local.writeUInt16LE(unixMode !== undefined ? 10 : 20, 4);
|
|
33
78
|
local.writeUInt16LE(0, 6);
|
|
34
79
|
local.writeUInt16LE(0, 8);
|
|
35
80
|
local.writeUInt16LE(0, 10);
|
|
@@ -38,13 +83,15 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
38
83
|
local.writeUInt32LE(file.data.length, 18);
|
|
39
84
|
local.writeUInt32LE(file.data.length, 22);
|
|
40
85
|
local.writeUInt32LE(nameBuf.length, 26);
|
|
41
|
-
local.writeUInt16LE(
|
|
86
|
+
local.writeUInt16LE(extra.length, 28);
|
|
42
87
|
nameBuf.copy(local, 30);
|
|
88
|
+
extra.copy(local, 30 + nameBuf.length);
|
|
43
89
|
|
|
44
|
-
const centralHdr = Buffer.alloc(46 + nameBuf.length);
|
|
90
|
+
const centralHdr = Buffer.alloc(46 + nameBuf.length + extra.length);
|
|
45
91
|
centralHdr.writeUInt32LE(0x02014b50, 0);
|
|
46
|
-
|
|
47
|
-
centralHdr.writeUInt16LE(20,
|
|
92
|
+
// macOS Info-ZIP stores version made by before version needed (non-APPNOTE layout).
|
|
93
|
+
centralHdr.writeUInt16LE(unixMode !== undefined ? 0x031e : 20, 4);
|
|
94
|
+
centralHdr.writeUInt16LE(unixMode !== undefined ? 10 : 20, 6);
|
|
48
95
|
centralHdr.writeUInt16LE(0, 8);
|
|
49
96
|
centralHdr.writeUInt16LE(0, 10);
|
|
50
97
|
centralHdr.writeUInt16LE(0, 12);
|
|
@@ -53,13 +100,19 @@ export function zipStore(files: { name: string; data: Buffer }[]): Buffer {
|
|
|
53
100
|
centralHdr.writeUInt32LE(file.data.length, 20);
|
|
54
101
|
centralHdr.writeUInt32LE(file.data.length, 24);
|
|
55
102
|
centralHdr.writeUInt32LE(nameBuf.length, 28);
|
|
56
|
-
centralHdr.writeUInt16LE(
|
|
103
|
+
centralHdr.writeUInt16LE(extra.length, 30);
|
|
57
104
|
centralHdr.writeUInt16LE(0, 32);
|
|
58
105
|
centralHdr.writeUInt16LE(0, 34);
|
|
59
106
|
centralHdr.writeUInt16LE(0, 36);
|
|
60
|
-
|
|
107
|
+
if (unixMode !== undefined) {
|
|
108
|
+
const externalAttr = (unixMode << 16) >>> 0;
|
|
109
|
+
centralHdr.writeUInt32LE(externalAttr, 38);
|
|
110
|
+
} else {
|
|
111
|
+
centralHdr.writeUInt32LE(0, 38);
|
|
112
|
+
}
|
|
61
113
|
centralHdr.writeUInt32LE(offset, 42);
|
|
62
114
|
nameBuf.copy(centralHdr, 46);
|
|
115
|
+
extra.copy(centralHdr, 46 + nameBuf.length);
|
|
63
116
|
|
|
64
117
|
parts.push(local, file.data);
|
|
65
118
|
central.push(centralHdr);
|
|
@@ -469,7 +469,7 @@ test("MCP ping returns empty result", async () => {
|
|
|
469
469
|
test("minimal.ts mcp without opt-in fails", async () => {
|
|
470
470
|
const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
|
|
471
471
|
expect(exitCode).toBe(1);
|
|
472
|
-
expect(stderr.toString()).toContain("MCP is not
|
|
472
|
+
expect(stderr.toString()).toContain("MCP is not available");
|
|
473
473
|
});
|
|
474
474
|
|
|
475
475
|
test("MCP resources/list includes custom resource", async () => {
|
|
@@ -480,9 +480,26 @@ test("MCP resources/list includes custom resource", async () => {
|
|
|
480
480
|
const res = responses.get(10) as { result: { resources: { uri: string }[] } };
|
|
481
481
|
const uris = res.result.resources.map((r) => r.uri);
|
|
482
482
|
expect(uris).toContain("mcp_test://schema");
|
|
483
|
+
expect(uris).toContain("mcp_test://docs/readme");
|
|
483
484
|
expect(uris).toContain("test://hello");
|
|
484
485
|
});
|
|
485
486
|
|
|
487
|
+
test("MCP resources/read returns docs topic resource body", async () => {
|
|
488
|
+
const responses = await mcpRequest(
|
|
489
|
+
[
|
|
490
|
+
{
|
|
491
|
+
jsonrpc: "2.0",
|
|
492
|
+
id: 13,
|
|
493
|
+
method: "resources/read",
|
|
494
|
+
params: { uri: "mcp_test://docs/readme" },
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
{ script: "examples/mcp-test.ts" },
|
|
498
|
+
);
|
|
499
|
+
const res = responses.get(13) as { result: { contents: { text: string }[] } };
|
|
500
|
+
expect(res.result.contents[0]?.text).toBe("# MCP test readme\n");
|
|
501
|
+
});
|
|
502
|
+
|
|
486
503
|
test("MCP resources/read returns custom resource body", async () => {
|
|
487
504
|
const responses = await mcpRequest(
|
|
488
505
|
[{ jsonrpc: "2.0", id: 11, method: "resources/read", params: { uri: "test://hello" } }],
|