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,99 @@
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 type { Surface } from "../model.js";
9
+ import {
10
+ compact,
11
+ configProperties,
12
+ envName,
13
+ has,
14
+ isSensitive,
15
+ json,
16
+ kernelLaunch,
17
+ mcpVerdict,
18
+ requiredConfig,
19
+ } from "./shared.js";
20
+
21
+ export const surface: Surface = {
22
+ id: "claude",
23
+ plan(project) {
24
+ const { identity } = project;
25
+ const userConfig = Object.fromEntries(
26
+ Object.entries(configProperties(project)).map(([key, property]) => [
27
+ key,
28
+ compact({
29
+ type:
30
+ property.type === "number" || property.type === "integer"
31
+ ? "number"
32
+ : property.type === "boolean"
33
+ ? "boolean"
34
+ : "string",
35
+ title: (property.title as string | undefined) ?? key,
36
+ description: (property.description as string | undefined) ?? key,
37
+ sensitive: isSensitive(property) || undefined,
38
+ required: requiredConfig(project).includes(key) || undefined,
39
+ default: property.default,
40
+ }),
41
+ ]),
42
+ );
43
+ const launch = kernelLaunch(project, "${CLAUDE_PLUGIN_ROOT}");
44
+ const env = Object.fromEntries(
45
+ Object.keys(configProperties(project)).map((key) => [envName(key), `\${user_config.${key}}`]),
46
+ );
47
+ const manifest = compact({
48
+ $schema: "https://json.schemastore.org/claude-code-plugin-manifest.json",
49
+ name: identity.name,
50
+ version: identity.version,
51
+ description: identity.description,
52
+ author: identity.author,
53
+ homepage: identity.homepage,
54
+ repository: identity.repository,
55
+ license: identity.license,
56
+ keywords: identity.keywords,
57
+ userConfig: Object.keys(userConfig).length ? userConfig : undefined,
58
+ // Inline rather than .mcp.json: a root .mcp.json doubles as the repo's own project MCP
59
+ // config in Claude Code, and repo templates commonly own that file.
60
+ mcpServers: has(project, "mcp")
61
+ ? {
62
+ [identity.name]: compact({ ...launch, env: Object.keys(env).length ? env : undefined }),
63
+ }
64
+ : undefined,
65
+ });
66
+ // Required by the marketplace schema; the plugin's own author is the honest owner.
67
+ const owner = compact({
68
+ name:
69
+ identity.author?.name ?? githubSlug(identity.repository)?.split("/")[0] ?? identity.name,
70
+ email: identity.author?.email,
71
+ url: identity.author?.url,
72
+ });
73
+ const marketplace = compact({
74
+ $schema: "https://json.schemastore.org/claude-code-marketplace.json",
75
+ name: identity.name,
76
+ description: identity.description,
77
+ owner,
78
+ // `source: "./"` is the marketplace root itself: one repository, one plugin, no submodule.
79
+ plugins: [compact({ name: identity.name, source: "./", description: identity.description })],
80
+ });
81
+ return [
82
+ { kind: "file", path: ".claude-plugin/plugin.json", content: json(manifest) },
83
+ { kind: "file", path: ".claude-plugin/marketplace.json", content: json(marketplace) },
84
+ ];
85
+ },
86
+ validate(project) {
87
+ return [
88
+ {
89
+ label: "claude plugin validate",
90
+ command: "claude",
91
+ // Not --strict: a repo-root CLAUDE.md (the AGENTS.md pointer every repo carries) is a
92
+ // strict-mode warning, and warnings never describe the generated manifest.
93
+ args: ["plugin", "validate", "."],
94
+ cwd: project.root,
95
+ },
96
+ ];
97
+ },
98
+ verdict: mcpVerdict,
99
+ };
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Operation, Project, SurfaceId } from "../model.js";
3
+ import { surface as codex } from "./codex.js";
4
+
5
+ const echo: Operation = { name: "echo", inputSchema: { type: "object" }, requires: [] };
6
+
7
+ function project(surfaces: SurfaceId[]): Project {
8
+ return {
9
+ root: "/repo",
10
+ tool: {
11
+ schemaVersion: 1,
12
+ identity: "plugin.json",
13
+ binding: "typescript",
14
+ surfaces,
15
+ bundle: { runtime: "package" },
16
+ tests: { examples: {} },
17
+ },
18
+ identity: {
19
+ name: "hello",
20
+ version: "0.1.0",
21
+ description: "Say hello",
22
+ author: { name: "GoatInAHat" },
23
+ repository: "https://github.com/GoatInAHat/Hello-Tool",
24
+ },
25
+ identityExtra: {},
26
+ operations: [echo],
27
+ toolfactoryVersion: "0.1.0",
28
+ packageManager: "npm",
29
+ };
30
+ }
31
+
32
+ describe("codex", () => {
33
+ it("makes the repository its own single-plugin marketplace, and validates with the real codex CLI", () => {
34
+ const files = codex.plan(project(["codex", "mcp"]));
35
+ const marketplace = files.find((entry) => entry.path === ".agents/plugins/marketplace.json");
36
+ if (marketplace?.kind !== "file") throw new Error("expected a whole file");
37
+ expect(JSON.parse(marketplace.content)).toMatchObject({
38
+ name: "hello",
39
+ interface: { displayName: "Hello" },
40
+ plugins: [
41
+ {
42
+ name: "hello",
43
+ source: { source: "local", path: "." },
44
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
45
+ category: "Productivity",
46
+ },
47
+ ],
48
+ });
49
+
50
+ const [command] = codex.validate?.(project(["codex", "mcp"])) ?? [];
51
+ const script = command?.args.join(" ") ?? "";
52
+ expect(command?.command).toBe("sh");
53
+ expect(script).toContain("plugin marketplace add . --json");
54
+ expect(script).toContain("plugin add hello@hello --json");
55
+ expect(script).toContain("plugin list --available --json");
56
+ expect(script).toContain('TF_CODEX_HOME="$(mktemp -d)"');
57
+ expect(script).toContain("--exclude=.cache --exclude=.env --exclude=.env.*");
58
+ expect(script).toContain("--exclude=.git --exclude=*/.git");
59
+ expect(script).toContain('CODEX_HOME="$TF_CODEX_HOME"');
60
+ expect(script).toContain('test ! -e "$TF_STAGE/.cache"');
61
+ });
62
+
63
+ it("ships only the skill when runtime is none", () => {
64
+ const target = project(["skill", "codex"]);
65
+ target.tool.runtime = "none";
66
+ const manifest = codex.plan(target).find((entry) => entry.path === ".codex-plugin/plugin.json");
67
+ if (manifest?.kind !== "file") throw new Error("expected a manifest");
68
+ expect(JSON.parse(manifest.content)).toMatchObject({ skills: "./skills/" });
69
+ expect(JSON.parse(manifest.content)).not.toHaveProperty("mcpServers");
70
+ });
71
+ });
@@ -0,0 +1,105 @@
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
+
11
+ import { projectName } from "../identity/name.js";
12
+ import type { Surface } from "../model.js";
13
+ import { compact, has, json, kernelLaunch, mcpVerdict } from "./shared.js";
14
+
15
+ /** The Codex CLI (`@openai/codex`) `validate()` is proven against — pin for a deterministic gate. */
16
+ export const CODEX_PIN = "0.152.1";
17
+
18
+ export const surface: Surface = {
19
+ id: "codex",
20
+ plan(project) {
21
+ const { identity } = project;
22
+ const displayName = projectName.display(identity.name);
23
+ const codexInterface = {
24
+ displayName,
25
+ shortDescription: (identity.description ?? identity.name).slice(0, 80),
26
+ longDescription: identity.description ?? identity.name,
27
+ developerName: identity.author?.name ?? "unknown",
28
+ category: "Productivity",
29
+ capabilities: [],
30
+ ...(project.tool.codex?.interface ?? {}),
31
+ };
32
+ const manifest = compact({
33
+ name: identity.name,
34
+ version: identity.version ?? "0.1.0",
35
+ description: identity.description ?? identity.name,
36
+ author: compact({
37
+ name: identity.author?.name ?? "unknown",
38
+ email: identity.author?.email,
39
+ url: identity.author?.url,
40
+ }),
41
+ homepage: identity.homepage,
42
+ repository: identity.repository,
43
+ license: identity.license,
44
+ keywords: identity.keywords,
45
+ skills: has(project, "skill") ? "./skills/" : undefined,
46
+ mcpServers: has(project, "mcp")
47
+ ? { [identity.name]: kernelLaunch(project, "${PLUGIN_ROOT}") }
48
+ : undefined,
49
+ interface: codexInterface,
50
+ });
51
+ // `source: "local"` + `path: "."` is the marketplace root itself: one repository, one
52
+ // plugin. `policy.installation`/`policy.authentication`/`category` are required on every
53
+ // entry per the build guide; `ON_INSTALL` mirrors the guide's own sample.
54
+ const marketplace = compact({
55
+ name: identity.name,
56
+ interface: { displayName: codexInterface.displayName },
57
+ plugins: [
58
+ compact({
59
+ name: identity.name,
60
+ source: { source: "local", path: "." },
61
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
62
+ category: codexInterface.category,
63
+ }),
64
+ ],
65
+ });
66
+ return [
67
+ { kind: "file", path: ".codex-plugin/plugin.json", content: json(manifest) },
68
+ { kind: "file", path: ".agents/plugins/marketplace.json", content: json(marketplace) },
69
+ ];
70
+ },
71
+ validate(project) {
72
+ // A fresh CODEX_HOME per run, matching the dsh surface's pattern: `marketplace add` reads
73
+ // marketplace.json, `plugin add` installs the plugin entry it names (parsing plugin.json),
74
+ // and `plugin list --available` confirms it resolves — the real install path end to end.
75
+ const pluginId = `${project.identity.name}@${project.identity.name}`;
76
+ // `plugin add` copies the plugin directory verbatim. Stage only the distributable tree: a
77
+ // source checkout may carry dependencies, a source-pinned toolfactory cache, nested Git data,
78
+ // or environment files that must never become part of an installed plugin.
79
+ const script = [
80
+ 'TF_CODEX_HOME="$(mktemp -d)"; TF_STAGE="$(mktemp -d)"',
81
+ 'tar --exclude=.git --exclude=*/.git --exclude=.cache --exclude=.env --exclude=.env.* --exclude=node_modules --exclude=dist -cf - . | tar -xf - -C "$TF_STAGE"',
82
+ '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; }',
83
+ 'cd "$TF_STAGE"',
84
+ `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)"`,
85
+ "status=$?",
86
+ 'rm -rf "$TF_CODEX_HOME" "$TF_STAGE"',
87
+ 'printf "%s\\n" "$out"',
88
+ "exit $status",
89
+ ].join("\n");
90
+ return [
91
+ {
92
+ label: "codex plugin marketplace add / plugin add / plugin list",
93
+ command: "sh",
94
+ args: ["-c", script],
95
+ cwd: project.root,
96
+ },
97
+ ];
98
+ },
99
+ verdict: (operation) => {
100
+ const verdict = mcpVerdict(operation);
101
+ return verdict.kind === "native"
102
+ ? { kind: "degraded", reason: "degraded:loader-unverified" }
103
+ : verdict;
104
+ },
105
+ };
@@ -0,0 +1,62 @@
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 type { Surface } from "../model.js";
7
+ import {
8
+ compact,
9
+ configProperties,
10
+ envName,
11
+ has,
12
+ json,
13
+ mcpVerdict,
14
+ requiredConfig,
15
+ } from "./shared.js";
16
+
17
+ export const surface: Surface = {
18
+ id: "cursor",
19
+ plan(project) {
20
+ const { identity } = project;
21
+ const properties = configProperties(project);
22
+ const variables = Object.keys(properties).length
23
+ ? {
24
+ type: "object",
25
+ properties: Object.fromEntries(
26
+ Object.entries(properties).map(([key, property]) => [
27
+ envName(key),
28
+ compact({
29
+ type: property.type ?? "string",
30
+ title: (property.title as string | undefined) ?? key,
31
+ description: property.description,
32
+ default: property.default,
33
+ }),
34
+ ]),
35
+ ),
36
+ required: requiredConfig(project).map((key) => envName(key)),
37
+ }
38
+ : undefined;
39
+ const manifest = compact({
40
+ name: projectName.cursor(identity.name),
41
+ version: identity.version,
42
+ description: identity.description,
43
+ author: identity.author
44
+ ? compact({ name: identity.author.name ?? "unknown", email: identity.author.email })
45
+ : undefined,
46
+ homepage: identity.homepage,
47
+ repository: identity.repository,
48
+ license: identity.license,
49
+ keywords: identity.keywords,
50
+ skills: has(project, "skill") ? "./skills/" : undefined,
51
+ mcpServers: has(project, "agent-plugins") ? "./mcp.json" : undefined,
52
+ variables,
53
+ });
54
+ return [{ kind: "file", path: ".cursor-plugin/plugin.json", content: json(manifest) }];
55
+ },
56
+ verdict: (operation) => {
57
+ const verdict = mcpVerdict(operation);
58
+ return verdict.kind === "native"
59
+ ? { kind: "degraded", reason: "degraded:loader-unverified" }
60
+ : verdict;
61
+ },
62
+ };
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Operation, Project, SurfaceId } from "../model.js";
3
+ import { buildPlan } from "../project/plan.js";
4
+ import { surface as claude } from "./claude.js";
5
+ import { surface as npm } from "./npm.js";
6
+ import { MCP_NAME_BEGIN, MCP_NAME_END } from "./pypi.js";
7
+ import { INSTALL_BEGIN, INSTALL_END, surface as readme } from "./readme.js";
8
+
9
+ const echo: Operation = { name: "echo", inputSchema: { type: "object" }, requires: [] };
10
+
11
+ function project(surfaces: SurfaceId[], overrides: Partial<Project> = {}): Project {
12
+ return {
13
+ root: "/repo",
14
+ tool: {
15
+ schemaVersion: 1,
16
+ identity: "plugin.json",
17
+ binding: "typescript",
18
+ surfaces,
19
+ bundle: { runtime: "package" },
20
+ tests: { examples: {} },
21
+ },
22
+ identity: {
23
+ name: "hello",
24
+ version: "0.1.0",
25
+ description: "Say hello",
26
+ author: { name: "GoatInAHat" },
27
+ repository: "https://github.com/GoatInAHat/Hello-Tool",
28
+ },
29
+ identityExtra: {},
30
+ operations: [echo],
31
+ toolfactoryVersion: "0.1.0",
32
+ packageManager: "npm",
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ function region(target: Project): string {
38
+ const [file] = readme.plan(target);
39
+ if (file?.kind !== "region") throw new Error("expected a region file");
40
+ return file.regions[0]?.content ?? "";
41
+ }
42
+
43
+ describe("npm", () => {
44
+ it("writes the repository in npm's object form, preserving the slug's case, and lints with publint", () => {
45
+ const [file] = npm.plan(project(["npm", "cli"]));
46
+ if (file?.kind !== "merge") throw new Error("expected a merge file");
47
+ expect(file.patch.repository).toEqual({
48
+ type: "git",
49
+ url: "git+https://github.com/GoatInAHat/Hello-Tool.git",
50
+ });
51
+ expect(npm.validate?.(project(["npm"])).map((command) => command.args.join(" "))).toContain(
52
+ "--yes publint --strict",
53
+ );
54
+ });
55
+ });
56
+
57
+ describe("claude", () => {
58
+ it("makes the repository its own single-plugin marketplace", () => {
59
+ const files = claude.plan(project(["claude", "mcp"]));
60
+ const file = files.find((entry) => entry.path === ".claude-plugin/marketplace.json");
61
+ if (file?.kind !== "file") throw new Error("expected a whole file");
62
+ expect(JSON.parse(file.content)).toMatchObject({
63
+ name: "hello",
64
+ owner: { name: "GoatInAHat" },
65
+ plugins: [{ name: "hello", source: "./", description: "Say hello" }],
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("readme", () => {
71
+ it("plans one install line per selected surface inside an author-owned template", () => {
72
+ const [file] = readme.plan(project(["skill", "mcp", "claude", "npm"]));
73
+ if (file?.kind !== "region") throw new Error("expected a region file");
74
+ expect(file.path).toBe("README.md");
75
+ expect(file.template).toContain(`${INSTALL_BEGIN}\n${INSTALL_END}`);
76
+ const body = file.regions[0]?.content ?? "";
77
+ expect(body).toContain("## Install");
78
+ expect(body).toContain("`npx skills add GoatInAHat/Hello-Tool`");
79
+ expect(body).toContain(
80
+ "[![Agent Skill](https://img.shields.io/badge/Agent_Skill-available-5B5BD6)](https://github.com/GoatInAHat/Hello-Tool)",
81
+ );
82
+ expect(body).toContain("`npx -y hello mcp`");
83
+ // The MCP line carries both first-party install badges once a package registry is selected,
84
+ // computed from the same unpinned `npx -y hello mcp` launch as the text line.
85
+ expect(body).toContain(
86
+ "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",
87
+ );
88
+ expect(body).toContain("https://cursor.com/en/install-mcp?name=hello&config=");
89
+ expect(body).toContain("https://cursor.com/deeplink/mcp-install-dark.svg");
90
+ expect(body).toContain("`claude plugin marketplace add GoatInAHat/Hello-Tool`");
91
+ expect(body).toContain("`claude plugin install hello@hello`");
92
+ expect(body).toContain("`npm install hello`");
93
+ expect(body).not.toContain("openclaw");
94
+ });
95
+
96
+ it("adds the Codex, Gemini, MCPB and browser-extension lines, and skips the badges without a registry", () => {
97
+ const body = region(project(["mcp", "codex", "gemini", "mcpb", "browser-extension"]));
98
+ expect(body).not.toContain("vscode.dev/redirect/mcp/install");
99
+ expect(body).not.toContain("cursor.com/en/install-mcp");
100
+ expect(body).toContain("`codex plugin marketplace add GoatInAHat/Hello-Tool`");
101
+ expect(body).toContain("`codex plugin add hello@hello`");
102
+ expect(body).toContain("`gemini extensions install https://github.com/GoatInAHat/Hello-Tool`");
103
+ expect(body).toContain(
104
+ "download `hello.mcpb` from the GitHub Release and double-click to install",
105
+ );
106
+ // The extension's three channels — unpacked from a checkout, the release assets (only the
107
+ // signed xpi installs itself), the store listings — and the pairing step that ends each.
108
+ expect(body).toContain("Load unpacked → `hosts/browser/.output/chrome-mv3`");
109
+ expect(body).toContain("web-ext run");
110
+ expect(body).toContain(
111
+ "`hello-0.1.0-chrome.zip`, `hello-0.1.0-firefox.zip`, `hello-0.1.0-edge.zip`",
112
+ );
113
+ expect(body).toContain("Mozilla-signed `.xpi`");
114
+ expect(body).toContain("`npx -y hello mcp --http --pair`");
115
+ });
116
+
117
+ it("names the one command that opens the web app, and only when that surface is selected", () => {
118
+ expect(region(project(["mcp", "web"]))).toContain("`npx -y hello mcp --http --open`");
119
+ expect(region(project(["mcp"]))).not.toContain("--http --open");
120
+ });
121
+
122
+ it("carries pypi's mcp-name region beside its own, in one README the plan merges", () => {
123
+ const surfaces: SurfaceId[] = ["pypi", "mcp-registry"];
124
+ const python = project(surfaces, {
125
+ root: "/nonexistent",
126
+ tool: { ...project(surfaces).tool, binding: "python" },
127
+ });
128
+ const file = buildPlan(python).find((planned) => planned.path === "README.md");
129
+ if (file?.kind !== "region") throw new Error("expected a region file");
130
+ expect(file.regions.map((entry) => entry.begin).sort()).toEqual(
131
+ [INSTALL_BEGIN, MCP_NAME_BEGIN].sort(),
132
+ );
133
+ expect(file.template).toContain(`${INSTALL_BEGIN}\n${INSTALL_END}`);
134
+ expect(file.template).toContain(`${MCP_NAME_BEGIN}\n${MCP_NAME_END}`);
135
+ });
136
+
137
+ it("falls back to the local checkout when the identity carries no GitHub repository", () => {
138
+ const surfaces: SurfaceId[] = [
139
+ "skill",
140
+ "claude",
141
+ "codex",
142
+ "gemini",
143
+ "openclaw-native",
144
+ "hermes-native",
145
+ "clawhub",
146
+ "dsh",
147
+ "pypi",
148
+ ];
149
+ const local = region(
150
+ project(surfaces, {
151
+ identity: { name: "hello", version: "0.1.0", description: "Say hello" },
152
+ tool: { ...project(surfaces).tool, binding: "python" },
153
+ }),
154
+ );
155
+ expect(local).not.toContain("skills.sh");
156
+ expect(local).not.toContain("npx skills add");
157
+ expect(local).toContain("`claude plugin marketplace add .`");
158
+ expect(local).toContain("`codex plugin marketplace add .`");
159
+ expect(local).toContain("`gemini extensions link .` from a checkout");
160
+ expect(local).toContain("`hermes plugins install file://$PWD#hosts/hermes/hello_hermes`");
161
+ expect(local).toContain("openclaw plugins install --link hosts/openclaw");
162
+ expect(local).toContain("openclaw plugins install clawhub:openclaw-plugin-hello");
163
+ expect(local).toContain("dsh plugin --profile <profile> add ./hosts/dsh");
164
+ expect(local).toContain("`uv add hello`");
165
+ });
166
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse as parseYaml } from "yaml";
3
+ import type { Operation, Project } from "../model.js";
4
+ import { DSH_PIN, surface as dsh } from "./dsh.js";
5
+
6
+ const echo: Operation = { name: "echo", inputSchema: { type: "object" }, requires: [] };
7
+ const browse: Operation = {
8
+ name: "browse",
9
+ inputSchema: { type: "object" },
10
+ requires: ["browser"],
11
+ };
12
+
13
+ const project: Project = {
14
+ root: "/repo",
15
+ tool: {
16
+ schemaVersion: 1,
17
+ identity: "plugin.json",
18
+ binding: "typescript",
19
+ surfaces: ["mcp", "npm", "dsh"],
20
+ bundle: { runtime: "package" },
21
+ tests: { examples: {} },
22
+ config: {
23
+ properties: {
24
+ apiKey: { type: "string", "x-toolfactory": { sensitive: true } },
25
+ region: { type: "string" },
26
+ },
27
+ required: ["apiKey"],
28
+ },
29
+ },
30
+ identity: { name: "hello.tool", version: "0.1.0", license: "MIT" },
31
+ identityExtra: {},
32
+ operations: [echo, browse],
33
+ toolfactoryVersion: "0.1.0",
34
+ packageManager: "npm",
35
+ };
36
+
37
+ const files = Object.fromEntries(
38
+ dsh.plan(project).map((file) => {
39
+ if (file.kind !== "file") throw new Error("expected whole files only");
40
+ return [file.path, file.content];
41
+ }),
42
+ );
43
+
44
+ describe("dsh", () => {
45
+ it("is two shipped files and no code: the bundle manifest and one mcp-client insert row", () => {
46
+ // `dsh.bundle.patch` is DSH's whole acceptance test for a bundle; a dependency would be wrong,
47
+ // because @deepseek-ai/dsh-mcp-client is @deepseek-ai/dsh's own.
48
+ expect(JSON.parse(files["hosts/dsh/package.json"] as string)).toMatchObject({
49
+ name: "hello.tool-dsh",
50
+ version: "0.1.0",
51
+ license: "MIT",
52
+ files: ["cordis.patch.yml"],
53
+ dsh: { bundle: { patch: "./cordis.patch.yml" } },
54
+ });
55
+ expect(JSON.parse(files["hosts/dsh/package.json"] as string).dependencies).toBeUndefined();
56
+
57
+ const patch = files["hosts/dsh/cordis.patch.yml"] as string;
58
+ expect(parseYaml(patch, { logLevel: "silent" })).toEqual([
59
+ {
60
+ insert: [
61
+ {
62
+ // serverName is `[A-Za-z0-9_-]{1,32}`, validated at DSH boot: the dot in N is illegal.
63
+ id: "mcp-hello-tool",
64
+ name: "@deepseek-ai/dsh-mcp-client",
65
+ config: {
66
+ serverName: "hello-tool",
67
+ transport: "stdio",
68
+ command: "npx",
69
+ args: ["-y", "hello.tool@0.1.0", "mcp"],
70
+ env: {
71
+ APIKEY: "process.env.APIKEY",
72
+ REGION: "process.env.REGION",
73
+ },
74
+ failOnStartupError: true,
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ ]);
80
+ // DSH scrubs KEY/PASSWORD/SECRET/TOKEN names before spawning, so every config variable is
81
+ // restated as the `!!js` expression DSH evaluates when it loads the patch.
82
+ expect(patch).toContain("APIKEY: !!js process.env.APIKEY");
83
+ });
84
+
85
+ it("validates by booting the checkout keylessly, and carries only MCP tool calls", () => {
86
+ // The local row is the same shape against this working tree, and stays out of `files`.
87
+ const local = parseYaml(files["hosts/dsh/cordis.local.patch.yml"] as string, {
88
+ logLevel: "silent",
89
+ });
90
+ expect(local[0].insert[0].config).toMatchObject({
91
+ command: "node",
92
+ args: ["--import", "tsx", "src/toolfactory/mcp.ts"],
93
+ });
94
+
95
+ const [command] = dsh.validate?.(project) ?? [];
96
+ expect(command?.command).toBe("sh");
97
+ const script = command?.args[1] ?? "";
98
+ expect(script).toContain(`@deepseek-ai/dsh@${DSH_PIN}`);
99
+ expect(script).toContain("--patch hosts/dsh/cordis.local.patch.yml");
100
+ // Pass signature: everything before the model call is a hard failure, so reaching the missing
101
+ // DeepSeek credential proves the patch, the row, the spawn and `tools/list` all succeeded.
102
+ expect(script).toContain("grep -qF 'MISSING_CREDENTIAL: llm-deepseek'");
103
+
104
+ expect(dsh.verdict?.(echo, project)).toEqual({ kind: "native" });
105
+ expect(dsh.verdict?.(browse, project)).toEqual({
106
+ kind: "excluded",
107
+ reason: "excluded:mcp-no-host-capabilities",
108
+ });
109
+ });
110
+ });