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
|
@@ -52,6 +52,29 @@ describe("mcp-registry Dockerfile", () => {
|
|
|
52
52
|
expect(npmBuilt.Dockerfile).not.toContain("corepack enable");
|
|
53
53
|
const py = files(project({ tool: { ...project().tool, binding: "python" } }));
|
|
54
54
|
expect(py.Dockerfile).toContain('ENTRYPOINT ["python","-m","hello.toolfactory.mcp"]');
|
|
55
|
+
expect(py.Dockerfile).toContain("COPY . .\nRUN uv sync --no-dev");
|
|
56
|
+
expect(py.Dockerfile).not.toContain("COPY pyproject.toml ./");
|
|
57
|
+
expect(py.Dockerfile).toContain('for path in README README.md README.rst LICENSE LICENSE.md LICENSE.txt NOTICE NOTICE.md NOTICE.txt; do if [ -f "$path" ]; then cp "$path" /app/runtime/; fi; done');
|
|
58
|
+
expect(py.Dockerfile).toContain("COPY --from=build /app/runtime/ ./");
|
|
59
|
+
expect(py.Dockerfile).not.toContain("COPY --from=build /app .");
|
|
60
|
+
const pythonWeb = files(project({
|
|
61
|
+
tool: { ...project().tool, binding: "python", surfaces: ["mcp-registry", "web"] },
|
|
62
|
+
}));
|
|
63
|
+
expect(pythonWeb.Dockerfile).toContain("FROM node:24-alpine AS web");
|
|
64
|
+
expect(pythonWeb.Dockerfile).toContain("COPY --from=web /app/web/dist ./src/hello/web");
|
|
65
|
+
});
|
|
66
|
+
it("ships an author-extensible Docker ignore region without excluding source metadata", () => {
|
|
67
|
+
const ignore = surface.plan(project()).find((file) => file.path === ".dockerignore");
|
|
68
|
+
if (ignore?.kind !== "region")
|
|
69
|
+
throw new Error("expected a Docker ignore region");
|
|
70
|
+
expect(ignore.template).toContain("# tf:dockerignore\n# /tf:dockerignore");
|
|
71
|
+
expect(ignore.regions[0]?.content).toContain(".git/");
|
|
72
|
+
expect(ignore.regions[0]?.content).toContain(".env.*");
|
|
73
|
+
expect(ignore.regions[0]?.content).toContain("!.env.example");
|
|
74
|
+
expect(ignore.regions[0]?.content).toContain("**/node_modules/");
|
|
75
|
+
expect(ignore.regions[0]?.content).toContain("**/.venv/");
|
|
76
|
+
expect(ignore.regions[0]?.content).not.toContain("README");
|
|
77
|
+
expect(ignore.regions[0]?.content).not.toContain("web/");
|
|
55
78
|
});
|
|
56
79
|
});
|
|
57
80
|
describe("mcp-registry server.json oci package", () => {
|
package/dist/surfaces/mcpb.d.ts
CHANGED
|
@@ -8,4 +8,5 @@ export declare const MANIFEST_PATH = "hosts/mcpb/manifest.json";
|
|
|
8
8
|
export declare const MCPB_PIN = "2.1.2";
|
|
9
9
|
/** What `mcpb init` writes for a new bundle, and the floor the directory submission requires. */
|
|
10
10
|
export declare const MANIFEST_VERSION = "0.2";
|
|
11
|
+
export declare const UV_MANIFEST_VERSION = "0.4";
|
|
11
12
|
export declare const surface: Surface;
|
package/dist/surfaces/mcpb.js
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
* Anthropic's Connectors Directory takes a *local* server through.
|
|
4
4
|
*
|
|
5
5
|
* A bundle is a zip whose root is the server plus its dependencies plus a `manifest.json`. That
|
|
6
|
-
* root is a staging tree, not the repository:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* packaging step copies it in; nothing here duplicates what `npm pack` already decides ships.
|
|
6
|
+
* root is a staging tree, not the repository: TypeScript stages the npm tarball with production
|
|
7
|
+
* dependencies, while Python stages its `pyproject.toml` and source for host-managed uv. This
|
|
8
|
+
* surface emits one tracked file — the manifest — and the packaging step copies it in.
|
|
10
9
|
*
|
|
11
|
-
* `manifest_version`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* TypeScript uses `manifest_version` `0.2`, the floor Anthropic's directory submission requires.
|
|
11
|
+
* Python uses v0.4's official `uv` server type, which keeps the bundle portable without shipping
|
|
12
|
+
* an interpreter or a virtual environment.
|
|
14
13
|
*
|
|
15
14
|
* `privacy_policies` (the URLs of the privacy policies of the services a bundle talks to, which
|
|
16
15
|
* Anthropic's directory submission requires) comes from `tool.json` `mcpb.privacyPolicies`; a
|
|
17
16
|
* homepage is not one, so nothing is invented when the key is absent.
|
|
18
17
|
*/
|
|
18
|
+
import { kernelDir as pythonKernelDir, pythonPackage } from "../bindings/python.js";
|
|
19
19
|
import { KERNEL_DIR, NODE_ENGINES } from "../bindings/typescript.js";
|
|
20
|
-
import { compact, configProperties, envName, isSensitive, json, mcpVerdict } from "./shared.js";
|
|
20
|
+
import { compact, configProperties, envName, has, isSensitive, json, mcpVerdict, } from "./shared.js";
|
|
21
21
|
export const HOST_DIR = "hosts/mcpb";
|
|
22
22
|
export const MANIFEST_PATH = `${HOST_DIR}/manifest.json`;
|
|
23
23
|
/**
|
|
@@ -27,6 +27,7 @@ export const MANIFEST_PATH = `${HOST_DIR}/manifest.json`;
|
|
|
27
27
|
export const MCPB_PIN = "2.1.2";
|
|
28
28
|
/** What `mcpb init` writes for a new bundle, and the floor the directory submission requires. */
|
|
29
29
|
export const MANIFEST_VERSION = "0.2";
|
|
30
|
+
export const UV_MANIFEST_VERSION = "0.4";
|
|
30
31
|
/**
|
|
31
32
|
* The kernel's built `mcp` entry inside the bundle root — the same file the `mcp-registry`
|
|
32
33
|
* surface's Dockerfile runs, because both start from the published package's `dist/`.
|
|
@@ -47,11 +48,9 @@ const USER_CONFIG_TYPES = new Set(["string", "number", "boolean"]);
|
|
|
47
48
|
export const surface = {
|
|
48
49
|
id: "mcpb",
|
|
49
50
|
plan(project) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// TypeScript binding for free and `uv` gives no equivalent of.
|
|
54
|
-
throw new Error('Surface "mcpb" requires the typescript binding: a bundle root is the npm tarball with its production dependencies installed into it.');
|
|
51
|
+
const python = project.tool.binding === "python";
|
|
52
|
+
if (python && !has(project, "pypi")) {
|
|
53
|
+
throw new Error('Surface "mcpb" with the python binding requires the "pypi" surface, the canonical runtime distribution.');
|
|
55
54
|
}
|
|
56
55
|
const { identity } = project;
|
|
57
56
|
const properties = configProperties(project);
|
|
@@ -70,7 +69,7 @@ export const surface = {
|
|
|
70
69
|
// kernel reads its configuration from the same environment names it does everywhere else.
|
|
71
70
|
const env = Object.fromEntries(Object.keys(properties).map((key) => [envName(key), `\${user_config.${key}}`]));
|
|
72
71
|
const manifest = compact({
|
|
73
|
-
manifest_version: MANIFEST_VERSION,
|
|
72
|
+
manifest_version: python ? UV_MANIFEST_VERSION : MANIFEST_VERSION,
|
|
74
73
|
name: identity.name,
|
|
75
74
|
display_name: identity.name,
|
|
76
75
|
version: identity.version,
|
|
@@ -83,15 +82,29 @@ export const surface = {
|
|
|
83
82
|
privacy_policies: project.tool.mcpb?.privacyPolicies,
|
|
84
83
|
keywords: identity.keywords,
|
|
85
84
|
server: {
|
|
86
|
-
type: "node",
|
|
87
|
-
entry_point: entryPoint(),
|
|
85
|
+
type: python ? "uv" : "node",
|
|
86
|
+
entry_point: python ? `${pythonKernelDir(project)}/mcp.py` : entryPoint(),
|
|
88
87
|
// `${__dirname}` is the installed bundle's own directory: an install is an unzip
|
|
89
|
-
// somewhere else, so nothing in the launch may be repository-relative.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// somewhere else, so nothing in the launch may be repository-relative. For `uv`, this
|
|
89
|
+
// delegates dependency resolution and Python provisioning to the host's supported runtime.
|
|
90
|
+
mcp_config: python
|
|
91
|
+
? compact({
|
|
92
|
+
command: "uv",
|
|
93
|
+
args: [
|
|
94
|
+
"run",
|
|
95
|
+
"--directory",
|
|
96
|
+
"${__dirname}",
|
|
97
|
+
"python",
|
|
98
|
+
"-m",
|
|
99
|
+
`${pythonPackage(project)}.toolfactory.mcp`,
|
|
100
|
+
],
|
|
101
|
+
env: Object.keys(env).length ? env : undefined,
|
|
102
|
+
})
|
|
103
|
+
: compact({
|
|
104
|
+
command: "node",
|
|
105
|
+
args: [`\${__dirname}/${entryPoint()}`],
|
|
106
|
+
env: Object.keys(env).length ? env : undefined,
|
|
107
|
+
}),
|
|
95
108
|
},
|
|
96
109
|
// Exactly the operations the kernel's own `tools/list` serves (§4.3): the manifest is what
|
|
97
110
|
// the install UI shows before the server ever runs, so it must not promise more.
|
|
@@ -99,7 +112,7 @@ export const surface = {
|
|
|
99
112
|
.filter((operation) => mcpVerdict(operation).kind !== "excluded")
|
|
100
113
|
.map((operation) => compact({ name: operation.name, description: operation.description })),
|
|
101
114
|
user_config: userConfig.length ? Object.fromEntries(userConfig) : undefined,
|
|
102
|
-
compatibility: { runtimes: { node: NODE_ENGINES } },
|
|
115
|
+
compatibility: { runtimes: python ? { python: ">=3.11" } : { node: NODE_ENGINES } },
|
|
103
116
|
});
|
|
104
117
|
return [{ kind: "file", path: MANIFEST_PATH, content: json(manifest) }];
|
|
105
118
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { NODE_ENGINES } from "../bindings/typescript.js";
|
|
3
|
-
import { MCPB_PIN, surface as mcpb } from "./mcpb.js";
|
|
3
|
+
import { MCPB_PIN, surface as mcpb, UV_MANIFEST_VERSION } from "./mcpb.js";
|
|
4
4
|
const echo = {
|
|
5
5
|
name: "echo",
|
|
6
6
|
description: "Echoes.",
|
|
@@ -90,8 +90,36 @@ describe("mcpb", () => {
|
|
|
90
90
|
// No privacy_policies unless tool.json names them (see the module docs).
|
|
91
91
|
expect(manifestFile.content).not.toContain("privacy_policies");
|
|
92
92
|
});
|
|
93
|
-
it("
|
|
94
|
-
|
|
93
|
+
it("uses the official v0.4 uv bundle contract for Python, with its canonical PyPI surface", () => {
|
|
94
|
+
const pythonProject = {
|
|
95
|
+
...project,
|
|
96
|
+
tool: { ...project.tool, binding: "python", surfaces: ["mcp", "pypi", "mcpb"] },
|
|
97
|
+
};
|
|
98
|
+
const [file] = mcpb.plan(pythonProject);
|
|
99
|
+
if (file?.kind !== "file")
|
|
100
|
+
throw new Error("expected one whole file");
|
|
101
|
+
expect(JSON.parse(file.content)).toMatchObject({
|
|
102
|
+
manifest_version: UV_MANIFEST_VERSION,
|
|
103
|
+
server: {
|
|
104
|
+
type: "uv",
|
|
105
|
+
entry_point: "src/hello_tool/toolfactory/mcp.py",
|
|
106
|
+
mcp_config: {
|
|
107
|
+
command: "uv",
|
|
108
|
+
args: [
|
|
109
|
+
"run",
|
|
110
|
+
"--directory",
|
|
111
|
+
"${__dirname}",
|
|
112
|
+
"python",
|
|
113
|
+
"-m",
|
|
114
|
+
"hello_tool.toolfactory.mcp",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
compatibility: { runtimes: { python: ">=3.11" } },
|
|
119
|
+
});
|
|
120
|
+
expect(() => mcpb.plan({ ...pythonProject, tool: { ...pythonProject.tool, surfaces: ["mcp", "mcpb"] } })).toThrow(/requires the "pypi" surface/);
|
|
121
|
+
});
|
|
122
|
+
it("declares the kernel's own Node floor and validates with the pinned official CLI", () => {
|
|
95
123
|
expect(mcpb.validate?.(project)).toEqual([
|
|
96
124
|
{
|
|
97
125
|
label: "mcpb validate",
|
package/dist/surfaces/npm.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { githubSlug } from "../hosts/github.js";
|
|
6
6
|
import { registryName } from "./mcp-registry.js";
|
|
7
|
-
import { compact, has, npmName } from "./shared.js";
|
|
7
|
+
import { compact, has, npmName, pypiName } from "./shared.js";
|
|
8
8
|
import { WEB_DIR } from "./web.js";
|
|
9
9
|
/**
|
|
10
10
|
* npm's object form. A bare URL string is only valid as one of npm's shorthands, so `publint`
|
|
@@ -17,14 +17,30 @@ function repositoryField(repository) {
|
|
|
17
17
|
const slug = githubSlug(repository);
|
|
18
18
|
return { type: "git", url: slug ? `git+https://github.com/${slug}.git` : repository };
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A deliberately thin cross-registry bridge: npm supplies discovery and a familiar `npx` entry,
|
|
22
|
+
* while uvx resolves the canonical Python distribution. `uvx` owns Python acquisition, caching,
|
|
23
|
+
* and the package install; the npm package never downloads an interpreter itself.
|
|
24
|
+
*/
|
|
25
|
+
function pythonLauncher(project) {
|
|
26
|
+
const { identity } = project;
|
|
27
|
+
return `#!/usr/bin/env node
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
|
+
|
|
30
|
+
const child = spawnSync("uvx", ["--from", ${JSON.stringify(`${pypiName(project)}==${identity.version}`)}, ${JSON.stringify(identity.name)}, ...process.argv.slice(2)], {
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
if (child.error) throw child.error;
|
|
34
|
+
process.exit(child.status ?? 1);
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
20
37
|
export const surface = {
|
|
21
38
|
id: "npm",
|
|
22
39
|
plan(project) {
|
|
23
|
-
if (project.tool.binding !== "typescript") {
|
|
24
|
-
throw new Error('Surface "npm" requires the typescript binding.');
|
|
25
|
-
}
|
|
26
40
|
const { identity } = project;
|
|
27
41
|
const authored = project.tool.identity === "package.json";
|
|
42
|
+
const python = project.tool.binding === "python";
|
|
43
|
+
const launcher = `bin/${identity.name}.mjs`;
|
|
28
44
|
const patch = compact({
|
|
29
45
|
name: authored ? undefined : npmName(project),
|
|
30
46
|
version: authored ? undefined : identity.version,
|
|
@@ -35,21 +51,36 @@ export const surface = {
|
|
|
35
51
|
keywords: authored ? undefined : identity.keywords,
|
|
36
52
|
type: "module",
|
|
37
53
|
bin: has(project, "cli") || has(project, "mcp")
|
|
38
|
-
? {
|
|
54
|
+
? {
|
|
55
|
+
[identity.name]: python ? `./${launcher}` : "./dist/toolfactory/cli.js",
|
|
56
|
+
}
|
|
39
57
|
: undefined,
|
|
40
58
|
// `web/dist` only with the `web` surface: the kernel serves the built page from inside
|
|
41
59
|
// the installed package, so `npx <tool> mcp --http --open` works without a checkout.
|
|
42
|
-
files:
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
files: python
|
|
61
|
+
? ["bin", "README.md", "LICENSE"]
|
|
62
|
+
: [
|
|
63
|
+
"dist",
|
|
64
|
+
"src",
|
|
65
|
+
"schemas",
|
|
66
|
+
...(has(project, "web") ? [`${WEB_DIR}/dist`] : []),
|
|
67
|
+
"README.md",
|
|
68
|
+
"LICENSE",
|
|
69
|
+
],
|
|
50
70
|
mcpName: has(project, "mcp-registry") ? registryName(project) : undefined,
|
|
51
71
|
});
|
|
52
|
-
return [
|
|
72
|
+
return [
|
|
73
|
+
...(python && (has(project, "cli") || has(project, "mcp"))
|
|
74
|
+
? [{ kind: "file", path: launcher, content: pythonLauncher(project) }]
|
|
75
|
+
: []),
|
|
76
|
+
{
|
|
77
|
+
kind: "merge",
|
|
78
|
+
path: "package.json",
|
|
79
|
+
format: "json",
|
|
80
|
+
patch,
|
|
81
|
+
owned: ["bin", "files"],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
53
84
|
},
|
|
54
85
|
validate(project) {
|
|
55
86
|
return [
|
|
@@ -216,7 +216,7 @@ function packageJson(project, operations, kase) {
|
|
|
216
216
|
: undefined;
|
|
217
217
|
const extra = project.tool.openclaw;
|
|
218
218
|
const e2e = kase ? { "test:e2e": OPENCLAW_ADDITIONS.e2eScript } : undefined;
|
|
219
|
-
return
|
|
219
|
+
return compact({
|
|
220
220
|
name: projectName.openclawPackage(project.identity.name),
|
|
221
221
|
version: project.identity.version ?? "0.0.0",
|
|
222
222
|
description: pluginDescription(project),
|
|
@@ -239,7 +239,7 @@ function packageJson(project, operations, kase) {
|
|
|
239
239
|
pluginInspector: expectedRegistrations(project, operations).length
|
|
240
240
|
? pluginInspector(project, operations)
|
|
241
241
|
: undefined,
|
|
242
|
-
})
|
|
242
|
+
});
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
245
|
* `openclaw plugins build` regenerates this file; emitting it keeps `--check` green from a clean
|
|
@@ -256,7 +256,7 @@ function pluginManifest(project, operations) {
|
|
|
256
256
|
registration.contract,
|
|
257
257
|
registration.ids,
|
|
258
258
|
]);
|
|
259
|
-
return
|
|
259
|
+
return compact({
|
|
260
260
|
id: project.identity.name,
|
|
261
261
|
name: displayName(project.identity.name),
|
|
262
262
|
description: pluginDescription(project),
|
|
@@ -268,7 +268,7 @@ function pluginManifest(project, operations) {
|
|
|
268
268
|
tools: operations.map((operation) => operation.name),
|
|
269
269
|
...Object.fromEntries(declared),
|
|
270
270
|
},
|
|
271
|
-
})
|
|
271
|
+
});
|
|
272
272
|
}
|
|
273
273
|
function executeBody(project, operation) {
|
|
274
274
|
if (project.tool.binding === "typescript") {
|
|
@@ -819,9 +819,10 @@ export const surface = {
|
|
|
819
819
|
const kase = e2eCase(project, operations);
|
|
820
820
|
return [
|
|
821
821
|
{
|
|
822
|
-
kind: "
|
|
822
|
+
kind: "merge",
|
|
823
823
|
path: `${HOST_DIR}/package.json`,
|
|
824
|
-
|
|
824
|
+
format: "json",
|
|
825
|
+
patch: packageJson(project, operations, kase),
|
|
825
826
|
},
|
|
826
827
|
{
|
|
827
828
|
kind: "file",
|
|
@@ -841,9 +842,10 @@ export const surface = {
|
|
|
841
842
|
content: OPENCLAW_SCAFFOLD.vitestConfig,
|
|
842
843
|
},
|
|
843
844
|
{
|
|
844
|
-
kind: "
|
|
845
|
+
kind: "merge",
|
|
845
846
|
path: `${HOST_DIR}/openclaw.plugin.json`,
|
|
846
|
-
|
|
847
|
+
format: "json",
|
|
848
|
+
patch: pluginManifest(project, operations),
|
|
847
849
|
},
|
|
848
850
|
{
|
|
849
851
|
kind: "region",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { describe, expect, it } from "vitest";
|
|
3
6
|
import { scaffoldDrift } from "../hosts/openclaw.js";
|
|
7
|
+
import { apply } from "../project/apply.js";
|
|
4
8
|
import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
|
|
5
9
|
import { HOST_DIR, OPENCLAW_ADDITIONS, OPENCLAW_SCAFFOLD, surface } from "./openclaw-native.js";
|
|
6
10
|
const echo = {
|
|
@@ -74,6 +78,32 @@ function emitted(target) {
|
|
|
74
78
|
]));
|
|
75
79
|
}
|
|
76
80
|
describe("openclaw-native", () => {
|
|
81
|
+
it("keeps hand-authored package and manifest extensions through regeneration", () => {
|
|
82
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-openclaw-"));
|
|
83
|
+
const target = { ...project(), root };
|
|
84
|
+
apply(root, surface.plan(target), "0.1.0");
|
|
85
|
+
const packagePath = join(root, HOST_DIR, "package.json");
|
|
86
|
+
const manifestPath = join(root, HOST_DIR, "openclaw.plugin.json");
|
|
87
|
+
writeFileSync(packagePath, JSON.stringify({
|
|
88
|
+
...JSON.parse(readFileSync(packagePath, "utf8")),
|
|
89
|
+
scripts: { custom: "node custom.mjs" },
|
|
90
|
+
exports: { "./native": "./src/native.ts" },
|
|
91
|
+
}));
|
|
92
|
+
writeFileSync(manifestPath, JSON.stringify({
|
|
93
|
+
...JSON.parse(readFileSync(manifestPath, "utf8")),
|
|
94
|
+
contracts: { customProviders: ["native"] },
|
|
95
|
+
nativeField: true,
|
|
96
|
+
}));
|
|
97
|
+
apply(root, surface.plan(target), "0.1.0");
|
|
98
|
+
expect(JSON.parse(readFileSync(packagePath, "utf8"))).toMatchObject({
|
|
99
|
+
scripts: { custom: "node custom.mjs" },
|
|
100
|
+
exports: { "./native": "./src/native.ts" },
|
|
101
|
+
});
|
|
102
|
+
expect(JSON.parse(readFileSync(manifestPath, "utf8"))).toMatchObject({
|
|
103
|
+
contracts: { customProviders: ["native"] },
|
|
104
|
+
nativeField: true,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
77
107
|
it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
|
|
78
108
|
const files = emitted(project());
|
|
79
109
|
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
package/dist/surfaces/pypi.js
CHANGED
|
@@ -26,19 +26,43 @@ export const surface = {
|
|
|
26
26
|
? { [project.identity.name]: cliEntryPoint(project) }
|
|
27
27
|
: undefined,
|
|
28
28
|
}),
|
|
29
|
-
//
|
|
30
|
-
//
|
|
29
|
+
// Hatch ignores a missing `only-include` path, unlike `force-include`, which makes
|
|
30
|
+
// `uv sync` work in a fresh checkout before the optional page is built. At package time,
|
|
31
|
+
// the gate builds `web/dist` first and Hatch maps it into the installed package.
|
|
31
32
|
...(has(project, "web")
|
|
32
33
|
? {
|
|
33
34
|
tool: {
|
|
34
35
|
hatch: {
|
|
35
36
|
build: {
|
|
37
|
+
// Hatch's normal file selection honors .gitignore; `web/dist` is ignored in
|
|
38
|
+
// every generated project, so artifacts is the standard explicit opt-in for
|
|
39
|
+
// the release build's compiled page.
|
|
40
|
+
artifacts: [`${WEB_DIR}/dist/**`],
|
|
36
41
|
targets: {
|
|
37
42
|
wheel: {
|
|
38
|
-
"
|
|
43
|
+
"only-include": [`src/${pythonPackage(project)}`, `${WEB_DIR}/dist`],
|
|
44
|
+
sources: {
|
|
45
|
+
src: "",
|
|
39
46
|
[`${WEB_DIR}/dist`]: `${pythonPackage(project)}/${WEB_DIR}`,
|
|
40
47
|
},
|
|
41
48
|
},
|
|
49
|
+
// This is the source MCPB stages, so include every declared project metadata
|
|
50
|
+
// file alongside the package and built page. `only-include` ignores absent
|
|
51
|
+
// legal files without making fresh checkouts fail.
|
|
52
|
+
sdist: {
|
|
53
|
+
"only-include": [
|
|
54
|
+
"pyproject.toml",
|
|
55
|
+
"README.md",
|
|
56
|
+
"LICENSE",
|
|
57
|
+
"LICENSE.md",
|
|
58
|
+
"LICENSE.txt",
|
|
59
|
+
"NOTICE",
|
|
60
|
+
"NOTICE.md",
|
|
61
|
+
"NOTICE.txt",
|
|
62
|
+
`src/${pythonPackage(project)}`,
|
|
63
|
+
`${WEB_DIR}/dist`,
|
|
64
|
+
],
|
|
65
|
+
},
|
|
42
66
|
},
|
|
43
67
|
},
|
|
44
68
|
},
|
|
@@ -1,3 +1,19 @@
|
|
|
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
|
+
*/
|
|
1
17
|
import type { Project, Surface } from "../model.js";
|
|
2
18
|
export declare const README_PATH = "README.md";
|
|
3
19
|
export declare const INSTALL_BEGIN = "<!-- tf:install -->";
|
package/dist/surfaces/readme.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* `cursor.com/en/install-mcp`), both computed from the exact same `{command, args, env}` launch
|
|
15
15
|
* the plain-text line renders — a badge can never point somewhere the text line does not.
|
|
16
16
|
*/
|
|
17
|
+
import { nativeInstallLines } from "../distribution/native.js";
|
|
18
|
+
import { systemInstallLines } from "../distribution/system.js";
|
|
17
19
|
import { githubSlug } from "../hosts/github.js";
|
|
18
20
|
import { projectName } from "../identity/name.js";
|
|
19
21
|
import { HOST_DIR as BROWSER_HOST_DIR, zipName } from "./browser-extension.js";
|
|
@@ -21,6 +23,7 @@ import { HOST_DIR as DSH_HOST_DIR, dshTarball } from "./dsh.js";
|
|
|
21
23
|
import { pluginDir as hermesPluginDir } from "./hermes-native.js";
|
|
22
24
|
import { HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
|
|
23
25
|
import { envName, has, npmName, pypiName, requiredConfig } from "./shared.js";
|
|
26
|
+
import { vsixName } from "./vscode-extension.js";
|
|
24
27
|
export const README_PATH = "README.md";
|
|
25
28
|
export const INSTALL_BEGIN = "<!-- tf:install -->";
|
|
26
29
|
export const INSTALL_END = "<!-- /tf:install -->";
|
|
@@ -70,6 +73,10 @@ function installLines(project) {
|
|
|
70
73
|
const { identity } = project;
|
|
71
74
|
const slug = githubSlug(identity.repository);
|
|
72
75
|
const lines = [];
|
|
76
|
+
lines.push(...nativeInstallLines(project).map((line) => `- **Native package** — ${line}`));
|
|
77
|
+
lines.push(...systemInstallLines(project).map((line) => `- **System package** — ${line}`));
|
|
78
|
+
if (has(project, "vscode-extension"))
|
|
79
|
+
lines.push(`- **VS Code extension** — download \`${vsixName(project)}\` from the release, then \`code --install-extension ${vsixName(project)}\``);
|
|
73
80
|
if (has(project, "skill") && slug) {
|
|
74
81
|
lines.push(`- **Agent Skill** — \`npx skills add ${slug}\``);
|
|
75
82
|
}
|
|
@@ -114,16 +121,15 @@ function installLines(project) {
|
|
|
114
121
|
}
|
|
115
122
|
if (has(project, "browser-extension")) {
|
|
116
123
|
// Three channels, in the order a reader needs them: build-and-load from a checkout, the
|
|
117
|
-
// release assets (
|
|
118
|
-
//
|
|
124
|
+
// release assets (the signed Firefox XPI depends on configured signing credentials), and
|
|
125
|
+
// store listings. Pairing is the last step of each.
|
|
119
126
|
const zips = ["chrome", "firefox", "edge"].map((browser) => `\`${zipName(project, browser)}\``);
|
|
120
127
|
lines.push([
|
|
121
128
|
`- **Browser extension** — from a checkout: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`,
|
|
122
129
|
` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\``,
|
|
123
130
|
` (Firefox: \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\`). Each GitHub Release attaches the`,
|
|
124
|
-
` store uploads ${zips.join(", ")},
|
|
125
|
-
"
|
|
126
|
-
" extensions; the Chrome Web Store, Firefox Add-ons and Edge Add-ons listings appear once the release's",
|
|
131
|
+
` store uploads ${zips.join(", ")}. When Firefox signing credentials are configured, it also attaches a`,
|
|
132
|
+
" Mozilla-signed `.xpi`; the Chrome Web Store, Firefox Add-ons and Edge Add-ons listings appear once the release's",
|
|
127
133
|
` submit step has each store's credentials. Then pair it: \`${mcpCommand(project)} --http --pair\``,
|
|
128
134
|
" prints the `<url>#<token>` the extension's options page accepts.",
|
|
129
135
|
].join("\n"));
|
|
@@ -133,8 +139,12 @@ function installLines(project) {
|
|
|
133
139
|
// tool already is. The same listener answers `/mcp`, so the page needs nothing else running.
|
|
134
140
|
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
141
|
}
|
|
136
|
-
if (has(project, "npm"))
|
|
137
|
-
|
|
142
|
+
if (has(project, "npm")) {
|
|
143
|
+
const pythonBridge = project.tool.binding === "python"
|
|
144
|
+
? `; requires \`uv\`, and \`${identity.name}\` delegates to \`uvx --from ${pypiName(project)}==${identity.version} ${identity.name}\``
|
|
145
|
+
: "";
|
|
146
|
+
lines.push(`- **npm package** — \`npm install ${npmName(project)}\`${pythonBridge}`);
|
|
147
|
+
}
|
|
138
148
|
if (has(project, "pypi"))
|
|
139
149
|
lines.push(`- **PyPI package** — \`uv add ${pypiName(project)}\``);
|
|
140
150
|
return lines;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nativePackageSurfaces } from "../distribution/native.js";
|
|
2
|
+
import { systemPackageSurfaces } from "../distribution/system.js";
|
|
1
3
|
import { surface as agentPlugins } from "./agent-plugins.js";
|
|
2
4
|
import { surface as agents } from "./agents.js";
|
|
3
5
|
import { surface as browserExtension } from "./browser-extension.js";
|
|
@@ -16,9 +18,12 @@ import { surface as openclawNative } from "./openclaw-native.js";
|
|
|
16
18
|
import { surface as pypi } from "./pypi.js";
|
|
17
19
|
import { surface as readme } from "./readme.js";
|
|
18
20
|
import { surface as skill } from "./skill.js";
|
|
21
|
+
import { surface as vscodeExtension } from "./vscode-extension.js";
|
|
19
22
|
import { surface as web } from "./web.js";
|
|
20
23
|
import { surface as workflows } from "./workflows.js";
|
|
21
24
|
const registry = {
|
|
25
|
+
...Object.fromEntries(nativePackageSurfaces.map((surface) => [surface.id, surface])),
|
|
26
|
+
...Object.fromEntries(systemPackageSurfaces.map((surface) => [surface.id, surface])),
|
|
22
27
|
"agent-plugins": agentPlugins,
|
|
23
28
|
agents,
|
|
24
29
|
"browser-extension": { ...browserExtension, requires: ["mcp"] },
|
|
@@ -36,9 +41,9 @@ const registry = {
|
|
|
36
41
|
cli,
|
|
37
42
|
mcp,
|
|
38
43
|
"mcp-registry": mcpRegistry,
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
mcpb: { ...mcpb, requires: ["mcp"
|
|
44
|
+
// TypeScript bundles stage their npm tarball; Python `uv` bundles stage the PyPI project's
|
|
45
|
+
// source. The projector enforces that binding-specific distribution dependency.
|
|
46
|
+
mcpb: { ...mcpb, requires: ["mcp"] },
|
|
42
47
|
npm,
|
|
43
48
|
skill,
|
|
44
49
|
"hermes-native": hermesNative,
|
|
@@ -46,6 +51,7 @@ const registry = {
|
|
|
46
51
|
pypi,
|
|
47
52
|
readme,
|
|
48
53
|
web: { ...web, requires: ["mcp"] },
|
|
54
|
+
"vscode-extension": { ...vscodeExtension, requires: ["mcp", "cli"] },
|
|
49
55
|
workflows,
|
|
50
56
|
};
|
|
51
57
|
export function registerSurface(surface) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Project, type Surface } from "../model.js";
|
|
3
|
+
export declare const HOST_DIR = "hosts/vscode";
|
|
4
|
+
export declare const vscodeConfigSchema: z.ZodObject<{
|
|
5
|
+
publisher: z.ZodOptional<z.ZodString>;
|
|
6
|
+
marketplace: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
openvsx: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export declare const VSCE_VERSION = "3.9.2";
|
|
10
|
+
export declare const OVSX_VERSION = "1.1.1";
|
|
11
|
+
export declare function extensionName(project: Project): string;
|
|
12
|
+
export declare function publisher(project: Project): string;
|
|
13
|
+
export declare function vsixName(project: Project): string;
|
|
14
|
+
export declare const surface: Surface;
|