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,264 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { createServer as createNetServer } from "node:http";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { ToolConfigSchema } from "../model.js";
|
|
9
|
+
import { cliCommand, cli as cliFiles, kernel, kernelCommand, liveTest, scaffold, } from "./typescript.js";
|
|
10
|
+
function project(surfaces, overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
root: "/tmp/none",
|
|
13
|
+
tool: ToolConfigSchema.parse({
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
identity: "plugin.json",
|
|
16
|
+
binding: "typescript",
|
|
17
|
+
surfaces,
|
|
18
|
+
config: { type: "object", properties: { apiKey: { type: "string" } } },
|
|
19
|
+
}),
|
|
20
|
+
identity: {
|
|
21
|
+
name: "hello.ts",
|
|
22
|
+
version: "0.2.0",
|
|
23
|
+
description: "Say hello.",
|
|
24
|
+
license: "MIT",
|
|
25
|
+
repository: "https://github.com/acme/hello",
|
|
26
|
+
author: { name: "Ada", email: "ada@example.com" },
|
|
27
|
+
},
|
|
28
|
+
identityExtra: {},
|
|
29
|
+
operations: [],
|
|
30
|
+
toolfactoryVersion: "0.1.0",
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** A project whose config declares one required, sensitive credential: what gates the T4 tier. */
|
|
35
|
+
function withCredential(surfaces) {
|
|
36
|
+
const base = project(surfaces);
|
|
37
|
+
base.tool.config = {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { passkey: { type: "string", "x-toolfactory": { sensitive: true } } },
|
|
40
|
+
required: ["passkey"],
|
|
41
|
+
};
|
|
42
|
+
base.tool.tests = { examples: { echo: { text: "hi" } } };
|
|
43
|
+
base.operations = [{ name: "echo", inputSchema: { type: "object" }, requires: [] }];
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
const paths = (files) => files.map((file) => file.path);
|
|
47
|
+
const text = (files, path) => files.find((file) => file.path === path).content;
|
|
48
|
+
describe("typescript binding", () => {
|
|
49
|
+
it("names every artifact from the canonical name", () => {
|
|
50
|
+
const both = project(["mcp", "cli"]);
|
|
51
|
+
expect(kernelCommand().args).toEqual(["--import", "tsx", "src/toolfactory/mcp.ts"]);
|
|
52
|
+
expect(cliCommand().args.at(-1)).toBe("src/toolfactory/cli.ts");
|
|
53
|
+
expect(paths([...kernel(both), ...cliFiles(both)])).toEqual([
|
|
54
|
+
"src/toolfactory/types.ts",
|
|
55
|
+
"src/toolfactory/config.ts",
|
|
56
|
+
"src/toolfactory/mcp.ts",
|
|
57
|
+
"src/toolfactory/cli.ts",
|
|
58
|
+
]);
|
|
59
|
+
// A file exists iff a selected surface owns it, and config keys become environment reads.
|
|
60
|
+
// The kernel is the same for every surface set; only the cli surface adds cli.ts.
|
|
61
|
+
expect(paths(kernel(project(["mcp"])))).not.toContain("src/toolfactory/cli.ts");
|
|
62
|
+
expect(text(kernel(both), "src/toolfactory/config.ts")).toContain('"apiKey": process.env["APIKEY"]');
|
|
63
|
+
});
|
|
64
|
+
it("reads the data directory from <N>_DATA_DIR, else the platform default", () => {
|
|
65
|
+
const files = kernel(project(["mcp"]));
|
|
66
|
+
expect(text(files, "src/toolfactory/types.ts")).toContain("dataDir: string;");
|
|
67
|
+
const config = text(files, "src/toolfactory/config.ts");
|
|
68
|
+
expect(config).toContain('process.env["HELLO_TS_DATA_DIR"]');
|
|
69
|
+
expect(config).toContain('process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")');
|
|
70
|
+
expect(config).toContain('process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local")');
|
|
71
|
+
expect(config).toContain('return join(base, "hello.ts");');
|
|
72
|
+
expect(config).toContain("dataDir: dataDir(),");
|
|
73
|
+
});
|
|
74
|
+
it("emits the T4 live test, and its scaffold script, only for a required sensitive credential", () => {
|
|
75
|
+
// No config key is both required and sensitive: nothing gates a live run, so no live tier.
|
|
76
|
+
expect(liveTest(project(["cli"]))).toEqual([]);
|
|
77
|
+
const live = liveTest(withCredential(["cli"]))[0];
|
|
78
|
+
if (live?.kind !== "region")
|
|
79
|
+
throw new Error("expected a region file");
|
|
80
|
+
expect(live.path).toBe("tests/live.test.ts");
|
|
81
|
+
// The generated region is the guard; the example test around it is the author's.
|
|
82
|
+
expect(live.regions[0]?.content).toContain('const CREDENTIALS = ["PASSKEY"];');
|
|
83
|
+
expect(live.regions[0]?.content).toContain("CREDENTIALS.every((name) => process.env[name])");
|
|
84
|
+
expect(live.template).toContain("describe.skipIf(!live)");
|
|
85
|
+
// The example arguments come from tool.json tests.examples, so the stub is a real call.
|
|
86
|
+
expect(live.template).toContain('operation.handler({"text":"hi"} as never, context())');
|
|
87
|
+
const scripts = JSON.parse(text(scaffold(project(["cli"])), "package.json")).scripts;
|
|
88
|
+
expect(scripts["test:live"]).toBe("node --env-file-if-exists=.env node_modules/vitest/vitest.mjs run tests/live.test.ts");
|
|
89
|
+
});
|
|
90
|
+
it("emits opt-in --http and --pair flags, defaulting to stdio, on the mcp module and the cli", () => {
|
|
91
|
+
const files = [...kernel(project(["mcp", "cli"])), ...cliFiles(project(["mcp", "cli"]))];
|
|
92
|
+
const mcp = text(files, "src/toolfactory/mcp.ts");
|
|
93
|
+
expect(mcp).toContain('import { createMcpHandler, McpServer } from "@modelcontextprotocol/server"');
|
|
94
|
+
expect(mcp).toContain("export async function serveHttp(");
|
|
95
|
+
expect(mcp).toContain('port = 3000, host = "127.0.0.1", path = "/mcp"');
|
|
96
|
+
expect(mcp).toContain("toNodeHandler(createMcpHandler(createServer))");
|
|
97
|
+
// The CLI's mcp subcommand exists only when the mcp surface does (the file it imports).
|
|
98
|
+
expect(text(cliFiles(project(["cli"])), "src/toolfactory/cli.ts")).not.toContain('command("mcp")');
|
|
99
|
+
// Stdio is still the default: the standalone entrypoint only switches transport when --http is present.
|
|
100
|
+
expect(mcp).toContain("const port = httpPortArg(process.argv.slice(2));");
|
|
101
|
+
expect(mcp).toContain("port === undefined && !pair ? serve() : serveHttp({ port: port ?? 3000");
|
|
102
|
+
// The pairing token: `<N>_MCP_TOKEN` or a `relay-token` file under the data directory, and
|
|
103
|
+
// `--pair` to mint one. Neither present means no token and no behaviour change.
|
|
104
|
+
expect(mcp).toContain('process.env["HELLO_TS_MCP_TOKEN"]');
|
|
105
|
+
expect(mcp).toContain("join(context().dataDir, TOKEN_FILE)");
|
|
106
|
+
expect(mcp).toContain('timingSafeEqual(digest(header?.replace(/^Bearer /, "") ?? "")');
|
|
107
|
+
const cli = text(files, "src/toolfactory/cli.ts");
|
|
108
|
+
expect(cli).toContain('"--http [port]"');
|
|
109
|
+
expect(cli).toContain("serve, serveHttp");
|
|
110
|
+
expect(cli).toContain("if (options.http === undefined && !options.pair) await serve();");
|
|
111
|
+
expect(cli).toContain('"--pair"');
|
|
112
|
+
// Without the `web` surface there is no page to serve, open or write secrets for.
|
|
113
|
+
expect(mcp).not.toContain("WEB_DIST");
|
|
114
|
+
expect(mcp).not.toContain("SECRETS");
|
|
115
|
+
expect(paths(kernel(project(["mcp", "cli"])))).not.toContain("src/toolfactory/web.ts");
|
|
116
|
+
});
|
|
117
|
+
it("serves the page, opens it and takes a secret when the web surface is selected", () => {
|
|
118
|
+
const target = withCredential(["mcp", "cli", "web"]);
|
|
119
|
+
const files = [...kernel(target), ...cliFiles(target)];
|
|
120
|
+
const mcp = text(files, "src/toolfactory/mcp.ts");
|
|
121
|
+
// One router, exported, so a host with its own listener mounts the same function.
|
|
122
|
+
expect(mcp).toContain("export function handler(options: HandlerOptions = {})");
|
|
123
|
+
expect(mcp).toContain('const { path = "/mcp", prefix = "", token, hostGuard, env = true }');
|
|
124
|
+
expect(mcp).toContain('fileURLToPath(new URL("../../web/dist/", import.meta.url))');
|
|
125
|
+
expect(mcp).toContain("run \\`npm -C web run build\\`");
|
|
126
|
+
// The token guards the API routes only — a browser cannot put a header on its own document.
|
|
127
|
+
expect(mcp).toContain("serveWeb(pathname.slice(prefix.length), response);");
|
|
128
|
+
expect(mcp).toContain('const SECRETS = ["PASSKEY"];');
|
|
129
|
+
expect(mcp).toContain("json(200, { declared: SECRETS, present: envPresent() })");
|
|
130
|
+
expect(mcp).toContain("chmodSync(envPath(), 0o600)");
|
|
131
|
+
expect(mcp).toContain("console.error(`.env: set ${name}`)");
|
|
132
|
+
expect(mcp).toContain("openBrowser(`${base}/#${token}`)");
|
|
133
|
+
expect(mcp).toContain("pairedToken() ?? (open ? mintToken() : undefined)");
|
|
134
|
+
// The `web` operation: a detached child holds the socket, never this process.
|
|
135
|
+
const web = text(files, "src/toolfactory/web.ts");
|
|
136
|
+
expect(web).toContain('[...process.execArgv, KERNEL, "--http", "0", "--open"]');
|
|
137
|
+
expect(web).toContain("detached: true");
|
|
138
|
+
expect(web).toContain("const SERVING = /Serving MCP streamable HTTP on (\\S+)/;");
|
|
139
|
+
expect(mcp).toContain("const operations = [...authored, web];");
|
|
140
|
+
expect(text(files, "src/toolfactory/cli.ts")).toContain("const operations = [...authored, web];");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
const repoRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
|
|
144
|
+
const repoNodeModules = join(repoRoot, "node_modules");
|
|
145
|
+
function freePort() {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const probe = createNetServer();
|
|
148
|
+
probe.on("error", reject);
|
|
149
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
150
|
+
const address = probe.address();
|
|
151
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
152
|
+
probe.close((error) => (error ? reject(error) : resolve(port)));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// The streamable HTTP transport requires `Accept: application/json, text/event-stream` on every
|
|
157
|
+
// POST, and `MCP-Protocol-Version` on every request *after* the one that negotiates it in `initialize`.
|
|
158
|
+
async function postRpc(url, body, protocolVersion, token) {
|
|
159
|
+
const headers = {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
Accept: "application/json, text/event-stream",
|
|
162
|
+
};
|
|
163
|
+
if (protocolVersion)
|
|
164
|
+
headers["MCP-Protocol-Version"] = protocolVersion;
|
|
165
|
+
if (token)
|
|
166
|
+
headers.Authorization = `Bearer ${token}`;
|
|
167
|
+
const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
|
|
168
|
+
const raw = await response.text();
|
|
169
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
170
|
+
const payload = isEventStream
|
|
171
|
+
? JSON.parse(raw
|
|
172
|
+
.split("\n")
|
|
173
|
+
.find((line) => line.startsWith("data: "))
|
|
174
|
+
?.slice("data: ".length) ?? "{}")
|
|
175
|
+
: JSON.parse(raw || "{}");
|
|
176
|
+
return { status: response.status, json: payload };
|
|
177
|
+
}
|
|
178
|
+
/** Retries `initialize` until the freshly-spawned server starts accepting connections. */
|
|
179
|
+
async function waitForInitialize(url, deadlineMs, token) {
|
|
180
|
+
const deadline = Date.now() + deadlineMs;
|
|
181
|
+
for (;;) {
|
|
182
|
+
try {
|
|
183
|
+
return await postRpc(url, {
|
|
184
|
+
jsonrpc: "2.0",
|
|
185
|
+
id: 1,
|
|
186
|
+
method: "initialize",
|
|
187
|
+
params: {
|
|
188
|
+
protocolVersion: "2026-07-28",
|
|
189
|
+
capabilities: {},
|
|
190
|
+
clientInfo: { name: "toolfactory-test", version: "0.0.0" },
|
|
191
|
+
},
|
|
192
|
+
}, undefined, token);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (Date.now() >= deadline)
|
|
196
|
+
throw error;
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
describe.skipIf(!existsSync(repoNodeModules))("typescript kernel, really run over http", () => {
|
|
202
|
+
it("serves tools/list over --http, and requires the token minted by --pair", {
|
|
203
|
+
timeout: 60_000,
|
|
204
|
+
}, async () => {
|
|
205
|
+
const root = mkdtempSync(join(tmpdir(), "toolfactory-typescript-"));
|
|
206
|
+
symlinkSync(repoNodeModules, join(root, "node_modules"), "dir");
|
|
207
|
+
const real = project(["mcp", "cli"], { root, identity: { name: "probe", version: "0.1.0" } });
|
|
208
|
+
for (const file of [...scaffold(real), ...kernel(real), ...cliFiles(real)]) {
|
|
209
|
+
const filePath = join(root, file.path);
|
|
210
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
211
|
+
writeFileSync(filePath, file.content);
|
|
212
|
+
}
|
|
213
|
+
const dataDir = join(root, "data");
|
|
214
|
+
const env = { ...process.env, PROBE_DATA_DIR: dataDir };
|
|
215
|
+
const port = await freePort();
|
|
216
|
+
const { command, args } = cliCommand();
|
|
217
|
+
const child = spawn(command, [...args, "mcp", "--http", String(port)], {
|
|
218
|
+
cwd: root,
|
|
219
|
+
env,
|
|
220
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
221
|
+
});
|
|
222
|
+
const stderr = [];
|
|
223
|
+
child.stderr?.on("data", (chunk) => stderr.push(String(chunk)));
|
|
224
|
+
try {
|
|
225
|
+
const url = `http://127.0.0.1:${port}/mcp`;
|
|
226
|
+
const init = await waitForInitialize(url, 20_000).catch((error) => {
|
|
227
|
+
throw new Error(`${error instanceof Error ? error.message : error}\n${stderr.join("")}`);
|
|
228
|
+
});
|
|
229
|
+
expect(init.status).toBe(200);
|
|
230
|
+
const negotiated = init.json.result
|
|
231
|
+
.protocolVersion;
|
|
232
|
+
const list = await postRpc(url, { jsonrpc: "2.0", id: 2, method: "tools/list" }, negotiated);
|
|
233
|
+
expect(list.status).toBe(200);
|
|
234
|
+
const tools = list.json.result.tools;
|
|
235
|
+
expect(tools.map((tool) => tool.name)).toEqual(["echo"]);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
child.kill();
|
|
239
|
+
}
|
|
240
|
+
// The same server started with `--pair`: it prints the pairing string the extension's options
|
|
241
|
+
// page takes, keeps the token under the data directory, and refuses a request without it.
|
|
242
|
+
const pairPort = await freePort();
|
|
243
|
+
const paired = spawn(command, [...args, "mcp", "--http", String(pairPort), "--pair"], {
|
|
244
|
+
cwd: root,
|
|
245
|
+
env,
|
|
246
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
+
});
|
|
248
|
+
const stdout = [];
|
|
249
|
+
paired.stdout?.on("data", (chunk) => stdout.push(String(chunk)));
|
|
250
|
+
try {
|
|
251
|
+
const url = `http://127.0.0.1:${pairPort}/mcp`;
|
|
252
|
+
const anonymous = await waitForInitialize(url, 20_000);
|
|
253
|
+
expect(anonymous.status).toBe(401);
|
|
254
|
+
const [printed, token] = stdout.join("").trim().split("#");
|
|
255
|
+
expect(printed).toBe(url);
|
|
256
|
+
expect(readFileSync(join(dataDir, "relay-token"), "utf8").trim()).toBe(token);
|
|
257
|
+
const init = await waitForInitialize(url, 20_000, token);
|
|
258
|
+
expect(init.status).toBe(200);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
paired.kill();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { type BootstrapResult, type CreateRepoResult } from "./hosts/github.js";
|
|
2
|
+
import type { Binding, Command, Project, SurfaceId } from "./model.js";
|
|
3
|
+
import { type Drift } from "./project/apply.js";
|
|
4
|
+
import { type GateStep } from "./project/gate.js";
|
|
5
|
+
import { type Coverage } from "./report/coverage.js";
|
|
6
|
+
export interface InitOptions {
|
|
7
|
+
root: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
binding: Binding;
|
|
11
|
+
/** `none` creates an instruction-only skill/Codex plugin with no executable payload. */
|
|
12
|
+
runtime?: "kernel" | "none";
|
|
13
|
+
/** Default: `defaultSurfaces(binding)`, the skills-first minimum. */
|
|
14
|
+
surfaces?: SurfaceId[];
|
|
15
|
+
license?: string;
|
|
16
|
+
repository?: string;
|
|
17
|
+
author?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Activation triggers for hosts that key off them (Kiro Powers, Agent Plugins); defaults to
|
|
20
|
+
* `[name]`.
|
|
21
|
+
*/
|
|
22
|
+
keywords?: string[];
|
|
23
|
+
/** `git init` when the directory is not a repository yet. `.agents/setup` needs one. */
|
|
24
|
+
git?: boolean;
|
|
25
|
+
/** Run `.agents/setup`: harness adapters, the git hooks, the dependency install. */
|
|
26
|
+
setup?: boolean;
|
|
27
|
+
/** `owner/name` of a GitHub repository to create with `gh` and push to. */
|
|
28
|
+
repo?: string;
|
|
29
|
+
/** Create that repository public; the default is private. */
|
|
30
|
+
public?: boolean;
|
|
31
|
+
/** Print the `gh` invocations instead of running them. */
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
/** GitHub logins that must approve a live run, when the repository gets a live tier. */
|
|
34
|
+
reviewers?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface InitResult {
|
|
37
|
+
written: string[];
|
|
38
|
+
/** What `.agents/setup` did here: whether it succeeded, and the harnesses it rendered for. */
|
|
39
|
+
agentConfig: {
|
|
40
|
+
setup: boolean;
|
|
41
|
+
harnesses: string[];
|
|
42
|
+
};
|
|
43
|
+
repository?: CreateRepoResult & {
|
|
44
|
+
secrets: string[];
|
|
45
|
+
};
|
|
46
|
+
/** What the caller — a human or the agent that ran `init` — has to do next, in order. */
|
|
47
|
+
nextSteps: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create dev.toolfactory/tool.json, the identity file and the binding's scaffold, build every
|
|
51
|
+
* selected surface, and leave a project that is already live in the harness it was run from:
|
|
52
|
+
* a git repository, the first-party skills, the rendered agent config, and — only when asked —
|
|
53
|
+
* a GitHub repository holding it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function init(options: InitOptions): InitResult;
|
|
56
|
+
export declare function build(root?: string): {
|
|
57
|
+
project: Project;
|
|
58
|
+
result: {
|
|
59
|
+
nudge: string | undefined;
|
|
60
|
+
written: string[];
|
|
61
|
+
deleted: string[];
|
|
62
|
+
unchanged: string[];
|
|
63
|
+
manual: string[];
|
|
64
|
+
stripped: string[];
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Throws when any generated file drifted, so CLI and MCP callers see a failure. */
|
|
68
|
+
/** The drift gate: the operation snapshot and every generated file must match the code. */
|
|
69
|
+
export declare function check(root?: string): Promise<{
|
|
70
|
+
project: Project;
|
|
71
|
+
drift: Drift[];
|
|
72
|
+
}>;
|
|
73
|
+
/** Regenerate the kernel first so the snapshot always reflects the current templates. */
|
|
74
|
+
export declare function introspect(root?: string): Promise<{
|
|
75
|
+
path: string;
|
|
76
|
+
changed: boolean;
|
|
77
|
+
ops: import("./project/load.js").Ops;
|
|
78
|
+
}>;
|
|
79
|
+
export declare function coverage(root?: string): Coverage;
|
|
80
|
+
export interface ValidationOutcome {
|
|
81
|
+
label: string;
|
|
82
|
+
command: string;
|
|
83
|
+
ok: boolean;
|
|
84
|
+
output: string;
|
|
85
|
+
}
|
|
86
|
+
/** Run every selected surface's upstream validator (tier 1 and the kernel smoke); throws if any fails. */
|
|
87
|
+
export declare function validate(root?: string, only?: SurfaceId): ValidationOutcome[];
|
|
88
|
+
export declare function run(command: Command): ValidationOutcome;
|
|
89
|
+
export interface StepResult {
|
|
90
|
+
name: string;
|
|
91
|
+
ok: boolean;
|
|
92
|
+
durationMs: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Run a step list from `project/gate.ts` in order, stopping at the first failure. `when: "ci"`
|
|
96
|
+
* steps are runner provisioning — a global install no checkout may perform on the author's
|
|
97
|
+
* machine — and are skipped here exactly as `renderShell` skips them. Every step's output goes to
|
|
98
|
+
* this process's stderr, never its stdout, so running the gate over the MCP surface cannot
|
|
99
|
+
* corrupt the protocol stream.
|
|
100
|
+
*/
|
|
101
|
+
export declare function runSteps(root: string, steps: GateStep[]): {
|
|
102
|
+
steps: StepResult[];
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* The gate: what "green" means for this project, run here instead of on a runner. `ci.yml`
|
|
106
|
+
* renders the same list, so a project with no CI at all — no GitHub, plain git or none — has the
|
|
107
|
+
* identical gate one command away.
|
|
108
|
+
*/
|
|
109
|
+
export declare function gate(root?: string): {
|
|
110
|
+
steps: StepResult[];
|
|
111
|
+
};
|
|
112
|
+
/** Every release asset into `dist/release/`, by the same steps the release workflow's job runs. */
|
|
113
|
+
export declare function packageRelease(root?: string): {
|
|
114
|
+
steps: StepResult[];
|
|
115
|
+
};
|
|
116
|
+
export interface SecretRow {
|
|
117
|
+
/** The environment variable name, on every surface and in every environment. */
|
|
118
|
+
name: string;
|
|
119
|
+
kind: "config" | "release";
|
|
120
|
+
required: boolean;
|
|
121
|
+
/** Selected surfaces (a config key) or registry ids (a release token) that consume it. */
|
|
122
|
+
needs: string[];
|
|
123
|
+
/** Present in `<root>/.env` or this process's environment. Presence only, never the value. */
|
|
124
|
+
local: boolean;
|
|
125
|
+
/** Named by `gh secret list`; absent when `gh` is not on PATH or no repository is declared. */
|
|
126
|
+
github?: boolean;
|
|
127
|
+
url?: string;
|
|
128
|
+
/** The exact next action, per selected host and per registry. */
|
|
129
|
+
howTo: string[];
|
|
130
|
+
/** `check`: what the registry's own CLI said about the credential in the environment. */
|
|
131
|
+
check?: {
|
|
132
|
+
ok: boolean;
|
|
133
|
+
command: string;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export interface SecretsReport {
|
|
137
|
+
secrets: SecretRow[];
|
|
138
|
+
/** The one-time human steps with no API; the same list `bootstrap-repo` prints. */
|
|
139
|
+
manual: string[];
|
|
140
|
+
}
|
|
141
|
+
export declare function secretsReport(project: Project, action?: "status" | "check", key?: string): SecretsReport;
|
|
142
|
+
/** The whole inventory: what each credential is for, where it is set, and (check) whether it works. */
|
|
143
|
+
export declare function secrets(args: {
|
|
144
|
+
root?: string;
|
|
145
|
+
action?: "status" | "check";
|
|
146
|
+
key?: string;
|
|
147
|
+
}): SecretsReport;
|
|
148
|
+
export interface UnpublishResult {
|
|
149
|
+
/** The tag the previous selection came from; absent when there is no earlier tag to diff against. */
|
|
150
|
+
from?: string;
|
|
151
|
+
/** Surfaces selected at that tag, no longer selected, and published somewhere. */
|
|
152
|
+
dropped: SurfaceId[];
|
|
153
|
+
steps: {
|
|
154
|
+
name: string;
|
|
155
|
+
run: string;
|
|
156
|
+
}[];
|
|
157
|
+
/**
|
|
158
|
+
* What each step did (omitted by `dryRun`). Best-effort on purpose: a registry that refuses a
|
|
159
|
+
* retraction must never stop the release it is riding along with from being cut.
|
|
160
|
+
*/
|
|
161
|
+
ran?: StepResult[];
|
|
162
|
+
}
|
|
163
|
+
export declare function unpublish(root?: string, options?: {
|
|
164
|
+
ref?: string;
|
|
165
|
+
dryRun?: boolean;
|
|
166
|
+
hard?: boolean;
|
|
167
|
+
}): UnpublishResult;
|
|
168
|
+
export declare function adopt(root: string, path: string): void;
|
|
169
|
+
export declare function unadopt(root: string, path: string): void;
|
|
170
|
+
export declare function eject(root: string, surface: SurfaceId): string[];
|
|
171
|
+
export interface DoctorReport {
|
|
172
|
+
toolfactory: string;
|
|
173
|
+
node: string;
|
|
174
|
+
tools: Record<string, string>;
|
|
175
|
+
}
|
|
176
|
+
/** Report the upstream CLIs this machine can delegate to. */
|
|
177
|
+
/**
|
|
178
|
+
* Prepare the GitHub repository: the `live-tests` environment and its secrets, the release
|
|
179
|
+
* registries' repository secrets, Pages, and npm's trusted publisher — everything GitHub only
|
|
180
|
+
* exposes over its API, from the one table and the one `.env`.
|
|
181
|
+
*/
|
|
182
|
+
export declare function bootstrapRepo(args: {
|
|
183
|
+
root: string;
|
|
184
|
+
reviewers?: string[];
|
|
185
|
+
dryRun?: boolean;
|
|
186
|
+
}): BootstrapResult;
|
|
187
|
+
export declare function doctor(): DoctorReport;
|
|
188
|
+
export declare const SURFACES: readonly ["skill", "agent-plugins", "claude", "codex", "cursor", "mcp", "mcp-registry", "cli", "npm", "pypi", "openclaw-native", "hermes-native", "clawhub", "web", "dsh", "workflows", "agents", "readme", "gemini", "mcpb", "browser-extension"];
|