toolfactory 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +166 -0
  2. package/dist/bindings/index.d.ts +23 -0
  3. package/dist/bindings/index.js +29 -0
  4. package/dist/bindings/python.d.ts +33 -0
  5. package/dist/bindings/python.js +805 -0
  6. package/dist/bindings/python.test.d.ts +1 -0
  7. package/dist/bindings/python.test.js +316 -0
  8. package/dist/bindings/typescript.d.ts +39 -0
  9. package/dist/bindings/typescript.js +758 -0
  10. package/dist/bindings/typescript.test.d.ts +1 -0
  11. package/dist/bindings/typescript.test.js +264 -0
  12. package/dist/commands.d.ts +188 -0
  13. package/dist/commands.js +662 -0
  14. package/dist/commands.test.d.ts +1 -0
  15. package/dist/commands.test.js +215 -0
  16. package/dist/e2e.test.d.ts +1 -0
  17. package/dist/e2e.test.js +124 -0
  18. package/dist/hosts/browser.d.ts +10 -0
  19. package/dist/hosts/browser.js +94 -0
  20. package/dist/hosts/github.d.ts +66 -0
  21. package/dist/hosts/github.js +247 -0
  22. package/dist/hosts/openclaw.d.ts +4 -0
  23. package/dist/hosts/openclaw.js +94 -0
  24. package/dist/hosts/template.d.ts +15 -0
  25. package/dist/hosts/template.js +142 -0
  26. package/dist/hosts/web.d.ts +4 -0
  27. package/dist/hosts/web.js +89 -0
  28. package/dist/identity/name.d.ts +20 -0
  29. package/dist/identity/name.js +33 -0
  30. package/dist/identity/name.test.d.ts +1 -0
  31. package/dist/identity/name.test.js +37 -0
  32. package/dist/identity/read.d.ts +9 -0
  33. package/dist/identity/read.js +106 -0
  34. package/dist/identity/read.test.d.ts +1 -0
  35. package/dist/identity/read.test.js +76 -0
  36. package/dist/index.d.ts +6 -0
  37. package/dist/index.js +6 -0
  38. package/dist/introspect/index.d.ts +15 -0
  39. package/dist/introspect/index.js +63 -0
  40. package/dist/model.d.ts +236 -0
  41. package/dist/model.js +214 -0
  42. package/dist/ops.d.ts +255 -0
  43. package/dist/ops.js +297 -0
  44. package/dist/project/apply.d.ts +35 -0
  45. package/dist/project/apply.js +360 -0
  46. package/dist/project/apply.test.d.ts +1 -0
  47. package/dist/project/apply.test.js +285 -0
  48. package/dist/project/gate.d.ts +129 -0
  49. package/dist/project/gate.js +597 -0
  50. package/dist/project/history.d.ts +62 -0
  51. package/dist/project/history.js +41 -0
  52. package/dist/project/load.d.ts +21 -0
  53. package/dist/project/load.js +82 -0
  54. package/dist/project/lock.d.ts +28 -0
  55. package/dist/project/lock.js +42 -0
  56. package/dist/project/plan.d.ts +5 -0
  57. package/dist/project/plan.js +96 -0
  58. package/dist/report/coverage.d.ts +22 -0
  59. package/dist/report/coverage.js +72 -0
  60. package/dist/report/coverage.test.d.ts +1 -0
  61. package/dist/report/coverage.test.js +93 -0
  62. package/dist/surfaces/agent-plugins.d.ts +10 -0
  63. package/dist/surfaces/agent-plugins.js +36 -0
  64. package/dist/surfaces/agents.d.ts +35 -0
  65. package/dist/surfaces/agents.js +609 -0
  66. package/dist/surfaces/agents.template.d.ts +8 -0
  67. package/dist/surfaces/agents.template.js +21 -0
  68. package/dist/surfaces/agents.test.d.ts +1 -0
  69. package/dist/surfaces/agents.test.js +181 -0
  70. package/dist/surfaces/browser-extension.d.ts +101 -0
  71. package/dist/surfaces/browser-extension.js +1034 -0
  72. package/dist/surfaces/browser-extension.test.d.ts +1 -0
  73. package/dist/surfaces/browser-extension.test.js +145 -0
  74. package/dist/surfaces/claude.d.ts +2 -0
  75. package/dist/surfaces/claude.js +81 -0
  76. package/dist/surfaces/codex.d.ts +13 -0
  77. package/dist/surfaces/codex.js +101 -0
  78. package/dist/surfaces/codex.test.d.ts +1 -0
  79. package/dist/surfaces/codex.test.js +67 -0
  80. package/dist/surfaces/cursor.d.ts +2 -0
  81. package/dist/surfaces/cursor.js +50 -0
  82. package/dist/surfaces/distribution.test.d.ts +1 -0
  83. package/dist/surfaces/distribution.test.js +146 -0
  84. package/dist/surfaces/dsh.d.ts +25 -0
  85. package/dist/surfaces/dsh.js +147 -0
  86. package/dist/surfaces/dsh.test.d.ts +1 -0
  87. package/dist/surfaces/dsh.test.js +100 -0
  88. package/dist/surfaces/external.d.ts +9 -0
  89. package/dist/surfaces/external.js +4 -0
  90. package/dist/surfaces/gemini.d.ts +29 -0
  91. package/dist/surfaces/gemini.js +68 -0
  92. package/dist/surfaces/gemini.test.d.ts +1 -0
  93. package/dist/surfaces/gemini.test.js +74 -0
  94. package/dist/surfaces/hermes-native.d.ts +53 -0
  95. package/dist/surfaces/hermes-native.js +507 -0
  96. package/dist/surfaces/hermes-native.test.d.ts +1 -0
  97. package/dist/surfaces/hermes-native.test.js +156 -0
  98. package/dist/surfaces/kernel.d.ts +5 -0
  99. package/dist/surfaces/kernel.js +63 -0
  100. package/dist/surfaces/mcp-registry.d.ts +19 -0
  101. package/dist/surfaces/mcp-registry.js +160 -0
  102. package/dist/surfaces/mcp-registry.test.d.ts +1 -0
  103. package/dist/surfaces/mcp-registry.test.js +79 -0
  104. package/dist/surfaces/mcpb.d.ts +11 -0
  105. package/dist/surfaces/mcpb.js +118 -0
  106. package/dist/surfaces/mcpb.test.d.ts +1 -0
  107. package/dist/surfaces/mcpb.test.js +104 -0
  108. package/dist/surfaces/npm.d.ts +6 -0
  109. package/dist/surfaces/npm.js +72 -0
  110. package/dist/surfaces/openclaw-native.d.ts +93 -0
  111. package/dist/surfaces/openclaw-native.js +956 -0
  112. package/dist/surfaces/openclaw-native.test.d.ts +1 -0
  113. package/dist/surfaces/openclaw-native.test.js +246 -0
  114. package/dist/surfaces/pypi.d.ts +5 -0
  115. package/dist/surfaces/pypi.js +82 -0
  116. package/dist/surfaces/readme.d.ts +7 -0
  117. package/dist/surfaces/readme.js +181 -0
  118. package/dist/surfaces/registry.d.ts +6 -0
  119. package/dist/surfaces/registry.js +73 -0
  120. package/dist/surfaces/shared.d.ts +49 -0
  121. package/dist/surfaces/shared.js +112 -0
  122. package/dist/surfaces/skill.d.ts +7 -0
  123. package/dist/surfaces/skill.js +118 -0
  124. package/dist/surfaces/skill.test.d.ts +1 -0
  125. package/dist/surfaces/skill.test.js +87 -0
  126. package/dist/surfaces/web.d.ts +97 -0
  127. package/dist/surfaces/web.js +1285 -0
  128. package/dist/surfaces/web.test.d.ts +1 -0
  129. package/dist/surfaces/web.test.js +255 -0
  130. package/dist/surfaces/workflows.d.ts +2 -0
  131. package/dist/surfaces/workflows.js +809 -0
  132. package/dist/surfaces/workflows.test.d.ts +1 -0
  133. package/dist/surfaces/workflows.test.js +563 -0
  134. package/dist/toolfactory/cli.d.ts +2 -0
  135. package/dist/toolfactory/cli.js +65 -0
  136. package/dist/toolfactory/config.d.ts +3 -0
  137. package/dist/toolfactory/config.js +27 -0
  138. package/dist/toolfactory/mcp.d.ts +37 -0
  139. package/dist/toolfactory/mcp.js +269 -0
  140. package/dist/toolfactory/types.d.ts +31 -0
  141. package/dist/toolfactory/types.js +10 -0
  142. package/dist/toolfactory/web.d.ts +4 -0
  143. package/dist/toolfactory/web.js +40 -0
  144. package/dist/validate/agent-plugins.d.ts +5 -0
  145. package/dist/validate/agent-plugins.js +46 -0
  146. package/package.json +68 -0
  147. package/schemas/agent-plugins/mcp.schema.json +120 -0
  148. package/schemas/agent-plugins/plugin.schema.json +65 -0
  149. package/src/bindings/index.ts +47 -0
  150. package/src/bindings/python.test.ts +374 -0
  151. package/src/bindings/python.ts +862 -0
  152. package/src/bindings/typescript.test.ts +329 -0
  153. package/src/bindings/typescript.ts +818 -0
  154. package/src/commands.test.ts +253 -0
  155. package/src/commands.ts +886 -0
  156. package/src/e2e.test.ts +169 -0
  157. package/src/hosts/browser.ts +106 -0
  158. package/src/hosts/github.ts +321 -0
  159. package/src/hosts/openclaw.ts +110 -0
  160. package/src/hosts/template.ts +149 -0
  161. package/src/hosts/web.ts +111 -0
  162. package/src/identity/name.test.ts +43 -0
  163. package/src/identity/name.ts +39 -0
  164. package/src/identity/read.test.ts +92 -0
  165. package/src/identity/read.ts +119 -0
  166. package/src/index.ts +11 -0
  167. package/src/introspect/index.ts +72 -0
  168. package/src/model.ts +386 -0
  169. package/src/ops.ts +333 -0
  170. package/src/project/apply.test.ts +348 -0
  171. package/src/project/apply.ts +417 -0
  172. package/src/project/gate.ts +717 -0
  173. package/src/project/history.ts +46 -0
  174. package/src/project/load.ts +104 -0
  175. package/src/project/lock.ts +54 -0
  176. package/src/project/plan.ts +101 -0
  177. package/src/report/coverage.test.ts +108 -0
  178. package/src/report/coverage.ts +113 -0
  179. package/src/surfaces/agent-plugins.ts +47 -0
  180. package/src/surfaces/agents.template.ts +32 -0
  181. package/src/surfaces/agents.test.ts +206 -0
  182. package/src/surfaces/agents.ts +729 -0
  183. package/src/surfaces/browser-extension.test.ts +175 -0
  184. package/src/surfaces/browser-extension.ts +1095 -0
  185. package/src/surfaces/claude.ts +99 -0
  186. package/src/surfaces/codex.test.ts +71 -0
  187. package/src/surfaces/codex.ts +105 -0
  188. package/src/surfaces/cursor.ts +62 -0
  189. package/src/surfaces/distribution.test.ts +166 -0
  190. package/src/surfaces/dsh.test.ts +110 -0
  191. package/src/surfaces/dsh.ts +159 -0
  192. package/src/surfaces/external.ts +13 -0
  193. package/src/surfaces/gemini.test.ts +80 -0
  194. package/src/surfaces/gemini.ts +84 -0
  195. package/src/surfaces/hermes-native.test.ts +179 -0
  196. package/src/surfaces/hermes-native.ts +584 -0
  197. package/src/surfaces/kernel.ts +68 -0
  198. package/src/surfaces/mcp-registry.test.ts +99 -0
  199. package/src/surfaces/mcp-registry.ts +190 -0
  200. package/src/surfaces/mcpb.test.ts +112 -0
  201. package/src/surfaces/mcpb.ts +130 -0
  202. package/src/surfaces/npm.ts +78 -0
  203. package/src/surfaces/openclaw-native.test.ts +283 -0
  204. package/src/surfaces/openclaw-native.ts +1070 -0
  205. package/src/surfaces/pypi.ts +84 -0
  206. package/src/surfaces/readme.ts +218 -0
  207. package/src/surfaces/registry.ts +79 -0
  208. package/src/surfaces/shared.ts +140 -0
  209. package/src/surfaces/skill.test.ts +103 -0
  210. package/src/surfaces/skill.ts +136 -0
  211. package/src/surfaces/web.test.ts +303 -0
  212. package/src/surfaces/web.ts +1310 -0
  213. package/src/surfaces/workflows.test.ts +677 -0
  214. package/src/surfaces/workflows.ts +936 -0
  215. package/src/toolfactory/cli.ts +74 -0
  216. package/src/toolfactory/config.ts +33 -0
  217. package/src/toolfactory/mcp.ts +311 -0
  218. package/src/toolfactory/types.ts +52 -0
  219. package/src/toolfactory/web.ts +45 -0
  220. package/src/validate/agent-plugins.ts +53 -0
  221. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  222. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  223. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  224. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  225. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  226. package/web/dist/assets/index-CCXhQKK3.css +2 -0
  227. package/web/dist/assets/index-D1PwZ3qS.js +10 -0
  228. package/web/dist/index.html +14 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,246 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { describe, expect, it } from "vitest";
