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.
Files changed (228) hide show
  1. package/README.md +166 -0
  2. package/dist/bindings/index.d.ts +23 -0
  3. package/dist/bindings/index.js +29 -0
  4. package/dist/bindings/python.d.ts +33 -0
  5. package/dist/bindings/python.js +805 -0
  6. package/dist/bindings/python.test.d.ts +1 -0
  7. package/dist/bindings/python.test.js +316 -0
  8. package/dist/bindings/typescript.d.ts +39 -0
  9. package/dist/bindings/typescript.js +758 -0
  10. package/dist/bindings/typescript.test.d.ts +1 -0
  11. package/dist/bindings/typescript.test.js +264 -0
  12. package/dist/commands.d.ts +188 -0
  13. package/dist/commands.js +662 -0
  14. package/dist/commands.test.d.ts +1 -0
  15. package/dist/commands.test.js +215 -0
  16. package/dist/e2e.test.d.ts +1 -0
  17. package/dist/e2e.test.js +124 -0
  18. package/dist/hosts/browser.d.ts +10 -0
  19. package/dist/hosts/browser.js +94 -0
  20. package/dist/hosts/github.d.ts +66 -0
  21. package/dist/hosts/github.js +247 -0
  22. package/dist/hosts/openclaw.d.ts +4 -0
  23. package/dist/hosts/openclaw.js +94 -0
  24. package/dist/hosts/template.d.ts +15 -0
  25. package/dist/hosts/template.js +142 -0
  26. package/dist/hosts/web.d.ts +4 -0
  27. package/dist/hosts/web.js +89 -0
  28. package/dist/identity/name.d.ts +20 -0
  29. package/dist/identity/name.js +33 -0
  30. package/dist/identity/name.test.d.ts +1 -0
  31. package/dist/identity/name.test.js +37 -0
  32. package/dist/identity/read.d.ts +9 -0
  33. package/dist/identity/read.js +106 -0
  34. package/dist/identity/read.test.d.ts +1 -0
  35. package/dist/identity/read.test.js +76 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +6 -0
  38. package/dist/introspect/index.d.ts +15 -0
  39. package/dist/introspect/index.js +63 -0
  40. package/dist/model.d.ts +236 -0
  41. package/dist/model.js +214 -0
  42. package/dist/ops.d.ts +255 -0
  43. package/dist/ops.js +297 -0
  44. package/dist/project/apply.d.ts +35 -0
  45. package/dist/project/apply.js +360 -0
  46. package/dist/project/apply.test.d.ts +1 -0
  47. package/dist/project/apply.test.js +285 -0
  48. package/dist/project/gate.d.ts +129 -0
  49. package/dist/project/gate.js +597 -0
  50. package/dist/project/history.d.ts +62 -0
  51. package/dist/project/history.js +41 -0
  52. package/dist/project/load.d.ts +21 -0
  53. package/dist/project/load.js +82 -0
  54. package/dist/project/lock.d.ts +28 -0
  55. package/dist/project/lock.js +42 -0
  56. package/dist/project/plan.d.ts +5 -0
  57. package/dist/project/plan.js +96 -0
  58. package/dist/report/coverage.d.ts +22 -0
  59. package/dist/report/coverage.js +72 -0
  60. package/dist/report/coverage.test.d.ts +1 -0
  61. package/dist/report/coverage.test.js +93 -0
  62. package/dist/surfaces/agent-plugins.d.ts +10 -0
  63. package/dist/surfaces/agent-plugins.js +36 -0
  64. package/dist/surfaces/agents.d.ts +35 -0
  65. package/dist/surfaces/agents.js +609 -0
  66. package/dist/surfaces/agents.template.d.ts +8 -0
  67. package/dist/surfaces/agents.template.js +21 -0
  68. package/dist/surfaces/agents.test.d.ts +1 -0
  69. package/dist/surfaces/agents.test.js +181 -0
  70. package/dist/surfaces/browser-extension.d.ts +101 -0
  71. package/dist/surfaces/browser-extension.js +1034 -0
  72. package/dist/surfaces/browser-extension.test.d.ts +1 -0
  73. package/dist/surfaces/browser-extension.test.js +145 -0
  74. package/dist/surfaces/claude.d.ts +2 -0
  75. package/dist/surfaces/claude.js +81 -0
  76. package/dist/surfaces/codex.d.ts +13 -0
  77. package/dist/surfaces/codex.js +101 -0
  78. package/dist/surfaces/codex.test.d.ts +1 -0
  79. package/dist/surfaces/codex.test.js +67 -0
  80. package/dist/surfaces/cursor.d.ts +2 -0
  81. package/dist/surfaces/cursor.js +50 -0
  82. package/dist/surfaces/distribution.test.d.ts +1 -0
  83. package/dist/surfaces/distribution.test.js +146 -0
  84. package/dist/surfaces/dsh.d.ts +25 -0
  85. package/dist/surfaces/dsh.js +147 -0
  86. package/dist/surfaces/dsh.test.d.ts +1 -0
  87. package/dist/surfaces/dsh.test.js +100 -0
  88. package/dist/surfaces/external.d.ts +9 -0
  89. package/dist/surfaces/external.js +4 -0
  90. package/dist/surfaces/gemini.d.ts +29 -0
  91. package/dist/surfaces/gemini.js +68 -0
  92. package/dist/surfaces/gemini.test.d.ts +1 -0
  93. package/dist/surfaces/gemini.test.js +74 -0
  94. package/dist/surfaces/hermes-native.d.ts +53 -0
  95. package/dist/surfaces/hermes-native.js +507 -0
  96. package/dist/surfaces/hermes-native.test.d.ts +1 -0
  97. package/dist/surfaces/hermes-native.test.js +156 -0
  98. package/dist/surfaces/kernel.d.ts +5 -0
  99. package/dist/surfaces/kernel.js +63 -0
  100. package/dist/surfaces/mcp-registry.d.ts +19 -0
  101. package/dist/surfaces/mcp-registry.js +160 -0
  102. package/dist/surfaces/mcp-registry.test.d.ts +1 -0
  103. package/dist/surfaces/mcp-registry.test.js +79 -0
  104. package/dist/surfaces/mcpb.d.ts +11 -0
  105. package/dist/surfaces/mcpb.js +118 -0
  106. package/dist/surfaces/mcpb.test.d.ts +1 -0
  107. package/dist/surfaces/mcpb.test.js +104 -0
  108. package/dist/surfaces/npm.d.ts +6 -0
  109. package/dist/surfaces/npm.js +72 -0
  110. package/dist/surfaces/openclaw-native.d.ts +93 -0
  111. package/dist/surfaces/openclaw-native.js +956 -0
  112. package/dist/surfaces/openclaw-native.test.d.ts +1 -0
  113. package/dist/surfaces/openclaw-native.test.js +246 -0
  114. package/dist/surfaces/pypi.d.ts +5 -0
  115. package/dist/surfaces/pypi.js +82 -0
  116. package/dist/surfaces/readme.d.ts +7 -0
  117. package/dist/surfaces/readme.js +181 -0
  118. package/dist/surfaces/registry.d.ts +6 -0
  119. package/dist/surfaces/registry.js +73 -0
  120. package/dist/surfaces/shared.d.ts +49 -0
  121. package/dist/surfaces/shared.js +112 -0
  122. package/dist/surfaces/skill.d.ts +7 -0
  123. package/dist/surfaces/skill.js +118 -0
  124. package/dist/surfaces/skill.test.d.ts +1 -0
  125. package/dist/surfaces/skill.test.js +87 -0
  126. package/dist/surfaces/web.d.ts +97 -0
  127. package/dist/surfaces/web.js +1285 -0
  128. package/dist/surfaces/web.test.d.ts +1 -0
  129. package/dist/surfaces/web.test.js +255 -0
  130. package/dist/surfaces/workflows.d.ts +2 -0
  131. package/dist/surfaces/workflows.js +809 -0
  132. package/dist/surfaces/workflows.test.d.ts +1 -0
  133. package/dist/surfaces/workflows.test.js +563 -0
  134. package/dist/toolfactory/cli.d.ts +2 -0
  135. package/dist/toolfactory/cli.js +65 -0
  136. package/dist/toolfactory/config.d.ts +3 -0
  137. package/dist/toolfactory/config.js +27 -0
  138. package/dist/toolfactory/mcp.d.ts +37 -0
  139. package/dist/toolfactory/mcp.js +269 -0
  140. package/dist/toolfactory/types.d.ts +31 -0
  141. package/dist/toolfactory/types.js +10 -0
  142. package/dist/toolfactory/web.d.ts +4 -0
  143. package/dist/toolfactory/web.js +40 -0
  144. package/dist/validate/agent-plugins.d.ts +5 -0
  145. package/dist/validate/agent-plugins.js +46 -0
  146. package/package.json +68 -0
  147. package/schemas/agent-plugins/mcp.schema.json +120 -0
  148. package/schemas/agent-plugins/plugin.schema.json +65 -0
  149. package/src/bindings/index.ts +47 -0
  150. package/src/bindings/python.test.ts +374 -0
  151. package/src/bindings/python.ts +862 -0
  152. package/src/bindings/typescript.test.ts +329 -0
  153. package/src/bindings/typescript.ts +818 -0
  154. package/src/commands.test.ts +253 -0
  155. package/src/commands.ts +886 -0
  156. package/src/e2e.test.ts +169 -0
  157. package/src/hosts/browser.ts +106 -0
  158. package/src/hosts/github.ts +321 -0
  159. package/src/hosts/openclaw.ts +110 -0
  160. package/src/hosts/template.ts +149 -0
  161. package/src/hosts/web.ts +111 -0
  162. package/src/identity/name.test.ts +43 -0
  163. package/src/identity/name.ts +39 -0
  164. package/src/identity/read.test.ts +92 -0
  165. package/src/identity/read.ts +119 -0
  166. package/src/index.ts +11 -0
  167. package/src/introspect/index.ts +72 -0
  168. package/src/model.ts +386 -0
  169. package/src/ops.ts +333 -0
  170. package/src/project/apply.test.ts +348 -0
  171. package/src/project/apply.ts +417 -0
  172. package/src/project/gate.ts +717 -0
  173. package/src/project/history.ts +46 -0
  174. package/src/project/load.ts +104 -0
  175. package/src/project/lock.ts +54 -0
  176. package/src/project/plan.ts +101 -0
  177. package/src/report/coverage.test.ts +108 -0
  178. package/src/report/coverage.ts +113 -0
  179. package/src/surfaces/agent-plugins.ts +47 -0
  180. package/src/surfaces/agents.template.ts +32 -0
  181. package/src/surfaces/agents.test.ts +206 -0
  182. package/src/surfaces/agents.ts +729 -0
  183. package/src/surfaces/browser-extension.test.ts +175 -0
  184. package/src/surfaces/browser-extension.ts +1095 -0
  185. package/src/surfaces/claude.ts +99 -0
  186. package/src/surfaces/codex.test.ts +71 -0
  187. package/src/surfaces/codex.ts +105 -0
  188. package/src/surfaces/cursor.ts +62 -0
  189. package/src/surfaces/distribution.test.ts +166 -0
  190. package/src/surfaces/dsh.test.ts +110 -0
  191. package/src/surfaces/dsh.ts +159 -0
  192. package/src/surfaces/external.ts +13 -0
  193. package/src/surfaces/gemini.test.ts +80 -0
  194. package/src/surfaces/gemini.ts +84 -0
  195. package/src/surfaces/hermes-native.test.ts +179 -0
  196. package/src/surfaces/hermes-native.ts +584 -0
  197. package/src/surfaces/kernel.ts +68 -0
  198. package/src/surfaces/mcp-registry.test.ts +99 -0
  199. package/src/surfaces/mcp-registry.ts +190 -0
  200. package/src/surfaces/mcpb.test.ts +112 -0
  201. package/src/surfaces/mcpb.ts +130 -0
  202. package/src/surfaces/npm.ts +78 -0
  203. package/src/surfaces/openclaw-native.test.ts +283 -0
  204. package/src/surfaces/openclaw-native.ts +1070 -0
  205. package/src/surfaces/pypi.ts +84 -0
  206. package/src/surfaces/readme.ts +218 -0
  207. package/src/surfaces/registry.ts +79 -0
  208. package/src/surfaces/shared.ts +140 -0
  209. package/src/surfaces/skill.test.ts +103 -0
  210. package/src/surfaces/skill.ts +136 -0
  211. package/src/surfaces/web.test.ts +303 -0
  212. package/src/surfaces/web.ts +1310 -0
  213. package/src/surfaces/workflows.test.ts +677 -0
  214. package/src/surfaces/workflows.ts +936 -0
  215. package/src/toolfactory/cli.ts +74 -0
  216. package/src/toolfactory/config.ts +33 -0
  217. package/src/toolfactory/mcp.ts +311 -0
  218. package/src/toolfactory/types.ts +52 -0
  219. package/src/toolfactory/web.ts +45 -0
  220. package/src/validate/agent-plugins.ts +53 -0
  221. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  222. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  223. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  224. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  225. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  226. package/web/dist/assets/index-CCXhQKK3.css +2 -0
  227. package/web/dist/assets/index-D1PwZ3qS.js +10 -0
  228. package/web/dist/index.html +14 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { surface as browserExtension, browserVerdict, HOST_DIR, permissions, WXT_PIN, zipName, } from "./browser-extension.js";
