toolfactory 0.1.1
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 +166 -0
- package/dist/bindings/index.d.ts +23 -0
- package/dist/bindings/index.js +29 -0
- package/dist/bindings/python.d.ts +33 -0
- package/dist/bindings/python.js +805 -0
- package/dist/bindings/python.test.d.ts +1 -0
- package/dist/bindings/python.test.js +316 -0
- package/dist/bindings/typescript.d.ts +39 -0
- package/dist/bindings/typescript.js +758 -0
- package/dist/bindings/typescript.test.d.ts +1 -0
- package/dist/bindings/typescript.test.js +264 -0
- package/dist/commands.d.ts +188 -0
- package/dist/commands.js +662 -0
- package/dist/commands.test.d.ts +1 -0
- package/dist/commands.test.js +215 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +124 -0
- package/dist/hosts/browser.d.ts +10 -0
- package/dist/hosts/browser.js +94 -0
- package/dist/hosts/github.d.ts +66 -0
- package/dist/hosts/github.js +247 -0
- package/dist/hosts/openclaw.d.ts +4 -0
- package/dist/hosts/openclaw.js +94 -0
- package/dist/hosts/template.d.ts +15 -0
- package/dist/hosts/template.js +142 -0
- package/dist/hosts/web.d.ts +4 -0
- package/dist/hosts/web.js +89 -0
- package/dist/identity/name.d.ts +20 -0
- package/dist/identity/name.js +33 -0
- package/dist/identity/name.test.d.ts +1 -0
- package/dist/identity/name.test.js +37 -0
- package/dist/identity/read.d.ts +9 -0
- package/dist/identity/read.js +106 -0
- package/dist/identity/read.test.d.ts +1 -0
- package/dist/identity/read.test.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/introspect/index.d.ts +15 -0
- package/dist/introspect/index.js +63 -0
- package/dist/model.d.ts +236 -0
- package/dist/model.js +214 -0
- package/dist/ops.d.ts +255 -0
- package/dist/ops.js +297 -0
- package/dist/project/apply.d.ts +35 -0
- package/dist/project/apply.js +360 -0
- package/dist/project/apply.test.d.ts +1 -0
- package/dist/project/apply.test.js +285 -0
- package/dist/project/gate.d.ts +129 -0
- package/dist/project/gate.js +597 -0
- package/dist/project/history.d.ts +62 -0
- package/dist/project/history.js +41 -0
- package/dist/project/load.d.ts +21 -0
- package/dist/project/load.js +82 -0
- package/dist/project/lock.d.ts +28 -0
- package/dist/project/lock.js +42 -0
- package/dist/project/plan.d.ts +5 -0
- package/dist/project/plan.js +96 -0
- package/dist/report/coverage.d.ts +22 -0
- package/dist/report/coverage.js +72 -0
- package/dist/report/coverage.test.d.ts +1 -0
- package/dist/report/coverage.test.js +93 -0
- package/dist/surfaces/agent-plugins.d.ts +10 -0
- package/dist/surfaces/agent-plugins.js +36 -0
- package/dist/surfaces/agents.d.ts +35 -0
- package/dist/surfaces/agents.js +609 -0
- package/dist/surfaces/agents.template.d.ts +8 -0
- package/dist/surfaces/agents.template.js +21 -0
- package/dist/surfaces/agents.test.d.ts +1 -0
- package/dist/surfaces/agents.test.js +181 -0
- package/dist/surfaces/browser-extension.d.ts +101 -0
- package/dist/surfaces/browser-extension.js +1034 -0
- package/dist/surfaces/browser-extension.test.d.ts +1 -0
- package/dist/surfaces/browser-extension.test.js +145 -0
- package/dist/surfaces/claude.d.ts +2 -0
- package/dist/surfaces/claude.js +81 -0
- package/dist/surfaces/codex.d.ts +13 -0
- package/dist/surfaces/codex.js +101 -0
- package/dist/surfaces/codex.test.d.ts +1 -0
- package/dist/surfaces/codex.test.js +67 -0
- package/dist/surfaces/cursor.d.ts +2 -0
- package/dist/surfaces/cursor.js +50 -0
- package/dist/surfaces/distribution.test.d.ts +1 -0
- package/dist/surfaces/distribution.test.js +146 -0
- package/dist/surfaces/dsh.d.ts +25 -0
- package/dist/surfaces/dsh.js +147 -0
- package/dist/surfaces/dsh.test.d.ts +1 -0
- package/dist/surfaces/dsh.test.js +100 -0
- package/dist/surfaces/external.d.ts +9 -0
- package/dist/surfaces/external.js +4 -0
- package/dist/surfaces/gemini.d.ts +29 -0
- package/dist/surfaces/gemini.js +68 -0
- package/dist/surfaces/gemini.test.d.ts +1 -0
- package/dist/surfaces/gemini.test.js +74 -0
- package/dist/surfaces/hermes-native.d.ts +53 -0
- package/dist/surfaces/hermes-native.js +507 -0
- package/dist/surfaces/hermes-native.test.d.ts +1 -0
- package/dist/surfaces/hermes-native.test.js +156 -0
- package/dist/surfaces/kernel.d.ts +5 -0
- package/dist/surfaces/kernel.js +63 -0
- package/dist/surfaces/mcp-registry.d.ts +19 -0
- package/dist/surfaces/mcp-registry.js +160 -0
- package/dist/surfaces/mcp-registry.test.d.ts +1 -0
- package/dist/surfaces/mcp-registry.test.js +79 -0
- package/dist/surfaces/mcpb.d.ts +11 -0
- package/dist/surfaces/mcpb.js +118 -0
- package/dist/surfaces/mcpb.test.d.ts +1 -0
- package/dist/surfaces/mcpb.test.js +104 -0
- package/dist/surfaces/npm.d.ts +6 -0
- package/dist/surfaces/npm.js +72 -0
- package/dist/surfaces/openclaw-native.d.ts +93 -0
- package/dist/surfaces/openclaw-native.js +956 -0
- package/dist/surfaces/openclaw-native.test.d.ts +1 -0
- package/dist/surfaces/openclaw-native.test.js +246 -0
- package/dist/surfaces/pypi.d.ts +5 -0
- package/dist/surfaces/pypi.js +82 -0
- package/dist/surfaces/readme.d.ts +7 -0
- package/dist/surfaces/readme.js +181 -0
- package/dist/surfaces/registry.d.ts +6 -0
- package/dist/surfaces/registry.js +73 -0
- package/dist/surfaces/shared.d.ts +49 -0
- package/dist/surfaces/shared.js +112 -0
- package/dist/surfaces/skill.d.ts +7 -0
- package/dist/surfaces/skill.js +118 -0
- package/dist/surfaces/skill.test.d.ts +1 -0
- package/dist/surfaces/skill.test.js +87 -0
- package/dist/surfaces/web.d.ts +97 -0
- package/dist/surfaces/web.js +1285 -0
- package/dist/surfaces/web.test.d.ts +1 -0
- package/dist/surfaces/web.test.js +255 -0
- package/dist/surfaces/workflows.d.ts +2 -0
- package/dist/surfaces/workflows.js +809 -0
- package/dist/surfaces/workflows.test.d.ts +1 -0
- package/dist/surfaces/workflows.test.js +563 -0
- package/dist/toolfactory/cli.d.ts +2 -0
- package/dist/toolfactory/cli.js +65 -0
- package/dist/toolfactory/config.d.ts +3 -0
- package/dist/toolfactory/config.js +27 -0
- package/dist/toolfactory/mcp.d.ts +37 -0
- package/dist/toolfactory/mcp.js +269 -0
- package/dist/toolfactory/types.d.ts +31 -0
- package/dist/toolfactory/types.js +10 -0
- package/dist/toolfactory/web.d.ts +4 -0
- package/dist/toolfactory/web.js +40 -0
- package/dist/validate/agent-plugins.d.ts +5 -0
- package/dist/validate/agent-plugins.js +46 -0
- package/package.json +68 -0
- package/schemas/agent-plugins/mcp.schema.json +120 -0
- package/schemas/agent-plugins/plugin.schema.json +65 -0
- package/src/bindings/index.ts +47 -0
- package/src/bindings/python.test.ts +374 -0
- package/src/bindings/python.ts +862 -0
- package/src/bindings/typescript.test.ts +329 -0
- package/src/bindings/typescript.ts +818 -0
- package/src/commands.test.ts +253 -0
- package/src/commands.ts +886 -0
- package/src/e2e.test.ts +169 -0
- package/src/hosts/browser.ts +106 -0
- package/src/hosts/github.ts +321 -0
- package/src/hosts/openclaw.ts +110 -0
- package/src/hosts/template.ts +149 -0
- package/src/hosts/web.ts +111 -0
- package/src/identity/name.test.ts +43 -0
- package/src/identity/name.ts +39 -0
- package/src/identity/read.test.ts +92 -0
- package/src/identity/read.ts +119 -0
- package/src/index.ts +11 -0
- package/src/introspect/index.ts +72 -0
- package/src/model.ts +386 -0
- package/src/ops.ts +333 -0
- package/src/project/apply.test.ts +348 -0
- package/src/project/apply.ts +417 -0
- package/src/project/gate.ts +717 -0
- package/src/project/history.ts +46 -0
- package/src/project/load.ts +104 -0
- package/src/project/lock.ts +54 -0
- package/src/project/plan.ts +101 -0
- package/src/report/coverage.test.ts +108 -0
- package/src/report/coverage.ts +113 -0
- package/src/surfaces/agent-plugins.ts +47 -0
- package/src/surfaces/agents.template.ts +32 -0
- package/src/surfaces/agents.test.ts +206 -0
- package/src/surfaces/agents.ts +729 -0
- package/src/surfaces/browser-extension.test.ts +175 -0
- package/src/surfaces/browser-extension.ts +1095 -0
- package/src/surfaces/claude.ts +99 -0
- package/src/surfaces/codex.test.ts +71 -0
- package/src/surfaces/codex.ts +105 -0
- package/src/surfaces/cursor.ts +62 -0
- package/src/surfaces/distribution.test.ts +166 -0
- package/src/surfaces/dsh.test.ts +110 -0
- package/src/surfaces/dsh.ts +159 -0
- package/src/surfaces/external.ts +13 -0
- package/src/surfaces/gemini.test.ts +80 -0
- package/src/surfaces/gemini.ts +84 -0
- package/src/surfaces/hermes-native.test.ts +179 -0
- package/src/surfaces/hermes-native.ts +584 -0
- package/src/surfaces/kernel.ts +68 -0
- package/src/surfaces/mcp-registry.test.ts +99 -0
- package/src/surfaces/mcp-registry.ts +190 -0
- package/src/surfaces/mcpb.test.ts +112 -0
- package/src/surfaces/mcpb.ts +130 -0
- package/src/surfaces/npm.ts +78 -0
- package/src/surfaces/openclaw-native.test.ts +283 -0
- package/src/surfaces/openclaw-native.ts +1070 -0
- package/src/surfaces/pypi.ts +84 -0
- package/src/surfaces/readme.ts +218 -0
- package/src/surfaces/registry.ts +79 -0
- package/src/surfaces/shared.ts +140 -0
- package/src/surfaces/skill.test.ts +103 -0
- package/src/surfaces/skill.ts +136 -0
- package/src/surfaces/web.test.ts +303 -0
- package/src/surfaces/web.ts +1310 -0
- package/src/surfaces/workflows.test.ts +677 -0
- package/src/surfaces/workflows.ts +936 -0
- package/src/toolfactory/cli.ts +74 -0
- package/src/toolfactory/config.ts +33 -0
- package/src/toolfactory/mcp.ts +311 -0
- package/src/toolfactory/types.ts +52 -0
- package/src/toolfactory/web.ts +45 -0
- package/src/validate/agent-plugins.ts +53 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-CCXhQKK3.css +2 -0
- package/web/dist/assets/index-D1PwZ3qS.js +10 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { scaffoldDrift } from "../hosts/openclaw.js";
|
|
4
|
+
import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
|
|
5
|
+
import { HOST_DIR, OPENCLAW_ADDITIONS, OPENCLAW_SCAFFOLD, surface } from "./openclaw-native.js";
|
|
6
|
+
const echo = {
|
|
7
|
+
name: "echo",
|
|
8
|
+
description: "Return the text you pass in.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { text: { type: "string" } },
|
|
13
|
+
required: ["text"],
|
|
14
|
+
},
|
|
15
|
+
outputSchema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: { echoed: { type: "string" } },
|
|
18
|
+
required: ["echoed"],
|
|
19
|
+
},
|
|
20
|
+
requires: [],
|
|
21
|
+
};
|
|
22
|
+
const shoot = { name: "shoot", inputSchema: { type: "object" }, requires: ["browser"] };
|
|
23
|
+
function project(overrides = {}) {
|
|
24
|
+
return {
|
|
25
|
+
root: "/repo",
|
|
26
|
+
tool: {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
identity: "package.json",
|
|
29
|
+
binding: "typescript",
|
|
30
|
+
surfaces: ["openclaw-native", "mcp"],
|
|
31
|
+
bundle: { runtime: "package" },
|
|
32
|
+
tests: { examples: { echo: { text: "hello" } } },
|
|
33
|
+
},
|
|
34
|
+
identity: { name: "hello", version: "0.1.0", description: "Say hello" },
|
|
35
|
+
identityExtra: {},
|
|
36
|
+
operations: [echo, shoot],
|
|
37
|
+
toolfactoryVersion: "0.1.0",
|
|
38
|
+
...overrides,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** A zero-operation plugin whose whole contribution is one declared host registration. */
|
|
42
|
+
function voice() {
|
|
43
|
+
const base = project();
|
|
44
|
+
return {
|
|
45
|
+
...base,
|
|
46
|
+
tool: {
|
|
47
|
+
...base.tool,
|
|
48
|
+
openclaw: {
|
|
49
|
+
registers: [
|
|
50
|
+
{
|
|
51
|
+
api: "registerRealtimeVoiceProvider",
|
|
52
|
+
contract: "realtimeVoiceProviders",
|
|
53
|
+
ids: ["codex"],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
pluginApi: ">=2026.8.2",
|
|
57
|
+
dependencies: { ws: "^8.18.0" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
operations: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Every planned file as text: full files verbatim, region files as the region toolfactory owns. */
|
|
64
|
+
function emitted(target) {
|
|
65
|
+
return Object.fromEntries(surface
|
|
66
|
+
.plan(target)
|
|
67
|
+
.map((file) => [
|
|
68
|
+
file.path,
|
|
69
|
+
file.kind === "file"
|
|
70
|
+
? file.content
|
|
71
|
+
: file.kind === "region"
|
|
72
|
+
? file.regions.map((region) => region.content).join("")
|
|
73
|
+
: JSON.stringify(file.patch),
|
|
74
|
+
]));
|
|
75
|
+
}
|
|
76
|
+
describe("openclaw-native", () => {
|
|
77
|
+
it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
|
|
78
|
+
const files = emitted(project());
|
|
79
|
+
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
|
80
|
+
expect(manifest.contracts).toEqual({ tools: ["echo"] });
|
|
81
|
+
expect(manifest.activation).toEqual({ onStartup: true });
|
|
82
|
+
expect(surface.verdict?.(shoot, project())).toEqual({
|
|
83
|
+
kind: "excluded",
|
|
84
|
+
reason: "excluded:implement-in-hosts",
|
|
85
|
+
});
|
|
86
|
+
// The escape hatch is where the author writes it; the README has to say so.
|
|
87
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("excluded:implement-in-hosts");
|
|
88
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
|
|
89
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
90
|
+
expect(pkg.openclaw).toEqual({
|
|
91
|
+
extensions: ["./dist/index.js"],
|
|
92
|
+
compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
|
|
93
|
+
build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
|
|
94
|
+
});
|
|
95
|
+
// Nothing to expect of the inspector until the plugin declares a registration.
|
|
96
|
+
expect(pkg.pluginInspector).toBeUndefined();
|
|
97
|
+
// TypeScript core: in-process import of the author's module, no subprocess.
|
|
98
|
+
expect(pkg.dependencies.hello).toBe("file:../..");
|
|
99
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain('from "hello/dist/ops.js"');
|
|
100
|
+
// JSON Schema goes through untouched via Type.Unsafe; $schema is what OpenClaw strips.
|
|
101
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain('"text"');
|
|
102
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).not.toContain("$schema");
|
|
103
|
+
// Module load must be side-effect-free: every host loader, and `plugin-inspector check
|
|
104
|
+
// --runtime`, import the entry long before a tool runs.
|
|
105
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain("const dataDir = () => join(");
|
|
106
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain("dataDir: dataDir(),");
|
|
107
|
+
});
|
|
108
|
+
it("serves the web app as a gateway route and a Control UI tab, in process, only in TypeScript", () => {
|
|
109
|
+
const withWeb = project({
|
|
110
|
+
tool: { ...project().tool, surfaces: ["openclaw-native", "mcp", "web", "npm"] },
|
|
111
|
+
});
|
|
112
|
+
const files = emitted(withWeb);
|
|
113
|
+
const index = files[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
114
|
+
// The route hands requests to the core's own router, so `<prefix>/mcp` needs no second
|
|
115
|
+
// listener and `/env` stays off — the gateway's working directory is not the author's.
|
|
116
|
+
expect(index).toContain('import { handler } from "hello/dist/toolfactory/mcp.js";');
|
|
117
|
+
expect(index).toContain('const WEB_PATH = "/plugins/hello/web";');
|
|
118
|
+
expect(index).toContain('auth: "gateway"');
|
|
119
|
+
expect(index).toContain("handler({ path: `${WEB_PATH}/mcp`, prefix: WEB_PATH, env: false })");
|
|
120
|
+
expect(index).toContain('surface: "tab" as const');
|
|
121
|
+
expect(index).toContain("api.registerControlUiDescriptor(descriptor)");
|
|
122
|
+
// Neither registrar is a manifest contract key; both are what the inspector expects to see.
|
|
123
|
+
expect(JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}").contracts).toEqual({
|
|
124
|
+
tools: ["echo"],
|
|
125
|
+
});
|
|
126
|
+
expect(JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}").pluginInspector.plugin.expect
|
|
127
|
+
.registrations).toEqual(["registerHttpRoute", "registerControlUiDescriptor"]);
|
|
128
|
+
expect(files[`${HOST_DIR}/src/index.test.ts`]).toContain("serves the web app as a Control UI tab");
|
|
129
|
+
// A Python core's plugin is an out-of-process shim: there is no handler to mount.
|
|
130
|
+
const python = emitted(project({
|
|
131
|
+
tool: {
|
|
132
|
+
...withWeb.tool,
|
|
133
|
+
binding: "python",
|
|
134
|
+
surfaces: ["openclaw-native", "mcp", "web", "pypi"],
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
expect(python[`${HOST_DIR}/src/index.ts`]).not.toContain("registerHttpRoute");
|
|
138
|
+
});
|
|
139
|
+
it("degrades a non-TypeScript core to an out-of-process shim", () => {
|
|
140
|
+
const python = project({
|
|
141
|
+
tool: { ...project().tool, binding: "python" },
|
|
142
|
+
});
|
|
143
|
+
expect(surface.verdict?.(echo, python)).toEqual({
|
|
144
|
+
kind: "degraded",
|
|
145
|
+
reason: "degraded:out-of-process",
|
|
146
|
+
});
|
|
147
|
+
const index = emitted(python)[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
148
|
+
expect(index).toContain('"run", "python", "-m", "hello.toolfactory.cli"');
|
|
149
|
+
// The host's state directory reaches an out-of-process kernel as the environment variable
|
|
150
|
+
// every surface reads it from.
|
|
151
|
+
expect(index).toContain('env["HELLO_DATA_DIR"] = dataDir();');
|
|
152
|
+
expect(index).not.toContain("ops.js");
|
|
153
|
+
expect(JSON.parse(emitted(python)[`${HOST_DIR}/package.json`] ?? "{}").dependencies).toEqual(OPENCLAW_SCAFFOLD.dependencies);
|
|
154
|
+
});
|
|
155
|
+
it("drives manifest, inspector and test from one `registers` declaration", () => {
|
|
156
|
+
const target = voice();
|
|
157
|
+
const files = emitted(target);
|
|
158
|
+
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
|
159
|
+
expect(manifest.contracts).toEqual({ tools: [], realtimeVoiceProviders: ["codex"] });
|
|
160
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
161
|
+
expect(pkg.pluginInspector.plugin.expect).toEqual({
|
|
162
|
+
registrations: ["registerRealtimeVoiceProvider"],
|
|
163
|
+
manifestContracts: ["realtimeVoiceProviders"],
|
|
164
|
+
});
|
|
165
|
+
// The author's extra dependency merges after the scaffold's; the core is not imported at all.
|
|
166
|
+
expect(pkg.dependencies).toEqual({ ...OPENCLAW_SCAFFOLD.dependencies, ws: "^8.18.0" });
|
|
167
|
+
expect(pkg.peerDependencies.openclaw).toBe(">=2026.8.2");
|
|
168
|
+
expect(pkg.openclaw.compat.pluginApi).toBe(">=2026.8.2");
|
|
169
|
+
// The generated region stops at the entry: the registration itself is the author's tail.
|
|
170
|
+
const region = files[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
171
|
+
expect(region).toContain("const entry = defineToolPlugin({");
|
|
172
|
+
expect(region).not.toContain("registerRealtimeVoiceProvider(");
|
|
173
|
+
expect(region).toContain('Parameters<OpenClawPluginApi["registerRealtimeVoiceProvider"]>[0]');
|
|
174
|
+
expect(region).not.toContain("typebox");
|
|
175
|
+
const template = surface.plan(target).find((file) => file.path.endsWith("src/index.ts"));
|
|
176
|
+
expect(template?.kind === "region" && template.template).toContain("export default entry;");
|
|
177
|
+
// The generated test records the registration the declaration promised.
|
|
178
|
+
const test = files[`${HOST_DIR}/src/index.test.ts`] ?? "";
|
|
179
|
+
expect(test).toContain("entry.register(api as OpenClawPluginApi)");
|
|
180
|
+
expect(test).toContain('registered["registerRealtimeVoiceProvider"]');
|
|
181
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("codex-app-server-extensions.ts");
|
|
182
|
+
});
|
|
183
|
+
it("projects a discriminating scripted-model e2e lane, and none without a tool to call", () => {
|
|
184
|
+
const files = emitted(project());
|
|
185
|
+
const fixtures = JSON.parse(files[`${HOST_DIR}/e2e/fixtures.json`] ?? "{}");
|
|
186
|
+
// Leg 1 asks for the operation with the authored example arguments; leg 2 answers OK only
|
|
187
|
+
// when the tool's own result carries a name its output schema promises; leg 3 catches the rest.
|
|
188
|
+
expect(fixtures.fixtures).toEqual([
|
|
189
|
+
{
|
|
190
|
+
match: { toolName: "echo", hasToolResult: false },
|
|
191
|
+
response: { toolCalls: [{ name: "echo", arguments: { text: "hello" } }] },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
match: { hasToolResult: true, toolResultContains: "echoed" },
|
|
195
|
+
response: { content: "HELLO_OK" },
|
|
196
|
+
},
|
|
197
|
+
{ match: { hasToolResult: true }, response: { content: "HELLO_FAIL" } },
|
|
198
|
+
]);
|
|
199
|
+
const e2e = files[`${HOST_DIR}/e2e/openclaw.e2e.test.ts`] ?? "";
|
|
200
|
+
expect(e2e).toContain('expect(turn.payloads[0].text).toBe("HELLO_OK")');
|
|
201
|
+
expect(e2e).toContain('"plugins", "install", "--link", pluginDir');
|
|
202
|
+
// The three constraints that are not discoverable anywhere else.
|
|
203
|
+
expect(e2e).toContain('key !== "VITEST" && !key.startsWith("VITEST_")');
|
|
204
|
+
expect(files[`${HOST_DIR}/vitest.e2e.config.ts`]).toContain('include: ["e2e/**/*.e2e.test.ts"]');
|
|
205
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
206
|
+
expect(pkg.scripts["test:e2e"]).toBe(OPENCLAW_ADDITIONS.e2eScript);
|
|
207
|
+
expect(pkg.devDependencies["@copilotkit/aimock"]).toBe(OPENCLAW_ADDITIONS.aimock);
|
|
208
|
+
// `file:../..` becomes a node_modules symlink out of the install root, which OpenClaw's
|
|
209
|
+
// install-time safety scan refuses; install-links materialises it instead.
|
|
210
|
+
expect(files[`${HOST_DIR}/.npmrc`]).toContain("install-links=true");
|
|
211
|
+
// Without it `tsc` emits a dist/ from source that failed type-checking.
|
|
212
|
+
expect(JSON.parse(files[`${HOST_DIR}/tsconfig.json`] ?? "{}").compilerOptions.noEmitOnError).toBe(true);
|
|
213
|
+
// The lane is opt-in: a real turn really runs the operation, so no `tests.examples` entry
|
|
214
|
+
// (and no operation at all) means no lane.
|
|
215
|
+
const base = project();
|
|
216
|
+
const unexampled = emitted({ ...base, tool: { ...base.tool, tests: { examples: {} } } });
|
|
217
|
+
expect(unexampled[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
|
|
218
|
+
const none = emitted(voice());
|
|
219
|
+
expect(none[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
|
|
220
|
+
expect(JSON.parse(none[`${HOST_DIR}/package.json`] ?? "{}").scripts["test:e2e"]).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
it("reports what a zero-operation plugin contributes instead of an empty table", () => {
|
|
223
|
+
const coverage = computeCoverage(voice(), [surface]);
|
|
224
|
+
expect(coverage.rows).toEqual([]);
|
|
225
|
+
const markdown = renderCoverageMarkdown(coverage, [], 7);
|
|
226
|
+
expect(markdown).not.toContain("| operation |");
|
|
227
|
+
expect(markdown).toContain("`registerRealtimeVoiceProvider` → `realtimeVoiceProviders`: `codex`");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
/** C2: the scaffold constants are only trustworthy while the real generator still writes them. */
|
|
231
|
+
function hasOpenclaw() {
|
|
232
|
+
try {
|
|
233
|
+
execFileSync("openclaw", ["--version"], { stdio: "ignore" });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
describe.skipIf(!hasOpenclaw())("openclaw scaffold", () => {
|
|
241
|
+
it("still matches `openclaw plugins init --type tool`", () => {
|
|
242
|
+
expect(scaffoldDrift(project())).toEqual([]);
|
|
243
|
+
// A declared `pluginApi` is an intentional override, not drift.
|
|
244
|
+
expect(scaffoldDrift(voice())).toEqual([]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PyPI package: identity projected into pyproject.toml (merge; the author owns the rest),
|
|
3
|
+
* the console script for the CLI, and the MCP Registry ownership marker, which for PyPI is
|
|
4
|
+
* an `mcp-name:` line in the README the registry reads back off the published project page.
|
|
5
|
+
*/
|
|
6
|
+
import { cliEntryPoint, projectTable, pythonPackage } from "../bindings/python.js";
|
|
7
|
+
import { registryName } from "./mcp-registry.js";
|
|
8
|
+
import { compact, has } from "./shared.js";
|
|
9
|
+
import { WEB_DIR } from "./web.js";
|
|
10
|
+
export const README_PATH = "README.md";
|
|
11
|
+
export const MCP_NAME_BEGIN = "<!-- tf:mcp-name -->";
|
|
12
|
+
export const MCP_NAME_END = "<!-- /tf:mcp-name -->";
|
|
13
|
+
export const surface = {
|
|
14
|
+
id: "pypi",
|
|
15
|
+
plan(project) {
|
|
16
|
+
if (project.tool.binding !== "python") {
|
|
17
|
+
throw new Error('Surface "pypi" requires the python binding.');
|
|
18
|
+
}
|
|
19
|
+
const authored = project.tool.identity === "pyproject.toml";
|
|
20
|
+
const registry = has(project, "mcp-registry");
|
|
21
|
+
const patch = {
|
|
22
|
+
project: compact({
|
|
23
|
+
...(authored ? {} : projectTable(project)),
|
|
24
|
+
readme: registry ? README_PATH : undefined,
|
|
25
|
+
scripts: has(project, "cli")
|
|
26
|
+
? { [project.identity.name]: cliEntryPoint(project) }
|
|
27
|
+
: undefined,
|
|
28
|
+
}),
|
|
29
|
+
// The kernel serves the built page from inside its own package, so the wheel has to
|
|
30
|
+
// carry it: `packages` only copies the package tree, `force-include` maps anything else.
|
|
31
|
+
...(has(project, "web")
|
|
32
|
+
? {
|
|
33
|
+
tool: {
|
|
34
|
+
hatch: {
|
|
35
|
+
build: {
|
|
36
|
+
targets: {
|
|
37
|
+
wheel: {
|
|
38
|
+
"force-include": {
|
|
39
|
+
[`${WEB_DIR}/dist`]: `${pythonPackage(project)}/${WEB_DIR}`,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
};
|
|
49
|
+
const files = [
|
|
50
|
+
{
|
|
51
|
+
kind: "merge",
|
|
52
|
+
path: "pyproject.toml",
|
|
53
|
+
format: "toml",
|
|
54
|
+
patch,
|
|
55
|
+
owned: ["project.scripts"],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
if (!registry)
|
|
59
|
+
return files;
|
|
60
|
+
const marker = `\n<!-- mcp-name: ${registryName(project)} -->\n`;
|
|
61
|
+
return [
|
|
62
|
+
...files,
|
|
63
|
+
{
|
|
64
|
+
kind: "region",
|
|
65
|
+
path: README_PATH,
|
|
66
|
+
regions: [{ begin: MCP_NAME_BEGIN, end: MCP_NAME_END, content: marker }],
|
|
67
|
+
template: [
|
|
68
|
+
`# ${project.identity.name}`,
|
|
69
|
+
"",
|
|
70
|
+
project.identity.description ?? "",
|
|
71
|
+
"",
|
|
72
|
+
MCP_NAME_BEGIN,
|
|
73
|
+
MCP_NAME_END,
|
|
74
|
+
"",
|
|
75
|
+
].join("\n"),
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
},
|
|
79
|
+
validate(project) {
|
|
80
|
+
return [{ label: "uv build", command: "uv", args: ["build"], cwd: project.root }];
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Project, Surface } from "../model.js";
|
|
2
|
+
export declare const README_PATH = "README.md";
|
|
3
|
+
export declare const INSTALL_BEGIN = "<!-- tf:install -->";
|
|
4
|
+
export declare const INSTALL_END = "<!-- /tf:install -->";
|
|
5
|
+
/** The content of the generated region — everything between the markers, not the markers. */
|
|
6
|
+
export declare function installContent(project: Project): string;
|
|
7
|
+
export declare const surface: Surface;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface "readme": the `<!-- tf:install -->` region of `README.md`, always generated (like
|
|
3
|
+
* `workflows` and `agents`; never in `tool.json.surfaces`). One install line per selected
|
|
4
|
+
* distribution surface, in the exact shape that surface's own installer accepts, so the
|
|
5
|
+
* humans-and-agents-facing half of distribution cannot drift from the artifacts. The prose
|
|
6
|
+
* around the region — what the tool is, how to use it — is the author's, forever.
|
|
7
|
+
*
|
|
8
|
+
* Every line works from a plain checkout; the ones that need a GitHub repository (`npx skills
|
|
9
|
+
* add`, the Agent Skill badge, the marketplace and Hermes clone URLs) appear only once the
|
|
10
|
+
* identity file carries one, so a tool built without GitHub still gets an accurate section.
|
|
11
|
+
*
|
|
12
|
+
* When `mcp` is selected alongside a package registry, the MCP line also carries the two
|
|
13
|
+
* first-party one-click badges (VS Code's `vscode.dev/redirect/mcp/install`, Cursor's
|
|
14
|
+
* `cursor.com/en/install-mcp`), both computed from the exact same `{command, args, env}` launch
|
|
15
|
+
* the plain-text line renders — a badge can never point somewhere the text line does not.
|
|
16
|
+
*/
|
|
17
|
+
import { githubSlug } from "../hosts/github.js";
|
|
18
|
+
import { projectName } from "../identity/name.js";
|
|
19
|
+
import { HOST_DIR as BROWSER_HOST_DIR, zipName } from "./browser-extension.js";
|
|
20
|
+
import { HOST_DIR as DSH_HOST_DIR, dshTarball } from "./dsh.js";
|
|
21
|
+
import { pluginDir as hermesPluginDir } from "./hermes-native.js";
|
|
22
|
+
import { HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
|
|
23
|
+
import { envName, has, npmName, pypiName, requiredConfig } from "./shared.js";
|
|
24
|
+
export const README_PATH = "README.md";
|
|
25
|
+
export const INSTALL_BEGIN = "<!-- tf:install -->";
|
|
26
|
+
export const INSTALL_END = "<!-- /tf:install -->";
|
|
27
|
+
/** A stable label for repositories that ship an Agent Skill; unlike skills.sh's live count badge,
|
|
28
|
+
* it does not depend on a separate registry index. */
|
|
29
|
+
function skillBadge(slug) {
|
|
30
|
+
return `[](https://github.com/${slug})`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The kernel MCP server's published launch, unpinned — `{command, args}`, plus an `env` map of
|
|
34
|
+
* blank placeholders for whatever config the tool requires. The one source both `mcpCommand`
|
|
35
|
+
* (the plain-text line) and the VS Code / Cursor install badges below encode: a badge can never
|
|
36
|
+
* name a launch the text line does not.
|
|
37
|
+
*/
|
|
38
|
+
function launchConfig(project) {
|
|
39
|
+
const { command, args } = project.tool.binding === "python"
|
|
40
|
+
? (() => {
|
|
41
|
+
const pypi = pypiName(project);
|
|
42
|
+
// `uvx <pkg>` runs the command of the same name; a dotted tool name renames the package.
|
|
43
|
+
return pypi === project.identity.name
|
|
44
|
+
? { command: "uvx", args: [pypi, "mcp"] }
|
|
45
|
+
: { command: "uvx", args: ["--from", pypi, project.identity.name, "mcp"] };
|
|
46
|
+
})()
|
|
47
|
+
: { command: "npx", args: ["-y", npmName(project), "mcp"] };
|
|
48
|
+
const required = requiredConfig(project);
|
|
49
|
+
return required.length
|
|
50
|
+
? { command, args, env: Object.fromEntries(required.map((key) => [envName(key), ""])) }
|
|
51
|
+
: { command, args };
|
|
52
|
+
}
|
|
53
|
+
function mcpCommand(project) {
|
|
54
|
+
const { command, args } = launchConfig(project);
|
|
55
|
+
return [command, ...args].join(" ");
|
|
56
|
+
}
|
|
57
|
+
/** github.com/github/github-mcp-server's README badge, verbatim shields.io style. */
|
|
58
|
+
function vscodeBadge(project) {
|
|
59
|
+
const config = encodeURIComponent(JSON.stringify(launchConfig(project)));
|
|
60
|
+
const url = `https://vscode.dev/redirect/mcp/install?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
|
|
61
|
+
return `[](${url})`;
|
|
62
|
+
}
|
|
63
|
+
/** cursor.com/docs/mcp/install-links's own badge SVG and `en/install-mcp` redirect. */
|
|
64
|
+
function cursorBadge(project) {
|
|
65
|
+
const config = Buffer.from(JSON.stringify(launchConfig(project))).toString("base64");
|
|
66
|
+
const url = `https://cursor.com/en/install-mcp?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
|
|
67
|
+
return `[](${url})`;
|
|
68
|
+
}
|
|
69
|
+
function installLines(project) {
|
|
70
|
+
const { identity } = project;
|
|
71
|
+
const slug = githubSlug(identity.repository);
|
|
72
|
+
const lines = [];
|
|
73
|
+
if (has(project, "skill") && slug) {
|
|
74
|
+
lines.push(`- **Agent Skill** — \`npx skills add ${slug}\``);
|
|
75
|
+
}
|
|
76
|
+
if (has(project, "mcp")) {
|
|
77
|
+
// The one-click badges need a published package to point at: they read the same launch
|
|
78
|
+
// config the text line does, so they only appear once there is somewhere to `npx`/`uvx` from.
|
|
79
|
+
const badges = has(project, "npm") || has(project, "pypi")
|
|
80
|
+
? ` ${vscodeBadge(project)} ${cursorBadge(project)}`
|
|
81
|
+
: "";
|
|
82
|
+
lines.push(`- **MCP server** — \`${mcpCommand(project)}\`${badges}`);
|
|
83
|
+
}
|
|
84
|
+
if (has(project, "mcpb")) {
|
|
85
|
+
lines.push(`- **Claude Desktop extension** — download \`${identity.name}.mcpb\` from the GitHub Release and double-click to install`);
|
|
86
|
+
}
|
|
87
|
+
if (has(project, "claude")) {
|
|
88
|
+
// The repository is its own marketplace (`.claude-plugin/marketplace.json`); Copilot CLI
|
|
89
|
+
// reads the same file with the same command.
|
|
90
|
+
lines.push(`- **Claude Code plugin** — \`claude plugin marketplace add ${slug ?? "."}\`, then \`claude plugin install ${identity.name}@${identity.name}\``);
|
|
91
|
+
}
|
|
92
|
+
if (has(project, "codex")) {
|
|
93
|
+
// `.agents/plugins/marketplace.json` lists the repository's own plugin (`codex.ts`).
|
|
94
|
+
lines.push(`- **Codex plugin** — \`codex plugin marketplace add ${slug ?? "."}\`, then \`codex plugin add ${identity.name}@${identity.name}\``);
|
|
95
|
+
}
|
|
96
|
+
if (has(project, "gemini")) {
|
|
97
|
+
lines.push(slug
|
|
98
|
+
? `- **Gemini CLI extension** — \`gemini extensions install https://github.com/${slug}\``
|
|
99
|
+
: "- **Gemini CLI extension** — `gemini extensions link .` from a checkout");
|
|
100
|
+
}
|
|
101
|
+
if (has(project, "openclaw-native")) {
|
|
102
|
+
const clawhub = has(project, "clawhub")
|
|
103
|
+
? `; published: \`openclaw plugins install clawhub:${projectName.openclawPackage(identity.name)}\``
|
|
104
|
+
: "";
|
|
105
|
+
lines.push(`- **OpenClaw plugin** — \`openclaw plugins install --link ${OPENCLAW_HOST_DIR}\` from a checkout${clawhub}`);
|
|
106
|
+
}
|
|
107
|
+
if (has(project, "hermes-native")) {
|
|
108
|
+
const source = slug ? `https://github.com/${slug}` : "file://$PWD";
|
|
109
|
+
lines.push(`- **Hermes plugin** — \`hermes plugins install ${source}#${hermesPluginDir(project)}\``);
|
|
110
|
+
}
|
|
111
|
+
if (has(project, "dsh")) {
|
|
112
|
+
// `dsh plugin` is a pnpm forwarder: it installs a path, a tarball or a registry spec.
|
|
113
|
+
lines.push(`- **DSH plugin** (experimental) — \`dsh plugin --profile <profile> add ./${DSH_HOST_DIR}\` from a checkout, or the release tarball \`${dshTarball(project)}\``);
|
|
114
|
+
}
|
|
115
|
+
if (has(project, "browser-extension")) {
|
|
116
|
+
// Three channels, in the order a reader needs them: build-and-load from a checkout, the
|
|
117
|
+
// release assets (only Mozilla's signed xpi is a real self-hosted install — Chrome drops
|
|
118
|
+
// side-loaded unpacked extensions), the store listings. Pairing is the last step of each.
|
|
119
|
+
const zips = ["chrome", "firefox", "edge"].map((browser) => `\`${zipName(project, browser)}\``);
|
|
120
|
+
lines.push([
|
|
121
|
+
`- **Browser extension** — from a checkout: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`,
|
|
122
|
+
` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\``,
|
|
123
|
+
` (Firefox: \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\`). Each GitHub Release attaches the`,
|
|
124
|
+
` store uploads ${zips.join(", ")}, and the Mozilla-signed \`.xpi\`,`,
|
|
125
|
+
" which is the only download-and-install channel now that Chrome no longer keeps side-loaded unpacked",
|
|
126
|
+
" extensions; the Chrome Web Store, Firefox Add-ons and Edge Add-ons listings appear once the release's",
|
|
127
|
+
` submit step has each store's credentials. Then pair it: \`${mcpCommand(project)} --http --pair\``,
|
|
128
|
+
" prints the `<url>#<token>` the extension's options page accepts.",
|
|
129
|
+
].join("\n"));
|
|
130
|
+
}
|
|
131
|
+
if (has(project, "web")) {
|
|
132
|
+
// Not an install: the one command that puts the page in front of a human, from wherever the
|
|
133
|
+
// tool already is. The same listener answers `/mcp`, so the page needs nothing else running.
|
|
134
|
+
lines.push(`- **Web app** — \`${mcpCommand(project)} --http --open\` serves the operations page beside the`, " MCP endpoint on one port and opens it; over MCP or a skill, the `web` operation does the same and", " returns the URL.");
|
|
135
|
+
}
|
|
136
|
+
if (has(project, "npm"))
|
|
137
|
+
lines.push(`- **npm package** — \`npm install ${npmName(project)}\``);
|
|
138
|
+
if (has(project, "pypi"))
|
|
139
|
+
lines.push(`- **PyPI package** — \`uv add ${pypiName(project)}\``);
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
142
|
+
/** The content of the generated region — everything between the markers, not the markers. */
|
|
143
|
+
export function installContent(project) {
|
|
144
|
+
const slug = githubSlug(project.identity.repository);
|
|
145
|
+
const badge = has(project, "skill") && slug ? [skillBadge(slug), ""] : [];
|
|
146
|
+
const lines = installLines(project);
|
|
147
|
+
return `${[
|
|
148
|
+
"",
|
|
149
|
+
"## Install",
|
|
150
|
+
"",
|
|
151
|
+
...badge,
|
|
152
|
+
...(lines.length
|
|
153
|
+
? lines
|
|
154
|
+
: ["_No installable surface is selected: this repository ships source only._"]),
|
|
155
|
+
"",
|
|
156
|
+
].join("\n")}\n`;
|
|
157
|
+
}
|
|
158
|
+
function template(project) {
|
|
159
|
+
return [
|
|
160
|
+
`# ${project.identity.name}`,
|
|
161
|
+
"",
|
|
162
|
+
project.identity.description ?? "One paragraph: what this tool does, and for whom.",
|
|
163
|
+
"",
|
|
164
|
+
INSTALL_BEGIN,
|
|
165
|
+
INSTALL_END,
|
|
166
|
+
"",
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
export const surface = {
|
|
170
|
+
id: "readme",
|
|
171
|
+
plan(project) {
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
kind: "region",
|
|
175
|
+
path: README_PATH,
|
|
176
|
+
regions: [{ begin: INSTALL_BEGIN, end: INSTALL_END, content: installContent(project) }],
|
|
177
|
+
template: template(project),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
},
|
|
181
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Surface, SurfaceId } from "../model.js";
|
|
2
|
+
export declare function registerSurface(surface: Surface): void;
|
|
3
|
+
export declare function getSurface(id: SurfaceId): Surface;
|
|
4
|
+
export declare function selectedSurfaces(ids: readonly SurfaceId[]): Surface[];
|
|
5
|
+
/** A selection is valid only when every surface's declared dependencies are selected too. */
|
|
6
|
+
export declare function assertSurfaceRequirements(ids: readonly SurfaceId[]): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { surface as agentPlugins } from "./agent-plugins.js";
|
|
2
|
+
import { surface as agents } from "./agents.js";
|
|
3
|
+
import { surface as browserExtension } from "./browser-extension.js";
|
|
4
|
+
import { surface as claude } from "./claude.js";
|
|
5
|
+
import { surface as codex } from "./codex.js";
|
|
6
|
+
import { surface as cursor } from "./cursor.js";
|
|
7
|
+
import { surface as dsh } from "./dsh.js";
|
|
8
|
+
import { clawhub } from "./external.js";
|
|
9
|
+
import { surface as gemini } from "./gemini.js";
|
|
10
|
+
import { surface as hermesNative } from "./hermes-native.js";
|
|
11
|
+
import { cli, mcp } from "./kernel.js";
|
|
12
|
+
import { surface as mcpRegistry } from "./mcp-registry.js";
|
|
13
|
+
import { surface as mcpb } from "./mcpb.js";
|
|
14
|
+
import { surface as npm } from "./npm.js";
|
|
15
|
+
import { surface as openclawNative } from "./openclaw-native.js";
|
|
16
|
+
import { surface as pypi } from "./pypi.js";
|
|
17
|
+
import { surface as readme } from "./readme.js";
|
|
18
|
+
import { surface as skill } from "./skill.js";
|
|
19
|
+
import { surface as web } from "./web.js";
|
|
20
|
+
import { surface as workflows } from "./workflows.js";
|
|
21
|
+
const registry = {
|
|
22
|
+
"agent-plugins": agentPlugins,
|
|
23
|
+
agents,
|
|
24
|
+
"browser-extension": { ...browserExtension, requires: ["mcp"] },
|
|
25
|
+
claude,
|
|
26
|
+
codex,
|
|
27
|
+
// Declared dependencies (§3): these projectors point at a file another surface writes, so a
|
|
28
|
+
// selection without it would ship an artifact referencing nothing. Refused at plan time.
|
|
29
|
+
cursor: { ...cursor, requires: ["agent-plugins"] },
|
|
30
|
+
clawhub,
|
|
31
|
+
// Reaches the kernel only through DSH's own MCP client, so the bundle is inert without it.
|
|
32
|
+
dsh: { ...dsh, requires: ["mcp"] },
|
|
33
|
+
// The manifest's only payload is the kernel's launch row: an extension without it is a
|
|
34
|
+
// context file and nothing else.
|
|
35
|
+
gemini: { ...gemini, requires: ["mcp"] },
|
|
36
|
+
cli,
|
|
37
|
+
mcp,
|
|
38
|
+
"mcp-registry": mcpRegistry,
|
|
39
|
+
// The bundle root is the npm tarball with its production dependencies installed into it, and
|
|
40
|
+
// its one row launches the kernel MCP server: without either surface there is nothing to pack.
|
|
41
|
+
mcpb: { ...mcpb, requires: ["mcp", "npm"] },
|
|
42
|
+
npm,
|
|
43
|
+
skill,
|
|
44
|
+
"hermes-native": hermesNative,
|
|
45
|
+
"openclaw-native": openclawNative,
|
|
46
|
+
pypi,
|
|
47
|
+
readme,
|
|
48
|
+
web: { ...web, requires: ["mcp"] },
|
|
49
|
+
workflows,
|
|
50
|
+
};
|
|
51
|
+
export function registerSurface(surface) {
|
|
52
|
+
registry[surface.id] = surface;
|
|
53
|
+
}
|
|
54
|
+
export function getSurface(id) {
|
|
55
|
+
const surface = registry[id];
|
|
56
|
+
if (!surface)
|
|
57
|
+
throw new Error(`Surface "${id}" is not implemented in this toolfactory version.`);
|
|
58
|
+
return surface;
|
|
59
|
+
}
|
|
60
|
+
export function selectedSurfaces(ids) {
|
|
61
|
+
return ids.map(getSurface);
|
|
62
|
+
}
|
|
63
|
+
/** A selection is valid only when every surface's declared dependencies are selected too. */
|
|
64
|
+
export function assertSurfaceRequirements(ids) {
|
|
65
|
+
const selected = new Set(ids);
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
for (const required of getSurface(id).requires ?? []) {
|
|
68
|
+
if (selected.has(required))
|
|
69
|
+
continue;
|
|
70
|
+
throw new Error(`Surface "${id}" requires surface "${required}", which is not selected (surfaces: ${ids.join(", ")}). Select "${required}" or drop "${id}".`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Operation, type Project, type Verdict } from "../model.js";
|
|
2
|
+
export declare const TOOLFACTORY_DIR = "dev.toolfactory";
|
|
3
|
+
export declare function json(value: unknown): string;
|
|
4
|
+
/** Drop undefined values so projections never emit `"key": undefined`. */
|
|
5
|
+
export declare function compact<T extends Record<string, unknown>>(record: T): Partial<T>;
|
|
6
|
+
export declare function has(project: Project, surface: string): boolean;
|
|
7
|
+
export declare function npmName(project: Project): string;
|
|
8
|
+
export declare function pypiName(project: Project): string;
|
|
9
|
+
/** The launch command for the kernel MCP server as an installed package or a committed build. */
|
|
10
|
+
export declare function kernelLaunch(project: Project, placeholderRoot: string): {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
};
|
|
14
|
+
/** Verdict for surfaces that only carry MCP tool calls: no host capabilities at all. */
|
|
15
|
+
export declare function mcpVerdict(operation: Operation): Verdict;
|
|
16
|
+
/** Verdict for skills hosts: non-portable operations are bridged by the agent driving the host's own tools. */
|
|
17
|
+
/** A CLI has a human at the terminal, so user-input is native there; other host capabilities are not. */
|
|
18
|
+
export declare function cliVerdict(operation: Operation): Verdict;
|
|
19
|
+
/** A skill never executes: the agent drives the CLI or MCP server, so everything it can reach is bridged. */
|
|
20
|
+
export declare function skillVerdict(operation: Operation): Verdict;
|
|
21
|
+
export declare function configProperties(project: Project): Record<string, Record<string, unknown>>;
|
|
22
|
+
export declare function isSensitive(property: Record<string, unknown>): boolean;
|
|
23
|
+
export declare function requiredConfig(project: Project): string[];
|
|
24
|
+
/** The environment variable a config key is read from, on every surface and in every environment. */
|
|
25
|
+
export declare function envName(key: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Environment variable a host-native shim reads to find the tool's checkout when the host
|
|
28
|
+
* copied the plugin elsewhere (Hermes' doctor, OpenClaw's extensions directory).
|
|
29
|
+
*/
|
|
30
|
+
export declare function rootEnvName(project: Project): string;
|
|
31
|
+
/**
|
|
32
|
+
* The environment variable a host passes the tool's per-instance data directory
|
|
33
|
+
* (caches, session state) through, on every surface. The kernel falls back to an
|
|
34
|
+
* XDG-style default when it is unset.
|
|
35
|
+
*/
|
|
36
|
+
export declare function dataDirEnvName(project: Project): string;
|
|
37
|
+
/**
|
|
38
|
+
* The credentials a live run needs: every config key that is both `required` and sensitive.
|
|
39
|
+
* A tool with none has nothing to gate a live tier on, so no live test and no `live` CI job.
|
|
40
|
+
*/
|
|
41
|
+
export declare function liveCredentials(project: Project): string[];
|
|
42
|
+
/**
|
|
43
|
+
* The operation the generated example test calls: the first one with arguments in
|
|
44
|
+
* `tool.json` `tests.examples`, else the first operation with no arguments.
|
|
45
|
+
*/
|
|
46
|
+
export declare function liveExample(project: Project): {
|
|
47
|
+
name: string;
|
|
48
|
+
args: Record<string, unknown>;
|
|
49
|
+
} | undefined;
|