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,84 @@
1
+ /**
2
+ * PyPI package: identity projected into pyproject.toml (merge; the author owns the rest),
3
+ * the console script for the CLI, and the MCP Registry ownership marker, which for PyPI is
4
+ * an `mcp-name:` line in the README the registry reads back off the published project page.
5
+ */
6
+ import { cliEntryPoint, projectTable, pythonPackage } from "../bindings/python.js";
7
+ import type { Surface } from "../model.js";
8
+ import { registryName } from "./mcp-registry.js";
9
+ import { compact, has } from "./shared.js";
10
+ import { WEB_DIR } from "./web.js";
11
+
12
+ export const README_PATH = "README.md";
13
+ export const MCP_NAME_BEGIN = "<!-- tf:mcp-name -->";
14
+ export const MCP_NAME_END = "<!-- /tf:mcp-name -->";
15
+
16
+ export const surface: Surface = {
17
+ id: "pypi",
18
+ plan(project) {
19
+ if (project.tool.binding !== "python") {
20
+ throw new Error('Surface "pypi" requires the python binding.');
21
+ }
22
+ const authored = project.tool.identity === "pyproject.toml";
23
+ const registry = has(project, "mcp-registry");
24
+ const patch = {
25
+ project: compact({
26
+ ...(authored ? {} : projectTable(project)),
27
+ readme: registry ? README_PATH : undefined,
28
+ scripts: has(project, "cli")
29
+ ? { [project.identity.name]: cliEntryPoint(project) }
30
+ : undefined,
31
+ }),
32
+ // The kernel serves the built page from inside its own package, so the wheel has to
33
+ // carry it: `packages` only copies the package tree, `force-include` maps anything else.
34
+ ...(has(project, "web")
35
+ ? {
36
+ tool: {
37
+ hatch: {
38
+ build: {
39
+ targets: {
40
+ wheel: {
41
+ "force-include": {
42
+ [`${WEB_DIR}/dist`]: `${pythonPackage(project)}/${WEB_DIR}`,
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ }
50
+ : {}),
51
+ };
52
+ const files = [
53
+ {
54
+ kind: "merge" as const,
55
+ path: "pyproject.toml",
56
+ format: "toml" as const,
57
+ patch,
58
+ owned: ["project.scripts"],
59
+ },
60
+ ];
61
+ if (!registry) return files;
62
+ const marker = `\n<!-- mcp-name: ${registryName(project)} -->\n`;
63
+ return [
64
+ ...files,
65
+ {
66
+ kind: "region" as const,
67
+ path: README_PATH,
68
+ regions: [{ begin: MCP_NAME_BEGIN, end: MCP_NAME_END, content: marker }],
69
+ template: [
70
+ `# ${project.identity.name}`,
71
+ "",
72
+ project.identity.description ?? "",
73
+ "",
74
+ MCP_NAME_BEGIN,
75
+ MCP_NAME_END,
76
+ "",
77
+ ].join("\n"),
78
+ },
79
+ ];
80
+ },
81
+ validate(project) {
82
+ return [{ label: "uv build", command: "uv", args: ["build"], cwd: project.root }];
83
+ },
84
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Surface "readme": the `<!-- tf:install -->` region of `README.md`, always generated (like
3
+ * `workflows` and `agents`; never in `tool.json.surfaces`). One install line per selected
4
+ * distribution surface, in the exact shape that surface's own installer accepts, so the
5
+ * humans-and-agents-facing half of distribution cannot drift from the artifacts. The prose
6
+ * around the region — what the tool is, how to use it — is the author's, forever.
7
+ *
8
+ * Every line works from a plain checkout; the ones that need a GitHub repository (`npx skills
9
+ * add`, the Agent Skill badge, the marketplace and Hermes clone URLs) appear only once the
10
+ * identity file carries one, so a tool built without GitHub still gets an accurate section.
11
+ *
12
+ * When `mcp` is selected alongside a package registry, the MCP line also carries the two
13
+ * first-party one-click badges (VS Code's `vscode.dev/redirect/mcp/install`, Cursor's
14
+ * `cursor.com/en/install-mcp`), both computed from the exact same `{command, args, env}` launch
15
+ * the plain-text line renders — a badge can never point somewhere the text line does not.
16
+ */
17
+ import { githubSlug } from "../hosts/github.js";
18
+ import { projectName } from "../identity/name.js";
19
+ import type { Project, Surface } from "../model.js";
20
+ import { HOST_DIR as BROWSER_HOST_DIR, zipName } from "./browser-extension.js";
21
+ import { HOST_DIR as DSH_HOST_DIR, dshTarball } from "./dsh.js";
22
+ import { pluginDir as hermesPluginDir } from "./hermes-native.js";
23
+ import { HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
24
+ import { envName, has, npmName, pypiName, requiredConfig } from "./shared.js";
25
+
26
+ export const README_PATH = "README.md";
27
+ export const INSTALL_BEGIN = "<!-- tf:install -->";
28
+ export const INSTALL_END = "<!-- /tf:install -->";
29
+
30
+ /** A stable label for repositories that ship an Agent Skill; unlike skills.sh's live count badge,
31
+ * it does not depend on a separate registry index. */
32
+ function skillBadge(slug: string): string {
33
+ return `[![Agent Skill](https://img.shields.io/badge/Agent_Skill-available-5B5BD6)](https://github.com/${slug})`;
34
+ }
35
+
36
+ /**
37
+ * The kernel MCP server's published launch, unpinned — `{command, args}`, plus an `env` map of
38
+ * blank placeholders for whatever config the tool requires. The one source both `mcpCommand`
39
+ * (the plain-text line) and the VS Code / Cursor install badges below encode: a badge can never
40
+ * name a launch the text line does not.
41
+ */
42
+ function launchConfig(project: Project): {
43
+ command: string;
44
+ args: string[];
45
+ env?: Record<string, string>;
46
+ } {
47
+ const { command, args } =
48
+ project.tool.binding === "python"
49
+ ? (() => {
50
+ const pypi = pypiName(project);
51
+ // `uvx <pkg>` runs the command of the same name; a dotted tool name renames the package.
52
+ return pypi === project.identity.name
53
+ ? { command: "uvx", args: [pypi, "mcp"] }
54
+ : { command: "uvx", args: ["--from", pypi, project.identity.name, "mcp"] };
55
+ })()
56
+ : { command: "npx", args: ["-y", npmName(project), "mcp"] };
57
+ const required = requiredConfig(project);
58
+ return required.length
59
+ ? { command, args, env: Object.fromEntries(required.map((key) => [envName(key), ""])) }
60
+ : { command, args };
61
+ }
62
+
63
+ function mcpCommand(project: Project): string {
64
+ const { command, args } = launchConfig(project);
65
+ return [command, ...args].join(" ");
66
+ }
67
+
68
+ /** github.com/github/github-mcp-server's README badge, verbatim shields.io style. */
69
+ function vscodeBadge(project: Project): string {
70
+ const config = encodeURIComponent(JSON.stringify(launchConfig(project)));
71
+ const url = `https://vscode.dev/redirect/mcp/install?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
72
+ return `[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](${url})`;
73
+ }
74
+
75
+ /** cursor.com/docs/mcp/install-links's own badge SVG and `en/install-mcp` redirect. */
76
+ function cursorBadge(project: Project): string {
77
+ const config = Buffer.from(JSON.stringify(launchConfig(project))).toString("base64");
78
+ const url = `https://cursor.com/en/install-mcp?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
79
+ return `[![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](${url})`;
80
+ }
81
+
82
+ function installLines(project: Project): string[] {
83
+ const { identity } = project;
84
+ const slug = githubSlug(identity.repository);
85
+ const lines: string[] = [];
86
+ if (has(project, "skill") && slug) {
87
+ lines.push(`- **Agent Skill** — \`npx skills add ${slug}\``);
88
+ }
89
+ if (has(project, "mcp")) {
90
+ // The one-click badges need a published package to point at: they read the same launch
91
+ // config the text line does, so they only appear once there is somewhere to `npx`/`uvx` from.
92
+ const badges =
93
+ has(project, "npm") || has(project, "pypi")
94
+ ? ` ${vscodeBadge(project)} ${cursorBadge(project)}`
95
+ : "";
96
+ lines.push(`- **MCP server** — \`${mcpCommand(project)}\`${badges}`);
97
+ }
98
+ if (has(project, "mcpb")) {
99
+ lines.push(
100
+ `- **Claude Desktop extension** — download \`${identity.name}.mcpb\` from the GitHub Release and double-click to install`,
101
+ );
102
+ }
103
+ if (has(project, "claude")) {
104
+ // The repository is its own marketplace (`.claude-plugin/marketplace.json`); Copilot CLI
105
+ // reads the same file with the same command.
106
+ lines.push(
107
+ `- **Claude Code plugin** — \`claude plugin marketplace add ${slug ?? "."}\`, then \`claude plugin install ${identity.name}@${identity.name}\``,
108
+ );
109
+ }
110
+ if (has(project, "codex")) {
111
+ // `.agents/plugins/marketplace.json` lists the repository's own plugin (`codex.ts`).
112
+ lines.push(
113
+ `- **Codex plugin** — \`codex plugin marketplace add ${slug ?? "."}\`, then \`codex plugin add ${identity.name}@${identity.name}\``,
114
+ );
115
+ }
116
+ if (has(project, "gemini")) {
117
+ lines.push(
118
+ slug
119
+ ? `- **Gemini CLI extension** — \`gemini extensions install https://github.com/${slug}\``
120
+ : "- **Gemini CLI extension** — `gemini extensions link .` from a checkout",
121
+ );
122
+ }
123
+ if (has(project, "openclaw-native")) {
124
+ const clawhub = has(project, "clawhub")
125
+ ? `; published: \`openclaw plugins install clawhub:${projectName.openclawPackage(identity.name)}\``
126
+ : "";
127
+ lines.push(
128
+ `- **OpenClaw plugin** — \`openclaw plugins install --link ${OPENCLAW_HOST_DIR}\` from a checkout${clawhub}`,
129
+ );
130
+ }
131
+ if (has(project, "hermes-native")) {
132
+ const source = slug ? `https://github.com/${slug}` : "file://$PWD";
133
+ lines.push(
134
+ `- **Hermes plugin** — \`hermes plugins install ${source}#${hermesPluginDir(project)}\``,
135
+ );
136
+ }
137
+ if (has(project, "dsh")) {
138
+ // `dsh plugin` is a pnpm forwarder: it installs a path, a tarball or a registry spec.
139
+ lines.push(
140
+ `- **DSH plugin** (experimental) — \`dsh plugin --profile <profile> add ./${DSH_HOST_DIR}\` from a checkout, or the release tarball \`${dshTarball(project)}\``,
141
+ );
142
+ }
143
+ if (has(project, "browser-extension")) {
144
+ // Three channels, in the order a reader needs them: build-and-load from a checkout, the
145
+ // release assets (only Mozilla's signed xpi is a real self-hosted install — Chrome drops
146
+ // side-loaded unpacked extensions), the store listings. Pairing is the last step of each.
147
+ const zips = (["chrome", "firefox", "edge"] as const).map(
148
+ (browser) => `\`${zipName(project, browser)}\``,
149
+ );
150
+ lines.push(
151
+ [
152
+ `- **Browser extension** — from a checkout: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`,
153
+ ` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\``,
154
+ ` (Firefox: \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\`). Each GitHub Release attaches the`,
155
+ ` store uploads ${zips.join(", ")}, and the Mozilla-signed \`.xpi\`,`,
156
+ " which is the only download-and-install channel now that Chrome no longer keeps side-loaded unpacked",
157
+ " extensions; the Chrome Web Store, Firefox Add-ons and Edge Add-ons listings appear once the release's",
158
+ ` submit step has each store's credentials. Then pair it: \`${mcpCommand(project)} --http --pair\``,
159
+ " prints the `<url>#<token>` the extension's options page accepts.",
160
+ ].join("\n"),
161
+ );
162
+ }
163
+ if (has(project, "web")) {
164
+ // Not an install: the one command that puts the page in front of a human, from wherever the
165
+ // tool already is. The same listener answers `/mcp`, so the page needs nothing else running.
166
+ lines.push(
167
+ `- **Web app** — \`${mcpCommand(project)} --http --open\` serves the operations page beside the`,
168
+ " MCP endpoint on one port and opens it; over MCP or a skill, the `web` operation does the same and",
169
+ " returns the URL.",
170
+ );
171
+ }
172
+ if (has(project, "npm")) lines.push(`- **npm package** — \`npm install ${npmName(project)}\``);
173
+ if (has(project, "pypi")) lines.push(`- **PyPI package** — \`uv add ${pypiName(project)}\``);
174
+ return lines;
175
+ }
176
+
177
+ /** The content of the generated region — everything between the markers, not the markers. */
178
+ export function installContent(project: Project): string {
179
+ const slug = githubSlug(project.identity.repository);
180
+ const badge = has(project, "skill") && slug ? [skillBadge(slug), ""] : [];
181
+ const lines = installLines(project);
182
+ return `${[
183
+ "",
184
+ "## Install",
185
+ "",
186
+ ...badge,
187
+ ...(lines.length
188
+ ? lines
189
+ : ["_No installable surface is selected: this repository ships source only._"]),
190
+ "",
191
+ ].join("\n")}\n`;
192
+ }
193
+
194
+ function template(project: Project): string {
195
+ return [
196
+ `# ${project.identity.name}`,
197
+ "",
198
+ project.identity.description ?? "One paragraph: what this tool does, and for whom.",
199
+ "",
200
+ INSTALL_BEGIN,
201
+ INSTALL_END,
202
+ "",
203
+ ].join("\n");
204
+ }
205
+
206
+ export const surface: Surface = {
207
+ id: "readme",
208
+ plan(project) {
209
+ return [
210
+ {
211
+ kind: "region",
212
+ path: README_PATH,
213
+ regions: [{ begin: INSTALL_BEGIN, end: INSTALL_END, content: installContent(project) }],
214
+ template: template(project),
215
+ },
216
+ ];
217
+ },
218
+ };
@@ -0,0 +1,79 @@
1
+ import type { Surface, SurfaceId } from "../model.js";
2
+ import { surface as agentPlugins } from "./agent-plugins.js";
3
+ import { surface as agents } from "./agents.js";
4
+ import { surface as browserExtension } from "./browser-extension.js";
5
+ import { surface as claude } from "./claude.js";
6
+ import { surface as codex } from "./codex.js";
7
+ import { surface as cursor } from "./cursor.js";
8
+ import { surface as dsh } from "./dsh.js";
9
+ import { clawhub } from "./external.js";
10
+ import { surface as gemini } from "./gemini.js";
11
+ import { surface as hermesNative } from "./hermes-native.js";
12
+ import { cli, mcp } from "./kernel.js";
13
+ import { surface as mcpRegistry } from "./mcp-registry.js";
14
+ import { surface as mcpb } from "./mcpb.js";
15
+ import { surface as npm } from "./npm.js";
16
+ import { surface as openclawNative } from "./openclaw-native.js";
17
+ import { surface as pypi } from "./pypi.js";
18
+ import { surface as readme } from "./readme.js";
19
+ import { surface as skill } from "./skill.js";
20
+ import { surface as web } from "./web.js";
21
+ import { surface as workflows } from "./workflows.js";
22
+
23
+ const registry: Partial<Record<SurfaceId, Surface>> = {
24
+ "agent-plugins": agentPlugins,
25
+ agents,
26
+ "browser-extension": { ...browserExtension, requires: ["mcp"] },
27
+ claude,
28
+ codex,
29
+ // Declared dependencies (§3): these projectors point at a file another surface writes, so a
30
+ // selection without it would ship an artifact referencing nothing. Refused at plan time.
31
+ cursor: { ...cursor, requires: ["agent-plugins"] },
32
+ clawhub,
33
+ // Reaches the kernel only through DSH's own MCP client, so the bundle is inert without it.
34
+ dsh: { ...dsh, requires: ["mcp"] },
35
+ // The manifest's only payload is the kernel's launch row: an extension without it is a
36
+ // context file and nothing else.
37
+ gemini: { ...gemini, requires: ["mcp"] },
38
+ cli,
39
+ mcp,
40
+ "mcp-registry": mcpRegistry,
41
+ // The bundle root is the npm tarball with its production dependencies installed into it, and
42
+ // its one row launches the kernel MCP server: without either surface there is nothing to pack.
43
+ mcpb: { ...mcpb, requires: ["mcp", "npm"] },
44
+ npm,
45
+ skill,
46
+ "hermes-native": hermesNative,
47
+ "openclaw-native": openclawNative,
48
+ pypi,
49
+ readme,
50
+ web: { ...web, requires: ["mcp"] },
51
+ workflows,
52
+ };
53
+
54
+ export function registerSurface(surface: Surface): void {
55
+ registry[surface.id] = surface;
56
+ }
57
+
58
+ export function getSurface(id: SurfaceId): Surface {
59
+ const surface = registry[id];
60
+ if (!surface) throw new Error(`Surface "${id}" is not implemented in this toolfactory version.`);
61
+ return surface;
62
+ }
63
+
64
+ export function selectedSurfaces(ids: readonly SurfaceId[]): Surface[] {
65
+ return ids.map(getSurface);
66
+ }
67
+
68
+ /** A selection is valid only when every surface's declared dependencies are selected too. */
69
+ export function assertSurfaceRequirements(ids: readonly SurfaceId[]): void {
70
+ const selected = new Set(ids);
71
+ for (const id of ids) {
72
+ for (const required of getSurface(id).requires ?? []) {
73
+ if (selected.has(required)) continue;
74
+ throw new Error(
75
+ `Surface "${id}" requires surface "${required}", which is not selected (surfaces: ${ids.join(", ")}). Select "${required}" or drop "${id}".`,
76
+ );
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,140 @@
1
+ /** Helpers shared by surface projectors. Pure. */
2
+ import { projectName } from "../identity/name.js";
3
+ import { type Operation, PORTABLE_CAPABILITIES, type Project, type Verdict } from "../model.js";
4
+ import { defaultVerdict } from "../report/coverage.js";
5
+
6
+ export const TOOLFACTORY_DIR = "dev.toolfactory";
7
+
8
+ export function json(value: unknown): string {
9
+ return `${JSON.stringify(value, null, 2)}\n`;
10
+ }
11
+
12
+ /** Drop undefined values so projections never emit `"key": undefined`. */
13
+ export function compact<T extends Record<string, unknown>>(record: T): Partial<T> {
14
+ return Object.fromEntries(
15
+ Object.entries(record).filter(([, value]) => value !== undefined),
16
+ ) as Partial<T>;
17
+ }
18
+
19
+ export function has(project: Project, surface: string): boolean {
20
+ return (project.tool.surfaces as string[]).includes(surface);
21
+ }
22
+
23
+ export function npmName(project: Project): string {
24
+ return projectName.npm(project.identity.name, project.tool.npm?.scope);
25
+ }
26
+
27
+ export function pypiName(project: Project): string {
28
+ return projectName.pypi(project.identity.name);
29
+ }
30
+
31
+ /** The launch command for the kernel MCP server as an installed package or a committed build. */
32
+ export function kernelLaunch(
33
+ project: Project,
34
+ placeholderRoot: string,
35
+ ): { command: string; args: string[] } {
36
+ const { binding, bundle } = project.tool;
37
+ const version = project.identity.version ?? "latest";
38
+ if (bundle.runtime === "bundled") {
39
+ return binding === "python"
40
+ ? { command: "python3", args: [`${placeholderRoot}/bin/mcp.py`] }
41
+ : { command: "node", args: [`${placeholderRoot}/bin/mcp.js`] };
42
+ }
43
+ return binding === "python"
44
+ ? {
45
+ command: "uvx",
46
+ args: ["--from", `${pypiName(project)}==${version}`, project.identity.name, "mcp"],
47
+ }
48
+ : { command: "npx", args: ["-y", `${npmName(project)}@${version}`, "mcp"] };
49
+ }
50
+
51
+ /** Verdict for surfaces that only carry MCP tool calls: no host capabilities at all. */
52
+ export function mcpVerdict(operation: Operation): Verdict {
53
+ const verdict = defaultVerdict(operation);
54
+ return verdict.kind === "excluded"
55
+ ? { kind: "excluded", reason: "excluded:mcp-no-host-capabilities" }
56
+ : verdict;
57
+ }
58
+
59
+ /** Verdict for skills hosts: non-portable operations are bridged by the agent driving the host's own tools. */
60
+ /** A CLI has a human at the terminal, so user-input is native there; other host capabilities are not. */
61
+ export function cliVerdict(operation: Operation): Verdict {
62
+ const portable = operation.requires.every(
63
+ (capability) => PORTABLE_CAPABILITIES.has(capability) || capability === "user-input",
64
+ );
65
+ return portable
66
+ ? { kind: "native" }
67
+ : { kind: "excluded", reason: "excluded:cli-no-host-capabilities" };
68
+ }
69
+
70
+ /** A skill never executes: the agent drives the CLI or MCP server, so everything it can reach is bridged. */
71
+ export function skillVerdict(operation: Operation): Verdict {
72
+ if (operation.requires.includes("channel"))
73
+ return { kind: "excluded", reason: "excluded:no-channel-bridge" };
74
+ return { kind: "bridged", reason: "bridged:agent-mediated" };
75
+ }
76
+
77
+ export function configProperties(project: Project): Record<string, Record<string, unknown>> {
78
+ const properties = project.tool.config?.properties;
79
+ return properties && typeof properties === "object"
80
+ ? (properties as Record<string, Record<string, unknown>>)
81
+ : {};
82
+ }
83
+
84
+ export function isSensitive(property: Record<string, unknown>): boolean {
85
+ const meta = property["x-toolfactory"] as Record<string, unknown> | undefined;
86
+ return meta?.sensitive === true;
87
+ }
88
+
89
+ export function requiredConfig(project: Project): string[] {
90
+ const required = project.tool.config?.required;
91
+ return Array.isArray(required) ? (required as string[]) : [];
92
+ }
93
+
94
+ /** The environment variable a config key is read from, on every surface and in every environment. */
95
+ export function envName(key: string): string {
96
+ return key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
97
+ }
98
+
99
+ /**
100
+ * Environment variable a host-native shim reads to find the tool's checkout when the host
101
+ * copied the plugin elsewhere (Hermes' doctor, OpenClaw's extensions directory).
102
+ */
103
+ export function rootEnvName(project: Project): string {
104
+ return `${project.identity.name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_ROOT`;
105
+ }
106
+
107
+ /**
108
+ * The environment variable a host passes the tool's per-instance data directory
109
+ * (caches, session state) through, on every surface. The kernel falls back to an
110
+ * XDG-style default when it is unset.
111
+ */
112
+ export function dataDirEnvName(project: Project): string {
113
+ return `${project.identity.name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_DATA_DIR`;
114
+ }
115
+
116
+ /**
117
+ * The credentials a live run needs: every config key that is both `required` and sensitive.
118
+ * A tool with none has nothing to gate a live tier on, so no live test and no `live` CI job.
119
+ */
120
+ export function liveCredentials(project: Project): string[] {
121
+ const properties = configProperties(project);
122
+ const required = new Set(requiredConfig(project));
123
+ return Object.keys(properties).filter(
124
+ (key) => required.has(key) && isSensitive(properties[key] as Record<string, unknown>),
125
+ );
126
+ }
127
+
128
+ /**
129
+ * The operation the generated example test calls: the first one with arguments in
130
+ * `tool.json` `tests.examples`, else the first operation with no arguments.
131
+ */
132
+ export function liveExample(
133
+ project: Project,
134
+ ): { name: string; args: Record<string, unknown> } | undefined {
135
+ const examples = project.tool.tests.examples;
136
+ const withExample = project.operations.find((operation) => examples[operation.name]);
137
+ const operation = withExample ?? project.operations[0];
138
+ if (!operation) return undefined;
139
+ return { name: operation.name, args: examples[operation.name] ?? {} };
140
+ }
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse as parseYaml } from "yaml";
3
+ import type { Operation, Project } from "../model.js";
4
+ import { frontmatter, renderOperations, skillPath, surface } from "./skill.js";
5
+
6
+ const echo: Operation = {
7
+ name: "echo",
8
+ description: "Return the text you pass in.",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: { text: { type: "string" } },
12
+ required: ["text"],
13
+ },
14
+ requires: [],
15
+ };
16
+ const screenshot: Operation = {
17
+ name: "screenshot",
18
+ description: "Capture the current page.",
19
+ inputSchema: { type: "object", properties: {} },
20
+ requires: ["browser"],
21
+ };
22
+ const notify: Operation = {
23
+ name: "notify",
24
+ description: "Post a message into the live conversation.",
25
+ inputSchema: { type: "object", properties: {} },
26
+ requires: ["channel"],
27
+ };
28
+
29
+ function project(overrides: Partial<Project> = {}): Project {
30
+ return {
31
+ root: "/repo",
32
+ tool: {
33
+ schemaVersion: 1,
34
+ identity: "package.json",
35
+ binding: "typescript",
36
+ surfaces: ["skill", "cli", "mcp"],
37
+ bundle: { runtime: "package" },
38
+ tests: { examples: {} },
39
+ },
40
+ identity: { name: "hello", version: "1.0.0", description: "Say hello", license: "MIT" },
41
+ identityExtra: {},
42
+ operations: [echo, screenshot, notify],
43
+ toolfactoryVersion: "0.1.0",
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ describe("skill surface", () => {
49
+ it("names the file after the tool and links it into the .agents canon", () => {
50
+ const target = project();
51
+ expect(skillPath(target)).toBe("skills/hello/SKILL.md");
52
+ expect(surface.plan(target)).toMatchObject([
53
+ { kind: "region", path: "skills/hello/SKILL.md" },
54
+ { kind: "file", path: ".agents/skills/hello", content: "../../skills/hello", symlink: true },
55
+ ]);
56
+ });
57
+
58
+ it("frontmatter carries the 6 spec fields, falling back to a description", () => {
59
+ const document = parseYaml(frontmatter(project()));
60
+ expect(document).toEqual({ name: "hello", description: "Say hello", license: "MIT" });
61
+
62
+ const noDescription = parseYaml(
63
+ frontmatter(project({ identity: { name: "hello", description: undefined } })),
64
+ );
65
+ expect(noDescription.description).toBe("Use the hello tool.");
66
+ });
67
+
68
+ it("the operations block includes a portable op's CLI/MCP invocation and a bridged op's text, and omits an excluded op", () => {
69
+ const body = renderOperations(project());
70
+
71
+ expect(body).toContain("### echo");
72
+ expect(body).toContain("`hello echo --json '<arguments>'` prints a JSON result.");
73
+ expect(body).toContain("MCP tool `echo` on server `hello`");
74
+ expect(body).toContain("Arguments: `text`.");
75
+
76
+ expect(body).toContain("### screenshot");
77
+ expect(body).toContain(
78
+ "This operation needs browser: drive this host's own browser tools and pass what they return as arguments.",
79
+ );
80
+
81
+ expect(body).not.toContain("### notify");
82
+ });
83
+
84
+ it("leaves the operations region empty for an instruction-only skill", () => {
85
+ const target = project({
86
+ tool: { ...project().tool, runtime: "none", surfaces: ["skill", "codex"] },
87
+ operations: [],
88
+ });
89
+ expect(renderOperations(target)).toBe("");
90
+ });
91
+
92
+ it("validate runs the upstream agentskills validator against skills/<name>", () => {
93
+ const commands = surface.validate?.(project()) ?? [];
94
+ expect(commands).toEqual([
95
+ {
96
+ label: "agentskills validate",
97
+ command: "uvx",
98
+ args: ["--from", "skills-ref", "agentskills", "validate", "skills/hello"],
99
+ cwd: "/repo",
100
+ },
101
+ ]);
102
+ });
103
+ });