toolfactory 0.1.1 → 0.2.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/README.md +31 -3
- package/dist/bindings/python.js +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/distribution/native.d.ts +50 -0
- package/dist/distribution/native.js +414 -0
- package/dist/distribution/native.test.d.ts +1 -0
- package/dist/distribution/native.test.js +127 -0
- package/dist/distribution/system.d.ts +57 -0
- package/dist/distribution/system.js +637 -0
- package/dist/distribution/system.test.d.ts +1 -0
- package/dist/distribution/system.test.js +116 -0
- package/dist/e2e.test.js +7 -1
- package/dist/hosts/openclaw.js +6 -2
- package/dist/hosts/vscode.d.ts +1 -0
- package/dist/hosts/vscode.js +67 -0
- package/dist/model.d.ts +63 -2
- package/dist/model.js +18 -1
- package/dist/ops.d.ts +52 -0
- package/dist/project/apply.d.ts +1 -1
- package/dist/project/apply.js +194 -17
- package/dist/project/apply.test.js +103 -0
- package/dist/project/gate.d.ts +17 -2
- package/dist/project/gate.js +102 -23
- package/dist/project/history.d.ts +30 -1
- package/dist/project/lock.d.ts +5 -0
- package/dist/project/lock.js +9 -0
- package/dist/surfaces/agent-plugins.js +5 -4
- package/dist/surfaces/agents.js +27 -3
- package/dist/surfaces/agents.test.js +12 -0
- package/dist/surfaces/claude.js +8 -3
- package/dist/surfaces/codex.js +9 -4
- package/dist/surfaces/codex.test.js +16 -6
- package/dist/surfaces/cursor.js +2 -2
- package/dist/surfaces/distribution.test.js +121 -7
- package/dist/surfaces/dsh.js +29 -7
- package/dist/surfaces/dsh.test.js +11 -6
- package/dist/surfaces/gemini.js +2 -2
- package/dist/surfaces/gemini.test.js +4 -4
- package/dist/surfaces/hermes-native.js +43 -12
- package/dist/surfaces/hermes-native.test.js +53 -11
- package/dist/surfaces/mcp-registry.js +59 -7
- package/dist/surfaces/mcp-registry.test.js +23 -0
- package/dist/surfaces/mcpb.d.ts +1 -0
- package/dist/surfaces/mcpb.js +36 -23
- package/dist/surfaces/mcpb.test.js +31 -3
- package/dist/surfaces/npm.js +45 -14
- package/dist/surfaces/openclaw-native.js +10 -8
- package/dist/surfaces/openclaw-native.test.js +30 -0
- package/dist/surfaces/pypi.js +27 -3
- package/dist/surfaces/readme.d.ts +16 -0
- package/dist/surfaces/readme.js +17 -7
- package/dist/surfaces/registry.js +9 -3
- package/dist/surfaces/vscode-extension.d.ts +14 -0
- package/dist/surfaces/vscode-extension.js +312 -0
- package/dist/surfaces/vscode-extension.test.d.ts +1 -0
- package/dist/surfaces/vscode-extension.test.js +81 -0
- package/dist/surfaces/vscode-scaffold.d.ts +43 -0
- package/dist/surfaces/vscode-scaffold.js +43 -0
- package/dist/surfaces/workflows.js +57 -11
- package/dist/surfaces/workflows.test.js +71 -3
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +2 -1
- package/src/bindings/python.ts +1 -1
- package/src/distribution/native.test.ts +161 -0
- package/src/distribution/native.ts +460 -0
- package/src/distribution/system.test.ts +136 -0
- package/src/distribution/system.ts +688 -0
- package/src/e2e.test.ts +7 -1
- package/src/hosts/openclaw.ts +4 -2
- package/src/hosts/vscode.ts +79 -0
- package/src/model.ts +26 -2
- package/src/project/apply.test.ts +120 -0
- package/src/project/apply.ts +257 -15
- package/src/project/gate.ts +131 -24
- package/src/project/lock.ts +12 -0
- package/src/surfaces/agent-plugins.ts +5 -4
- package/src/surfaces/agents.test.ts +16 -0
- package/src/surfaces/agents.ts +38 -3
- package/src/surfaces/claude.ts +7 -3
- package/src/surfaces/codex.test.ts +15 -5
- package/src/surfaces/codex.ts +12 -6
- package/src/surfaces/cursor.ts +2 -10
- package/src/surfaces/distribution.test.ts +141 -6
- package/src/surfaces/dsh.test.ts +11 -5
- package/src/surfaces/dsh.ts +30 -7
- package/src/surfaces/gemini.test.ts +3 -3
- package/src/surfaces/gemini.ts +1 -2
- package/src/surfaces/hermes-native.test.ts +61 -10
- package/src/surfaces/hermes-native.ts +46 -13
- package/src/surfaces/mcp-registry.test.ts +28 -0
- package/src/surfaces/mcp-registry.ts +66 -8
- package/src/surfaces/mcpb.test.ts +32 -5
- package/src/surfaces/mcpb.ts +44 -23
- package/src/surfaces/npm.ts +47 -15
- package/src/surfaces/openclaw-native.test.ts +37 -0
- package/src/surfaces/openclaw-native.ts +49 -47
- package/src/surfaces/pypi.ts +27 -3
- package/src/surfaces/readme.ts +21 -6
- package/src/surfaces/registry.ts +9 -3
- package/src/surfaces/vscode-extension.test.ts +89 -0
- package/src/surfaces/vscode-extension.ts +345 -0
- package/src/surfaces/vscode-scaffold.ts +55 -0
- package/src/surfaces/workflows.test.ts +86 -3
- package/src/surfaces/workflows.ts +57 -11
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/{index-CCXhQKK3.css → index-BuT2czb2.css} +1 -1
- package/web/dist/assets/{index-D1PwZ3qS.js → index-CVm6m4s0.js} +1 -1
- package/web/dist/index.html +2 -2
package/dist/project/gate.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
|
|
10
10
|
* (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
|
|
11
11
|
*/
|
|
12
|
+
import { nativePackageSteps, nativeRegistryRows, nativeToolchainSteps, } from "../distribution/native.js";
|
|
13
|
+
import { systemPackageSteps, systemRegistryRows, } from "../distribution/system.js";
|
|
12
14
|
import { githubSlug } from "../hosts/github.js";
|
|
13
15
|
import { githubOwner, projectName } from "../identity/name.js";
|
|
14
16
|
import { isInstructionOnly } from "../model.js";
|
|
@@ -17,6 +19,7 @@ import { HOST_DIR as DSH_HOST_DIR } from "../surfaces/dsh.js";
|
|
|
17
19
|
import { MANIFEST_PATH as MCPB_MANIFEST_PATH, MCPB_PIN } from "../surfaces/mcpb.js";
|
|
18
20
|
import { HOST_DIR as OPENCLAW_HOST_DIR } from "../surfaces/openclaw-native.js";
|
|
19
21
|
import { configProperties, envName, has, isSensitive, npmName, pypiName, } from "../surfaces/shared.js";
|
|
22
|
+
import { extensionName, OVSX_VERSION, publisher, VSCE_VERSION, HOST_DIR as VSCODE_HOST_DIR, vsixName, } from "../surfaces/vscode-extension.js";
|
|
20
23
|
/** Where `toolfactory package` and the release `package` job leave every asset. */
|
|
21
24
|
export const RELEASE_DIR = "dist/release";
|
|
22
25
|
/** The web app's build, shared by the release tarball and the Pages job (which sets `PAGES_BASE`). */
|
|
@@ -88,11 +91,18 @@ export function gateSteps(project) {
|
|
|
88
91
|
const pm = commands(project);
|
|
89
92
|
const cli = toolfactoryCli(project);
|
|
90
93
|
const openclaw = has(project, "openclaw-native");
|
|
91
|
-
const steps = [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const steps = [...bootstrapSteps(project)];
|
|
95
|
+
if (!typescript && has(project, "hermes-native")) {
|
|
96
|
+
// Hermes' launcher clears PYTHONPATH and runs its own venv. Its plugin dependency
|
|
97
|
+
// declaration is advisory, so Doctor can import a native Python core only after the
|
|
98
|
+
// project is installed into that interpreter. Run after bootstrap's `uv sync` on CI.
|
|
99
|
+
steps.push({
|
|
100
|
+
name: "Install core into Hermes",
|
|
101
|
+
run: 'uv pip install --python "$HOME/.hermes/hermes-agent/venv/bin/python" --editable .',
|
|
102
|
+
when: "ci",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
steps.push({ name: "toolfactory check", run: `${cli} check` }, outputsStep(project));
|
|
96
106
|
if (has(project, "claude")) {
|
|
97
107
|
steps.push({
|
|
98
108
|
name: "Install Claude Code CLI",
|
|
@@ -100,7 +110,9 @@ export function gateSteps(project) {
|
|
|
100
110
|
when: "ci",
|
|
101
111
|
});
|
|
102
112
|
}
|
|
103
|
-
if (has(project, "web") ||
|
|
113
|
+
if (has(project, "web") ||
|
|
114
|
+
has(project, "browser-extension") ||
|
|
115
|
+
has(project, "vscode-extension")) {
|
|
104
116
|
// The web smoke and the browser extension's own Playwright smoke both drive Chromium;
|
|
105
117
|
// their own npm installs fetch the browser, the runner needs its system libraries.
|
|
106
118
|
steps.push({
|
|
@@ -109,6 +121,7 @@ export function gateSteps(project) {
|
|
|
109
121
|
when: "ci",
|
|
110
122
|
});
|
|
111
123
|
}
|
|
124
|
+
steps.push(...nativeToolchainSteps(project));
|
|
112
125
|
steps.push({
|
|
113
126
|
name: "toolfactory validate",
|
|
114
127
|
run: `${cli} validate`,
|
|
@@ -121,6 +134,8 @@ export function gateSteps(project) {
|
|
|
121
134
|
name: "author tests",
|
|
122
135
|
run: typescript ? pm.test : "uv run --with pytest pytest -q",
|
|
123
136
|
});
|
|
137
|
+
if (typescript)
|
|
138
|
+
steps.push({ name: "author integration tests", run: pm.run("test:integration") });
|
|
124
139
|
if (openclaw) {
|
|
125
140
|
// T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
|
|
126
141
|
// exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
|
|
@@ -183,13 +198,16 @@ const LEGAL_BUNDLE_PATHS = [
|
|
|
183
198
|
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
184
199
|
* to a registry stays a CI concern; producing the artifacts never is.
|
|
185
200
|
*/
|
|
186
|
-
export function packageSteps(project) {
|
|
201
|
+
export function packageSteps(project, options = {}) {
|
|
187
202
|
const cli = toolfactoryCli(project);
|
|
188
203
|
const name = project.identity.name;
|
|
189
204
|
const steps = [
|
|
190
205
|
{ name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
|
|
191
206
|
...bootstrapSteps(project),
|
|
192
207
|
outputsStep(project),
|
|
208
|
+
...nativeToolchainSteps(project),
|
|
209
|
+
...nativePackageSteps(project),
|
|
210
|
+
...systemPackageSteps(project, { include: options.system !== false }),
|
|
193
211
|
];
|
|
194
212
|
if (has(project, "web")) {
|
|
195
213
|
steps.push({
|
|
@@ -202,27 +220,34 @@ export function packageSteps(project) {
|
|
|
202
220
|
if (has(project, "npm")) {
|
|
203
221
|
steps.push({ name: "npm tarball", run: `npm pack --pack-destination ${RELEASE_DIR}` });
|
|
204
222
|
}
|
|
223
|
+
if (has(project, "pypi")) {
|
|
224
|
+
// Keep the wheel and sdist apart from the release's npm tarballs, plugin bundles and web
|
|
225
|
+
// archive: PyPA's publisher accepts only Python distribution files. The MCPB stage below
|
|
226
|
+
// unpacks this same canonical sdist rather than reproducing its source-file selection.
|
|
227
|
+
steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}/pypi` });
|
|
228
|
+
}
|
|
205
229
|
if (has(project, "mcpb")) {
|
|
206
|
-
// A bundle root is the published package with its production dependencies installed into
|
|
207
|
-
// it, so it is built from the tarball `npm pack` just wrote rather than from the checkout:
|
|
208
|
-
// whatever ships to npm is exactly what ships to Claude Desktop. `--ignore-scripts` because
|
|
209
|
-
// nothing in a bundle root may run a build; the tarball already carries `dist/`.
|
|
210
230
|
const stage = "dist/mcpb";
|
|
211
|
-
const tarball = npmTarball(project);
|
|
212
231
|
steps.push({
|
|
213
232
|
name: "MCPB bundle",
|
|
214
|
-
run:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
run: project.tool.binding === "python"
|
|
234
|
+
? [
|
|
235
|
+
`rm -rf ${stage} && mkdir -p ${stage}`,
|
|
236
|
+
`tar -xzf ${RELEASE_DIR}/pypi/*.tar.gz -C ${stage} --strip-components=1`,
|
|
237
|
+
`cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
|
|
238
|
+
`npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
|
|
239
|
+
].join(" && ")
|
|
240
|
+
: [
|
|
241
|
+
// The Node bundle root is the published package with production dependencies
|
|
242
|
+
// installed, so whatever ships to npm is exactly what ships to Claude Desktop.
|
|
243
|
+
`rm -rf ${stage} && mkdir -p ${stage}`,
|
|
244
|
+
`tar -xzf ${RELEASE_DIR}/${npmTarball(project)} -C ${stage} --strip-components=1`,
|
|
245
|
+
`npm --prefix ${stage} install --omit=dev --ignore-scripts`,
|
|
246
|
+
`cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
|
|
247
|
+
`npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
|
|
248
|
+
].join(" && "),
|
|
221
249
|
});
|
|
222
250
|
}
|
|
223
|
-
if (has(project, "pypi")) {
|
|
224
|
-
steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}` });
|
|
225
|
-
}
|
|
226
251
|
if (has(project, "openclaw-native")) {
|
|
227
252
|
// ClawHub's reusable workflow has no build step of its own, so the tarball it publishes has
|
|
228
253
|
// to arrive already built (`package_artifact_name`).
|
|
@@ -281,6 +306,12 @@ export function packageSteps(project) {
|
|
|
281
306
|
});
|
|
282
307
|
}
|
|
283
308
|
// COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
|
|
309
|
+
if (has(project, "vscode-extension")) {
|
|
310
|
+
steps.push({
|
|
311
|
+
name: "VS Code extension VSIX",
|
|
312
|
+
run: `npm --prefix ${VSCODE_HOST_DIR} install && npm --prefix ${VSCODE_HOST_DIR} run vsix && cp ${VSCODE_HOST_DIR}/extension.vsix ${RELEASE_DIR}/${vsixName(project)}`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
284
315
|
steps.push({
|
|
285
316
|
name: "coverage report",
|
|
286
317
|
run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
|
|
@@ -341,6 +372,22 @@ const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
|
|
|
341
372
|
export const RELEASE_SECRET_NAMES = [
|
|
342
373
|
"NPM_TOKEN",
|
|
343
374
|
"CLAWHUB_TOKEN",
|
|
375
|
+
"VSCE_PAT",
|
|
376
|
+
"OVSX_PAT",
|
|
377
|
+
"CARGO_REGISTRY_TOKEN",
|
|
378
|
+
"NUGET_API_KEY",
|
|
379
|
+
"MAVEN_CENTRAL_USERNAME",
|
|
380
|
+
"MAVEN_CENTRAL_PASSWORD",
|
|
381
|
+
"MAVEN_GPG_PRIVATE_KEY",
|
|
382
|
+
"MAVEN_GPG_PASSPHRASE",
|
|
383
|
+
"RUBYGEMS_API_KEY",
|
|
384
|
+
"HOMEBREW_TAP_TOKEN",
|
|
385
|
+
"SCOOP_BUCKET_TOKEN",
|
|
386
|
+
"CHOCOLATEY_API_KEY",
|
|
387
|
+
"APT_GPG_PRIVATE_KEY",
|
|
388
|
+
"APT_GPG_KEY_ID",
|
|
389
|
+
"COPR_LOGIN",
|
|
390
|
+
"COPR_TOKEN",
|
|
344
391
|
...CHROME_SECRETS,
|
|
345
392
|
...FIREFOX_SECRETS,
|
|
346
393
|
...EDGE_SECRETS,
|
|
@@ -365,6 +412,29 @@ export function registries(project) {
|
|
|
365
412
|
const browserZip = (browser) => `${BROWSER_HOST_DIR}/.output/${zipName(project, browser)}`;
|
|
366
413
|
const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
|
|
367
414
|
const rows = [
|
|
415
|
+
...nativeRegistryRows(project),
|
|
416
|
+
...systemRegistryRows(project),
|
|
417
|
+
{
|
|
418
|
+
id: "vscode-marketplace",
|
|
419
|
+
surfaces: ["vscode-extension"],
|
|
420
|
+
secrets: ["VSCE_PAT"],
|
|
421
|
+
url: "https://marketplace.visualstudio.com/manage",
|
|
422
|
+
probe: `npx --yes @vscode/vsce@${VSCE_VERSION} verify-pat ${publisher(project)}`,
|
|
423
|
+
exists: `npx --yes @vscode/vsce@${VSCE_VERSION} show ${publisher(project)}.${extensionName(project)} --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.exit(JSON.parse(s).versions.some(v=>v.version==="${version}")?0:1))'`,
|
|
424
|
+
gate: allSet(["VSCE_PAT"]),
|
|
425
|
+
retractUrl: "https://marketplace.visualstudio.com/manage",
|
|
426
|
+
retractHard: `npx --yes @vscode/vsce@${VSCE_VERSION} unpublish ${publisher(project)}.${extensionName(project)} --force`,
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "open-vsx",
|
|
430
|
+
surfaces: ["vscode-extension"],
|
|
431
|
+
secrets: ["OVSX_PAT"],
|
|
432
|
+
url: "https://open-vsx.org/user-settings/tokens",
|
|
433
|
+
probe: `npx --yes ovsx@${OVSX_VERSION} verify-pat ${publisher(project)}`,
|
|
434
|
+
exists: `curl -fsS -o /dev/null https://open-vsx.org/api/${publisher(project)}/${extensionName(project)}/${version}`,
|
|
435
|
+
gate: allSet(["OVSX_PAT"]),
|
|
436
|
+
retractUrl: "https://open-vsx.org/user-settings/extensions",
|
|
437
|
+
},
|
|
368
438
|
{
|
|
369
439
|
id: "npm",
|
|
370
440
|
surfaces: ["npm"],
|
|
@@ -513,7 +583,9 @@ export function registries(project) {
|
|
|
513
583
|
},
|
|
514
584
|
];
|
|
515
585
|
const selected = rows.filter((row) => row.surfaces.every((surface) => has(project, surface)) &&
|
|
516
|
-
(row.id !== "safari" || project.tool.browserExtension?.safari === true)
|
|
586
|
+
(row.id !== "safari" || project.tool.browserExtension?.safari === true) &&
|
|
587
|
+
(row.id !== "vscode-marketplace" || project.tool.vscode?.marketplace !== false) &&
|
|
588
|
+
(row.id !== "open-vsx" || project.tool.vscode?.openvsx !== false));
|
|
517
589
|
const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
|
|
518
590
|
if (mcpRegistry) {
|
|
519
591
|
const packages = selected.filter((row) => row.id === "npm" || row.id === "pypi" || row.id === "oci");
|
|
@@ -521,6 +593,11 @@ export function registries(project) {
|
|
|
521
593
|
? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
|
|
522
594
|
: "true";
|
|
523
595
|
}
|
|
596
|
+
const runtime = selected.find((row) => row.id === (project.tool.binding === "python" ? "pypi" : "npm"));
|
|
597
|
+
for (const row of selected.filter((row) => row.id === "vscode-marketplace" || row.id === "open-vsx")) {
|
|
598
|
+
if (runtime)
|
|
599
|
+
row.gate = `(${row.gate}) && { ${runtime.exists} || { ${runtime.gate}; }; }`;
|
|
600
|
+
}
|
|
524
601
|
return selected;
|
|
525
602
|
}
|
|
526
603
|
/** The shell variable the release gate holds a row's presence in; the job output takes the same name. */
|
|
@@ -544,6 +621,8 @@ export function manualSteps(project) {
|
|
|
544
621
|
const has_ = (id) => rows.some((row) => row.id === id);
|
|
545
622
|
const slug = githubSlug(project.identity.repository);
|
|
546
623
|
const steps = [];
|
|
624
|
+
if (has_("vscode-marketplace") || has_("open-vsx"))
|
|
625
|
+
steps.push("VS Code: register the publisher in Visual Studio Marketplace; create the Open VSX namespace and accept its publisher agreement before setting VSCE_PAT / OVSX_PAT. tool.vscode.publisher selects that identity.");
|
|
547
626
|
if (has_("oci")) {
|
|
548
627
|
steps.push(`ghcr.io: the first push publishes a private image whatever the repository's visibility, and GitHub has no visibility API — flip it once at ${rows.find((row) => row.id === "oci")?.url}.`);
|
|
549
628
|
}
|
|
@@ -10,7 +10,7 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
10
10
|
schemaVersion: 1;
|
|
11
11
|
identity: string;
|
|
12
12
|
binding: "typescript" | "python";
|
|
13
|
-
surfaces: ("skill" | "agent-plugins" | "claude" | "codex" | "cursor" | "mcp" | "mcp-registry" | "cli" | "npm" | "pypi" | "openclaw-native" | "hermes-native" | "clawhub" | "web" | "dsh" | "workflows" | "agents" | "readme" | "gemini" | "mcpb" | "browser-extension")[];
|
|
13
|
+
surfaces: ("homebrew" | "winget" | "scoop" | "chocolatey" | "apt" | "rpm" | "skill" | "agent-plugins" | "claude" | "codex" | "cursor" | "mcp" | "mcp-registry" | "cli" | "npm" | "pypi" | "openclaw-native" | "hermes-native" | "clawhub" | "web" | "dsh" | "workflows" | "agents" | "readme" | "gemini" | "mcpb" | "browser-extension" | "vscode-extension" | "cargo" | "nuget" | "maven-central" | "rubygems" | "packagist" | "go-module")[];
|
|
14
14
|
bundle: {
|
|
15
15
|
runtime: "package" | "bundled";
|
|
16
16
|
};
|
|
@@ -28,6 +28,7 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
28
28
|
scope?: string | undefined;
|
|
29
29
|
} | undefined;
|
|
30
30
|
codex?: {
|
|
31
|
+
mcp: boolean;
|
|
31
32
|
interface?: Record<string, unknown> | undefined;
|
|
32
33
|
} | undefined;
|
|
33
34
|
browserExtension?: {
|
|
@@ -45,6 +46,34 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
45
46
|
hermes?: {
|
|
46
47
|
toolset?: string | undefined;
|
|
47
48
|
} | undefined;
|
|
49
|
+
vscode?: {
|
|
50
|
+
marketplace: boolean;
|
|
51
|
+
openvsx: boolean;
|
|
52
|
+
publisher?: string | undefined;
|
|
53
|
+
} | undefined;
|
|
54
|
+
nativePackages?: {
|
|
55
|
+
id: "cargo" | "nuget" | "maven-central" | "rubygems" | "packagist" | "go-module";
|
|
56
|
+
path: string;
|
|
57
|
+
identity: string;
|
|
58
|
+
name: string;
|
|
59
|
+
versionCommand?: string | undefined;
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
systemPackages?: {
|
|
62
|
+
id: "homebrew" | "winget" | "scoop" | "chocolatey" | "apt" | "rpm";
|
|
63
|
+
path: string;
|
|
64
|
+
identity: string;
|
|
65
|
+
name: string;
|
|
66
|
+
versionCommand: string;
|
|
67
|
+
buildDependencies: string[];
|
|
68
|
+
buildCommand?: string | undefined;
|
|
69
|
+
asset?: string | undefined;
|
|
70
|
+
tap?: string | undefined;
|
|
71
|
+
bucket?: string | undefined;
|
|
72
|
+
bucketName?: string | undefined;
|
|
73
|
+
catalogPath?: string | undefined;
|
|
74
|
+
ppa?: string | undefined;
|
|
75
|
+
coprProject?: string | undefined;
|
|
76
|
+
}[] | undefined;
|
|
48
77
|
openclaw?: {
|
|
49
78
|
registers?: {
|
|
50
79
|
api: string;
|
package/dist/project/lock.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export declare const LockSchema: z.ZodObject<{
|
|
|
16
16
|
manual: "manual";
|
|
17
17
|
}>;
|
|
18
18
|
keys: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
19
|
+
keyedArrays: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
20
|
+
id: z.ZodString;
|
|
21
|
+
values: z.ZodArray<z.ZodString>;
|
|
22
|
+
entries: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
23
|
+
}, z.core.$strip>>>;
|
|
19
24
|
regions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
20
25
|
begin: z.ZodString;
|
|
21
26
|
end: z.ZodString;
|
package/dist/project/lock.js
CHANGED
|
@@ -17,6 +17,15 @@ export const LockSchema = z.object({
|
|
|
17
17
|
* patch — or deselecting the surface that added it — uninstalls it instead of stranding it.
|
|
18
18
|
*/
|
|
19
19
|
keys: z.array(z.string()).optional(),
|
|
20
|
+
/** A keyed array's inverse: the entries Toolfactory owns at each dotted path. */
|
|
21
|
+
keyedArrays: z
|
|
22
|
+
.record(z.string(), z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
values: z.array(z.string()),
|
|
25
|
+
/** The last projected entry shape lets a later plan remove only stale generated leaves. */
|
|
26
|
+
entries: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
|
27
|
+
}))
|
|
28
|
+
.optional(),
|
|
20
29
|
/**
|
|
21
30
|
* A region file's inverse, exactly as `keys` is a merge file's: the marker pairs its
|
|
22
31
|
* projector wrote. Regions the current plan no longer writes are emptied before the current
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kernelLaunch, mcpVerdict } from "./shared.js";
|
|
2
2
|
export const MCP_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
3
3
|
export const PLUGIN_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
4
4
|
export function assertBundleIdentity(project) {
|
|
@@ -13,12 +13,13 @@ export const surface = {
|
|
|
13
13
|
const launch = kernelLaunch(project, "${PLUGIN_ROOT}");
|
|
14
14
|
return [
|
|
15
15
|
{
|
|
16
|
-
kind: "
|
|
16
|
+
kind: "merge",
|
|
17
17
|
path: "mcp.json",
|
|
18
|
-
|
|
18
|
+
format: "json",
|
|
19
|
+
patch: {
|
|
19
20
|
$schema: MCP_SCHEMA_ID,
|
|
20
21
|
mcpServers: { [project.identity.name]: { type: "stdio", ...launch } },
|
|
21
|
-
}
|
|
22
|
+
},
|
|
22
23
|
},
|
|
23
24
|
];
|
|
24
25
|
},
|
package/dist/surfaces/agents.js
CHANGED
|
@@ -58,6 +58,12 @@ export const RELOAD = [
|
|
|
58
58
|
instructions: "`hermes skills trust` once in this repo, then a new conversation: the resolved skill directories are stable for a conversation.",
|
|
59
59
|
line: "Run `hermes skills trust` once in this repo, then start a new Hermes conversation — every invocation re-scans plugins, skills and AGENTS.md. `/reload-mcp` only affects an already-open session; `hermes gateway restart` is only for the messaging gateway.",
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
harness: "GSD",
|
|
63
|
+
env: [],
|
|
64
|
+
mcp: "`python3 .agents/sync.py --all` renders the shared `.mcp.json` even on a GSD-only machine. Refresh with `mcp_servers(refresh=true)`; start a new session after changing a connected server. Approve `mcp_discover` interactively once before unattended stdio use.",
|
|
65
|
+
instructions: "Reads `AGENTS.md` and `.agents/skills/` natively; `/reload` refreshes resources. `--bare` omits them.",
|
|
66
|
+
},
|
|
61
67
|
{
|
|
62
68
|
harness: "Gemini CLI",
|
|
63
69
|
env: [],
|
|
@@ -154,7 +160,8 @@ function layoutSection(project) {
|
|
|
154
160
|
if (has(project, "openclaw-native") ||
|
|
155
161
|
has(project, "hermes-native") ||
|
|
156
162
|
has(project, "dsh") ||
|
|
157
|
-
has(project, "browser-extension")
|
|
163
|
+
has(project, "browser-extension") ||
|
|
164
|
+
has(project, "vscode-extension")) {
|
|
158
165
|
lines.push("- `hosts/<id>/` — the host-native escape hatch for a selected host; nothing else creates it.");
|
|
159
166
|
}
|
|
160
167
|
if (has(project, "web"))
|
|
@@ -210,6 +217,9 @@ function installSection(project) {
|
|
|
210
217
|
if (has(project, "dsh")) {
|
|
211
218
|
lines.push(`- **DSH** (experimental): \`dsh plugin --profile ${DSH_PROFILE} add ./${DSH_HOST_DIR}\` installs the`, " bundle (`dsh plugin` forwards to pnpm, so pnpm must be on `PATH`); the operations reach the", ` model as \`${dshToolName(project, "<operation>")}\`, and \`dsh --profile ${DSH_PROFILE} --dump-config\` shows`, ` the composed \`id: ${dshRowId(project)}\` row. Bundle patches are read once per boot and never watched,`, ` so restart DSH after editing \`${DSH_HOST_DIR}/${PATCH_FILE}\` or reinstalling. Boot with`, ` \`--patch ${DSH_HOST_DIR}/${LOCAL_PATCH_FILE}\` and no bundle installed to drive this checkout`, " instead. Every config variable is restated in the row's `env:` because DSH scrubs", " KEY/PASSWORD/SECRET/TOKEN names before spawning an MCP server.");
|
|
212
219
|
}
|
|
220
|
+
if (has(project, "vscode-extension")) {
|
|
221
|
+
lines.push("- **VS Code extension**: `npm --prefix hosts/vscode install`, then open `hosts/vscode` in VS Code", " and press F5. Development and `npm --prefix hosts/vscode test` use the checkout's kernel.", " `npm --prefix hosts/vscode run vsix` packages it; install with `code --install-extension hosts/vscode/extension.vsix`.", " Use Developer: Reload Window after rebuilding an installed extension. Native activation, contributions", " and every VS Code API are available in your own files; include your license in the extension directory.");
|
|
222
|
+
}
|
|
213
223
|
if (has(project, "browser-extension")) {
|
|
214
224
|
const { command, args } = getBinding(project.tool.binding).cliCommand(project);
|
|
215
225
|
lines.push(`- **Browser extension**: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`, ` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\`;`, ` \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\` does the same for Firefox. Pair it with this`, ` checkout's kernel: \`${[command, ...args].join(" ")} mcp --http --pair\` mints a token under the data`, ` directory and prints the \`<url>#<token>\` the extension's options page accepts (with no token minted`, ` and no \`${envName(project.identity.name)}_MCP_TOKEN\` in the environment the endpoint stays open on loopback). The content`, " script's selectors are yours to maintain: it is authored escape-hatch code against a page that", " changes on its own schedule, and nothing here can version-proof it.");
|
|
@@ -218,7 +228,8 @@ function installSection(project) {
|
|
|
218
228
|
!openclaw &&
|
|
219
229
|
!hermes &&
|
|
220
230
|
!has(project, "dsh") &&
|
|
221
|
-
!has(project, "browser-extension")
|
|
231
|
+
!has(project, "browser-extension") &&
|
|
232
|
+
!has(project, "vscode-extension")) {
|
|
222
233
|
lines.push("_No host-native surface (`openclaw-native`, `hermes-native`, `dsh`, `browser-extension`) or", "`gemini` extension is selected; nothing installs into a running host._");
|
|
223
234
|
}
|
|
224
235
|
lines.push("", "Codex and Cursor read this file as is; Claude Code and Gemini read `CLAUDE.md` / `GEMINI.md`,", "each of which `.agents/sync.py` renders as the one line `@AGENTS.md`.", "");
|
|
@@ -564,6 +575,19 @@ function devcontainer(project) {
|
|
|
564
575
|
: document.features;
|
|
565
576
|
return `${JSON.stringify({ ...document, features }, null, 2)}\n`;
|
|
566
577
|
}
|
|
578
|
+
/** The vendor-owned workflow gains the Python prerequisite it cannot know. */
|
|
579
|
+
function agentConfigWorkflow(project) {
|
|
580
|
+
const path = ".github/workflows/agent-config.yml";
|
|
581
|
+
const workflow = TEMPLATE_FILES[path] ?? "";
|
|
582
|
+
if (isInstructionOnly(project.tool) || project.tool.binding !== "python")
|
|
583
|
+
return workflow;
|
|
584
|
+
const setup = " - name: Run the shared setup entry point\n";
|
|
585
|
+
if (!workflow.includes(setup))
|
|
586
|
+
throw new Error(`${path} no longer has the setup step; re-vendor before adding uv.`);
|
|
587
|
+
return workflow.replace(setup, " - name: Install uv\n" +
|
|
588
|
+
" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0\n\n" +
|
|
589
|
+
setup);
|
|
590
|
+
}
|
|
567
591
|
/** Whole-file carriers: no per-project content, `adopt` is the escape hatch. */
|
|
568
592
|
function vendored(project) {
|
|
569
593
|
const skip = new Set([SETUP_PATH, IGNORE_PATH, ".devcontainer/devcontainer.json"]);
|
|
@@ -573,7 +597,7 @@ function vendored(project) {
|
|
|
573
597
|
.map(([path, content]) => ({
|
|
574
598
|
kind: "file",
|
|
575
599
|
path,
|
|
576
|
-
content,
|
|
600
|
+
content: path === ".github/workflows/agent-config.yml" ? agentConfigWorkflow(project) : content,
|
|
577
601
|
...(path.endsWith("sync.py") ? { mode: 0o755 } : {}),
|
|
578
602
|
})),
|
|
579
603
|
{ kind: "file", path: ".devcontainer/devcontainer.json", content: devcontainer(project) },
|
|
@@ -108,6 +108,18 @@ describe("agents", () => {
|
|
|
108
108
|
expect(setup).toContain("npx toolfactory check >/dev/null");
|
|
109
109
|
expect(setup).toContain("npm install --no-audit --no-fund");
|
|
110
110
|
});
|
|
111
|
+
it("installs uv before Python projects run the vendored setup workflow", () => {
|
|
112
|
+
const workflow = planned(project(["cli"], { binding: "python" }), ".github/workflows/agent-config.yml");
|
|
113
|
+
if (workflow.kind !== "file")
|
|
114
|
+
throw new Error("expected an agent-config workflow file");
|
|
115
|
+
const install = "uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0";
|
|
116
|
+
expect(workflow.content).toContain(install);
|
|
117
|
+
expect(workflow.content.indexOf(install)).toBeLessThan(workflow.content.indexOf("run: bash .agents/setup --all"));
|
|
118
|
+
const typescript = planned(project(["cli"]), ".github/workflows/agent-config.yml");
|
|
119
|
+
if (typescript.kind !== "file")
|
|
120
|
+
throw new Error("expected an agent-config workflow file");
|
|
121
|
+
expect(typescript.content).toBe(TEMPLATE_FILES[".github/workflows/agent-config.yml"]);
|
|
122
|
+
});
|
|
111
123
|
it("owns the agent adapters, the binding's ignores and the build outputs in .gitignore", () => {
|
|
112
124
|
const bare = region(project(["cli"]), IGNORE_PATH);
|
|
113
125
|
const agentBlocks = (TEMPLATE_FILES[IGNORE_PATH] ?? "").split("# ─── Secrets")[0] ?? "";
|
package/dist/surfaces/claude.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `claude plugin marketplace add <owner>/<repo>` (and Copilot CLI's identical command) installs.
|
|
6
6
|
*/
|
|
7
7
|
import { githubSlug } from "../hosts/github.js";
|
|
8
|
-
import { compact, configProperties, envName, has, isSensitive,
|
|
8
|
+
import { compact, configProperties, envName, has, isSensitive, kernelLaunch, mcpVerdict, requiredConfig, } from "./shared.js";
|
|
9
9
|
export const surface = {
|
|
10
10
|
id: "claude",
|
|
11
11
|
plan(project) {
|
|
@@ -61,8 +61,13 @@ export const surface = {
|
|
|
61
61
|
plugins: [compact({ name: identity.name, source: "./", description: identity.description })],
|
|
62
62
|
});
|
|
63
63
|
return [
|
|
64
|
-
{ kind: "
|
|
65
|
-
{
|
|
64
|
+
{ kind: "merge", path: ".claude-plugin/plugin.json", format: "json", patch: manifest },
|
|
65
|
+
{
|
|
66
|
+
kind: "merge",
|
|
67
|
+
path: ".claude-plugin/marketplace.json",
|
|
68
|
+
format: "json",
|
|
69
|
+
patch: marketplace,
|
|
70
|
+
},
|
|
66
71
|
];
|
|
67
72
|
},
|
|
68
73
|
validate(project) {
|
package/dist/surfaces/codex.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* this surface does not reach.
|
|
9
9
|
*/
|
|
10
10
|
import { projectName } from "../identity/name.js";
|
|
11
|
-
import { compact, has,
|
|
11
|
+
import { compact, has, kernelLaunch, mcpVerdict } from "./shared.js";
|
|
12
12
|
/** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
|
|
13
13
|
export const CODEX_PIN = "0.152.1";
|
|
14
14
|
export const surface = {
|
|
@@ -39,7 +39,7 @@ export const surface = {
|
|
|
39
39
|
license: identity.license,
|
|
40
40
|
keywords: identity.keywords,
|
|
41
41
|
skills: has(project, "skill") ? "./skills/" : undefined,
|
|
42
|
-
mcpServers: has(project, "mcp")
|
|
42
|
+
mcpServers: has(project, "mcp") && project.tool.codex?.mcp !== false
|
|
43
43
|
? { [identity.name]: kernelLaunch(project, "${PLUGIN_ROOT}") }
|
|
44
44
|
: undefined,
|
|
45
45
|
interface: codexInterface,
|
|
@@ -60,8 +60,13 @@ export const surface = {
|
|
|
60
60
|
],
|
|
61
61
|
});
|
|
62
62
|
return [
|
|
63
|
-
{ kind: "
|
|
64
|
-
{
|
|
63
|
+
{ kind: "merge", path: ".codex-plugin/plugin.json", format: "json", patch: manifest },
|
|
64
|
+
{
|
|
65
|
+
kind: "merge",
|
|
66
|
+
path: ".agents/plugins/marketplace.json",
|
|
67
|
+
format: "json",
|
|
68
|
+
patch: marketplace,
|
|
69
|
+
},
|
|
65
70
|
];
|
|
66
71
|
},
|
|
67
72
|
validate(project) {
|
|
@@ -29,9 +29,9 @@ describe("codex", () => {
|
|
|
29
29
|
it("makes the repository its own single-plugin marketplace, and validates with the real codex CLI", () => {
|
|
30
30
|
const files = codex.plan(project(["codex", "mcp"]));
|
|
31
31
|
const marketplace = files.find((entry) => entry.path === ".agents/plugins/marketplace.json");
|
|
32
|
-
if (marketplace?.kind !== "
|
|
33
|
-
throw new Error("expected a
|
|
34
|
-
expect(
|
|
32
|
+
if (marketplace?.kind !== "merge")
|
|
33
|
+
throw new Error("expected a merge manifest");
|
|
34
|
+
expect(marketplace.patch).toMatchObject({
|
|
35
35
|
name: "hello",
|
|
36
36
|
interface: { displayName: "Hello" },
|
|
37
37
|
plugins: [
|
|
@@ -59,9 +59,19 @@ describe("codex", () => {
|
|
|
59
59
|
const target = project(["skill", "codex"]);
|
|
60
60
|
target.tool.runtime = "none";
|
|
61
61
|
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
62
|
-
if (manifest?.kind !== "
|
|
62
|
+
if (manifest?.kind !== "merge")
|
|
63
63
|
throw new Error("expected a manifest");
|
|
64
|
-
expect(
|
|
65
|
-
expect(
|
|
64
|
+
expect(manifest.patch).toMatchObject({ skills: "./skills/" });
|
|
65
|
+
expect(manifest.patch).not.toHaveProperty("mcpServers");
|
|
66
|
+
});
|
|
67
|
+
it("can omit the eager MCP server while retaining the plugin skill", () => {
|
|
68
|
+
const target = project(["skill", "codex", "mcp"]);
|
|
69
|
+
target.tool.codex = { mcp: false };
|
|
70
|
+
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
71
|
+
if (manifest?.kind !== "merge")
|
|
72
|
+
throw new Error("expected a manifest");
|
|
73
|
+
const parsed = manifest.patch;
|
|
74
|
+
expect(parsed.skills).toBe("./skills/");
|
|
75
|
+
expect(parsed).not.toHaveProperty("mcpServers");
|
|
66
76
|
});
|
|
67
77
|
});
|
package/dist/surfaces/cursor.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* and the bundle's `mcp.json` is shared. Loader unverified, like Codex.
|
|
4
4
|
*/
|
|
5
5
|
import { projectName } from "../identity/name.js";
|
|
6
|
-
import { compact, configProperties, envName, has,
|
|
6
|
+
import { compact, configProperties, envName, has, mcpVerdict, requiredConfig } from "./shared.js";
|
|
7
7
|
export const surface = {
|
|
8
8
|
id: "cursor",
|
|
9
9
|
plan(project) {
|
|
@@ -39,7 +39,7 @@ export const surface = {
|
|
|
39
39
|
mcpServers: has(project, "agent-plugins") ? "./mcp.json" : undefined,
|
|
40
40
|
variables,
|
|
41
41
|
});
|
|
42
|
-
return [{ kind: "
|
|
42
|
+
return [{ kind: "merge", path: ".cursor-plugin/plugin.json", format: "json", patch: manifest }];
|
|
43
43
|
},
|
|
44
44
|
verdict: (operation) => {
|
|
45
45
|
const verdict = mcpVerdict(operation);
|