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,597 @@
1
+ /**
2
+ * The gate and the release assets, as data.
3
+ *
4
+ * The gate is a command; CI is its projection. One list of steps is rendered three ways — the
5
+ * `test` job of `ci.yml`, the `gate`/`package` jobs of `release.yml`, and a shell script a local
6
+ * `toolfactory gate` / `toolfactory package` spawns — so "what green means" has exactly one
7
+ * definition whether or not the project ever sees GitHub.
8
+ *
9
+ * A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
10
+ * (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
11
+ */
12
+ import { githubSlug } from "../hosts/github.js";
13
+ import { githubOwner, projectName } from "../identity/name.js";
14
+ import { isInstructionOnly } from "../model.js";
15
+ import { HOST_DIR as BROWSER_HOST_DIR, sourcesZipName, zipName, } from "../surfaces/browser-extension.js";
16
+ import { HOST_DIR as DSH_HOST_DIR } from "../surfaces/dsh.js";
17
+ import { MANIFEST_PATH as MCPB_MANIFEST_PATH, MCPB_PIN } from "../surfaces/mcpb.js";
18
+ import { HOST_DIR as OPENCLAW_HOST_DIR } from "../surfaces/openclaw-native.js";
19
+ import { configProperties, envName, has, isSensitive, npmName, pypiName, } from "../surfaces/shared.js";
20
+ /** Where `toolfactory package` and the release `package` job leave every asset. */
21
+ export const RELEASE_DIR = "dist/release";
22
+ /** The web app's build, shared by the release tarball and the Pages job (which sets `PAGES_BASE`). */
23
+ export const WEB_BUILD = "npm -C web ci && npm -C web run build";
24
+ /** The commands each supported package manager spells; `package.json` `packageManager` picks the row. */
25
+ export const PACKAGE_MANAGER_COMMANDS = {
26
+ npm: {
27
+ install: "npm ci",
28
+ run: (script) => `npm run --if-present ${script}`,
29
+ test: "npm test",
30
+ },
31
+ pnpm: {
32
+ install: "pnpm install --frozen-lockfile",
33
+ run: (script) => `pnpm run --if-present ${script}`,
34
+ test: "pnpm test",
35
+ },
36
+ };
37
+ /** How the project invokes toolfactory: the devDependency in a TypeScript project, a pinned fetch otherwise. */
38
+ export function toolfactoryCli(project) {
39
+ if (isInstructionOnly(project.tool))
40
+ return `npx --yes toolfactory@${project.toolfactoryVersion}`;
41
+ return project.tool.binding === "typescript"
42
+ ? "npx toolfactory"
43
+ : `npx --yes toolfactory@${project.toolfactoryVersion}`;
44
+ }
45
+ function commands(project) {
46
+ return PACKAGE_MANAGER_COMMANDS[project.packageManager ?? "npm"];
47
+ }
48
+ /**
49
+ * Dependencies and, for TypeScript, the build — everything both the gate and the packaging run
50
+ * need before any command that imports the project's own code.
51
+ */
52
+ /**
53
+ * Output files (§2.2 S4) are build products, not tracked: a fresh checkout has none, and the
54
+ * validators and the web build read them. On a tree `check` has just proved current, `build`
55
+ * writes exactly those.
56
+ */
57
+ export function outputsStep(project) {
58
+ return { name: "toolfactory build (output files)", run: `${toolfactoryCli(project)} build` };
59
+ }
60
+ export function bootstrapSteps(project) {
61
+ if (isInstructionOnly(project.tool))
62
+ return [];
63
+ const pm = commands(project);
64
+ return project.tool.binding === "typescript"
65
+ ? [
66
+ { name: "install", run: pm.install, when: "ci" },
67
+ { name: "build", run: pm.run("build") },
68
+ ]
69
+ : [{ name: "install", run: "uv sync", when: "ci" }];
70
+ }
71
+ /**
72
+ * The gate: bootstrap, the drift check, the validator CLIs the selected surfaces need on PATH,
73
+ * `toolfactory validate` (every surface's own upstream validator), the author's `check` and
74
+ * tests, and the credential-free OpenClaw turn. Nothing is transcribed from a surface — the
75
+ * commands a surface owns are reached through `toolfactory validate`, so this list cannot drift
76
+ * away from what `validate` actually runs.
77
+ */
78
+ export function gateSteps(project) {
79
+ if (isInstructionOnly(project.tool)) {
80
+ const cli = toolfactoryCli(project);
81
+ return [
82
+ { name: "toolfactory check", run: `${cli} check` },
83
+ outputsStep(project),
84
+ { name: "toolfactory validate", run: `${cli} validate` },
85
+ ];
86
+ }
87
+ const typescript = project.tool.binding === "typescript";
88
+ const pm = commands(project);
89
+ const cli = toolfactoryCli(project);
90
+ const openclaw = has(project, "openclaw-native");
91
+ const steps = [
92
+ ...bootstrapSteps(project),
93
+ { name: "toolfactory check", run: `${cli} check` },
94
+ outputsStep(project),
95
+ ];
96
+ if (has(project, "claude")) {
97
+ steps.push({
98
+ name: "Install Claude Code CLI",
99
+ run: "npm i -g @anthropic-ai/claude-code",
100
+ when: "ci",
101
+ });
102
+ }
103
+ if (has(project, "web") || has(project, "browser-extension")) {
104
+ // The web smoke and the browser extension's own Playwright smoke both drive Chromium;
105
+ // their own npm installs fetch the browser, the runner needs its system libraries.
106
+ steps.push({
107
+ name: "Install Chromium dependencies",
108
+ run: "npx --yes playwright install-deps chromium",
109
+ when: "ci",
110
+ });
111
+ }
112
+ steps.push({
113
+ name: "toolfactory validate",
114
+ run: `${cli} validate`,
115
+ // openclaw-native's validate() shells to the openclaw CLI, which needs Node >=24.
116
+ ...(openclaw ? { when: "node24" } : {}),
117
+ });
118
+ if (typescript)
119
+ steps.push({ name: "author checks", run: pm.run("check") });
120
+ steps.push({
121
+ name: "author tests",
122
+ run: typescript ? pm.test : "uv run --with pytest pytest -q",
123
+ });
124
+ if (openclaw) {
125
+ // T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
126
+ // exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
127
+ // exactly what npm's own `--if-present` asks.
128
+ steps.push({
129
+ name: "openclaw end-to-end (scripted model, no LLM key)",
130
+ run: `npm --prefix ${OPENCLAW_HOST_DIR} run --if-present test:e2e`,
131
+ when: "node24",
132
+ });
133
+ }
134
+ return steps;
135
+ }
136
+ /**
137
+ * §2.2: one version, in the identity file, projected everywhere by `build`. A release therefore
138
+ * asserts the tag against it and never writes it — rewriting a SHA-locked file from the tag would
139
+ * be a change `build` did not make and `check` would fail on the next run.
140
+ */
141
+ export function tagVersionAssert(project) {
142
+ const identity = project.tool.identity;
143
+ const read = identity.endsWith(".json")
144
+ ? `node -p "require('./${identity}').version"`
145
+ : identity.endsWith(".toml")
146
+ ? `python3 -c "import tomllib,pathlib;print(tomllib.loads(pathlib.Path('${identity}').read_text())['project']['version'])"`
147
+ : undefined;
148
+ if (!read)
149
+ return undefined;
150
+ return {
151
+ // `RELEASE_TAG`, not `GITHUB_REF_NAME`: `release.yml` also runs on `workflow_dispatch` with a
152
+ // `tag` input — the only re-run that reads current secrets — and then the ref is a branch.
153
+ name: `tag matches ${identity}`,
154
+ run: `test "v$(${read})" = "$RELEASE_TAG"`,
155
+ };
156
+ }
157
+ /** The bundle paths a plugin zip carries, in the order a reader wants them: identity, then payload. */
158
+ function bundlePaths(project) {
159
+ const paths = [];
160
+ if (has(project, "agent-plugins"))
161
+ paths.push(project.tool.identity, "mcp.json");
162
+ if (has(project, "skill"))
163
+ paths.push("skills");
164
+ if (has(project, "claude"))
165
+ paths.push(".claude-plugin");
166
+ if (has(project, "codex"))
167
+ paths.push(".codex-plugin");
168
+ if (has(project, "cursor"))
169
+ paths.push(".cursor-plugin");
170
+ return paths;
171
+ }
172
+ /** License notices stay with a plugin bundle when the author carries them at the repository root. */
173
+ const LEGAL_BUNDLE_PATHS = [
174
+ "LICENSE",
175
+ "LICENSE.md",
176
+ "LICENSE.txt",
177
+ "NOTICE",
178
+ "NOTICE.md",
179
+ "NOTICE.txt",
180
+ ];
181
+ /**
182
+ * The release assets, into `dist/release/`: the same list locally (`toolfactory package`) and in
183
+ * the release workflow's `package` job, which uploads the directory as one artifact. Publishing
184
+ * to a registry stays a CI concern; producing the artifacts never is.
185
+ */
186
+ export function packageSteps(project) {
187
+ const cli = toolfactoryCli(project);
188
+ const name = project.identity.name;
189
+ const steps = [
190
+ { name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
191
+ ...bootstrapSteps(project),
192
+ outputsStep(project),
193
+ ];
194
+ if (has(project, "web")) {
195
+ steps.push({
196
+ name: "web build",
197
+ // Build before packing: npm and MCPB both carry this same root-relative page.
198
+ env: { PAGES_BASE: "/" },
199
+ run: WEB_BUILD,
200
+ });
201
+ }
202
+ if (has(project, "npm")) {
203
+ steps.push({ name: "npm tarball", run: `npm pack --pack-destination ${RELEASE_DIR}` });
204
+ }
205
+ if (has(project, "mcpb")) {
206
+ // A bundle root is the published package with its production dependencies installed into
207
+ // it, so it is built from the tarball `npm pack` just wrote rather than from the checkout:
208
+ // whatever ships to npm is exactly what ships to Claude Desktop. `--ignore-scripts` because
209
+ // nothing in a bundle root may run a build; the tarball already carries `dist/`.
210
+ const stage = "dist/mcpb";
211
+ const tarball = npmTarball(project);
212
+ steps.push({
213
+ name: "MCPB bundle",
214
+ run: [
215
+ `rm -rf ${stage} && mkdir -p ${stage}`,
216
+ `tar -xzf ${RELEASE_DIR}/${tarball} -C ${stage} --strip-components=1`,
217
+ `npm --prefix ${stage} install --omit=dev --ignore-scripts`,
218
+ `cp ${MCPB_MANIFEST_PATH} ${stage}/manifest.json`,
219
+ `npx -y @anthropic-ai/mcpb@${MCPB_PIN} pack ${stage} ${RELEASE_DIR}/${name}.mcpb`,
220
+ ].join(" && "),
221
+ });
222
+ }
223
+ if (has(project, "pypi")) {
224
+ steps.push({ name: "python distributions", run: `uv build --out-dir ${RELEASE_DIR}` });
225
+ }
226
+ if (has(project, "openclaw-native")) {
227
+ // ClawHub's reusable workflow has no build step of its own, so the tarball it publishes has
228
+ // to arrive already built (`package_artifact_name`).
229
+ steps.push({
230
+ name: "OpenClaw plugin tarball",
231
+ run: [
232
+ `npm --prefix ${OPENCLAW_HOST_DIR} install`,
233
+ `npm --prefix ${OPENCLAW_HOST_DIR} run build`,
234
+ `npm pack ./${OPENCLAW_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
235
+ ].join(" && "),
236
+ });
237
+ }
238
+ if (has(project, "dsh")) {
239
+ // Two files and no code: nothing to build, and `dsh plugin add` takes a tarball directly.
240
+ steps.push({
241
+ name: "DSH bundle tarball",
242
+ run: `npm pack ./${DSH_HOST_DIR} --pack-destination ${RELEASE_DIR}`,
243
+ });
244
+ }
245
+ const bundle = bundlePaths(project);
246
+ if (bundle.length) {
247
+ steps.push({
248
+ name: "plugin bundle",
249
+ run: `set -- ${bundle.join(" ")}; for path in ${LEGAL_BUNDLE_PATHS.join(" ")}; do if [ -f "$path" ]; then set -- "$@" "$path"; fi; done; zip -qr ${RELEASE_DIR}/${name}-plugin.zip "$@"`,
250
+ });
251
+ }
252
+ if (has(project, "web")) {
253
+ steps.push({
254
+ name: "web tarball",
255
+ run: `tar -czf ${RELEASE_DIR}/${name}-web.tar.gz -C web/dist .`,
256
+ });
257
+ }
258
+ if (has(project, "browser-extension")) {
259
+ // `wxt zip` writes into hosts/browser/.output/ under exactly the names `zipName`/
260
+ // `sourcesZipName` predict (verified live), so no rename is needed, only a copy.
261
+ const zipTargets = ["chrome", "firefox", "edge"];
262
+ const outDir = `${BROWSER_HOST_DIR}/.output`;
263
+ steps.push({
264
+ name: "browser extension zips",
265
+ run: [
266
+ `npm --prefix ${BROWSER_HOST_DIR} install`,
267
+ // `npm exec` never changes the working directory (unlike `npm run`), so `wxt` needs its
268
+ // root passed explicitly — verified live: without it, `wxt zip` looks for `./entrypoints`
269
+ // relative to the repo root instead of hosts/browser.
270
+ ...zipTargets.map((browser) => `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt zip ${BROWSER_HOST_DIR} -b ${browser}`),
271
+ ...zipTargets.map((browser) => `cp ${outDir}/${zipName(project, browser)} ${RELEASE_DIR}/`),
272
+ `cp ${outDir}/${sourcesZipName(project)} ${RELEASE_DIR}/`,
273
+ ].join(" && "),
274
+ });
275
+ steps.push({
276
+ // Self-hosted Firefox distribution (§7): a Mozilla-signed, install-anywhere xpi. Opt-in on
277
+ // the JWT pair alone — no store listing is created (`--channel=unlisted`) — so a project
278
+ // with the surface selected but no Firefox credentials configured yet still packages clean.
279
+ name: "Firefox signed xpi (only when FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET are set)",
280
+ run: `if [ -n "$FIREFOX_JWT_ISSUER" ] && [ -n "$FIREFOX_JWT_SECRET" ]; then npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext sign --channel=unlisted --api-key="$FIREFOX_JWT_ISSUER" --api-secret="$FIREFOX_JWT_SECRET" --source-dir ${outDir}/firefox-mv2 --artifacts-dir ${RELEASE_DIR}; fi`,
281
+ });
282
+ }
283
+ // COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
284
+ steps.push({
285
+ name: "coverage report",
286
+ run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
287
+ });
288
+ return steps;
289
+ }
290
+ /** npm's filename for the root package, including its flattened scope. */
291
+ export function npmTarball(project) {
292
+ return `${npmName(project).replace(/^@/, "").replace("/", "-")}-${project.identity.version ?? "0.0.0"}.tgz`;
293
+ }
294
+ /** The ClawHub tarball `npm pack ./hosts/openclaw` writes, by name, inside the release artifact. */
295
+ export function openclawTarball(project) {
296
+ const pkg = projectName.openclawPackage(project.identity.name);
297
+ return `${pkg}-${project.identity.version ?? "0.0.0"}.tgz`;
298
+ }
299
+ /** ClawHub's CLI, pinned: no runner carries `clawhub` on PATH and every ClawHub row shells to it. */
300
+ export const CLAWHUB_PIN = "0.23.3";
301
+ /** The `mcp-publisher` binary: the publish leg and the MCP Registry retraction fetch the same one. */
302
+ export const MCP_PUBLISHER_FETCH = 'curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher';
303
+ const CLAWHUB = `npx -y clawhub@${CLAWHUB_PIN}`;
304
+ /**
305
+ * `clawhub` reads its token from `~/.config/clawhub/config.json`, never from the environment
306
+ * (verified: `CLAWHUB_TOKEN` in the environment still yields "Not logged in"). A throwaway HOME
307
+ * makes `login --token` — which validates the token itself, exit 1 on a revoked one — safe to run
308
+ * on a developer's machine without replacing the login they already have.
309
+ */
310
+ function clawhubAuthed(command) {
311
+ return `d=$(mktemp -d) && HOME="$d" ${CLAWHUB} login --token "$CLAWHUB_TOKEN" --no-input >/dev/null && HOME="$d" ${CLAWHUB} ${command}`;
312
+ }
313
+ /**
314
+ * ClawHub's anonymous rate limit presents as a not-found carrying "(reset in Ns)", so a plain
315
+ * failure cannot be read as "absent" — retry once past the window before believing it.
316
+ */
317
+ function clawhubExists(command) {
318
+ return `${CLAWHUB} ${command} >/dev/null 2>&1 || { ${CLAWHUB} ${command} 2>&1 | grep -q "reset in" && sleep 60 && ${CLAWHUB} ${command} >/dev/null 2>&1; }`;
319
+ }
320
+ /** npm names its registry credential as a config key, not a variable, so `env` is the only prefix that can set it. */
321
+ function npmAuthed(command) {
322
+ return `env "npm_config_//registry.npmjs.org/:_authToken=$NPM_TOKEN" ${command}`;
323
+ }
324
+ /** Every secret name of a set must be non-empty for its leg to run. */
325
+ function allSet(names) {
326
+ return names.map((name) => `[ -n "$${name}" ]`).join(" && ");
327
+ }
328
+ const CHROME_SECRETS = [
329
+ "CHROME_EXTENSION_ID",
330
+ "CHROME_PUBLISHER_ID",
331
+ "CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL",
332
+ "CHROME_SERVICE_ACCOUNT_PRIVATE_KEY",
333
+ ];
334
+ const FIREFOX_SECRETS = ["FIREFOX_EXTENSION_ID", "FIREFOX_JWT_ISSUER", "FIREFOX_JWT_SECRET"];
335
+ const EDGE_SECRETS = ["EDGE_PRODUCT_ID", "EDGE_CLIENT_ID", "EDGE_API_KEY"];
336
+ const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
337
+ /**
338
+ * Every name any row can consume, selected or not: the release's retraction step runs against
339
+ * the previous tag's selection, so its environment carries them all (an unset one is empty).
340
+ */
341
+ export const RELEASE_SECRET_NAMES = [
342
+ "NPM_TOKEN",
343
+ "CLAWHUB_TOKEN",
344
+ ...CHROME_SECRETS,
345
+ ...FIREFOX_SECRETS,
346
+ ...EDGE_SECRETS,
347
+ ...ASC_SECRETS,
348
+ ];
349
+ /**
350
+ * Every registry row whose surfaces are all selected, with the exact upstream command for each
351
+ * column. Four consumers read this and nothing else: `secrets status`, `secrets check`, the
352
+ * release legs' presence gating, and the unpublish step.
353
+ */
354
+ export function registries(project) {
355
+ const name = project.identity.name;
356
+ const version = project.identity.version ?? "0.0.0";
357
+ const pkg = npmName(project);
358
+ const pypi = pypiName(project);
359
+ const owner = githubOwner(project.identity.repository)?.toLowerCase();
360
+ const slug = githubSlug(project.identity.repository);
361
+ const server = owner ? `io.github.${owner}/${name}` : undefined;
362
+ const registry = "https://registry.modelcontextprotocol.io";
363
+ // The retraction's own message, on every registry that carries one: why the artifact stopped.
364
+ const why = `${name} no longer publishes here: the surface was deselected.`;
365
+ const browserZip = (browser) => `${BROWSER_HOST_DIR}/.output/${zipName(project, browser)}`;
366
+ const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
367
+ const rows = [
368
+ {
369
+ id: "npm",
370
+ surfaces: ["npm"],
371
+ secrets: ["NPM_TOKEN"],
372
+ url: "https://www.npmjs.com/settings/~/tokens",
373
+ probe: npmAuthed("npm whoami"),
374
+ exists: `npm view ${pkg}@${version} version >/dev/null 2>&1`,
375
+ // An existing package is only eligible for trusted publishing; it does not prove the
376
+ // relationship was configured. OIDC is enabled explicitly after `npm trust` succeeds;
377
+ // otherwise a token remains a valid fallback, including for the first publish.
378
+ gate: `[ "$NPM_TRUSTED_PUBLISHER" = true ] || [ -n "$NPM_TOKEN" ]`,
379
+ confirmVariable: "NPM_TRUSTED_PUBLISHER",
380
+ // Reversible (an empty message undeprecates) and it never breaks an install, which
381
+ // `npm unpublish` does; the destructive form is behind `--hard`.
382
+ retract: npmAuthed(`npm deprecate ${pkg}@'*' "${why}"`),
383
+ retractHard: npmAuthed(`npm unpublish ${pkg} --force`),
384
+ },
385
+ {
386
+ id: "pypi",
387
+ surfaces: ["pypi"],
388
+ secrets: [],
389
+ url: "https://pypi.org/manage/account/publishing/",
390
+ exists: `curl -fsS -o /dev/null https://pypi.org/pypi/${pypi}/${version}/json`,
391
+ // A project that has published once proves its trusted publisher converted; before that the
392
+ // pending publisher is a web-only step, so a human confirms it with the variable.
393
+ gate: `curl -fsS -o /dev/null https://pypi.org/pypi/${pypi}/json || [ -n "$PYPI_TRUSTED_PUBLISHER" ]`,
394
+ // Yanking and deleting are dashboard actions: PyPI's only write API is Upload.
395
+ retractUrl: `https://pypi.org/manage/project/${pypi}/releases/`,
396
+ confirmVariable: "PYPI_TRUSTED_PUBLISHER",
397
+ },
398
+ {
399
+ id: "oci",
400
+ surfaces: ["mcp-registry"],
401
+ secrets: [],
402
+ // Publishing rides GITHUB_TOKEN's `packages: write`; deleting needs `delete:packages`,
403
+ // which that token does not carry, so retraction reads a PAT from the environment.
404
+ retractSecrets: ["GH_TOKEN"],
405
+ url: "https://github.com/settings/packages",
406
+ exists: owner
407
+ ? `token=$(curl -fsS "https://ghcr.io/token?scope=repository:${owner}/${name}:pull&service=ghcr.io" | sed -n 's/.*"token":"\\([^"]*\\)".*/\\1/p') && curl -fsS -o /dev/null -H "Authorization: Bearer $token" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json" "https://ghcr.io/v2/${owner}/${name}/manifests/${version}"`
408
+ : undefined,
409
+ // GITHUB_TOKEN is always there, so the leg always runs; what it cannot do is make the image
410
+ // public, and GitHub has no API for that at all (verified: the Packages REST surface is
411
+ // GET/DELETE/restore only), so the gate says so once per release instead of failing.
412
+ gate: `echo "::notice::ghcr.io publishes a private image on the first push whatever the repository's visibility; make it public once at https://github.com/${owner ?? "<owner>"}?tab=packages -> Package settings -> Danger Zone."`,
413
+ retract: owner
414
+ ? `gh api --method DELETE /user/packages/container/${name} || gh api --method DELETE /orgs/${owner}/packages/container/${name} || echo "::notice::ghcr: delete the container package by hand at https://github.com/${owner}?tab=packages (a public package past 5,000 downloads needs GitHub support)."`
415
+ : undefined,
416
+ },
417
+ {
418
+ id: "mcp-registry",
419
+ surfaces: ["mcp-registry"],
420
+ secrets: [],
421
+ url: registry,
422
+ exists: server
423
+ ? `curl -fsS -o /dev/null "${registry}/v0/servers/${encodeURIComponent(server)}/versions/${version}"`
424
+ : undefined,
425
+ // mcp-publisher validates that every `packages[]` entry already exists in its own registry,
426
+ // so this leg can only run when each package leg is going to.
427
+ retract: server
428
+ ? `${MCP_PUBLISHER_FETCH} && ./mcp-publisher login github-oidc && ./mcp-publisher status --status deleted --all-versions --message "${why}" ${server}`
429
+ : undefined,
430
+ },
431
+ {
432
+ id: "clawhub-package",
433
+ surfaces: ["clawhub", "openclaw-native"],
434
+ secrets: ["CLAWHUB_TOKEN"],
435
+ url: "https://clawhub.ai",
436
+ probe: clawhubAuthed("whoami"),
437
+ exists: clawhubExists(`package inspect ${projectName.openclawPackage(name)} --version ${version} --json`),
438
+ gate: allSet(["CLAWHUB_TOKEN"]),
439
+ // Soft: `package undelete` restores it, and the version number stays reserved either way.
440
+ retract: clawhubAuthed(`package delete ${projectName.openclawPackage(name)} --yes --json`),
441
+ },
442
+ {
443
+ id: "clawhub-skill",
444
+ surfaces: ["clawhub", "skill"],
445
+ secrets: ["CLAWHUB_TOKEN"],
446
+ url: "https://clawhub.ai",
447
+ probe: clawhubAuthed("whoami"),
448
+ exists: clawhubExists(`inspect ${name} --version ${version} --json`),
449
+ gate: allSet(["CLAWHUB_TOKEN"]),
450
+ retract: clawhubAuthed(`delete ${name} --yes --reason "${why}"`),
451
+ },
452
+ {
453
+ id: "pages",
454
+ surfaces: ["web"],
455
+ secrets: [],
456
+ retractSecrets: ["GH_TOKEN"],
457
+ url: slug ? `https://github.com/${slug}/settings/pages` : "https://docs.github.com/en/pages",
458
+ // Pages must be enabled with Source = GitHub Actions before `configure-pages` can run; a
459
+ // workflow token can read that (`pages: read`) but never set it — `bootstrap-repo` does.
460
+ gate: slug ? `gh api repos/${slug}/pages >/dev/null 2>&1` : undefined,
461
+ exists: slug
462
+ ? `curl -fsS -o /dev/null https://${slug.replace("/", ".github.io/")}/`
463
+ : undefined,
464
+ // Doc-cited only: the docs require repository-admin or the Pages-settings permission and do
465
+ // not say whether a workflow token satisfies it, so a failure is a notice, not a red run.
466
+ retract: slug
467
+ ? `gh api --method DELETE repos/${slug}/pages || echo "::notice::pages: disable the site by hand at https://github.com/${slug}/settings/pages"`
468
+ : undefined,
469
+ },
470
+ {
471
+ id: "chrome",
472
+ surfaces: ["browser-extension"],
473
+ secrets: CHROME_SECRETS,
474
+ url: "https://chrome.google.com/webstore/devconsole",
475
+ // Web Store API v2's `fetchStatus` is the only read method and the only one `chromewebstore
476
+ // .readonly` reaches; `wxt submit --dry-run` calls it before it bails, so this is a real
477
+ // credential check (it is not on the v1.1 path, which bails first — hence the pinned v2).
478
+ probe: `CHROME_API_VERSION=v2 ${wxtSubmit} --chrome-zip ${browserZip("chrome")}`,
479
+ gate: allSet(CHROME_SECRETS),
480
+ // Google removed listing visibility from the API deliberately; it is dashboard-only.
481
+ },
482
+ {
483
+ id: "firefox",
484
+ surfaces: ["browser-extension"],
485
+ secrets: FIREFOX_SECRETS,
486
+ url: "https://addons.mozilla.org/developers/addon/api/key/",
487
+ // `GET /api/v5/addons/addon/{id}` under the JWT, before the dry run bails.
488
+ probe: `${wxtSubmit} --firefox-zip ${browserZip("firefox")} --firefox-sources-zip ${BROWSER_HOST_DIR}/.output/${sourcesZipName(project)}`,
489
+ exists: `curl -fsS -o /dev/null "https://addons.mozilla.org/api/v5/addons/addon/$FIREFOX_EXTENSION_ID/"`,
490
+ gate: allSet(FIREFOX_SECRETS),
491
+ // AMO's PATCH/DELETE exist but only over hand-rolled JWT HTTP, which C1 forbids.
492
+ retractUrl: "https://addons.mozilla.org/developers/addons",
493
+ },
494
+ {
495
+ id: "edge",
496
+ surfaces: ["browser-extension"],
497
+ secrets: EDGE_SECRETS,
498
+ url: "https://partner.microsoft.com/dashboard/microsoftedge/publishapi",
499
+ // No probe: the Edge API's only GETs need an operationId from a prior mutating call, and
500
+ // `wxt submit --dry-run` makes no network call at all for Edge (verified in its bundle).
501
+ gate: allSet(EDGE_SECRETS),
502
+ retractUrl: "https://partner.microsoft.com/dashboard/microsoftedge/overview",
503
+ },
504
+ {
505
+ id: "safari",
506
+ surfaces: ["browser-extension"],
507
+ secrets: ASC_SECRETS,
508
+ url: "https://appstoreconnect.apple.com/access/integrations/api",
509
+ // No probe here: the whole leg is macOS-only, and the ASC read needs an ES256-signed JWT,
510
+ // which is a hand-rolled HTTP client (C1).
511
+ gate: allSet(ASC_SECRETS),
512
+ retractUrl: "https://appstoreconnect.apple.com/apps",
513
+ },
514
+ ];
515
+ const selected = rows.filter((row) => row.surfaces.every((surface) => has(project, surface)) &&
516
+ (row.id !== "safari" || project.tool.browserExtension?.safari === true));
517
+ const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
518
+ if (mcpRegistry) {
519
+ const packages = selected.filter((row) => row.id === "npm" || row.id === "pypi" || row.id === "oci");
520
+ mcpRegistry.gate = packages.length
521
+ ? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
522
+ : "true";
523
+ }
524
+ return selected;
525
+ }
526
+ /** The shell variable the release gate holds a row's presence in; the job output takes the same name. */
527
+ export function gateVariable(row) {
528
+ return row.id.replace(/-/g, "_");
529
+ }
530
+ /** Every environment name a secret can be set under: sensitive config keys, then the release registries' tokens. */
531
+ export function secretsOf(project) {
532
+ const config = Object.entries(configProperties(project))
533
+ .filter(([, property]) => isSensitive(property))
534
+ .map(([key]) => envName(key));
535
+ const release = registries(project).flatMap((row) => row.secrets);
536
+ return [...new Set([...config, ...release])];
537
+ }
538
+ /**
539
+ * The one-time human steps the table cannot automate — no API at all, or one no token in CI can
540
+ * reach. `secrets status` and `bootstrap-repo` print the same list.
541
+ */
542
+ export function manualSteps(project) {
543
+ const rows = registries(project);
544
+ const has_ = (id) => rows.some((row) => row.id === id);
545
+ const slug = githubSlug(project.identity.repository);
546
+ const steps = [];
547
+ if (has_("oci")) {
548
+ steps.push(`ghcr.io: the first push publishes a private image whatever the repository's visibility, and GitHub has no visibility API — flip it once at ${rows.find((row) => row.id === "oci")?.url}.`);
549
+ }
550
+ if (has_("pypi")) {
551
+ steps.push(`PyPI: register a pending trusted publisher (repository, \`release.yml\`, environment \`pypi\`) at https://pypi.org/manage/account/publishing/, then \`gh variable set PYPI_TRUSTED_PUBLISHER -b true${slug ? ` -R ${slug}` : ""}\`.`);
552
+ }
553
+ if (has_("npm")) {
554
+ steps.push(`npm: after the package exists, use npm@11.15+ from a logged-in 2FA session to run \`npm trust github ${npmName(project)} --file release.yml --repo ${slug ?? "<owner/repo>"} --allow-publish -y\`; granular access tokens with bypass 2FA are unsupported. After it succeeds, set \`gh variable set NPM_TRUSTED_PUBLISHER -b true${slug ? ` -R ${slug}` : ""}\`. Until then, NPM_TOKEN is the release fallback.`);
555
+ }
556
+ for (const id of ["chrome", "firefox", "edge", "safari"]) {
557
+ const row = rows.find((entry) => entry.id === id);
558
+ if (!row)
559
+ continue;
560
+ steps.push(`${id}: the listing itself — creating it, and unlisting it again — is a dashboard action with no API: ${row.retractUrl ?? row.url}.`);
561
+ }
562
+ steps.push("Curated directories are listed with their URLs in AGENTS.md's Listing section: each is a one-time human-reviewed submission, and removing one is a reverting pull request.");
563
+ return steps;
564
+ }
565
+ /**
566
+ * The retraction of every registry a dropped surface used to publish to, one step per row:
567
+ * `exists` → the secret guard → `retract`, so re-running the same tag is a no-op and a registry
568
+ * with no machine retraction prints the exact page instead of failing the release.
569
+ *
570
+ * `previous` is the project as the previous tag left it — its surfaces and its version — so the
571
+ * existence probes ask about what that tag actually published, not about the tag being cut now.
572
+ */
573
+ export function unpublishSteps(previous, dropped, hard = false) {
574
+ const version = previous.identity.version ?? "0.0.0";
575
+ return registries(previous)
576
+ .filter((row) => row.surfaces.some((surface) => dropped.includes(surface)))
577
+ .map((row) => {
578
+ const retract = (hard ? row.retractHard : undefined) ?? row.retract;
579
+ const where = row.retractUrl ?? row.url;
580
+ const guard = row.retractSecrets ?? row.secrets;
581
+ const body = !retract
582
+ ? `echo "::notice::${row.id}: retract this listing by hand at ${where}"`
583
+ : guard.length
584
+ ? `if ${allSet(guard)}; then ${retract}; else echo "::notice::${row.id}: set ${guard.join(", ")} to retract automatically, or do it by hand at ${where}"; fi`
585
+ : retract;
586
+ return {
587
+ name: `unpublish ${row.id}`,
588
+ run: row.exists
589
+ ? `if ${row.exists}; then ${body}; else echo "${row.id}: nothing published at ${version}"; fi`
590
+ : body,
591
+ };
592
+ });
593
+ }
594
+ /** The one line CI runs, exactly as `toolfactory validate` is one gate step: the whole diff-and-retract. */
595
+ export function unpublishStep(project) {
596
+ return { name: "unpublish dropped registries", run: `${toolfactoryCli(project)} unpublish` };
597
+ }
@@ -0,0 +1,62 @@
1
+ import { type SurfaceId } from "../model.js";
2
+ /**
3
+ * The tag released before `ref`: `git describe --tags --abbrev=0 <ref>^`. The `^` is what makes it
4
+ * *previous* — on the release runner `ref` is the tag being cut, and on a working tree it is the
5
+ * last release behind HEAD. Undefined at the first tag, which is exactly "nothing was dropped".
6
+ */
7
+ export declare function previousTag(root: string, ref?: string): string | undefined;
8
+ /** `dev.toolfactory/tool.json` as of a ref, or undefined when the ref has none / it does not parse. */
9
+ export declare function toolAtRef(root: string, ref: string): {
10
+ schemaVersion: 1;
11
+ identity: string;
12
+ binding: "typescript" | "python";
13
+ surfaces: ("skill" | "agent-plugins" | "claude" | "codex" | "cursor" | "mcp" | "mcp-registry" | "cli" | "npm" | "pypi" | "openclaw-native" | "hermes-native" | "clawhub" | "web" | "dsh" | "workflows" | "agents" | "readme" | "gemini" | "mcpb" | "browser-extension")[];
14
+ bundle: {
15
+ runtime: "package" | "bundled";
16
+ };
17
+ tests: {
18
+ examples: Record<string, Record<string, unknown>>;
19
+ };
20
+ $schema?: string | undefined;
21
+ runtime?: "kernel" | "none" | undefined;
22
+ kernel?: {
23
+ command: string;
24
+ args: string[];
25
+ } | undefined;
26
+ config?: Record<string, unknown> | undefined;
27
+ npm?: {
28
+ scope?: string | undefined;
29
+ } | undefined;
30
+ codex?: {
31
+ interface?: Record<string, unknown> | undefined;
32
+ } | undefined;
33
+ browserExtension?: {
34
+ endpoint?: string | undefined;
35
+ authDomains?: string[] | undefined;
36
+ cookieExport?: boolean | undefined;
37
+ geckoId?: string | undefined;
38
+ icon?: string | undefined;
39
+ sidePanel?: boolean | undefined;
40
+ safari?: boolean | undefined;
41
+ } | undefined;
42
+ mcpb?: {
43
+ privacyPolicies?: string[] | undefined;
44
+ } | undefined;
45
+ hermes?: {
46
+ toolset?: string | undefined;
47
+ } | undefined;
48
+ openclaw?: {
49
+ registers?: {
50
+ api: string;
51
+ contract: string;
52
+ ids: string[];
53
+ }[] | undefined;
54
+ activation?: Record<string, unknown> | undefined;
55
+ pluginApi?: string | undefined;
56
+ dependencies?: Record<string, string> | undefined;
57
+ peerDependencies?: Record<string, string> | undefined;
58
+ devDependencies?: Record<string, string> | undefined;
59
+ } | undefined;
60
+ } | undefined;
61
+ /** The surfaces a ref selected that the current selection no longer does, in the ref's own order. */
62
+ export declare function droppedSurfaces(previous: readonly SurfaceId[], current: readonly SurfaceId[]): SurfaceId[];