toolfactory 0.1.2 → 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 +24 -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 +62 -2
- package/dist/model.js +9 -0
- 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 +68 -3
- package/dist/project/history.d.ts +29 -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 +21 -3
- package/dist/surfaces/agents.test.js +12 -0
- package/dist/surfaces/claude.js +8 -3
- package/dist/surfaces/codex.js +8 -3
- package/dist/surfaces/codex.test.js +8 -8
- package/dist/surfaces/cursor.js +2 -2
- package/dist/surfaces/distribution.test.js +3 -3
- package/dist/surfaces/dsh.js +25 -6
- package/dist/surfaces/dsh.test.js +8 -3
- package/dist/surfaces/gemini.js +2 -2
- package/dist/surfaces/gemini.test.js +4 -4
- package/dist/surfaces/hermes-native.js +38 -9
- package/dist/surfaces/hermes-native.test.js +32 -11
- package/dist/surfaces/mcp-registry.js +43 -5
- package/dist/surfaces/mcp-registry.test.js +18 -0
- package/dist/surfaces/openclaw-native.js +10 -8
- package/dist/surfaces/openclaw-native.test.js +30 -0
- package/dist/surfaces/readme.d.ts +16 -0
- package/dist/surfaces/readme.js +7 -0
- package/dist/surfaces/registry.js +6 -0
- 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 +49 -10
- package/dist/surfaces/workflows.test.js +3 -2
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +2 -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 +15 -1
- package/src/project/apply.test.ts +120 -0
- package/src/project/apply.ts +257 -15
- package/src/project/gate.ts +96 -4
- 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 +31 -3
- package/src/surfaces/claude.ts +7 -3
- package/src/surfaces/codex.test.ts +7 -7
- package/src/surfaces/codex.ts +8 -3
- package/src/surfaces/cursor.ts +2 -10
- package/src/surfaces/distribution.test.ts +2 -2
- package/src/surfaces/dsh.test.ts +8 -2
- package/src/surfaces/dsh.ts +26 -6
- package/src/surfaces/gemini.test.ts +3 -3
- package/src/surfaces/gemini.ts +1 -2
- package/src/surfaces/hermes-native.test.ts +39 -10
- package/src/surfaces/hermes-native.ts +41 -11
- package/src/surfaces/mcp-registry.test.ts +20 -0
- package/src/surfaces/mcp-registry.ts +46 -6
- package/src/surfaces/openclaw-native.test.ts +37 -0
- package/src/surfaces/openclaw-native.ts +49 -47
- package/src/surfaces/readme.ts +10 -0
- package/src/surfaces/registry.ts +6 -0
- 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 +3 -4
- package/src/surfaces/workflows.ts +49 -10
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/{index-Spapkkvh.js → index-CVm6m4s0.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/project/gate.ts
CHANGED
|
@@ -9,6 +9,18 @@
|
|
|
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
|
+
|
|
13
|
+
import {
|
|
14
|
+
type NativePackageId,
|
|
15
|
+
nativePackageSteps,
|
|
16
|
+
nativeRegistryRows,
|
|
17
|
+
nativeToolchainSteps,
|
|
18
|
+
} from "../distribution/native.js";
|
|
19
|
+
import {
|
|
20
|
+
type SystemPackageId,
|
|
21
|
+
systemPackageSteps,
|
|
22
|
+
systemRegistryRows,
|
|
23
|
+
} from "../distribution/system.js";
|
|
12
24
|
import { githubSlug } from "../hosts/github.js";
|
|
13
25
|
import { githubOwner, projectName } from "../identity/name.js";
|
|
14
26
|
import { isInstructionOnly, type PackageManager, type Project, type SurfaceId } from "../model.js";
|
|
@@ -28,6 +40,14 @@ import {
|
|
|
28
40
|
npmName,
|
|
29
41
|
pypiName,
|
|
30
42
|
} from "../surfaces/shared.js";
|
|
43
|
+
import {
|
|
44
|
+
extensionName,
|
|
45
|
+
OVSX_VERSION,
|
|
46
|
+
publisher,
|
|
47
|
+
VSCE_VERSION,
|
|
48
|
+
HOST_DIR as VSCODE_HOST_DIR,
|
|
49
|
+
vsixName,
|
|
50
|
+
} from "../surfaces/vscode-extension.js";
|
|
31
51
|
|
|
32
52
|
export interface GateStep {
|
|
33
53
|
/** Step label: the CI step name, and the banner the shell script echoes. */
|
|
@@ -140,7 +160,11 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
140
160
|
when: "ci",
|
|
141
161
|
});
|
|
142
162
|
}
|
|
143
|
-
if (
|
|
163
|
+
if (
|
|
164
|
+
has(project, "web") ||
|
|
165
|
+
has(project, "browser-extension") ||
|
|
166
|
+
has(project, "vscode-extension")
|
|
167
|
+
) {
|
|
144
168
|
// The web smoke and the browser extension's own Playwright smoke both drive Chromium;
|
|
145
169
|
// their own npm installs fetch the browser, the runner needs its system libraries.
|
|
146
170
|
steps.push({
|
|
@@ -149,6 +173,7 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
149
173
|
when: "ci",
|
|
150
174
|
});
|
|
151
175
|
}
|
|
176
|
+
steps.push(...nativeToolchainSteps(project));
|
|
152
177
|
steps.push({
|
|
153
178
|
name: "toolfactory validate",
|
|
154
179
|
run: `${cli} validate`,
|
|
@@ -160,6 +185,7 @@ export function gateSteps(project: Project): GateStep[] {
|
|
|
160
185
|
name: "author tests",
|
|
161
186
|
run: typescript ? pm.test : "uv run --with pytest pytest -q",
|
|
162
187
|
});
|
|
188
|
+
if (typescript) steps.push({ name: "author integration tests", run: pm.run("test:integration") });
|
|
163
189
|
if (openclaw) {
|
|
164
190
|
// T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
|
|
165
191
|
// exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
|
|
@@ -220,13 +246,16 @@ const LEGAL_BUNDLE_PATHS = [
|
|
|
220
246
|
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
221
247
|
* to a registry stays a CI concern; producing the artifacts never is.
|
|
222
248
|
*/
|
|
223
|
-
export function packageSteps(project: Project): GateStep[] {
|
|
249
|
+
export function packageSteps(project: Project, options: { system?: boolean } = {}): GateStep[] {
|
|
224
250
|
const cli = toolfactoryCli(project);
|
|
225
251
|
const name = project.identity.name;
|
|
226
252
|
const steps: GateStep[] = [
|
|
227
253
|
{ name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
|
|
228
254
|
...bootstrapSteps(project),
|
|
229
255
|
outputsStep(project),
|
|
256
|
+
...nativeToolchainSteps(project),
|
|
257
|
+
...nativePackageSteps(project),
|
|
258
|
+
...systemPackageSteps(project, { include: options.system !== false }),
|
|
230
259
|
];
|
|
231
260
|
if (has(project, "web")) {
|
|
232
261
|
steps.push({
|
|
@@ -329,6 +358,12 @@ export function packageSteps(project: Project): GateStep[] {
|
|
|
329
358
|
});
|
|
330
359
|
}
|
|
331
360
|
// COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
|
|
361
|
+
if (has(project, "vscode-extension")) {
|
|
362
|
+
steps.push({
|
|
363
|
+
name: "VS Code extension VSIX",
|
|
364
|
+
run: `npm --prefix ${VSCODE_HOST_DIR} install && npm --prefix ${VSCODE_HOST_DIR} run vsix && cp ${VSCODE_HOST_DIR}/extension.vsix ${RELEASE_DIR}/${vsixName(project)}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
332
367
|
steps.push({
|
|
333
368
|
name: "coverage report",
|
|
334
369
|
run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
|
|
@@ -355,6 +390,8 @@ export function openclawTarball(project: Project): string {
|
|
|
355
390
|
export interface Registry {
|
|
356
391
|
/** Stable id: the release job suffix, the `secrets status` row, the unpublish step name. */
|
|
357
392
|
id:
|
|
393
|
+
| NativePackageId
|
|
394
|
+
| SystemPackageId
|
|
358
395
|
| "npm"
|
|
359
396
|
| "pypi"
|
|
360
397
|
| "mcp-registry"
|
|
@@ -365,7 +402,9 @@ export interface Registry {
|
|
|
365
402
|
| "chrome"
|
|
366
403
|
| "firefox"
|
|
367
404
|
| "edge"
|
|
368
|
-
| "safari"
|
|
405
|
+
| "safari"
|
|
406
|
+
| "vscode-marketplace"
|
|
407
|
+
| "open-vsx";
|
|
369
408
|
/** The surfaces whose selection publishes here; all must be selected. */
|
|
370
409
|
surfaces: SurfaceId[];
|
|
371
410
|
/** Environment names the publish leg consumes (repository scope); [] for OIDC/GITHUB_TOKEN legs. */
|
|
@@ -447,6 +486,22 @@ const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
|
|
|
447
486
|
export const RELEASE_SECRET_NAMES = [
|
|
448
487
|
"NPM_TOKEN",
|
|
449
488
|
"CLAWHUB_TOKEN",
|
|
489
|
+
"VSCE_PAT",
|
|
490
|
+
"OVSX_PAT",
|
|
491
|
+
"CARGO_REGISTRY_TOKEN",
|
|
492
|
+
"NUGET_API_KEY",
|
|
493
|
+
"MAVEN_CENTRAL_USERNAME",
|
|
494
|
+
"MAVEN_CENTRAL_PASSWORD",
|
|
495
|
+
"MAVEN_GPG_PRIVATE_KEY",
|
|
496
|
+
"MAVEN_GPG_PASSPHRASE",
|
|
497
|
+
"RUBYGEMS_API_KEY",
|
|
498
|
+
"HOMEBREW_TAP_TOKEN",
|
|
499
|
+
"SCOOP_BUCKET_TOKEN",
|
|
500
|
+
"CHOCOLATEY_API_KEY",
|
|
501
|
+
"APT_GPG_PRIVATE_KEY",
|
|
502
|
+
"APT_GPG_KEY_ID",
|
|
503
|
+
"COPR_LOGIN",
|
|
504
|
+
"COPR_TOKEN",
|
|
450
505
|
...CHROME_SECRETS,
|
|
451
506
|
...FIREFOX_SECRETS,
|
|
452
507
|
...EDGE_SECRETS,
|
|
@@ -474,6 +529,29 @@ export function registries(project: Project): Registry[] {
|
|
|
474
529
|
const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
|
|
475
530
|
|
|
476
531
|
const rows: Registry[] = [
|
|
532
|
+
...nativeRegistryRows(project),
|
|
533
|
+
...systemRegistryRows(project),
|
|
534
|
+
{
|
|
535
|
+
id: "vscode-marketplace",
|
|
536
|
+
surfaces: ["vscode-extension"],
|
|
537
|
+
secrets: ["VSCE_PAT"],
|
|
538
|
+
url: "https://marketplace.visualstudio.com/manage",
|
|
539
|
+
probe: `npx --yes @vscode/vsce@${VSCE_VERSION} verify-pat ${publisher(project)}`,
|
|
540
|
+
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))'`,
|
|
541
|
+
gate: allSet(["VSCE_PAT"]),
|
|
542
|
+
retractUrl: "https://marketplace.visualstudio.com/manage",
|
|
543
|
+
retractHard: `npx --yes @vscode/vsce@${VSCE_VERSION} unpublish ${publisher(project)}.${extensionName(project)} --force`,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "open-vsx",
|
|
547
|
+
surfaces: ["vscode-extension"],
|
|
548
|
+
secrets: ["OVSX_PAT"],
|
|
549
|
+
url: "https://open-vsx.org/user-settings/tokens",
|
|
550
|
+
probe: `npx --yes ovsx@${OVSX_VERSION} verify-pat ${publisher(project)}`,
|
|
551
|
+
exists: `curl -fsS -o /dev/null https://open-vsx.org/api/${publisher(project)}/${extensionName(project)}/${version}`,
|
|
552
|
+
gate: allSet(["OVSX_PAT"]),
|
|
553
|
+
retractUrl: "https://open-vsx.org/user-settings/extensions",
|
|
554
|
+
},
|
|
477
555
|
{
|
|
478
556
|
id: "npm",
|
|
479
557
|
surfaces: ["npm"],
|
|
@@ -627,7 +705,9 @@ export function registries(project: Project): Registry[] {
|
|
|
627
705
|
const selected = rows.filter(
|
|
628
706
|
(row) =>
|
|
629
707
|
row.surfaces.every((surface) => has(project, surface)) &&
|
|
630
|
-
(row.id !== "safari" || project.tool.browserExtension?.safari === true)
|
|
708
|
+
(row.id !== "safari" || project.tool.browserExtension?.safari === true) &&
|
|
709
|
+
(row.id !== "vscode-marketplace" || project.tool.vscode?.marketplace !== false) &&
|
|
710
|
+
(row.id !== "open-vsx" || project.tool.vscode?.openvsx !== false),
|
|
631
711
|
);
|
|
632
712
|
const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
|
|
633
713
|
if (mcpRegistry) {
|
|
@@ -638,6 +718,14 @@ export function registries(project: Project): Registry[] {
|
|
|
638
718
|
? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
|
|
639
719
|
: "true";
|
|
640
720
|
}
|
|
721
|
+
const runtime = selected.find(
|
|
722
|
+
(row) => row.id === (project.tool.binding === "python" ? "pypi" : "npm"),
|
|
723
|
+
);
|
|
724
|
+
for (const row of selected.filter(
|
|
725
|
+
(row) => row.id === "vscode-marketplace" || row.id === "open-vsx",
|
|
726
|
+
)) {
|
|
727
|
+
if (runtime) row.gate = `(${row.gate}) && { ${runtime.exists} || { ${runtime.gate}; }; }`;
|
|
728
|
+
}
|
|
641
729
|
return selected;
|
|
642
730
|
}
|
|
643
731
|
|
|
@@ -664,6 +752,10 @@ export function manualSteps(project: Project): string[] {
|
|
|
664
752
|
const has_ = (id: Registry["id"]) => rows.some((row) => row.id === id);
|
|
665
753
|
const slug = githubSlug(project.identity.repository);
|
|
666
754
|
const steps: string[] = [];
|
|
755
|
+
if (has_("vscode-marketplace") || has_("open-vsx"))
|
|
756
|
+
steps.push(
|
|
757
|
+
"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.",
|
|
758
|
+
);
|
|
667
759
|
if (has_("oci")) {
|
|
668
760
|
steps.push(
|
|
669
761
|
`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}.`,
|
package/src/project/lock.ts
CHANGED
|
@@ -23,6 +23,18 @@ export const LockSchema = z.object({
|
|
|
23
23
|
* patch — or deselecting the surface that added it — uninstalls it instead of stranding it.
|
|
24
24
|
*/
|
|
25
25
|
keys: z.array(z.string()).optional(),
|
|
26
|
+
/** A keyed array's inverse: the entries Toolfactory owns at each dotted path. */
|
|
27
|
+
keyedArrays: z
|
|
28
|
+
.record(
|
|
29
|
+
z.string(),
|
|
30
|
+
z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
values: z.array(z.string()),
|
|
33
|
+
/** The last projected entry shape lets a later plan remove only stale generated leaves. */
|
|
34
|
+
entries: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
.optional(),
|
|
26
38
|
/**
|
|
27
39
|
* A region file's inverse, exactly as `keys` is a merge file's: the marker pairs its
|
|
28
40
|
* projector wrote. Regions the current plan no longer writes are emptied before the current
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Validation is Ajv against the cached official schemas; the spec forbids fetching at load.
|
|
5
5
|
*/
|
|
6
6
|
import type { Project, Surface } from "../model.js";
|
|
7
|
-
import {
|
|
7
|
+
import { kernelLaunch, mcpVerdict } from "./shared.js";
|
|
8
8
|
|
|
9
9
|
export const MCP_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
10
10
|
export const PLUGIN_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
@@ -24,12 +24,13 @@ export const surface: Surface = {
|
|
|
24
24
|
const launch = kernelLaunch(project, "${PLUGIN_ROOT}");
|
|
25
25
|
return [
|
|
26
26
|
{
|
|
27
|
-
kind: "
|
|
27
|
+
kind: "merge",
|
|
28
28
|
path: "mcp.json",
|
|
29
|
-
|
|
29
|
+
format: "json",
|
|
30
|
+
patch: {
|
|
30
31
|
$schema: MCP_SCHEMA_ID,
|
|
31
32
|
mcpServers: { [project.identity.name]: { type: "stdio", ...launch } },
|
|
32
|
-
}
|
|
33
|
+
},
|
|
33
34
|
},
|
|
34
35
|
];
|
|
35
36
|
},
|
|
@@ -129,6 +129,22 @@ describe("agents", () => {
|
|
|
129
129
|
expect(setup).toContain("npm install --no-audit --no-fund");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
it("installs uv before Python projects run the vendored setup workflow", () => {
|
|
133
|
+
const workflow = planned(
|
|
134
|
+
project(["cli"], { binding: "python" }),
|
|
135
|
+
".github/workflows/agent-config.yml",
|
|
136
|
+
);
|
|
137
|
+
if (workflow.kind !== "file") throw new Error("expected an agent-config workflow file");
|
|
138
|
+
const install = "uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0";
|
|
139
|
+
expect(workflow.content).toContain(install);
|
|
140
|
+
expect(workflow.content.indexOf(install)).toBeLessThan(
|
|
141
|
+
workflow.content.indexOf("run: bash .agents/setup --all"),
|
|
142
|
+
);
|
|
143
|
+
const typescript = planned(project(["cli"]), ".github/workflows/agent-config.yml");
|
|
144
|
+
if (typescript.kind !== "file") throw new Error("expected an agent-config workflow file");
|
|
145
|
+
expect(typescript.content).toBe(TEMPLATE_FILES[".github/workflows/agent-config.yml"]);
|
|
146
|
+
});
|
|
147
|
+
|
|
132
148
|
it("owns the agent adapters, the binding's ignores and the build outputs in .gitignore", () => {
|
|
133
149
|
const bare = region(project(["cli"]), IGNORE_PATH);
|
|
134
150
|
const agentBlocks = (TEMPLATE_FILES[IGNORE_PATH] ?? "").split("# ─── Secrets")[0] ?? "";
|
package/src/surfaces/agents.ts
CHANGED
|
@@ -198,7 +198,8 @@ function layoutSection(project: Project): string[] {
|
|
|
198
198
|
has(project, "openclaw-native") ||
|
|
199
199
|
has(project, "hermes-native") ||
|
|
200
200
|
has(project, "dsh") ||
|
|
201
|
-
has(project, "browser-extension")
|
|
201
|
+
has(project, "browser-extension") ||
|
|
202
|
+
has(project, "vscode-extension")
|
|
202
203
|
) {
|
|
203
204
|
lines.push(
|
|
204
205
|
"- `hosts/<id>/` — the host-native escape hatch for a selected host; nothing else creates it.",
|
|
@@ -281,6 +282,15 @@ function installSection(project: Project): string[] {
|
|
|
281
282
|
" KEY/PASSWORD/SECRET/TOKEN names before spawning an MCP server.",
|
|
282
283
|
);
|
|
283
284
|
}
|
|
285
|
+
if (has(project, "vscode-extension")) {
|
|
286
|
+
lines.push(
|
|
287
|
+
"- **VS Code extension**: `npm --prefix hosts/vscode install`, then open `hosts/vscode` in VS Code",
|
|
288
|
+
" and press F5. Development and `npm --prefix hosts/vscode test` use the checkout's kernel.",
|
|
289
|
+
" `npm --prefix hosts/vscode run vsix` packages it; install with `code --install-extension hosts/vscode/extension.vsix`.",
|
|
290
|
+
" Use Developer: Reload Window after rebuilding an installed extension. Native activation, contributions",
|
|
291
|
+
" and every VS Code API are available in your own files; include your license in the extension directory.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
284
294
|
if (has(project, "browser-extension")) {
|
|
285
295
|
const { command, args } = getBinding(project.tool.binding).cliCommand(project);
|
|
286
296
|
lines.push(
|
|
@@ -299,7 +309,8 @@ function installSection(project: Project): string[] {
|
|
|
299
309
|
!openclaw &&
|
|
300
310
|
!hermes &&
|
|
301
311
|
!has(project, "dsh") &&
|
|
302
|
-
!has(project, "browser-extension")
|
|
312
|
+
!has(project, "browser-extension") &&
|
|
313
|
+
!has(project, "vscode-extension")
|
|
303
314
|
) {
|
|
304
315
|
lines.push(
|
|
305
316
|
"_No host-native surface (`openclaw-native`, `hermes-native`, `dsh`, `browser-extension`) or",
|
|
@@ -688,6 +699,22 @@ function devcontainer(project: Project): string {
|
|
|
688
699
|
return `${JSON.stringify({ ...document, features }, null, 2)}\n`;
|
|
689
700
|
}
|
|
690
701
|
|
|
702
|
+
/** The vendor-owned workflow gains the Python prerequisite it cannot know. */
|
|
703
|
+
function agentConfigWorkflow(project: Project): string {
|
|
704
|
+
const path = ".github/workflows/agent-config.yml";
|
|
705
|
+
const workflow = TEMPLATE_FILES[path] ?? "";
|
|
706
|
+
if (isInstructionOnly(project.tool) || project.tool.binding !== "python") return workflow;
|
|
707
|
+
const setup = " - name: Run the shared setup entry point\n";
|
|
708
|
+
if (!workflow.includes(setup))
|
|
709
|
+
throw new Error(`${path} no longer has the setup step; re-vendor before adding uv.`);
|
|
710
|
+
return workflow.replace(
|
|
711
|
+
setup,
|
|
712
|
+
" - name: Install uv\n" +
|
|
713
|
+
" uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0\n\n" +
|
|
714
|
+
setup,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
691
718
|
/** Whole-file carriers: no per-project content, `adopt` is the escape hatch. */
|
|
692
719
|
function vendored(project: Project): PlannedFile[] {
|
|
693
720
|
const skip = new Set([SETUP_PATH, IGNORE_PATH, ".devcontainer/devcontainer.json"]);
|
|
@@ -698,7 +725,8 @@ function vendored(project: Project): PlannedFile[] {
|
|
|
698
725
|
([path, content]): PlannedFile => ({
|
|
699
726
|
kind: "file",
|
|
700
727
|
path,
|
|
701
|
-
content
|
|
728
|
+
content:
|
|
729
|
+
path === ".github/workflows/agent-config.yml" ? agentConfigWorkflow(project) : content,
|
|
702
730
|
...(path.endsWith("sync.py") ? { mode: 0o755 } : {}),
|
|
703
731
|
}),
|
|
704
732
|
),
|
package/src/surfaces/claude.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
envName,
|
|
13
13
|
has,
|
|
14
14
|
isSensitive,
|
|
15
|
-
json,
|
|
16
15
|
kernelLaunch,
|
|
17
16
|
mcpVerdict,
|
|
18
17
|
requiredConfig,
|
|
@@ -79,8 +78,13 @@ export const surface: Surface = {
|
|
|
79
78
|
plugins: [compact({ name: identity.name, source: "./", description: identity.description })],
|
|
80
79
|
});
|
|
81
80
|
return [
|
|
82
|
-
{ kind: "
|
|
83
|
-
{
|
|
81
|
+
{ kind: "merge", path: ".claude-plugin/plugin.json", format: "json", patch: manifest },
|
|
82
|
+
{
|
|
83
|
+
kind: "merge",
|
|
84
|
+
path: ".claude-plugin/marketplace.json",
|
|
85
|
+
format: "json",
|
|
86
|
+
patch: marketplace,
|
|
87
|
+
},
|
|
84
88
|
];
|
|
85
89
|
},
|
|
86
90
|
validate(project) {
|
|
@@ -33,8 +33,8 @@ describe("codex", () => {
|
|
|
33
33
|
it("makes the repository its own single-plugin marketplace, and validates with the real codex CLI", () => {
|
|
34
34
|
const files = codex.plan(project(["codex", "mcp"]));
|
|
35
35
|
const marketplace = files.find((entry) => entry.path === ".agents/plugins/marketplace.json");
|
|
36
|
-
if (marketplace?.kind !== "
|
|
37
|
-
expect(
|
|
36
|
+
if (marketplace?.kind !== "merge") throw new Error("expected a merge manifest");
|
|
37
|
+
expect(marketplace.patch).toMatchObject({
|
|
38
38
|
name: "hello",
|
|
39
39
|
interface: { displayName: "Hello" },
|
|
40
40
|
plugins: [
|
|
@@ -64,17 +64,17 @@ describe("codex", () => {
|
|
|
64
64
|
const target = project(["skill", "codex"]);
|
|
65
65
|
target.tool.runtime = "none";
|
|
66
66
|
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
67
|
-
if (manifest?.kind !== "
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
67
|
+
if (manifest?.kind !== "merge") throw new Error("expected a manifest");
|
|
68
|
+
expect(manifest.patch).toMatchObject({ skills: "./skills/" });
|
|
69
|
+
expect(manifest.patch).not.toHaveProperty("mcpServers");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it("can omit the eager MCP server while retaining the plugin skill", () => {
|
|
73
73
|
const target = project(["skill", "codex", "mcp"]);
|
|
74
74
|
target.tool.codex = { mcp: false };
|
|
75
75
|
const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
|
|
76
|
-
if (manifest?.kind !== "
|
|
77
|
-
const parsed =
|
|
76
|
+
if (manifest?.kind !== "merge") throw new Error("expected a manifest");
|
|
77
|
+
const parsed = manifest.patch;
|
|
78
78
|
expect(parsed.skills).toBe("./skills/");
|
|
79
79
|
expect(parsed).not.toHaveProperty("mcpServers");
|
|
80
80
|
});
|
package/src/surfaces/codex.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { projectName } from "../identity/name.js";
|
|
12
12
|
import type { Surface } from "../model.js";
|
|
13
|
-
import { compact, has,
|
|
13
|
+
import { compact, has, kernelLaunch, mcpVerdict } from "./shared.js";
|
|
14
14
|
|
|
15
15
|
/** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
|
|
16
16
|
export const CODEX_PIN = "0.152.1";
|
|
@@ -65,8 +65,13 @@ export const surface: Surface = {
|
|
|
65
65
|
],
|
|
66
66
|
});
|
|
67
67
|
return [
|
|
68
|
-
{ kind: "
|
|
69
|
-
{
|
|
68
|
+
{ kind: "merge", path: ".codex-plugin/plugin.json", format: "json", patch: manifest },
|
|
69
|
+
{
|
|
70
|
+
kind: "merge",
|
|
71
|
+
path: ".agents/plugins/marketplace.json",
|
|
72
|
+
format: "json",
|
|
73
|
+
patch: marketplace,
|
|
74
|
+
},
|
|
70
75
|
];
|
|
71
76
|
},
|
|
72
77
|
validate(project) {
|
package/src/surfaces/cursor.ts
CHANGED
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { projectName } from "../identity/name.js";
|
|
6
6
|
import type { Surface } from "../model.js";
|
|
7
|
-
import {
|
|
8
|
-
compact,
|
|
9
|
-
configProperties,
|
|
10
|
-
envName,
|
|
11
|
-
has,
|
|
12
|
-
json,
|
|
13
|
-
mcpVerdict,
|
|
14
|
-
requiredConfig,
|
|
15
|
-
} from "./shared.js";
|
|
7
|
+
import { compact, configProperties, envName, has, mcpVerdict, requiredConfig } from "./shared.js";
|
|
16
8
|
|
|
17
9
|
export const surface: Surface = {
|
|
18
10
|
id: "cursor",
|
|
@@ -51,7 +43,7 @@ export const surface: Surface = {
|
|
|
51
43
|
mcpServers: has(project, "agent-plugins") ? "./mcp.json" : undefined,
|
|
52
44
|
variables,
|
|
53
45
|
});
|
|
54
|
-
return [{ kind: "
|
|
46
|
+
return [{ kind: "merge", path: ".cursor-plugin/plugin.json", format: "json", patch: manifest }];
|
|
55
47
|
},
|
|
56
48
|
verdict: (operation) => {
|
|
57
49
|
const verdict = mcpVerdict(operation);
|
|
@@ -190,8 +190,8 @@ describe("claude", () => {
|
|
|
190
190
|
it("makes the repository its own single-plugin marketplace", () => {
|
|
191
191
|
const files = claude.plan(project(["claude", "mcp"]));
|
|
192
192
|
const file = files.find((entry) => entry.path === ".claude-plugin/marketplace.json");
|
|
193
|
-
if (file?.kind !== "
|
|
194
|
-
expect(
|
|
193
|
+
if (file?.kind !== "merge") throw new Error("expected a merge file");
|
|
194
|
+
expect(file.patch).toMatchObject({
|
|
195
195
|
name: "hello",
|
|
196
196
|
owner: { name: "GoatInAHat" },
|
|
197
197
|
plugins: [{ name: "hello", source: "./", description: "Say hello" }],
|
package/src/surfaces/dsh.test.ts
CHANGED
|
@@ -36,8 +36,14 @@ const project: Project = {
|
|
|
36
36
|
|
|
37
37
|
const files = Object.fromEntries(
|
|
38
38
|
dsh.plan(project).map((file) => {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
return [
|
|
40
|
+
file.path,
|
|
41
|
+
file.kind === "file"
|
|
42
|
+
? file.content
|
|
43
|
+
: file.kind === "merge"
|
|
44
|
+
? JSON.stringify(file.patch)
|
|
45
|
+
: file.regions.map((region) => region.content).join(""),
|
|
46
|
+
];
|
|
41
47
|
}),
|
|
42
48
|
);
|
|
43
49
|
|
package/src/surfaces/dsh.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { Scalar, stringify as yamlStringify } from "yaml";
|
|
|
22
22
|
import { getBinding } from "../bindings/index.js";
|
|
23
23
|
import { projectName } from "../identity/name.js";
|
|
24
24
|
import type { Project, Surface } from "../model.js";
|
|
25
|
-
import { compact, configProperties, envName,
|
|
25
|
+
import { compact, configProperties, envName, kernelLaunch, mcpVerdict } from "./shared.js";
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The `@deepseek-ai/dsh` release `validate()` boots. Pinned, not `@alpha`: npm's `latest` tag
|
|
@@ -40,6 +40,8 @@ export const PATCH_FILE = "cordis.patch.yml";
|
|
|
40
40
|
* passes to `dsh --patch` to drive the working tree.
|
|
41
41
|
*/
|
|
42
42
|
export const LOCAL_PATCH_FILE = "cordis.local.patch.yml";
|
|
43
|
+
const PATCH_BEGIN = "# tf:mcp";
|
|
44
|
+
const PATCH_END = "# /tf:mcp";
|
|
43
45
|
|
|
44
46
|
/** DSH's first-party MCP bridge; the only plugin the bundle names. */
|
|
45
47
|
const MCP_CLIENT = "@deepseek-ai/dsh-mcp-client";
|
|
@@ -91,6 +93,10 @@ function patch(project: Project, launch: { command: string; args: string[] }): s
|
|
|
91
93
|
return yamlStringify([{ insert: [row] }], { lineWidth: 0 });
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
function patchTemplate(): string {
|
|
97
|
+
return `${PATCH_BEGIN}\n${PATCH_END}\n\n# Yours: add Cordis rows or local code configuration below this marker.\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
94
100
|
/** The public name a bridged MCP tool takes in DSH; `mcp__<serverName>__<rawName>` is its contract. */
|
|
95
101
|
export function toolName(project: Project, operation: string): string {
|
|
96
102
|
return `mcp__${projectName.dshServer(project.identity.name)}__${operation}`;
|
|
@@ -120,20 +126,34 @@ export const surface: Surface = {
|
|
|
120
126
|
dsh: { bundle: { patch: `./${PATCH_FILE}` } },
|
|
121
127
|
});
|
|
122
128
|
return [
|
|
123
|
-
{ kind: "
|
|
129
|
+
{ kind: "merge", path: `${HOST_DIR}/package.json`, format: "json", patch: manifest },
|
|
124
130
|
{
|
|
125
|
-
kind: "
|
|
131
|
+
kind: "region",
|
|
126
132
|
path: `${HOST_DIR}/${PATCH_FILE}`,
|
|
127
133
|
// `cwd` is deliberately absent: an installed bundle has no path to point at, and the
|
|
128
134
|
// published launch resolves through the registry from wherever DSH is running.
|
|
129
|
-
|
|
135
|
+
regions: [
|
|
136
|
+
{
|
|
137
|
+
begin: PATCH_BEGIN,
|
|
138
|
+
end: PATCH_END,
|
|
139
|
+
content: patch(project, kernelLaunch(project, ".")),
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
template: patchTemplate(),
|
|
130
143
|
},
|
|
131
144
|
{
|
|
132
|
-
kind: "
|
|
145
|
+
kind: "region",
|
|
133
146
|
path: `${HOST_DIR}/${LOCAL_PATCH_FILE}`,
|
|
134
147
|
// The kernel as `introspect` and `inspector.json` spawn it: repo-relative, so it resolves
|
|
135
148
|
// against the directory DSH itself was started in.
|
|
136
|
-
|
|
149
|
+
regions: [
|
|
150
|
+
{
|
|
151
|
+
begin: PATCH_BEGIN,
|
|
152
|
+
end: PATCH_END,
|
|
153
|
+
content: patch(project, getBinding(project.tool.binding).kernelCommand(project)),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
template: patchTemplate(),
|
|
137
157
|
},
|
|
138
158
|
];
|
|
139
159
|
},
|
|
@@ -34,8 +34,8 @@ const project: Project = {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const [manifestFile, ...rest] = gemini.plan(project);
|
|
37
|
-
if (manifestFile?.kind !== "
|
|
38
|
-
const manifest =
|
|
37
|
+
if (manifestFile?.kind !== "merge") throw new Error("expected one merge file");
|
|
38
|
+
const manifest = manifestFile.patch as Record<string, unknown>;
|
|
39
39
|
|
|
40
40
|
describe("gemini", () => {
|
|
41
41
|
it("is one root manifest: a dashed name, the kernel launch, AGENTS.md as the context file", () => {
|
|
@@ -58,7 +58,7 @@ describe("gemini", () => {
|
|
|
58
58
|
],
|
|
59
59
|
});
|
|
60
60
|
// `skills/` and `commands/` are auto-discovered from the extension root; no key names them.
|
|
61
|
-
expect(manifestFile.
|
|
61
|
+
expect(JSON.stringify(manifestFile.patch)).not.toContain('"skills"');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it("validates through Gemini's own manifest validator, and carries only MCP tool calls", () => {
|
package/src/surfaces/gemini.ts
CHANGED
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
configProperties,
|
|
27
27
|
envName,
|
|
28
28
|
isSensitive,
|
|
29
|
-
json,
|
|
30
29
|
kernelLaunch,
|
|
31
30
|
mcpVerdict,
|
|
32
31
|
} from "./shared.js";
|
|
@@ -64,7 +63,7 @@ export const surface: Surface = {
|
|
|
64
63
|
contextFileName: AGENTS_PATH,
|
|
65
64
|
settings: settings.length ? settings : undefined,
|
|
66
65
|
});
|
|
67
|
-
return [{ kind: "
|
|
66
|
+
return [{ kind: "merge", path: MANIFEST_PATH, format: "json", patch: manifest }];
|
|
68
67
|
},
|
|
69
68
|
validate(project) {
|
|
70
69
|
// Gemini's own manifest validator: it parses the manifest, enforces the name and version
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6
6
|
import { describe, expect, it } from "vitest";
|
|
7
|
-
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
8
8
|
import type { Operation, Project } from "../model.js";
|
|
9
|
+
import { apply } from "../project/apply.js";
|
|
9
10
|
import { HOST_DIR, pluginDir, surface } from "./hermes-native.js";
|
|
10
11
|
|
|
11
12
|
const echo: Operation = {
|
|
@@ -61,13 +62,45 @@ function project(overrides: Partial<Project> = {}): Project {
|
|
|
61
62
|
|
|
62
63
|
function emitted(target: Project): Record<string, string> {
|
|
63
64
|
return Object.fromEntries(
|
|
64
|
-
surface
|
|
65
|
+
surface
|
|
66
|
+
.plan(target)
|
|
67
|
+
.map((file) => [
|
|
68
|
+
file.path,
|
|
69
|
+
file.kind === "file"
|
|
70
|
+
? file.content
|
|
71
|
+
: file.kind === "merge"
|
|
72
|
+
? file.format === "yaml"
|
|
73
|
+
? stringifyYaml(file.patch)
|
|
74
|
+
: file.format === "toml"
|
|
75
|
+
? stringifyToml(file.patch)
|
|
76
|
+
: JSON.stringify(file.patch)
|
|
77
|
+
: file.regions.map((region) => region.content).join(""),
|
|
78
|
+
]),
|
|
65
79
|
);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
const python = project({ tool: { ...project().tool, binding: "python" } });
|
|
69
83
|
|
|
70
84
|
describe("hermes-native", () => {
|
|
85
|
+
it("keeps native registrations in the author hook while refreshing generated registrations", () => {
|
|
86
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-hermes-region-"));
|
|
87
|
+
const target = { ...project(), root };
|
|
88
|
+
apply(root, surface.plan(target), "0.1.0");
|
|
89
|
+
const path = join(root, pluginDir(target), "__init__.py");
|
|
90
|
+
writeFileSync(
|
|
91
|
+
path,
|
|
92
|
+
readFileSync(path, "utf8").replace(
|
|
93
|
+
" pass\n",
|
|
94
|
+
' ctx.register_tool(name="native", toolset="native", schema={}, handler=lambda _: "{}")\n',
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
apply(root, surface.plan({ ...target, operations: [echo] }), "0.1.0");
|
|
98
|
+
const text = readFileSync(path, "utf8");
|
|
99
|
+
expect(text).toContain('name="native"');
|
|
100
|
+
expect(text).toContain("def _register_generated(ctx: Any)");
|
|
101
|
+
expect(text).toContain("_register_generated(ctx)\n register_native(ctx)");
|
|
102
|
+
});
|
|
103
|
+
|
|
71
104
|
it("projects the manifest Hermes parses and the credentials it prompts for", () => {
|
|
72
105
|
const files = emitted(project());
|
|
73
106
|
const manifest = parseYaml(files[`${pluginDir(project())}/plugin.yaml`] ?? "") as Record<
|
|
@@ -184,11 +217,7 @@ describe.skipIf(!hasHermes())("hermes plugins doctor", () => {
|
|
|
184
217
|
it("loads and registers the generated plugin", () => {
|
|
185
218
|
const root = mkdtempSync(join(tmpdir(), "toolfactory-hermes-"));
|
|
186
219
|
try {
|
|
187
|
-
|
|
188
|
-
if (file.kind !== "file") continue;
|
|
189
|
-
mkdirSync(dirname(join(root, file.path)), { recursive: true });
|
|
190
|
-
writeFileSync(join(root, file.path), file.content);
|
|
191
|
-
}
|
|
220
|
+
apply(root, surface.plan(project()), "0.1.0");
|
|
192
221
|
const commands = surface.validate?.({ ...project(), root }) ?? [];
|
|
193
222
|
const command = commands.find((c) => c.label === "hermes plugins doctor");
|
|
194
223
|
if (!command) throw new Error("the hermes surface no longer declares a doctor validator");
|