3
+ const echo = { name: "echo", inputSchema: { type: "object" }, requires: [] };
4
+ const scrape = {
5
+ name: "scrape",
6
+ inputSchema: { type: "object" },
7
+ requires: ["browser"],
8
+ };
9
+ const summarize = {
10
+ name: "summarize",
11
+ inputSchema: { type: "object" },
12
+ requires: ["model"],
13
+ };
14
+ function fixture(overrides = {}) {
15
+ return {
16
+ root: "/repo",
17
+ tool: {
18
+ schemaVersion: 1,
19
+ identity: "plugin.json",
20
+ binding: "typescript",
21
+ surfaces: ["mcp", "web", "browser-extension"],
22
+ bundle: { runtime: "package" },
23
+ tests: { examples: { echo: { text: "hi" } } },
24
+ ...overrides,
25
+ },
26
+ identity: {
27
+ name: "hello.tool",
28
+ version: "0.1.0",
29
+ repository: "https://github.com/octo/hello",
30
+ },
31
+ identityExtra: {},
32
+ operations: [echo, scrape, summarize],
33
+ toolfactoryVersion: "0.1.0",
34
+ packageManager: "npm",
35
+ };
36
+ }
37
+ function planned(project) {
38
+ return Object.fromEntries(browserExtension.plan(project).map((file) => [file.path, file]));
39
+ }
40
+ function content(file) {
41
+ if (file?.kind === "file")
42
+ return file.content;
43
+ if (file?.kind === "region")
44
+ return file.regions.map((region) => region.content).join("");
45
+ throw new Error("expected a whole or region file");
46
+ }
47
+ describe("browser-extension", () => {
48
+ it("plans one WXT project: generated whole files, two regions the author owns, one merge", () => {
49
+ const files = planned(fixture());
50
+ expect(Object.keys(files).sort()).toEqual([
51
+ `${HOST_DIR}/.gitignore`,
52
+ `${HOST_DIR}/README.md`,
53
+ `${HOST_DIR}/TARGETS.md`,
54
+ `${HOST_DIR}/entrypoints/background.ts`,
55
+ `${HOST_DIR}/entrypoints/example.content.ts`,
56
+ `${HOST_DIR}/entrypoints/options/index.html`,
57
+ `${HOST_DIR}/entrypoints/options/main.tsx`,
58
+ `${HOST_DIR}/entrypoints/popup/index.html`,
59
+ `${HOST_DIR}/entrypoints/popup/main.tsx`,
60
+ `${HOST_DIR}/env.d.ts`,
61
+ `${HOST_DIR}/package.json`,
62
+ `${HOST_DIR}/tests/background.test.ts`,
63
+ `${HOST_DIR}/tests/smoke.mjs`,
64
+ `${HOST_DIR}/tsconfig.json`,
65
+ `${HOST_DIR}/utils/mcp.ts`,
66
+ `${HOST_DIR}/utils/page.css`,
67
+ `${HOST_DIR}/utils/page.tsx`,
68
+ `${HOST_DIR}/vitest.config.ts`,
69
+ `${HOST_DIR}/web-ext-config.mjs`,
70
+ `${HOST_DIR}/wxt.config.ts`,
71
+ ]);
72
+ // The manifest is never written here: WXT emits one per target out of these two inputs.
73
+ expect(Object.keys(files)).not.toContain(`${HOST_DIR}/manifest.json`);
74
+ expect(files[`${HOST_DIR}/package.json`]).toMatchObject({
75
+ kind: "merge",
76
+ patch: { name: "hello.tool-browser", devDependencies: { wxt: WXT_PIN } },
77
+ });
78
+ expect(files[`${HOST_DIR}/entrypoints/background.ts`]?.kind).toBe("region");
79
+ expect(files[`${HOST_DIR}/wxt.config.ts`]?.kind).toBe("region");
80
+ // WXT names an archive after `zip.name`, which the config sets to the tool's own name.
81
+ expect(zipName(fixture(), "firefox")).toBe("hello.tool-0.1.0-firefox.zip");
82
+ expect(content(files[`${HOST_DIR}/wxt.config.ts`])).toContain('zip: { name: "hello.tool" }');
83
+ // The popup is the `web` surface's tree, imported; without it, only the pairing form ships.
84
+ expect(content(files[`${HOST_DIR}/entrypoints/popup/main.tsx`])).toContain('from "@web/App"');
85
+ // Launching the web app here means popping the same tree out of the 540px popup into a tab;
86
+ // the options page is already that tab, so it gets no button.
87
+ expect(content(files[`${HOST_DIR}/entrypoints/popup/main.tsx`])).toContain("<OpenFullPage />");
88
+ expect(content(files[`${HOST_DIR}/entrypoints/options/main.tsx`])).not.toContain("OpenFullPage");
89
+ expect(content(files[`${HOST_DIR}/utils/page.tsx`])).toContain("browser.runtime.openOptionsPage()");
90
+ const alone = planned(fixture({ surfaces: ["mcp", "browser-extension"] }));
91
+ expect(content(alone[`${HOST_DIR}/entrypoints/popup/main.tsx`])).not.toContain("@web/App");
92
+ expect(alone[`${HOST_DIR}/utils/page.css`]).toBeUndefined();
93
+ });
94
+ it("projects the manifest from capabilities, never from a hand-written permission list", () => {
95
+ // `storage` for the pairing store; `activeTab` because an operation declares `browser`.
96
+ expect(permissions(fixture(), [echo, scrape])).toEqual(["storage", "activeTab"]);
97
+ expect(permissions(fixture(), [echo])).toEqual(["storage"]);
98
+ expect(permissions(fixture({ browserExtension: { cookieExport: true, sidePanel: true } }), [echo])).toEqual(["storage", "cookies", "sidePanel"]);
99
+ const config = content(planned(fixture())[`${HOST_DIR}/wxt.config.ts`]);
100
+ // Loopback is portable only without a port (Firefox rejects a port in a match pattern), and
101
+ // the declared session domains are added to it — never `<all_urls>`.
102
+ expect(config).toContain('const HOST_PERMISSIONS = ["http://127.0.0.1/*", "http://localhost/*"];');
103
+ expect(config).toContain('id: "hello.tool@octo.github.io"');
104
+ const vandy = content(planned(fixture({ browserExtension: { authDomains: ["*://*.vanderbilt.edu/*"] } }))[`${HOST_DIR}/wxt.config.ts`]);
105
+ expect(vandy).toContain('"*://*.vanderbilt.edu/*"');
106
+ // A Chromium-only permission is dropped from the other engines' manifests, not shipped dead.
107
+ expect(config).toContain("CHROMIUM.includes(browser) || !CHROMIUM_ONLY.includes(permission)");
108
+ });
109
+ it("is native for browser and user-input operations, and degrades the export on Safari", () => {
110
+ const project = fixture();
111
+ expect(browserExtension.verdict?.(scrape, project)).toEqual({ kind: "native" });
112
+ expect(browserExtension.verdict?.(summarize, project)).toEqual({
113
+ kind: "excluded",
114
+ reason: "excluded:no-model-bridge",
115
+ });
116
+ for (const engine of ["chromium", "firefox", "safari"]) {
117
+ expect(browserVerdict(scrape, project, engine)).toEqual({ kind: "native" });
118
+ expect(browserVerdict(summarize, project, engine).kind).toBe("excluded");
119
+ }
120
+ // Safari Web Extensions cannot read httpOnly cookies, so an opted-in export degrades there.
121
+ const exporting = fixture({ browserExtension: { cookieExport: true } });
122
+ expect(browserVerdict(scrape, exporting, "firefox")).toEqual({ kind: "native" });
123
+ expect(browserVerdict(scrape, exporting, "safari")).toEqual({
124
+ kind: "degraded",
125
+ reason: "degraded:no-httponly-cookies",
126
+ });
127
+ });
128
+ it("validates with the extension's own pinned CLIs, from inside the extension", () => {
129
+ const project = fixture();
130
+ const commands = browserExtension.validate?.(project) ?? [];
131
+ expect(commands.map((command) => [command.command, ...command.args].join(" "))).toEqual([
132
+ "npm install",
133
+ "npm exec --no -- wxt prepare",
134
+ expect.stringContaining("hosts/browser"),
135
+ "npm exec --no -- wxt build",
136
+ "npm exec --no -- wxt build -b firefox",
137
+ "npm exec --no -- web-ext lint -s .output/firefox-mv2",
138
+ "npm exec --no -- playwright install chromium",
139
+ "npm run test",
140
+ "node tests/smoke.mjs",
141
+ ]);
142
+ // wxt reads its entrypoints from the working directory, so every step runs in the host dir.
143
+ expect(new Set(commands.map((command) => command.cwd))).toEqual(new Set([`/repo/${HOST_DIR}`, "/repo"]));
144
+ });
145
+ });
@@ -0,0 +1,2 @@
1
+ import type { Surface } from "../model.js";
2
+ export declare const surface: Surface;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Claude Code plugin: an unconditional projection whenever selected, validated by
3
+ * `claude plugin validate .`. Reuses the root `skills/`; MCP servers are declared inline.
4
+ * The repository is also its own single-plugin marketplace, because that is the only shape
5
+ * `claude plugin marketplace add <owner>/<repo>` (and Copilot CLI's identical command) installs.
6
+ */
7
+ import { githubSlug } from "../hosts/github.js";
8
+ import { compact, configProperties, envName, has, isSensitive, json, kernelLaunch, mcpVerdict, requiredConfig, } from "./shared.js";
9
+ export const surface = {
10
+ id: "claude",
11
+ plan(project) {
12
+ const { identity } = project;
13
+ const userConfig = Object.fromEntries(Object.entries(configProperties(project)).map(([key, property]) => [
14
+ key,
15
+ compact({
16
+ type: property.type === "number" || property.type === "integer"
17
+ ? "number"
18
+ : property.type === "boolean"
19
+ ? "boolean"
20
+ : "string",
21
+ title: property.title ?? key,
22
+ description: property.description ?? key,
23
+ sensitive: isSensitive(property) || undefined,
24
+ required: requiredConfig(project).includes(key) || undefined,
25
+ default: property.default,
26
+ }),
27
+ ]));
28
+ const launch = kernelLaunch(project, "${CLAUDE_PLUGIN_ROOT}");
29
+ const env = Object.fromEntries(Object.keys(configProperties(project)).map((key) => [envName(key), `\${user_config.${key}}`]));
30
+ const manifest = compact({
31
+ $schema: "https://json.schemastore.org/claude-code-plugin-manifest.json",
32
+ name: identity.name,
33
+ version: identity.version,
34
+ description: identity.description,
35
+ author: identity.author,
36
+ homepage: identity.homepage,
37
+ repository: identity.repository,
38
+ license: identity.license,
39
+ keywords: identity.keywords,
40
+ userConfig: Object.keys(userConfig).length ? userConfig : undefined,
41
+ // Inline rather than .mcp.json: a root .mcp.json doubles as the repo's own project MCP
42
+ // config in Claude Code, and repo templates commonly own that file.
43
+ mcpServers: has(project, "mcp")
44
+ ? {
45
+ [identity.name]: compact({ ...launch, env: Object.keys(env).length ? env : undefined }),
46
+ }
47
+ : undefined,
48
+ });
49
+ // Required by the marketplace schema; the plugin's own author is the honest owner.
50
+ const owner = compact({
51
+ name: identity.author?.name ?? githubSlug(identity.repository)?.split("/")[0] ?? identity.name,
52
+ email: identity.author?.email,
53
+ url: identity.author?.url,
54
+ });
55
+ const marketplace = compact({
56
+ $schema: "https://json.schemastore.org/claude-code-marketplace.json",
57
+ name: identity.name,
58
+ description: identity.description,
59
+ owner,
60
+ // `source: "./"` is the marketplace root itself: one repository, one plugin, no submodule.
61
+ plugins: [compact({ name: identity.name, source: "./", description: identity.description })],
62
+ });
63
+ return [
64
+ { kind: "file", path: ".claude-plugin/plugin.json", content: json(manifest) },
65
+ { kind: "file", path: ".claude-plugin/marketplace.json", content: json(marketplace) },
66
+ ];
67
+ },
68
+ validate(project) {
69
+ return [
70
+ {
71
+ label: "claude plugin validate",
72
+ command: "claude",
73
+ // Not --strict: a repo-root CLAUDE.md (the AGENTS.md pointer every repo carries) is a
74
+ // strict-mode warning, and warnings never describe the generated manifest.
75
+ args: ["plugin", "validate", "."],
76
+ cwd: project.root,
77
+ },
78
+ ];
79
+ },
80
+ verdict: mcpVerdict,
81
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Codex plugin: `.codex-plugin/plugin.json` with the required `interface` block, plus
3
+ * `.agents/plugins/marketplace.json` listing the repository's own plugin (`source: {source:
4
+ * "local", path: "."}`) — the shape `codex plugin marketplace add <owner>/<repo>` (or a local
5
+ * path) reads, confirmed against developers.openai.com/codex/plugins/build and proven end to end
6
+ * in `validate()` below (`marketplace add` → `plugin add` → `plugin list --available`, real Codex
7
+ * CLI, no mocks). The public ChatGPT/Codex plugin directory is a separate hosted-review portal
8
+ * this surface does not reach.
9
+ */
10
+ import type { Surface } from "../model.js";
11
+ /** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
12
+ export declare const CODEX_PIN = "0.152.1";
13
+ export declare const surface: Surface;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Codex plugin: `.codex-plugin/plugin.json` with the required `interface` block, plus
3
+ * `.agents/plugins/marketplace.json` listing the repository's own plugin (`source: {source:
4
+ * "local", path: "."}`) — the shape `codex plugin marketplace add <owner>/<repo>` (or a local
5
+ * path) reads, confirmed against developers.openai.com/codex/plugins/build and proven end to end
6
+ * in `validate()` below (`marketplace add` → `plugin add` → `plugin list --available`, real Codex
7
+ * CLI, no mocks). The public ChatGPT/Codex plugin directory is a separate hosted-review portal
8
+ * this surface does not reach.
9
+ */
10
+ import { projectName } from "../identity/name.js";
11
+ import { compact, has, json, kernelLaunch, mcpVerdict } from "./shared.js";
12
+ /** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
13
+ export const CODEX_PIN = "0.152.1";
14
+ export const surface = {
15
+ id: "codex",
16
+ plan(project) {
17
+ const { identity } = project;
18
+ const displayName = projectName.display(identity.name);
19
+ const codexInterface = {
20
+ displayName,
21
+ shortDescription: (identity.description ?? identity.name).slice(0, 80),
22
+ longDescription: identity.description ?? identity.name,
23
+ developerName: identity.author?.name ?? "unknown",
24
+ category: "Productivity",
25
+ capabilities: [],
26
+ ...(project.tool.codex?.interface ?? {}),
27
+ };
28
+ const manifest = compact({
29
+ name: identity.name,
30
+ version: identity.version ?? "0.1.0",
31
+ description: identity.description ?? identity.name,
32
+ author: compact({
33
+ name: identity.author?.name ?? "unknown",
34
+ email: identity.author?.email,
35
+ url: identity.author?.url,
36
+ }),
37
+ homepage: identity.homepage,
38
+ repository: identity.repository,
39
+ license: identity.license,
40
+ keywords: identity.keywords,
41
+ skills: has(project, "skill") ? "./skills/" : undefined,
42
+ mcpServers: has(project, "mcp")
43
+ ? { [identity.name]: kernelLaunch(project, "${PLUGIN_ROOT}") }
44
+ : undefined,
45
+ interface: codexInterface,
46
+ });
47
+ // `source: "local"` + `path: "."` is the marketplace root itself: one repository, one
48
+ // plugin. `policy.installation`/`policy.authentication`/`category` are required on every
49
+ // entry per the build guide; `ON_INSTALL` mirrors the guide's own sample.
50
+ const marketplace = compact({
51
+ name: identity.name,
52
+ interface: { displayName: codexInterface.displayName },
53
+ plugins: [
54
+ compact({
55
+ name: identity.name,
56
+ source: { source: "local", path: "." },
57
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
58
+ category: codexInterface.category,
59
+ }),
60
+ ],
61
+ });
62
+ return [
63
+ { kind: "file", path: ".codex-plugin/plugin.json", content: json(manifest) },
64
+ { kind: "file", path: ".agents/plugins/marketplace.json", content: json(marketplace) },
65
+ ];
66
+ },
67
+ validate(project) {
68
+ // A fresh CODEX_HOME per run, matching the dsh surface's pattern: `marketplace add` reads
69
+ // marketplace.json, `plugin add` installs the plugin entry it names (parsing plugin.json),
70
+ // and `plugin list --available` confirms it resolves — the real install path end to end.
71
+ const pluginId = `${project.identity.name}@${project.identity.name}`;
72
+ // `plugin add` copies the plugin directory verbatim. Stage only the distributable tree: a
73
+ // source checkout may carry dependencies, a source-pinned toolfactory cache, nested Git data,
74
+ // or environment files that must never become part of an installed plugin.
75
+ const script = [
76
+ 'TF_CODEX_HOME="$(mktemp -d)"; TF_STAGE="$(mktemp -d)"',
77
+ 'tar --exclude=.git --exclude=*/.git --exclude=.cache --exclude=.env --exclude=.env.* --exclude=node_modules --exclude=dist -cf - . | tar -xf - -C "$TF_STAGE"',
78
+ 'test ! -e "$TF_STAGE/.cache" && test ! -e "$TF_STAGE/.env" && test -z "$(find "$TF_STAGE" -type d -name .git -print -quit)" || { rm -rf "$TF_CODEX_HOME" "$TF_STAGE"; exit 1; }',
79
+ 'cd "$TF_STAGE"',
80
+ `out="$({ CODEX_HOME="$TF_CODEX_HOME" npx -y @openai/codex@${CODEX_PIN} plugin marketplace add . --json && CODEX_HOME="$TF_CODEX_HOME" npx -y @openai/codex@${CODEX_PIN} plugin add ${pluginId} --json && CODEX_HOME="$TF_CODEX_HOME" npx -y @openai/codex@${CODEX_PIN} plugin list --available --json; } 2>&1)"`,
81
+ "status=$?",
82
+ 'rm -rf "$TF_CODEX_HOME" "$TF_STAGE"',
83
+ 'printf "%s\\n" "$out"',
84
+ "exit $status",
85
+ ].join("\n");
86
+ return [
87
+ {
88
+ label: "codex plugin marketplace add / plugin add / plugin list",
89
+ command: "sh",
90
+ args: ["-c", script],
91
+ cwd: project.root,
92
+ },
93
+ ];
94
+ },
95
+ verdict: (operation) => {
96
+ const verdict = mcpVerdict(operation);
97
+ return verdict.kind === "native"
98
+ ? { kind: "degraded", reason: "degraded:loader-unverified" }
99
+ : verdict;
100
+ },
101
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { surface as codex } from "./codex.js";
3
+ const echo = { name: "echo", inputSchema: { type: "object" }, requires: [] };
4
+ function project(surfaces) {
5
+ return {
6
+ root: "/repo",
7
+ tool: {
8
+ schemaVersion: 1,
9
+ identity: "plugin.json",
10
+ binding: "typescript",
11
+ surfaces,
12
+ bundle: { runtime: "package" },
13
+ tests: { examples: {} },
14
+ },
15
+ identity: {
16
+ name: "hello",
17
+ version: "0.1.0",
18
+ description: "Say hello",
19
+ author: { name: "GoatInAHat" },
20
+ repository: "https://github.com/GoatInAHat/Hello-Tool",
21
+ },
22
+ identityExtra: {},
23
+ operations: [echo],
24
+ toolfactoryVersion: "0.1.0",
25
+ packageManager: "npm",
26
+ };
27
+ }
28
+ describe("codex", () => {
29
+ it("makes the repository its own single-plugin marketplace, and validates with the real codex CLI", () => {
30
+ const files = codex.plan(project(["codex", "mcp"]));
31
+ const marketplace = files.find((entry) => entry.path === ".agents/plugins/marketplace.json");
32
+ if (marketplace?.kind !== "file")
33
+ throw new Error("expected a whole file");
34
+ expect(JSON.parse(marketplace.content)).toMatchObject({
35
+ name: "hello",
36
+ interface: { displayName: "Hello" },
37
+ plugins: [
38
+ {
39
+ name: "hello",
40
+ source: { source: "local", path: "." },
41
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
42
+ category: "Productivity",
43
+ },
44
+ ],
45
+ });
46
+ const [command] = codex.validate?.(project(["codex", "mcp"])) ?? [];
47
+ const script = command?.args.join(" ") ?? "";
48
+ expect(command?.command).toBe("sh");
49
+ expect(script).toContain("plugin marketplace add . --json");
50
+ expect(script).toContain("plugin add hello@hello --json");
51
+ expect(script).toContain("plugin list --available --json");
52
+ expect(script).toContain('TF_CODEX_HOME="$(mktemp -d)"');
53
+ expect(script).toContain("--exclude=.cache --exclude=.env --exclude=.env.*");
54
+ expect(script).toContain("--exclude=.git --exclude=*/.git");
55
+ expect(script).toContain('CODEX_HOME="$TF_CODEX_HOME"');
56
+ expect(script).toContain('test ! -e "$TF_STAGE/.cache"');
57
+ });
58
+ it("ships only the skill when runtime is none", () => {
59
+ const target = project(["skill", "codex"]);
60
+ target.tool.runtime = "none";
61
+ const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
62
+ if (manifest?.kind !== "file")
63
+ throw new Error("expected a manifest");
64
+ expect(JSON.parse(manifest.content)).toMatchObject({ skills: "./skills/" });
65
+ expect(JSON.parse(manifest.content)).not.toHaveProperty("mcpServers");
66
+ });
67
+ });
@@ -0,0 +1,2 @@
1
+ import type { Surface } from "../model.js";
2
+ export declare const surface: Surface;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Cursor plugin: `.cursor-plugin/plugin.json`; config becomes `variables` (JSON Schema)
3
+ * and the bundle's `mcp.json` is shared. Loader unverified, like Codex.
4
+ */
5
+ import { projectName } from "../identity/name.js";
6
+ import { compact, configProperties, envName, has, json, mcpVerdict, requiredConfig, } from "./shared.js";
7
+ export const surface = {
8
+ id: "cursor",
9
+ plan(project) {
10
+ const { identity } = project;
11
+ const properties = configProperties(project);
12
+ const variables = Object.keys(properties).length
13
+ ? {
14
+ type: "object",
15
+ properties: Object.fromEntries(Object.entries(properties).map(([key, property]) => [
16
+ envName(key),
17
+ compact({
18
+ type: property.type ?? "string",
19
+ title: property.title ?? key,
20
+ description: property.description,
21
+ default: property.default,
22
+ }),
23
+ ])),
24
+ required: requiredConfig(project).map((key) => envName(key)),
25
+ }
26
+ : undefined;
27
+ const manifest = compact({
28
+ name: projectName.cursor(identity.name),
29
+ version: identity.version,
30
+ description: identity.description,
31
+ author: identity.author
32
+ ? compact({ name: identity.author.name ?? "unknown", email: identity.author.email })
33
+ : undefined,
34
+ homepage: identity.homepage,
35
+ repository: identity.repository,
36
+ license: identity.license,
37
+ keywords: identity.keywords,
38
+ skills: has(project, "skill") ? "./skills/" : undefined,
39
+ mcpServers: has(project, "agent-plugins") ? "./mcp.json" : undefined,
40
+ variables,
41
+ });
42
+ return [{ kind: "file", path: ".cursor-plugin/plugin.json", content: json(manifest) }];
43
+ },
44
+ verdict: (operation) => {
45
+ const verdict = mcpVerdict(operation);
46
+ return verdict.kind === "native"
47
+ ? { kind: "degraded", reason: "degraded:loader-unverified" }
48
+ : verdict;
49
+ },
50
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildPlan } from "../project/plan.js";
3
+ import { surface as claude } from "./claude.js";
4
+ import { surface as npm } from "./npm.js";
5
+ import { MCP_NAME_BEGIN, MCP_NAME_END } from "./pypi.js";
6
+ import { INSTALL_BEGIN, INSTALL_END, surface as readme } from "./readme.js";
7
+ const echo = { name: "echo", inputSchema: { type: "object" }, requires: [] };
8
+ function project(surfaces, overrides = {}) {
9
+ return {
10
+ root: "/repo",
11
+ tool: {
12
+ schemaVersion: 1,
13
+ identity: "plugin.json",
14
+ binding: "typescript",
15
+ surfaces,
16
+ bundle: { runtime: "package" },
17
+ tests: { examples: {} },
18
+ },
19
+ identity: {
20
+ name: "hello",
21
+ version: "0.1.0",
22
+ description: "Say hello",
23
+ author: { name: "GoatInAHat" },
24
+ repository: "https://github.com/GoatInAHat/Hello-Tool",
25
+ },
26
+ identityExtra: {},
27
+ operations: [echo],
28
+ toolfactoryVersion: "0.1.0",
29
+ packageManager: "npm",
30
+ ...overrides,
31
+ };
32
+ }
33
+ function region(target) {
34
+ const [file] = readme.plan(target);
35
+ if (file?.kind !== "region")
36
+ throw new Error("expected a region file");
37
+ return file.regions[0]?.content ?? "";
38
+ }
39
+ describe("npm", () => {
40
+ it("writes the repository in npm's object form, preserving the slug's case, and lints with publint", () => {
41
+ const [file] = npm.plan(project(["npm", "cli"]));
42
+ if (file?.kind !== "merge")
43
+ throw new Error("expected a merge file");
44
+ expect(file.patch.repository).toEqual({
45
+ type: "git",
46
+ url: "git+https://github.com/GoatInAHat/Hello-Tool.git",
47
+ });
48
+ expect(npm.validate?.(project(["npm"])).map((command) => command.args.join(" "))).toContain("--yes publint --strict");
49
+ });
50
+ });
51
+ describe("claude", () => {
52
+ it("makes the repository its own single-plugin marketplace", () => {
53
+ const files = claude.plan(project(["claude", "mcp"]));
54
+ const file = files.find((entry) => entry.path === ".claude-plugin/marketplace.json");
55
+ if (file?.kind !== "file")
56
+ throw new Error("expected a whole file");
57
+ expect(JSON.parse(file.content)).toMatchObject({
58
+ name: "hello",
59
+ owner: { name: "GoatInAHat" },
60
+ plugins: [{ name: "hello", source: "./", description: "Say hello" }],
61
+ });
62
+ });
63
+ });
64
+ describe("readme", () => {
65
+ it("plans one install line per selected surface inside an author-owned template", () => {
66
+ const [file] = readme.plan(project(["skill", "mcp", "claude", "npm"]));
67
+ if (file?.kind !== "region")
68
+ throw new Error("expected a region file");
69
+ expect(file.path).toBe("README.md");
70
+ expect(file.template).toContain(`${INSTALL_BEGIN}\n${INSTALL_END}`);
71
+ const body = file.regions[0]?.content ?? "";
72
+ expect(body).toContain("## Install");
73
+ expect(body).toContain("`npx skills add GoatInAHat/Hello-Tool`");
74
+ expect(body).toContain("[![Agent Skill](https://img.shields.io/badge/Agent_Skill-available-5B5BD6)](https://github.com/GoatInAHat/Hello-Tool)");
75
+ expect(body).toContain("`npx -y hello mcp`");
76
+ // The MCP line carries both first-party install badges once a package registry is selected,
77
+ // computed from the same unpinned `npx -y hello mcp` launch as the text line.
78
+ expect(body).toContain("https://vscode.dev/redirect/mcp/install?name=hello&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22hello%22%2C%22mcp%22%5D%7D");
79
+ expect(body).toContain("https://cursor.com/en/install-mcp?name=hello&config=");
80
+ expect(body).toContain("https://cursor.com/deeplink/mcp-install-dark.svg");
81
+ expect(body).toContain("`claude plugin marketplace add GoatInAHat/Hello-Tool`");
82
+ expect(body).toContain("`claude plugin install hello@hello`");
83
+ expect(body).toContain("`npm install hello`");
84
+ expect(body).not.toContain("openclaw");
85
+ });
86
+ it("adds the Codex, Gemini, MCPB and browser-extension lines, and skips the badges without a registry", () => {
87
+ const body = region(project(["mcp", "codex", "gemini", "mcpb", "browser-extension"]));
88
+ expect(body).not.toContain("vscode.dev/redirect/mcp/install");
89
+ expect(body).not.toContain("cursor.com/en/install-mcp");
90
+ expect(body).toContain("`codex plugin marketplace add GoatInAHat/Hello-Tool`");
91
+ expect(body).toContain("`codex plugin add hello@hello`");
92
+ expect(body).toContain("`gemini extensions install https://github.com/GoatInAHat/Hello-Tool`");
93
+ expect(body).toContain("download `hello.mcpb` from the GitHub Release and double-click to install");
94
+ // The extension's three channels — unpacked from a checkout, the release assets (only the
95
+ // signed xpi installs itself), the store listings — and the pairing step that ends each.
96
+ expect(body).toContain("Load unpacked → `hosts/browser/.output/chrome-mv3`");
97
+ expect(body).toContain("web-ext run");
98
+ expect(body).toContain("`hello-0.1.0-chrome.zip`, `hello-0.1.0-firefox.zip`, `hello-0.1.0-edge.zip`");
99
+ expect(body).toContain("Mozilla-signed `.xpi`");
100
+ expect(body).toContain("`npx -y hello mcp --http --pair`");
101
+ });
102
+ it("names the one command that opens the web app, and only when that surface is selected", () => {
103
+ expect(region(project(["mcp", "web"]))).toContain("`npx -y hello mcp --http --open`");
104
+ expect(region(project(["mcp"]))).not.toContain("--http --open");
105
+ });
106
+ it("carries pypi's mcp-name region beside its own, in one README the plan merges", () => {
107
+ const surfaces = ["pypi", "mcp-registry"];
108
+ const python = project(surfaces, {
109
+ root: "/nonexistent",
110
+ tool: { ...project(surfaces).tool, binding: "python" },
111
+ });
112
+ const file = buildPlan(python).find((planned) => planned.path === "README.md");
113
+ if (file?.kind !== "region")
114
+ throw new Error("expected a region file");
115
+ expect(file.regions.map((entry) => entry.begin).sort()).toEqual([INSTALL_BEGIN, MCP_NAME_BEGIN].sort());
116
+ expect(file.template).toContain(`${INSTALL_BEGIN}\n${INSTALL_END}`);
117
+ expect(file.template).toContain(`${MCP_NAME_BEGIN}\n${MCP_NAME_END}`);
118
+ });
119
+ it("falls back to the local checkout when the identity carries no GitHub repository", () => {
120
+ const surfaces = [
121
+ "skill",
122
+ "claude",
123
+ "codex",
124
+ "gemini",
125
+ "openclaw-native",
126
+ "hermes-native",
127
+ "clawhub",
128
+ "dsh",
129
+ "pypi",
130
+ ];
131
+ const local = region(project(surfaces, {
132
+ identity: { name: "hello", version: "0.1.0", description: "Say hello" },
133
+ tool: { ...project(surfaces).tool, binding: "python" },
134
+ }));
135
+ expect(local).not.toContain("skills.sh");
136
+ expect(local).not.toContain("npx skills add");
137
+ expect(local).toContain("`claude plugin marketplace add .`");
138
+ expect(local).toContain("`codex plugin marketplace add .`");
139
+ expect(local).toContain("`gemini extensions link .` from a checkout");
140
+ expect(local).toContain("`hermes plugins install file://$PWD#hosts/hermes/hello_hermes`");
141
+ expect(local).toContain("openclaw plugins install --link hosts/openclaw");
142
+ expect(local).toContain("openclaw plugins install clawhub:openclaw-plugin-hello");
143
+ expect(local).toContain("dsh plugin --profile <profile> add ./hosts/dsh");
144
+ expect(local).toContain("`uv add hello`");
145
+ });
146
+ });
@@ -0,0 +1,25 @@
1
+ import type { Project, Surface } from "../model.js";
2
+ /**
3
+ * The `@deepseek-ai/dsh` release `validate()` boots. Pinned, not `@alpha`: npm's `latest` tag
4
+ * lags the alpha line by weeks, so an unpinned install is neither current nor reproducible.
5
+ * Renovate bumps this constant (`renovate.json` `customManagers`).
6
+ */
7
+ export declare const DSH_PIN = "0.1.2-alpha.5";
8
+ export declare const HOST_DIR = "hosts/dsh";
9
+ /** The patch file name `dsh.bundle.patch` points at; DSH's own convention for a Cordis overlay. */
10
+ export declare const PATCH_FILE = "cordis.patch.yml";
11
+ /**
12
+ * The same row against this checkout instead of the published package. Kept out of `files`, so
13
+ * `npm pack ./hosts/dsh` never ships it: it is what `validate()` boots and what a developer
14
+ * passes to `dsh --patch` to drive the working tree.
15
+ */
16
+ export declare const LOCAL_PATCH_FILE = "cordis.local.patch.yml";
17
+ /** `headless` is one-shot and ships an app bundle; a profile without one blocks forever. */
18
+ export declare const PROFILE = "headless";
19
+ /** The public name a bridged MCP tool takes in DSH; `mcp__<serverName>__<rawName>` is its contract. */
20
+ export declare function toolName(project: Project, operation: string): string;
21
+ /** The row id and, with it, the `# == <bundle>` layer `--dump-config` prints. */
22
+ export declare function rowId(project: Project): string;
23
+ /** The tarball `npm pack ./hosts/dsh` writes into the release assets, by name. */
24
+ export declare function dshTarball(project: Project): string;
25
+ export declare const surface: Surface;