3
+ import { scaffoldDrift } from "../hosts/openclaw.js";
4
+ import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
5
+ import { HOST_DIR, OPENCLAW_ADDITIONS, OPENCLAW_SCAFFOLD, surface } from "./openclaw-native.js";
6
+ const echo = {
7
+ name: "echo",
8
+ description: "Return the text you pass in.",
9
+ inputSchema: {
10
+ $schema: "https://json-schema.org/draft/2020-12/schema",
11
+ type: "object",
12
+ properties: { text: { type: "string" } },
13
+ required: ["text"],
14
+ },
15
+ outputSchema: {
16
+ type: "object",
17
+ properties: { echoed: { type: "string" } },
18
+ required: ["echoed"],
19
+ },
20
+ requires: [],
21
+ };
22
+ const shoot = { name: "shoot", inputSchema: { type: "object" }, requires: ["browser"] };
23
+ function project(overrides = {}) {
24
+ return {
25
+ root: "/repo",
26
+ tool: {
27
+ schemaVersion: 1,
28
+ identity: "package.json",
29
+ binding: "typescript",
30
+ surfaces: ["openclaw-native", "mcp"],
31
+ bundle: { runtime: "package" },
32
+ tests: { examples: { echo: { text: "hello" } } },
33
+ },
34
+ identity: { name: "hello", version: "0.1.0", description: "Say hello" },
35
+ identityExtra: {},
36
+ operations: [echo, shoot],
37
+ toolfactoryVersion: "0.1.0",
38
+ ...overrides,
39
+ };
40
+ }
41
+ /** A zero-operation plugin whose whole contribution is one declared host registration. */
42
+ function voice() {
43
+ const base = project();
44
+ return {
45
+ ...base,
46
+ tool: {
47
+ ...base.tool,
48
+ openclaw: {
49
+ registers: [
50
+ {
51
+ api: "registerRealtimeVoiceProvider",
52
+ contract: "realtimeVoiceProviders",
53
+ ids: ["codex"],
54
+ },
55
+ ],
56
+ pluginApi: ">=2026.8.2",
57
+ dependencies: { ws: "^8.18.0" },
58
+ },
59
+ },
60
+ operations: [],
61
+ };
62
+ }
63
+ /** Every planned file as text: full files verbatim, region files as the region toolfactory owns. */
64
+ function emitted(target) {
65
+ return Object.fromEntries(surface
66
+ .plan(target)
67
+ .map((file) => [
68
+ file.path,
69
+ file.kind === "file"
70
+ ? file.content
71
+ : file.kind === "region"
72
+ ? file.regions.map((region) => region.content).join("")
73
+ : JSON.stringify(file.patch),
74
+ ]));
75
+ }
76
+ describe("openclaw-native", () => {
77
+ it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
78
+ const files = emitted(project());
79
+ const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
80
+ expect(manifest.contracts).toEqual({ tools: ["echo"] });
81
+ expect(manifest.activation).toEqual({ onStartup: true });
82
+ expect(surface.verdict?.(shoot, project())).toEqual({
83
+ kind: "excluded",
84
+ reason: "excluded:implement-in-hosts",
85
+ });
86
+ // The escape hatch is where the author writes it; the README has to say so.
87
+ expect(files[`${HOST_DIR}/README.md`]).toContain("excluded:implement-in-hosts");
88
+ expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
89
+ const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
90
+ expect(pkg.openclaw).toEqual({
91
+ extensions: ["./dist/index.js"],
92
+ compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
93
+ build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
94
+ });
95
+ // Nothing to expect of the inspector until the plugin declares a registration.
96
+ expect(pkg.pluginInspector).toBeUndefined();
97
+ // TypeScript core: in-process import of the author's module, no subprocess.
98
+ expect(pkg.dependencies.hello).toBe("file:../..");
99
+ expect(files[`${HOST_DIR}/src/index.ts`]).toContain('from "hello/dist/ops.js"');
100
+ // JSON Schema goes through untouched via Type.Unsafe; $schema is what OpenClaw strips.
101
+ expect(files[`${HOST_DIR}/src/index.ts`]).toContain('"text"');
102
+ expect(files[`${HOST_DIR}/src/index.ts`]).not.toContain("$schema");
103
+ // Module load must be side-effect-free: every host loader, and `plugin-inspector check
104
+ // --runtime`, import the entry long before a tool runs.
105
+ expect(files[`${HOST_DIR}/src/index.ts`]).toContain("const dataDir = () => join(");
106
+ expect(files[`${HOST_DIR}/src/index.ts`]).toContain("dataDir: dataDir(),");
107
+ });
108
+ it("serves the web app as a gateway route and a Control UI tab, in process, only in TypeScript", () => {
109
+ const withWeb = project({
110
+ tool: { ...project().tool, surfaces: ["openclaw-native", "mcp", "web", "npm"] },
111
+ });
112
+ const files = emitted(withWeb);
113
+ const index = files[`${HOST_DIR}/src/index.ts`] ?? "";
114
+ // The route hands requests to the core's own router, so `<prefix>/mcp` needs no second
115
+ // listener and `/env` stays off — the gateway's working directory is not the author's.
116
+ expect(index).toContain('import { handler } from "hello/dist/toolfactory/mcp.js";');
117
+ expect(index).toContain('const WEB_PATH = "/plugins/hello/web";');
118
+ expect(index).toContain('auth: "gateway"');
119
+ expect(index).toContain("handler({ path: `${WEB_PATH}/mcp`, prefix: WEB_PATH, env: false })");
120
+ expect(index).toContain('surface: "tab" as const');
121
+ expect(index).toContain("api.registerControlUiDescriptor(descriptor)");
122
+ // Neither registrar is a manifest contract key; both are what the inspector expects to see.
123
+ expect(JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}").contracts).toEqual({
124
+ tools: ["echo"],
125
+ });
126
+ expect(JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}").pluginInspector.plugin.expect
127
+ .registrations).toEqual(["registerHttpRoute", "registerControlUiDescriptor"]);
128
+ expect(files[`${HOST_DIR}/src/index.test.ts`]).toContain("serves the web app as a Control UI tab");
129
+ // A Python core's plugin is an out-of-process shim: there is no handler to mount.
130
+ const python = emitted(project({
131
+ tool: {
132
+ ...withWeb.tool,
133
+ binding: "python",
134
+ surfaces: ["openclaw-native", "mcp", "web", "pypi"],
135
+ },
136
+ }));
137
+ expect(python[`${HOST_DIR}/src/index.ts`]).not.toContain("registerHttpRoute");
138
+ });
139
+ it("degrades a non-TypeScript core to an out-of-process shim", () => {
140
+ const python = project({
141
+ tool: { ...project().tool, binding: "python" },
142
+ });
143
+ expect(surface.verdict?.(echo, python)).toEqual({
144
+ kind: "degraded",
145
+ reason: "degraded:out-of-process",
146
+ });
147
+ const index = emitted(python)[`${HOST_DIR}/src/index.ts`] ?? "";
148
+ expect(index).toContain('"run", "python", "-m", "hello.toolfactory.cli"');
149
+ // The host's state directory reaches an out-of-process kernel as the environment variable
150
+ // every surface reads it from.
151
+ expect(index).toContain('env["HELLO_DATA_DIR"] = dataDir();');
152
+ expect(index).not.toContain("ops.js");
153
+ expect(JSON.parse(emitted(python)[`${HOST_DIR}/package.json`] ?? "{}").dependencies).toEqual(OPENCLAW_SCAFFOLD.dependencies);
154
+ });
155
+ it("drives manifest, inspector and test from one `registers` declaration", () => {
156
+ const target = voice();
157
+ const files = emitted(target);
158
+ const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
159
+ expect(manifest.contracts).toEqual({ tools: [], realtimeVoiceProviders: ["codex"] });
160
+ const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
161
+ expect(pkg.pluginInspector.plugin.expect).toEqual({
162
+ registrations: ["registerRealtimeVoiceProvider"],
163
+ manifestContracts: ["realtimeVoiceProviders"],
164
+ });
165
+ // The author's extra dependency merges after the scaffold's; the core is not imported at all.
166
+ expect(pkg.dependencies).toEqual({ ...OPENCLAW_SCAFFOLD.dependencies, ws: "^8.18.0" });
167
+ expect(pkg.peerDependencies.openclaw).toBe(">=2026.8.2");
168
+ expect(pkg.openclaw.compat.pluginApi).toBe(">=2026.8.2");
169
+ // The generated region stops at the entry: the registration itself is the author's tail.
170
+ const region = files[`${HOST_DIR}/src/index.ts`] ?? "";
171
+ expect(region).toContain("const entry = defineToolPlugin({");
172
+ expect(region).not.toContain("registerRealtimeVoiceProvider(");
173
+ expect(region).toContain('Parameters<OpenClawPluginApi["registerRealtimeVoiceProvider"]>[0]');
174
+ expect(region).not.toContain("typebox");
175
+ const template = surface.plan(target).find((file) => file.path.endsWith("src/index.ts"));
176
+ expect(template?.kind === "region" && template.template).toContain("export default entry;");
177
+ // The generated test records the registration the declaration promised.
178
+ const test = files[`${HOST_DIR}/src/index.test.ts`] ?? "";
179
+ expect(test).toContain("entry.register(api as OpenClawPluginApi)");
180
+ expect(test).toContain('registered["registerRealtimeVoiceProvider"]');
181
+ expect(files[`${HOST_DIR}/README.md`]).toContain("codex-app-server-extensions.ts");
182
+ });
183
+ it("projects a discriminating scripted-model e2e lane, and none without a tool to call", () => {
184
+ const files = emitted(project());
185
+ const fixtures = JSON.parse(files[`${HOST_DIR}/e2e/fixtures.json`] ?? "{}");
186
+ // Leg 1 asks for the operation with the authored example arguments; leg 2 answers OK only
187
+ // when the tool's own result carries a name its output schema promises; leg 3 catches the rest.
188
+ expect(fixtures.fixtures).toEqual([
189
+ {
190
+ match: { toolName: "echo", hasToolResult: false },
191
+ response: { toolCalls: [{ name: "echo", arguments: { text: "hello" } }] },
192
+ },
193
+ {
194
+ match: { hasToolResult: true, toolResultContains: "echoed" },
195
+ response: { content: "HELLO_OK" },
196
+ },
197
+ { match: { hasToolResult: true }, response: { content: "HELLO_FAIL" } },
198
+ ]);
199
+ const e2e = files[`${HOST_DIR}/e2e/openclaw.e2e.test.ts`] ?? "";
200
+ expect(e2e).toContain('expect(turn.payloads[0].text).toBe("HELLO_OK")');
201
+ expect(e2e).toContain('"plugins", "install", "--link", pluginDir');
202
+ // The three constraints that are not discoverable anywhere else.
203
+ expect(e2e).toContain('key !== "VITEST" && !key.startsWith("VITEST_")');
204
+ expect(files[`${HOST_DIR}/vitest.e2e.config.ts`]).toContain('include: ["e2e/**/*.e2e.test.ts"]');
205
+ const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
206
+ expect(pkg.scripts["test:e2e"]).toBe(OPENCLAW_ADDITIONS.e2eScript);
207
+ expect(pkg.devDependencies["@copilotkit/aimock"]).toBe(OPENCLAW_ADDITIONS.aimock);
208
+ // `file:../..` becomes a node_modules symlink out of the install root, which OpenClaw's
209
+ // install-time safety scan refuses; install-links materialises it instead.
210
+ expect(files[`${HOST_DIR}/.npmrc`]).toContain("install-links=true");
211
+ // Without it `tsc` emits a dist/ from source that failed type-checking.
212
+ expect(JSON.parse(files[`${HOST_DIR}/tsconfig.json`] ?? "{}").compilerOptions.noEmitOnError).toBe(true);
213
+ // The lane is opt-in: a real turn really runs the operation, so no `tests.examples` entry
214
+ // (and no operation at all) means no lane.
215
+ const base = project();
216
+ const unexampled = emitted({ ...base, tool: { ...base.tool, tests: { examples: {} } } });
217
+ expect(unexampled[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
218
+ const none = emitted(voice());
219
+ expect(none[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
220
+ expect(JSON.parse(none[`${HOST_DIR}/package.json`] ?? "{}").scripts["test:e2e"]).toBeUndefined();
221
+ });
222
+ it("reports what a zero-operation plugin contributes instead of an empty table", () => {
223
+ const coverage = computeCoverage(voice(), [surface]);
224
+ expect(coverage.rows).toEqual([]);
225
+ const markdown = renderCoverageMarkdown(coverage, [], 7);
226
+ expect(markdown).not.toContain("| operation |");
227
+ expect(markdown).toContain("`registerRealtimeVoiceProvider` → `realtimeVoiceProviders`: `codex`");
228
+ });
229
+ });
230
+ /** C2: the scaffold constants are only trustworthy while the real generator still writes them. */
231
+ function hasOpenclaw() {
232
+ try {
233
+ execFileSync("openclaw", ["--version"], { stdio: "ignore" });
234
+ return true;
235
+ }
236
+ catch {
237
+ return false;
238
+ }
239
+ }
240
+ describe.skipIf(!hasOpenclaw())("openclaw scaffold", () => {
241
+ it("still matches `openclaw plugins init --type tool`", () => {
242
+ expect(scaffoldDrift(project())).toEqual([]);
243
+ // A declared `pluginApi` is an intentional override, not drift.
244
+ expect(scaffoldDrift(voice())).toEqual([]);
245
+ });
246
+ });
@@ -0,0 +1,5 @@
1
+ import type { Surface } from "../model.js";
2
+ export declare const README_PATH = "README.md";
3
+ export declare const MCP_NAME_BEGIN = "<!-- tf:mcp-name -->";
4
+ export declare const MCP_NAME_END = "<!-- /tf:mcp-name -->";
5
+ export declare const surface: Surface;
@@ -0,0 +1,82 @@
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 { registryName } from "./mcp-registry.js";
8
+ import { compact, has } from "./shared.js";
9
+ import { WEB_DIR } from "./web.js";
10
+ export const README_PATH = "README.md";
11
+ export const MCP_NAME_BEGIN = "<!-- tf:mcp-name -->";
12
+ export const MCP_NAME_END = "<!-- /tf:mcp-name -->";
13
+ export const surface = {
14
+ id: "pypi",
15
+ plan(project) {
16
+ if (project.tool.binding !== "python") {
17
+ throw new Error('Surface "pypi" requires the python binding.');
18
+ }
19
+ const authored = project.tool.identity === "pyproject.toml";
20
+ const registry = has(project, "mcp-registry");
21
+ const patch = {
22
+ project: compact({
23
+ ...(authored ? {} : projectTable(project)),
24
+ readme: registry ? README_PATH : undefined,
25
+ scripts: has(project, "cli")
26
+ ? { [project.identity.name]: cliEntryPoint(project) }
27
+ : undefined,
28
+ }),
29
+ // The kernel serves the built page from inside its own package, so the wheel has to
30
+ // carry it: `packages` only copies the package tree, `force-include` maps anything else.
31
+ ...(has(project, "web")
32
+ ? {
33
+ tool: {
34
+ hatch: {
35
+ build: {
36
+ targets: {
37
+ wheel: {
38
+ "force-include": {
39
+ [`${WEB_DIR}/dist`]: `${pythonPackage(project)}/${WEB_DIR}`,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ }
47
+ : {}),
48
+ };
49
+ const files = [
50
+ {
51
+ kind: "merge",
52
+ path: "pyproject.toml",
53
+ format: "toml",
54
+ patch,
55
+ owned: ["project.scripts"],
56
+ },
57
+ ];
58
+ if (!registry)
59
+ return files;
60
+ const marker = `\n<!-- mcp-name: ${registryName(project)} -->\n`;
61
+ return [
62
+ ...files,
63
+ {
64
+ kind: "region",
65
+ path: README_PATH,
66
+ regions: [{ begin: MCP_NAME_BEGIN, end: MCP_NAME_END, content: marker }],
67
+ template: [
68
+ `# ${project.identity.name}`,
69
+ "",
70
+ project.identity.description ?? "",
71
+ "",
72
+ MCP_NAME_BEGIN,
73
+ MCP_NAME_END,
74
+ "",
75
+ ].join("\n"),
76
+ },
77
+ ];
78
+ },
79
+ validate(project) {
80
+ return [{ label: "uv build", command: "uv", args: ["build"], cwd: project.root }];
81
+ },
82
+ };
@@ -0,0 +1,7 @@
1
+ import type { Project, Surface } from "../model.js";
2
+ export declare const README_PATH = "README.md";
3
+ export declare const INSTALL_BEGIN = "<!-- tf:install -->";
4
+ export declare const INSTALL_END = "<!-- /tf:install -->";
5
+ /** The content of the generated region — everything between the markers, not the markers. */
6
+ export declare function installContent(project: Project): string;
7
+ export declare const surface: Surface;
@@ -0,0 +1,181 @@
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 { HOST_DIR as BROWSER_HOST_DIR, zipName } from "./browser-extension.js";
20
+ import { HOST_DIR as DSH_HOST_DIR, dshTarball } from "./dsh.js";
21
+ import { pluginDir as hermesPluginDir } from "./hermes-native.js";
22
+ import { HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
23
+ import { envName, has, npmName, pypiName, requiredConfig } from "./shared.js";
24
+ export const README_PATH = "README.md";
25
+ export const INSTALL_BEGIN = "<!-- tf:install -->";
26
+ export const INSTALL_END = "<!-- /tf:install -->";
27
+ /** A stable label for repositories that ship an Agent Skill; unlike skills.sh's live count badge,
28
+ * it does not depend on a separate registry index. */
29
+ function skillBadge(slug) {
30
+ return `[![Agent Skill](https://img.shields.io/badge/Agent_Skill-available-5B5BD6)](https://github.com/${slug})`;
31
+ }
32
+ /**
33
+ * The kernel MCP server's published launch, unpinned — `{command, args}`, plus an `env` map of
34
+ * blank placeholders for whatever config the tool requires. The one source both `mcpCommand`
35
+ * (the plain-text line) and the VS Code / Cursor install badges below encode: a badge can never
36
+ * name a launch the text line does not.
37
+ */
38
+ function launchConfig(project) {
39
+ const { command, args } = project.tool.binding === "python"
40
+ ? (() => {
41
+ const pypi = pypiName(project);
42
+ // `uvx <pkg>` runs the command of the same name; a dotted tool name renames the package.
43
+ return pypi === project.identity.name
44
+ ? { command: "uvx", args: [pypi, "mcp"] }
45
+ : { command: "uvx", args: ["--from", pypi, project.identity.name, "mcp"] };
46
+ })()
47
+ : { command: "npx", args: ["-y", npmName(project), "mcp"] };
48
+ const required = requiredConfig(project);
49
+ return required.length
50
+ ? { command, args, env: Object.fromEntries(required.map((key) => [envName(key), ""])) }
51
+ : { command, args };
52
+ }
53
+ function mcpCommand(project) {
54
+ const { command, args } = launchConfig(project);
55
+ return [command, ...args].join(" ");
56
+ }
57
+ /** github.com/github/github-mcp-server's README badge, verbatim shields.io style. */
58
+ function vscodeBadge(project) {
59
+ const config = encodeURIComponent(JSON.stringify(launchConfig(project)));
60
+ const url = `https://vscode.dev/redirect/mcp/install?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
61
+ return `[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](${url})`;
62
+ }
63
+ /** cursor.com/docs/mcp/install-links's own badge SVG and `en/install-mcp` redirect. */
64
+ function cursorBadge(project) {
65
+ const config = Buffer.from(JSON.stringify(launchConfig(project))).toString("base64");
66
+ const url = `https://cursor.com/en/install-mcp?name=${encodeURIComponent(project.identity.name)}&config=${config}`;
67
+ return `[![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](${url})`;
68
+ }
69
+ function installLines(project) {
70
+ const { identity } = project;
71
+ const slug = githubSlug(identity.repository);
72
+ const lines = [];
73
+ if (has(project, "skill") && slug) {
74
+ lines.push(`- **Agent Skill** — \`npx skills add ${slug}\``);
75
+ }
76
+ if (has(project, "mcp")) {
77
+ // The one-click badges need a published package to point at: they read the same launch
78
+ // config the text line does, so they only appear once there is somewhere to `npx`/`uvx` from.
79
+ const badges = has(project, "npm") || has(project, "pypi")
80
+ ? ` ${vscodeBadge(project)} ${cursorBadge(project)}`
81
+ : "";
82
+ lines.push(`- **MCP server** — \`${mcpCommand(project)}\`${badges}`);
83
+ }
84
+ if (has(project, "mcpb")) {
85
+ lines.push(`- **Claude Desktop extension** — download \`${identity.name}.mcpb\` from the GitHub Release and double-click to install`);
86
+ }
87
+ if (has(project, "claude")) {
88
+ // The repository is its own marketplace (`.claude-plugin/marketplace.json`); Copilot CLI
89
+ // reads the same file with the same command.
90
+ lines.push(`- **Claude Code plugin** — \`claude plugin marketplace add ${slug ?? "."}\`, then \`claude plugin install ${identity.name}@${identity.name}\``);
91
+ }
92
+ if (has(project, "codex")) {
93
+ // `.agents/plugins/marketplace.json` lists the repository's own plugin (`codex.ts`).
94
+ lines.push(`- **Codex plugin** — \`codex plugin marketplace add ${slug ?? "."}\`, then \`codex plugin add ${identity.name}@${identity.name}\``);
95
+ }
96
+ if (has(project, "gemini")) {
97
+ lines.push(slug
98
+ ? `- **Gemini CLI extension** — \`gemini extensions install https://github.com/${slug}\``
99
+ : "- **Gemini CLI extension** — `gemini extensions link .` from a checkout");
100
+ }
101
+ if (has(project, "openclaw-native")) {
102
+ const clawhub = has(project, "clawhub")
103
+ ? `; published: \`openclaw plugins install clawhub:${projectName.openclawPackage(identity.name)}\``
104
+ : "";
105
+ lines.push(`- **OpenClaw plugin** — \`openclaw plugins install --link ${OPENCLAW_HOST_DIR}\` from a checkout${clawhub}`);
106
+ }
107
+ if (has(project, "hermes-native")) {
108
+ const source = slug ? `https://github.com/${slug}` : "file://$PWD";
109
+ lines.push(`- **Hermes plugin** — \`hermes plugins install ${source}#${hermesPluginDir(project)}\``);
110
+ }
111
+ if (has(project, "dsh")) {
112
+ // `dsh plugin` is a pnpm forwarder: it installs a path, a tarball or a registry spec.
113
+ lines.push(`- **DSH plugin** (experimental) — \`dsh plugin --profile <profile> add ./${DSH_HOST_DIR}\` from a checkout, or the release tarball \`${dshTarball(project)}\``);
114
+ }
115
+ if (has(project, "browser-extension")) {
116
+ // Three channels, in the order a reader needs them: build-and-load from a checkout, the
117
+ // release assets (only Mozilla's signed xpi is a real self-hosted install — Chrome drops
118
+ // side-loaded unpacked extensions), the store listings. Pairing is the last step of each.
119
+ const zips = ["chrome", "firefox", "edge"].map((browser) => `\`${zipName(project, browser)}\``);
120
+ lines.push([
121
+ `- **Browser extension** — from a checkout: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`,
122
+ ` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\``,
123
+ ` (Firefox: \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\`). Each GitHub Release attaches the`,
124
+ ` store uploads ${zips.join(", ")}, and the Mozilla-signed \`.xpi\`,`,
125
+ " which is the only download-and-install channel now that Chrome no longer keeps side-loaded unpacked",
126
+ " extensions; the Chrome Web Store, Firefox Add-ons and Edge Add-ons listings appear once the release's",
127
+ ` submit step has each store's credentials. Then pair it: \`${mcpCommand(project)} --http --pair\``,
128
+ " prints the `<url>#<token>` the extension's options page accepts.",
129
+ ].join("\n"));
130
+ }
131
+ if (has(project, "web")) {
132
+ // Not an install: the one command that puts the page in front of a human, from wherever the
133
+ // tool already is. The same listener answers `/mcp`, so the page needs nothing else running.
134
+ lines.push(`- **Web app** — \`${mcpCommand(project)} --http --open\` serves the operations page beside the`, " MCP endpoint on one port and opens it; over MCP or a skill, the `web` operation does the same and", " returns the URL.");
135
+ }
136
+ if (has(project, "npm"))
137
+ lines.push(`- **npm package** — \`npm install ${npmName(project)}\``);
138
+ if (has(project, "pypi"))
139
+ lines.push(`- **PyPI package** — \`uv add ${pypiName(project)}\``);
140
+ return lines;
141
+ }
142
+ /** The content of the generated region — everything between the markers, not the markers. */
143
+ export function installContent(project) {
144
+ const slug = githubSlug(project.identity.repository);
145
+ const badge = has(project, "skill") && slug ? [skillBadge(slug), ""] : [];
146
+ const lines = installLines(project);
147
+ return `${[
148
+ "",
149
+ "## Install",
150
+ "",
151
+ ...badge,
152
+ ...(lines.length
153
+ ? lines
154
+ : ["_No installable surface is selected: this repository ships source only._"]),
155
+ "",
156
+ ].join("\n")}\n`;
157
+ }
158
+ function template(project) {
159
+ return [
160
+ `# ${project.identity.name}`,
161
+ "",
162
+ project.identity.description ?? "One paragraph: what this tool does, and for whom.",
163
+ "",
164
+ INSTALL_BEGIN,
165
+ INSTALL_END,
166
+ "",
167
+ ].join("\n");
168
+ }
169
+ export const surface = {
170
+ id: "readme",
171
+ plan(project) {
172
+ return [
173
+ {
174
+ kind: "region",
175
+ path: README_PATH,
176
+ regions: [{ begin: INSTALL_BEGIN, end: INSTALL_END, content: installContent(project) }],
177
+ template: template(project),
178
+ },
179
+ ];
180
+ },
181
+ };
@@ -0,0 +1,6 @@
1
+ import type { Surface, SurfaceId } from "../model.js";
2
+ export declare function registerSurface(surface: Surface): void;
3
+ export declare function getSurface(id: SurfaceId): Surface;
4
+ export declare function selectedSurfaces(ids: readonly SurfaceId[]): Surface[];
5
+ /** A selection is valid only when every surface's declared dependencies are selected too. */
6
+ export declare function assertSurfaceRequirements(ids: readonly SurfaceId[]): void;
@@ -0,0 +1,73 @@
1
+ import { surface as agentPlugins } from "./agent-plugins.js";
2
+ import { surface as agents } from "./agents.js";
3
+ import { surface as browserExtension } from "./browser-extension.js";
4
+ import { surface as claude } from "./claude.js";
5
+ import { surface as codex } from "./codex.js";
6
+ import { surface as cursor } from "./cursor.js";
7
+ import { surface as dsh } from "./dsh.js";
8
+ import { clawhub } from "./external.js";
9
+ import { surface as gemini } from "./gemini.js";
10
+ import { surface as hermesNative } from "./hermes-native.js";
11
+ import { cli, mcp } from "./kernel.js";
12
+ import { surface as mcpRegistry } from "./mcp-registry.js";
13
+ import { surface as mcpb } from "./mcpb.js";
14
+ import { surface as npm } from "./npm.js";
15
+ import { surface as openclawNative } from "./openclaw-native.js";
16
+ import { surface as pypi } from "./pypi.js";
17
+ import { surface as readme } from "./readme.js";
18
+ import { surface as skill } from "./skill.js";
19
+ import { surface as web } from "./web.js";
20
+ import { surface as workflows } from "./workflows.js";
21
+ const registry = {
22
+ "agent-plugins": agentPlugins,
23
+ agents,
24
+ "browser-extension": { ...browserExtension, requires: ["mcp"] },
25
+ claude,
26
+ codex,
27
+ // Declared dependencies (§3): these projectors point at a file another surface writes, so a
28
+ // selection without it would ship an artifact referencing nothing. Refused at plan time.
29
+ cursor: { ...cursor, requires: ["agent-plugins"] },
30
+ clawhub,
31
+ // Reaches the kernel only through DSH's own MCP client, so the bundle is inert without it.
32
+ dsh: { ...dsh, requires: ["mcp"] },
33
+ // The manifest's only payload is the kernel's launch row: an extension without it is a
34
+ // context file and nothing else.
35
+ gemini: { ...gemini, requires: ["mcp"] },
36
+ cli,
37
+ mcp,
38
+ "mcp-registry": mcpRegistry,
39
+ // The bundle root is the npm tarball with its production dependencies installed into it, and
40
+ // its one row launches the kernel MCP server: without either surface there is nothing to pack.
41
+ mcpb: { ...mcpb, requires: ["mcp", "npm"] },
42
+ npm,
43
+ skill,
44
+ "hermes-native": hermesNative,
45
+ "openclaw-native": openclawNative,
46
+ pypi,
47
+ readme,
48
+ web: { ...web, requires: ["mcp"] },
49
+ workflows,
50
+ };
51
+ export function registerSurface(surface) {
52
+ registry[surface.id] = surface;
53
+ }
54
+ export function getSurface(id) {
55
+ const surface = registry[id];
56
+ if (!surface)
57
+ throw new Error(`Surface "${id}" is not implemented in this toolfactory version.`);
58
+ return surface;
59
+ }
60
+ export function selectedSurfaces(ids) {
61
+ return ids.map(getSurface);
62
+ }
63
+ /** A selection is valid only when every surface's declared dependencies are selected too. */
64
+ export function assertSurfaceRequirements(ids) {
65
+ const selected = new Set(ids);
66
+ for (const id of ids) {
67
+ for (const required of getSurface(id).requires ?? []) {
68
+ if (selected.has(required))
69
+ continue;
70
+ throw new Error(`Surface "${id}" requires surface "${required}", which is not selected (surfaces: ${ids.join(", ")}). Select "${required}" or drop "${id}".`);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,49 @@
1
+ import { type Operation, type Project, type Verdict } from "../model.js";
2
+ export declare const TOOLFACTORY_DIR = "dev.toolfactory";
3
+ export declare function json(value: unknown): string;
4
+ /** Drop undefined values so projections never emit `"key": undefined`. */
5
+ export declare function compact<T extends Record<string, unknown>>(record: T): Partial<T>;
6
+ export declare function has(project: Project, surface: string): boolean;
7
+ export declare function npmName(project: Project): string;
8
+ export declare function pypiName(project: Project): string;
9
+ /** The launch command for the kernel MCP server as an installed package or a committed build. */
10
+ export declare function kernelLaunch(project: Project, placeholderRoot: string): {
11
+ command: string;
12
+ args: string[];
13
+ };
14
+ /** Verdict for surfaces that only carry MCP tool calls: no host capabilities at all. */
15
+ export declare function mcpVerdict(operation: Operation): Verdict;
16
+ /** Verdict for skills hosts: non-portable operations are bridged by the agent driving the host's own tools. */
17
+ /** A CLI has a human at the terminal, so user-input is native there; other host capabilities are not. */
18
+ export declare function cliVerdict(operation: Operation): Verdict;
19
+ /** A skill never executes: the agent drives the CLI or MCP server, so everything it can reach is bridged. */
20
+ export declare function skillVerdict(operation: Operation): Verdict;
21
+ export declare function configProperties(project: Project): Record<string, Record<string, unknown>>;
22
+ export declare function isSensitive(property: Record<string, unknown>): boolean;
23
+ export declare function requiredConfig(project: Project): string[];
24
+ /** The environment variable a config key is read from, on every surface and in every environment. */
25
+ export declare function envName(key: string): string;
26
+ /**
27
+ * Environment variable a host-native shim reads to find the tool's checkout when the host
28
+ * copied the plugin elsewhere (Hermes' doctor, OpenClaw's extensions directory).
29
+ */
30
+ export declare function rootEnvName(project: Project): string;
31
+ /**
32
+ * The environment variable a host passes the tool's per-instance data directory
33
+ * (caches, session state) through, on every surface. The kernel falls back to an
34
+ * XDG-style default when it is unset.
35
+ */
36
+ export declare function dataDirEnvName(project: Project): string;
37
+ /**
38
+ * The credentials a live run needs: every config key that is both `required` and sensitive.
39
+ * A tool with none has nothing to gate a live tier on, so no live test and no `live` CI job.
40
+ */
41
+ export declare function liveCredentials(project: Project): string[];
42
+ /**
43
+ * The operation the generated example test calls: the first one with arguments in
44
+ * `tool.json` `tests.examples`, else the first operation with no arguments.
45
+ */
46
+ export declare function liveExample(project: Project): {
47
+ name: string;
48
+ args: Record<string, unknown>;
49
+ } | undefined;