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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { pythonPackage } from "../bindings/python.js";
|
|
7
7
|
import { KERNEL_DIR } from "../bindings/typescript.js";
|
|
8
8
|
import { githubOwner, projectName } from "../identity/name.js";
|
|
9
|
-
import type { Project, Surface } from "../model.js";
|
|
9
|
+
import type { PlannedFile, Project, Surface } from "../model.js";
|
|
10
10
|
import {
|
|
11
11
|
compact,
|
|
12
12
|
configProperties,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
pypiName,
|
|
19
19
|
requiredConfig,
|
|
20
20
|
} from "./shared.js";
|
|
21
|
+
import { WEB_DIR } from "./web.js";
|
|
21
22
|
|
|
22
23
|
export const SERVER_SCHEMA_ID =
|
|
23
24
|
"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
|
|
@@ -87,6 +88,44 @@ const DOCKER_PACKAGE_MANAGERS: Record<
|
|
|
87
88
|
},
|
|
88
89
|
};
|
|
89
90
|
|
|
91
|
+
const DOCKERIGNORE_BEGIN = "# tf:dockerignore";
|
|
92
|
+
const DOCKERIGNORE_END = "# /tf:dockerignore";
|
|
93
|
+
|
|
94
|
+
/** Build context hygiene is generated, while the rest of `.dockerignore` stays author-owned. */
|
|
95
|
+
function dockerignore(): PlannedFile {
|
|
96
|
+
return {
|
|
97
|
+
kind: "region",
|
|
98
|
+
path: ".dockerignore",
|
|
99
|
+
regions: [
|
|
100
|
+
{
|
|
101
|
+
begin: DOCKERIGNORE_BEGIN,
|
|
102
|
+
end: DOCKERIGNORE_END,
|
|
103
|
+
content: `
|
|
104
|
+
.git/
|
|
105
|
+
.env
|
|
106
|
+
.env.*
|
|
107
|
+
!.env.example
|
|
108
|
+
node_modules/
|
|
109
|
+
**/node_modules/
|
|
110
|
+
.venv/
|
|
111
|
+
**/.venv/
|
|
112
|
+
dist/
|
|
113
|
+
coverage/
|
|
114
|
+
test-results/
|
|
115
|
+
playwright-report/
|
|
116
|
+
.pytest_cache/
|
|
117
|
+
.mypy_cache/
|
|
118
|
+
.ruff_cache/
|
|
119
|
+
__pycache__/
|
|
120
|
+
**/__pycache__/
|
|
121
|
+
.vscode-test/
|
|
122
|
+
`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
template: `${DOCKERIGNORE_BEGIN}\n${DOCKERIGNORE_END}\n\n# Add local build inputs to exclude below.\n`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
/**
|
|
91
130
|
* The root `Dockerfile`, stdio only (§4.1: no `--host` flag exists to bind past loopback), one
|
|
92
131
|
* multi-stage build per binding. `label` is `server.json`'s own `name`, byte for byte — the MCP
|
|
@@ -94,20 +133,38 @@ const DOCKER_PACKAGE_MANAGERS: Record<
|
|
|
94
133
|
*/
|
|
95
134
|
export function dockerfileTemplate(project: Project, label: string): string {
|
|
96
135
|
if (project.tool.binding === "python") {
|
|
136
|
+
const web = has(project, "web");
|
|
97
137
|
return `# syntax=docker/dockerfile:1
|
|
98
|
-
|
|
138
|
+
${
|
|
139
|
+
web
|
|
140
|
+
? `FROM ${NODE_IMAGE} AS web
|
|
99
141
|
WORKDIR /app
|
|
100
|
-
COPY
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
COPY web/package.json web/package-lock.json ./web/
|
|
143
|
+
RUN npm -C web ci
|
|
144
|
+
COPY web ./web
|
|
145
|
+
RUN npm -C web run build
|
|
146
|
+
|
|
147
|
+
`
|
|
148
|
+
: ""
|
|
149
|
+
}FROM ${UV_IMAGE} AS build
|
|
150
|
+
WORKDIR /app
|
|
151
|
+
# pyproject metadata can name README, license, and other package assets; copy the project without
|
|
152
|
+
# assuming optional metadata files exist.
|
|
153
|
+
COPY . .
|
|
154
|
+
${web ? "COPY --from=web /app/web/dist ./web/dist\n" : ""}RUN uv sync --no-dev
|
|
155
|
+
RUN mkdir /app/runtime && cp pyproject.toml /app/runtime/ && cp -R src /app/runtime/src && 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
|
|
103
156
|
|
|
104
157
|
FROM ${PYTHON_IMAGE}
|
|
105
158
|
LABEL io.modelcontextprotocol.server.name="${label}"
|
|
106
159
|
WORKDIR /app
|
|
107
160
|
COPY --from=build /app/.venv ./.venv
|
|
108
|
-
COPY --from=build /app/
|
|
109
|
-
|
|
110
|
-
|
|
161
|
+
COPY --from=build /app/runtime/ ./
|
|
162
|
+
${
|
|
163
|
+
web
|
|
164
|
+
? `COPY --from=web /app/web/dist ./src/${pythonPackage(project)}/${WEB_DIR}
|
|
165
|
+
`
|
|
166
|
+
: ""
|
|
167
|
+
}ENV PATH="/app/.venv/bin:$PATH"
|
|
111
168
|
ENTRYPOINT ${JSON.stringify(dockerEntrypoint(project))}
|
|
112
169
|
`;
|
|
113
170
|
}
|
|
@@ -185,6 +242,7 @@ export const surface: Surface = {
|
|
|
185
242
|
return [
|
|
186
243
|
{ kind: "file", path: "server.json", content: json(server) },
|
|
187
244
|
{ kind: "file", path: "Dockerfile", content: dockerfileTemplate(project, name) },
|
|
245
|
+
dockerignore(),
|
|
188
246
|
];
|
|
189
247
|
},
|
|
190
248
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { NODE_ENGINES } from "../bindings/typescript.js";
|
|
3
3
|
import type { Operation, Project } from "../model.js";
|
|
4
|
-
import { MCPB_PIN, surface as mcpb } from "./mcpb.js";
|
|
4
|
+
import { MCPB_PIN, surface as mcpb, UV_MANIFEST_VERSION } from "./mcpb.js";
|
|
5
5
|
|
|
6
6
|
const echo: Operation = {
|
|
7
7
|
name: "echo",
|
|
@@ -95,11 +95,38 @@ describe("mcpb", () => {
|
|
|
95
95
|
expect(manifestFile.content).not.toContain("privacy_policies");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it("
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
it("uses the official v0.4 uv bundle contract for Python, with its canonical PyPI surface", () => {
|
|
99
|
+
const pythonProject: Project = {
|
|
100
|
+
...project,
|
|
101
|
+
tool: { ...project.tool, binding: "python", surfaces: ["mcp", "pypi", "mcpb"] },
|
|
102
|
+
};
|
|
103
|
+
const [file] = mcpb.plan(pythonProject);
|
|
104
|
+
if (file?.kind !== "file") throw new Error("expected one whole file");
|
|
105
|
+
expect(JSON.parse(file.content)).toMatchObject({
|
|
106
|
+
manifest_version: UV_MANIFEST_VERSION,
|
|
107
|
+
server: {
|
|
108
|
+
type: "uv",
|
|
109
|
+
entry_point: "src/hello_tool/toolfactory/mcp.py",
|
|
110
|
+
mcp_config: {
|
|
111
|
+
command: "uv",
|
|
112
|
+
args: [
|
|
113
|
+
"run",
|
|
114
|
+
"--directory",
|
|
115
|
+
"${__dirname}",
|
|
116
|
+
"python",
|
|
117
|
+
"-m",
|
|
118
|
+
"hello_tool.toolfactory.mcp",
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
compatibility: { runtimes: { python: ">=3.11" } },
|
|
123
|
+
});
|
|
124
|
+
expect(() =>
|
|
125
|
+
mcpb.plan({ ...pythonProject, tool: { ...pythonProject.tool, surfaces: ["mcp", "mcpb"] } }),
|
|
126
|
+
).toThrow(/requires the "pypi" surface/);
|
|
127
|
+
});
|
|
102
128
|
|
|
129
|
+
it("declares the kernel's own Node floor and validates with the pinned official CLI", () => {
|
|
103
130
|
expect(mcpb.validate?.(project)).toEqual([
|
|
104
131
|
{
|
|
105
132
|
label: "mcpb validate",
|
package/src/surfaces/mcpb.ts
CHANGED
|
@@ -3,22 +3,30 @@
|
|
|
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
20
|
import type { Project, Surface } from "../model.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
compact,
|
|
23
|
+
configProperties,
|
|
24
|
+
envName,
|
|
25
|
+
has,
|
|
26
|
+
isSensitive,
|
|
27
|
+
json,
|
|
28
|
+
mcpVerdict,
|
|
29
|
+
} from "./shared.js";
|
|
22
30
|
|
|
23
31
|
export const HOST_DIR = "hosts/mcpb";
|
|
24
32
|
export const MANIFEST_PATH = `${HOST_DIR}/manifest.json`;
|
|
@@ -31,6 +39,7 @@ export const MCPB_PIN = "2.1.2";
|
|
|
31
39
|
|
|
32
40
|
/** What `mcpb init` writes for a new bundle, and the floor the directory submission requires. */
|
|
33
41
|
export const MANIFEST_VERSION = "0.2";
|
|
42
|
+
export const UV_MANIFEST_VERSION = "0.4";
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* The kernel's built `mcp` entry inside the bundle root — the same file the `mcp-registry`
|
|
@@ -55,12 +64,10 @@ const USER_CONFIG_TYPES = new Set(["string", "number", "boolean"]);
|
|
|
55
64
|
export const surface: Surface = {
|
|
56
65
|
id: "mcpb",
|
|
57
66
|
plan(project: Project) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// (Claude Desktop ships Node, not Python), which `npm pack` plus `npm install` gives the
|
|
61
|
-
// TypeScript binding for free and `uv` gives no equivalent of.
|
|
67
|
+
const python = project.tool.binding === "python";
|
|
68
|
+
if (python && !has(project, "pypi")) {
|
|
62
69
|
throw new Error(
|
|
63
|
-
'Surface "mcpb"
|
|
70
|
+
'Surface "mcpb" with the python binding requires the "pypi" surface, the canonical runtime distribution.',
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
const { identity } = project;
|
|
@@ -82,7 +89,7 @@ export const surface: Surface = {
|
|
|
82
89
|
Object.keys(properties).map((key) => [envName(key), `\${user_config.${key}}`]),
|
|
83
90
|
);
|
|
84
91
|
const manifest = compact({
|
|
85
|
-
manifest_version: MANIFEST_VERSION,
|
|
92
|
+
manifest_version: python ? UV_MANIFEST_VERSION : MANIFEST_VERSION,
|
|
86
93
|
name: identity.name,
|
|
87
94
|
display_name: identity.name,
|
|
88
95
|
version: identity.version,
|
|
@@ -95,15 +102,29 @@ export const surface: Surface = {
|
|
|
95
102
|
privacy_policies: project.tool.mcpb?.privacyPolicies,
|
|
96
103
|
keywords: identity.keywords,
|
|
97
104
|
server: {
|
|
98
|
-
type: "node",
|
|
99
|
-
entry_point: entryPoint(),
|
|
105
|
+
type: python ? "uv" : "node",
|
|
106
|
+
entry_point: python ? `${pythonKernelDir(project)}/mcp.py` : entryPoint(),
|
|
100
107
|
// `${__dirname}` is the installed bundle's own directory: an install is an unzip
|
|
101
|
-
// somewhere else, so nothing in the launch may be repository-relative.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
// somewhere else, so nothing in the launch may be repository-relative. For `uv`, this
|
|
109
|
+
// delegates dependency resolution and Python provisioning to the host's supported runtime.
|
|
110
|
+
mcp_config: python
|
|
111
|
+
? compact({
|
|
112
|
+
command: "uv",
|
|
113
|
+
args: [
|
|
114
|
+
"run",
|
|
115
|
+
"--directory",
|
|
116
|
+
"${__dirname}",
|
|
117
|
+
"python",
|
|
118
|
+
"-m",
|
|
119
|
+
`${pythonPackage(project)}.toolfactory.mcp`,
|
|
120
|
+
],
|
|
121
|
+
env: Object.keys(env).length ? env : undefined,
|
|
122
|
+
})
|
|
123
|
+
: compact({
|
|
124
|
+
command: "node",
|
|
125
|
+
args: [`\${__dirname}/${entryPoint()}`],
|
|
126
|
+
env: Object.keys(env).length ? env : undefined,
|
|
127
|
+
}),
|
|
107
128
|
},
|
|
108
129
|
// Exactly the operations the kernel's own `tools/list` serves (§4.3): the manifest is what
|
|
109
130
|
// the install UI shows before the server ever runs, so it must not promise more.
|
|
@@ -111,7 +132,7 @@ export const surface: Surface = {
|
|
|
111
132
|
.filter((operation) => mcpVerdict(operation).kind !== "excluded")
|
|
112
133
|
.map((operation) => compact({ name: operation.name, description: operation.description })),
|
|
113
134
|
user_config: userConfig.length ? Object.fromEntries(userConfig) : undefined,
|
|
114
|
-
compatibility: { runtimes: { node: NODE_ENGINES } },
|
|
135
|
+
compatibility: { runtimes: python ? { python: ">=3.11" } : { node: NODE_ENGINES } },
|
|
115
136
|
});
|
|
116
137
|
return [{ kind: "file", path: MANIFEST_PATH, content: json(manifest) }];
|
|
117
138
|
},
|
package/src/surfaces/npm.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { githubSlug } from "../hosts/github.js";
|
|
7
|
-
import type { Surface } from "../model.js";
|
|
7
|
+
import type { Project, Surface } from "../model.js";
|
|
8
8
|
import { registryName } from "./mcp-registry.js";
|
|
9
|
-
import { compact, has, npmName } from "./shared.js";
|
|
9
|
+
import { compact, has, npmName, pypiName } from "./shared.js";
|
|
10
10
|
import { WEB_DIR } from "./web.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -22,14 +22,31 @@ function repositoryField(
|
|
|
22
22
|
return { type: "git", url: slug ? `git+https://github.com/${slug}.git` : repository };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* A deliberately thin cross-registry bridge: npm supplies discovery and a familiar `npx` entry,
|
|
27
|
+
* while uvx resolves the canonical Python distribution. `uvx` owns Python acquisition, caching,
|
|
28
|
+
* and the package install; the npm package never downloads an interpreter itself.
|
|
29
|
+
*/
|
|
30
|
+
function pythonLauncher(project: Project): string {
|
|
31
|
+
const { identity } = project;
|
|
32
|
+
return `#!/usr/bin/env node
|
|
33
|
+
import { spawnSync } from "node:child_process";
|
|
34
|
+
|
|
35
|
+
const child = spawnSync("uvx", ["--from", ${JSON.stringify(`${pypiName(project)}==${identity.version}`)}, ${JSON.stringify(identity.name)}, ...process.argv.slice(2)], {
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
});
|
|
38
|
+
if (child.error) throw child.error;
|
|
39
|
+
process.exit(child.status ?? 1);
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export const surface: Surface = {
|
|
26
44
|
id: "npm",
|
|
27
45
|
plan(project) {
|
|
28
|
-
if (project.tool.binding !== "typescript") {
|
|
29
|
-
throw new Error('Surface "npm" requires the typescript binding.');
|
|
30
|
-
}
|
|
31
46
|
const { identity } = project;
|
|
32
47
|
const authored = project.tool.identity === "package.json";
|
|
48
|
+
const python = project.tool.binding === "python";
|
|
49
|
+
const launcher = `bin/${identity.name}.mjs`;
|
|
33
50
|
const patch = compact({
|
|
34
51
|
name: authored ? undefined : npmName(project),
|
|
35
52
|
version: authored ? undefined : identity.version,
|
|
@@ -41,21 +58,36 @@ export const surface: Surface = {
|
|
|
41
58
|
type: "module",
|
|
42
59
|
bin:
|
|
43
60
|
has(project, "cli") || has(project, "mcp")
|
|
44
|
-
? {
|
|
61
|
+
? {
|
|
62
|
+
[identity.name]: python ? `./${launcher}` : "./dist/toolfactory/cli.js",
|
|
63
|
+
}
|
|
45
64
|
: undefined,
|
|
46
65
|
// `web/dist` only with the `web` surface: the kernel serves the built page from inside
|
|
47
66
|
// the installed package, so `npx <tool> mcp --http --open` works without a checkout.
|
|
48
|
-
files:
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
files: python
|
|
68
|
+
? ["bin", "README.md", "LICENSE"]
|
|
69
|
+
: [
|
|
70
|
+
"dist",
|
|
71
|
+
"src",
|
|
72
|
+
"schemas",
|
|
73
|
+
...(has(project, "web") ? [`${WEB_DIR}/dist`] : []),
|
|
74
|
+
"README.md",
|
|
75
|
+
"LICENSE",
|
|
76
|
+
],
|
|
56
77
|
mcpName: has(project, "mcp-registry") ? registryName(project) : undefined,
|
|
57
78
|
});
|
|
58
|
-
return [
|
|
79
|
+
return [
|
|
80
|
+
...(python && (has(project, "cli") || has(project, "mcp"))
|
|
81
|
+
? [{ kind: "file" as const, path: launcher, content: pythonLauncher(project) }]
|
|
82
|
+
: []),
|
|
83
|
+
{
|
|
84
|
+
kind: "merge" as const,
|
|
85
|
+
path: "package.json",
|
|
86
|
+
format: "json" as const,
|
|
87
|
+
patch,
|
|
88
|
+
owned: ["bin", "files"],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
59
91
|
},
|
|
60
92
|
validate(project) {
|
|
61
93
|
return [
|
|
@@ -1,7 +1,11 @@
|
|
|
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";
|
|
4
7
|
import type { Operation, Project } from "../model.js";
|
|
8
|
+
import { apply } from "../project/apply.js";
|
|
5
9
|
import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
|
|
6
10
|
import { HOST_DIR, OPENCLAW_ADDITIONS, OPENCLAW_SCAFFOLD, surface } from "./openclaw-native.js";
|
|
7
11
|
|
|
@@ -82,6 +86,39 @@ function emitted(target: Project): Record<string, string> {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
describe("openclaw-native", () => {
|
|
89
|
+
it("keeps hand-authored package and manifest extensions through regeneration", () => {
|
|
90
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-openclaw-"));
|
|
91
|
+
const target = { ...project(), root };
|
|
92
|
+
apply(root, surface.plan(target), "0.1.0");
|
|
93
|
+
const packagePath = join(root, HOST_DIR, "package.json");
|
|
94
|
+
const manifestPath = join(root, HOST_DIR, "openclaw.plugin.json");
|
|
95
|
+
writeFileSync(
|
|
96
|
+
packagePath,
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
...JSON.parse(readFileSync(packagePath, "utf8")),
|
|
99
|
+
scripts: { custom: "node custom.mjs" },
|
|
100
|
+
exports: { "./native": "./src/native.ts" },
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
writeFileSync(
|
|
104
|
+
manifestPath,
|
|
105
|
+
JSON.stringify({
|
|
106
|
+
...JSON.parse(readFileSync(manifestPath, "utf8")),
|
|
107
|
+
contracts: { customProviders: ["native"] },
|
|
108
|
+
nativeField: true,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
apply(root, surface.plan(target), "0.1.0");
|
|
112
|
+
expect(JSON.parse(readFileSync(packagePath, "utf8"))).toMatchObject({
|
|
113
|
+
scripts: { custom: "node custom.mjs" },
|
|
114
|
+
exports: { "./native": "./src/native.ts" },
|
|
115
|
+
});
|
|
116
|
+
expect(JSON.parse(readFileSync(manifestPath, "utf8"))).toMatchObject({
|
|
117
|
+
contracts: { customProviders: ["native"] },
|
|
118
|
+
nativeField: true,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
85
122
|
it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
|
|
86
123
|
const files = emitted(project());
|
|
87
124
|
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
|
@@ -238,7 +238,11 @@ function pluginInspector(project: Project, operations: Operation[]): Record<stri
|
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
function packageJson(
|
|
241
|
+
function packageJson(
|
|
242
|
+
project: Project,
|
|
243
|
+
operations: Operation[],
|
|
244
|
+
kase?: E2eCase,
|
|
245
|
+
): Record<string, unknown> {
|
|
242
246
|
// The core is imported in-process only when an operation reaches this surface.
|
|
243
247
|
const core =
|
|
244
248
|
project.tool.binding === "typescript" && operations.length
|
|
@@ -246,32 +250,30 @@ function packageJson(project: Project, operations: Operation[], kase?: E2eCase):
|
|
|
246
250
|
: undefined;
|
|
247
251
|
const extra = project.tool.openclaw;
|
|
248
252
|
const e2e = kase ? { "test:e2e": OPENCLAW_ADDITIONS.e2eScript } : undefined;
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
devDependencies
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
pluginInspector
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}),
|
|
274
|
-
);
|
|
253
|
+
return compact({
|
|
254
|
+
name: projectName.openclawPackage(project.identity.name),
|
|
255
|
+
version: project.identity.version ?? "0.0.0",
|
|
256
|
+
description: pluginDescription(project),
|
|
257
|
+
type: "module",
|
|
258
|
+
private: OPENCLAW_SCAFFOLD.private,
|
|
259
|
+
scripts: { ...OPENCLAW_SCAFFOLD.scripts, ...e2e },
|
|
260
|
+
files: OPENCLAW_SCAFFOLD.files,
|
|
261
|
+
peerDependencies: { openclaw: pluginApi(project), ...extra?.peerDependencies },
|
|
262
|
+
dependencies: { ...OPENCLAW_SCAFFOLD.dependencies, ...core, ...extra?.dependencies },
|
|
263
|
+
devDependencies: {
|
|
264
|
+
...OPENCLAW_SCAFFOLD.devDependencies,
|
|
265
|
+
...(kase ? { "@copilotkit/aimock": OPENCLAW_ADDITIONS.aimock } : {}),
|
|
266
|
+
...extra?.devDependencies,
|
|
267
|
+
},
|
|
268
|
+
openclaw: {
|
|
269
|
+
extensions: [OPENCLAW_SCAFFOLD.entry],
|
|
270
|
+
compat: { pluginApi: pluginApi(project) },
|
|
271
|
+
build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
|
|
272
|
+
},
|
|
273
|
+
pluginInspector: expectedRegistrations(project, operations).length
|
|
274
|
+
? pluginInspector(project, operations)
|
|
275
|
+
: undefined,
|
|
276
|
+
}) as Record<string, unknown>;
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
/**
|
|
@@ -281,7 +283,7 @@ function packageJson(project: Project, operations: Operation[], kase?: E2eCase):
|
|
|
281
283
|
* config UI learns which keys to mask, from the same `x-toolfactory.sensitive` declaration every
|
|
282
284
|
* other surface reads.
|
|
283
285
|
*/
|
|
284
|
-
function pluginManifest(project: Project, operations: Operation[]): string {
|
|
286
|
+
function pluginManifest(project: Project, operations: Operation[]): Record<string, unknown> {
|
|
285
287
|
const sensitive = Object.entries(configProperties(project))
|
|
286
288
|
.filter(([, property]) => isSensitive(property))
|
|
287
289
|
.map(([key]) => [key, { sensitive: true }]);
|
|
@@ -289,21 +291,19 @@ function pluginManifest(project: Project, operations: Operation[]): string {
|
|
|
289
291
|
registration.contract,
|
|
290
292
|
registration.ids,
|
|
291
293
|
]);
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}),
|
|
306
|
-
);
|
|
294
|
+
return compact({
|
|
295
|
+
id: project.identity.name,
|
|
296
|
+
name: displayName(project.identity.name),
|
|
297
|
+
description: pluginDescription(project),
|
|
298
|
+
version: project.identity.version ?? "0.0.0",
|
|
299
|
+
configSchema: configSchema(project),
|
|
300
|
+
uiHints: sensitive.length ? Object.fromEntries(sensitive) : undefined,
|
|
301
|
+
activation: activation(project),
|
|
302
|
+
contracts: {
|
|
303
|
+
tools: operations.map((operation) => operation.name),
|
|
304
|
+
...Object.fromEntries(declared),
|
|
305
|
+
},
|
|
306
|
+
}) as Record<string, unknown>;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
function executeBody(project: Project, operation: Operation): string {
|
|
@@ -933,9 +933,10 @@ export const surface: Surface = {
|
|
|
933
933
|
const kase = e2eCase(project, operations);
|
|
934
934
|
return [
|
|
935
935
|
{
|
|
936
|
-
kind: "
|
|
936
|
+
kind: "merge",
|
|
937
937
|
path: `${HOST_DIR}/package.json`,
|
|
938
|
-
|
|
938
|
+
format: "json",
|
|
939
|
+
patch: packageJson(project, operations, kase),
|
|
939
940
|
},
|
|
940
941
|
{
|
|
941
942
|
kind: "file",
|
|
@@ -955,9 +956,10 @@ export const surface: Surface = {
|
|
|
955
956
|
content: OPENCLAW_SCAFFOLD.vitestConfig,
|
|
956
957
|
},
|
|
957
958
|
{
|
|
958
|
-
kind: "
|
|
959
|
+
kind: "merge",
|
|
959
960
|
path: `${HOST_DIR}/openclaw.plugin.json`,
|
|
960
|
-
|
|
961
|
+
format: "json",
|
|
962
|
+
patch: pluginManifest(project, operations),
|
|
961
963
|
},
|
|
962
964
|
{
|
|
963
965
|
kind: "region",
|
package/src/surfaces/pypi.ts
CHANGED
|
@@ -29,19 +29,43 @@ export const surface: Surface = {
|
|
|
29
29
|
? { [project.identity.name]: cliEntryPoint(project) }
|
|
30
30
|
: undefined,
|
|
31
31
|
}),
|
|
32
|
-
//
|
|
33
|
-
//
|
|
32
|
+
// Hatch ignores a missing `only-include` path, unlike `force-include`, which makes
|
|
33
|
+
// `uv sync` work in a fresh checkout before the optional page is built. At package time,
|
|
34
|
+
// the gate builds `web/dist` first and Hatch maps it into the installed package.
|
|
34
35
|
...(has(project, "web")
|
|
35
36
|
? {
|
|
36
37
|
tool: {
|
|
37
38
|
hatch: {
|
|
38
39
|
build: {
|
|
40
|
+
// Hatch's normal file selection honors .gitignore; `web/dist` is ignored in
|
|
41
|
+
// every generated project, so artifacts is the standard explicit opt-in for
|
|
42
|
+
// the release build's compiled page.
|
|
43
|
+
artifacts: [`${WEB_DIR}/dist/**`],
|
|
39
44
|
targets: {
|
|
40
45
|
wheel: {
|
|
41
|
-
"
|
|
46
|
+
"only-include": [`src/${pythonPackage(project)}`, `${WEB_DIR}/dist`],
|
|
47
|
+
sources: {
|
|
48
|
+
src: "",
|
|
42
49
|
[`${WEB_DIR}/dist`]: `${pythonPackage(project)}/${WEB_DIR}`,
|
|
43
50
|
},
|
|
44
51
|
},
|
|
52
|
+
// This is the source MCPB stages, so include every declared project metadata
|
|
53
|
+
// file alongside the package and built page. `only-include` ignores absent
|
|
54
|
+
// legal files without making fresh checkouts fail.
|
|
55
|
+
sdist: {
|
|
56
|
+
"only-include": [
|
|
57
|
+
"pyproject.toml",
|
|
58
|
+
"README.md",
|
|
59
|
+
"LICENSE",
|
|
60
|
+
"LICENSE.md",
|
|
61
|
+
"LICENSE.txt",
|
|
62
|
+
"NOTICE",
|
|
63
|
+
"NOTICE.md",
|
|
64
|
+
"NOTICE.txt",
|
|
65
|
+
`src/${pythonPackage(project)}`,
|
|
66
|
+
`${WEB_DIR}/dist`,
|
|
67
|
+
],
|
|
68
|
+
},
|
|
45
69
|
},
|
|
46
70
|
},
|
|
47
71
|
},
|