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,41 @@
1
+ /**
2
+ * Git is the release ledger (§7). Nothing records "what this project published at v0.1.0" except
3
+ * the commit that tag points at, so the previous selection is read straight out of it: no state
4
+ * file, no per-registry ledger, and nothing to keep in sync.
5
+ *
6
+ * Every function here is best-effort and never throws: a checkout with no tags, no git at all, or
7
+ * a tag predating `dev.toolfactory/tool.json` simply has nothing to compare against, and
8
+ * `unpublish` then has nothing to do.
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+ import { ToolConfigSchema } from "../model.js";
12
+ import { TOOL_PATH } from "./load.js";
13
+ function git(root, args) {
14
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", timeout: 30_000 });
15
+ return result.status === 0 ? result.stdout.trim() : undefined;
16
+ }
17
+ /**
18
+ * The tag released before `ref`: `git describe --tags --abbrev=0 <ref>^`. The `^` is what makes it
19
+ * *previous* — on the release runner `ref` is the tag being cut, and on a working tree it is the
20
+ * last release behind HEAD. Undefined at the first tag, which is exactly "nothing was dropped".
21
+ */
22
+ export function previousTag(root, ref = "HEAD") {
23
+ return git(root, ["describe", "--tags", "--abbrev=0", `${ref}^`]) || undefined;
24
+ }
25
+ /** `dev.toolfactory/tool.json` as of a ref, or undefined when the ref has none / it does not parse. */
26
+ export function toolAtRef(root, ref) {
27
+ const text = git(root, ["show", `${ref}:${TOOL_PATH}`]);
28
+ if (!text)
29
+ return undefined;
30
+ try {
31
+ const parsed = ToolConfigSchema.safeParse(JSON.parse(text));
32
+ return parsed.success ? parsed.data : undefined;
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ }
38
+ /** The surfaces a ref selected that the current selection no longer does, in the ref's own order. */
39
+ export function droppedSurfaces(previous, current) {
40
+ return previous.filter((surface) => !current.includes(surface));
41
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { type Operation, type Project } from "../model.js";
3
+ export declare const TOOL_PATH = "dev.toolfactory/tool.json";
4
+ export declare const OPS_PATH = "dev.toolfactory/ops.json";
5
+ export declare const TOOLFACTORY_VERSION: string;
6
+ /** `ops.json`: the `tools/list` result, verbatim, sorted by name. */
7
+ export declare const OpsSchema: z.ZodObject<{
8
+ tools: z.ZodArray<z.ZodObject<{
9
+ name: z.ZodString;
10
+ title: z.ZodOptional<z.ZodString>;
11
+ description: z.ZodOptional<z.ZodString>;
12
+ inputSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
13
+ outputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
+ annotations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
15
+ _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
16
+ }, z.core.$loose>>;
17
+ }, z.core.$strip>;
18
+ export type Ops = z.infer<typeof OpsSchema>;
19
+ export declare function toOperation(tool: Ops["tools"][number]): Operation;
20
+ export declare function readOps(root: string): Operation[];
21
+ export declare function loadProject(rootInput?: string): Project;
@@ -0,0 +1,82 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join, resolve } from "node:path";
4
+ import { z } from "zod";
5
+ import { readIdentityFile } from "../identity/read.js";
6
+ import { CAPABILITIES, isInstructionOnly, PACKAGE_MANAGERS, ToolConfigSchema, } from "../model.js";
7
+ import { TOOLFACTORY_DIR } from "./lock.js";
8
+ export const TOOL_PATH = `${TOOLFACTORY_DIR}/tool.json`;
9
+ export const OPS_PATH = `${TOOLFACTORY_DIR}/ops.json`;
10
+ const require = createRequire(import.meta.url);
11
+ export const TOOLFACTORY_VERSION = require("../../package.json").version;
12
+ /** `ops.json`: the `tools/list` result, verbatim, sorted by name. */
13
+ export const OpsSchema = z.object({
14
+ tools: z.array(z
15
+ .object({
16
+ name: z.string(),
17
+ title: z.string().optional(),
18
+ description: z.string().optional(),
19
+ inputSchema: z.record(z.string(), z.unknown()),
20
+ outputSchema: z.record(z.string(), z.unknown()).optional(),
21
+ annotations: z.record(z.string(), z.unknown()).optional(),
22
+ _meta: z.record(z.string(), z.unknown()).optional(),
23
+ })
24
+ .passthrough()),
25
+ });
26
+ export function toOperation(tool) {
27
+ const meta = (tool._meta?.["dev.toolfactory"] ?? {});
28
+ const declared = Array.isArray(meta.requires) ? meta.requires : [];
29
+ const requires = declared.filter((c) => CAPABILITIES.includes(String(c)));
30
+ const unknown = declared.filter((c) => !CAPABILITIES.includes(String(c)));
31
+ if (unknown.length) {
32
+ throw new Error(`Operation ${tool.name} declares unknown capabilities ${JSON.stringify(unknown)}; allowed: ${CAPABILITIES.join(", ")}`);
33
+ }
34
+ return {
35
+ name: tool.name,
36
+ title: tool.title,
37
+ description: tool.description,
38
+ inputSchema: tool.inputSchema,
39
+ outputSchema: tool.outputSchema,
40
+ annotations: tool.annotations,
41
+ requires,
42
+ };
43
+ }
44
+ export function readOps(root) {
45
+ const path = join(root, OPS_PATH);
46
+ if (!existsSync(path))
47
+ return [];
48
+ return OpsSchema.parse(JSON.parse(readFileSync(path, "utf8"))).tools.map(toOperation);
49
+ }
50
+ /** The package manager the root package.json declares (`packageManager: "pnpm@10.0.0"`), if any. */
51
+ function readPackageManager(root) {
52
+ const path = join(root, "package.json");
53
+ if (!existsSync(path))
54
+ return undefined;
55
+ const declared = JSON.parse(readFileSync(path, "utf8"))
56
+ .packageManager;
57
+ if (!declared)
58
+ return undefined;
59
+ const name = declared.split("@")[0] ?? "";
60
+ if (!PACKAGE_MANAGERS.includes(name)) {
61
+ throw new Error(`package.json packageManager "${declared}" is not supported; toolfactory generates workflows for ${PACKAGE_MANAGERS.join(", ")}.`);
62
+ }
63
+ return name;
64
+ }
65
+ export function loadProject(rootInput = ".") {
66
+ const root = resolve(rootInput);
67
+ const toolPath = join(root, TOOL_PATH);
68
+ if (!existsSync(toolPath)) {
69
+ throw new Error(`${TOOL_PATH} not found in ${root}; run \`toolfactory init\` first.`);
70
+ }
71
+ const tool = ToolConfigSchema.parse(JSON.parse(readFileSync(toolPath, "utf8")));
72
+ const identityFile = readIdentityFile(join(root, tool.identity));
73
+ return {
74
+ root,
75
+ tool,
76
+ identity: identityFile.identity,
77
+ identityExtra: identityFile.extra,
78
+ operations: isInstructionOnly(tool) ? [] : readOps(root),
79
+ toolfactoryVersion: TOOLFACTORY_VERSION,
80
+ packageManager: readPackageManager(root),
81
+ };
82
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ export declare const TOOLFACTORY_DIR = "dev.toolfactory";
3
+ export declare const LOCK_PATH = "dev.toolfactory/lock.json";
4
+ /** A region's marker pair, without its content: what the lock records, and the inverse it enables. */
5
+ export declare const MarkersSchema: z.ZodObject<{
6
+ begin: z.ZodString;
7
+ end: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export type Markers = z.infer<typeof MarkersSchema>;
10
+ export declare const LockSchema: z.ZodObject<{
11
+ toolfactoryVersion: z.ZodString;
12
+ files: z.ZodRecord<z.ZodString, z.ZodObject<{
13
+ sha256: z.ZodString;
14
+ state: z.ZodEnum<{
15
+ generated: "generated";
16
+ manual: "manual";
17
+ }>;
18
+ keys: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ regions: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ begin: z.ZodString;
21
+ end: z.ZodString;
22
+ }, z.core.$strip>>>;
23
+ }, z.core.$strip>>;
24
+ }, z.core.$strip>;
25
+ export type Lock = z.infer<typeof LockSchema>;
26
+ export declare function sha256(content: string): string;
27
+ export declare function readLock(root: string): Lock | undefined;
28
+ export declare function serializeLock(lock: Lock): string;
@@ -0,0 +1,42 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ export const TOOLFACTORY_DIR = "dev.toolfactory";
6
+ export const LOCK_PATH = `${TOOLFACTORY_DIR}/lock.json`;
7
+ /** A region's marker pair, without its content: what the lock records, and the inverse it enables. */
8
+ export const MarkersSchema = z.object({ begin: z.string(), end: z.string() });
9
+ export const LockSchema = z.object({
10
+ toolfactoryVersion: z.string(),
11
+ files: z.record(z.string(), z.object({
12
+ sha256: z.string(),
13
+ state: z.enum(["generated", "manual"]),
14
+ /**
15
+ * A merge file's inverse: the dotted key paths its patch wrote. Keys the current patch no
16
+ * longer carries are removed from the document before merging, so dropping a key from a
17
+ * patch — or deselecting the surface that added it — uninstalls it instead of stranding it.
18
+ */
19
+ keys: z.array(z.string()).optional(),
20
+ /**
21
+ * A region file's inverse, exactly as `keys` is a merge file's: the marker pairs its
22
+ * projector wrote. Regions the current plan no longer writes are emptied before the current
23
+ * ones are replaced, so dropping a region from a projector — or deselecting the surface that
24
+ * added it — uninstalls its content instead of stranding it, and a region file that leaves
25
+ * the plan loses its regions, not its existence.
26
+ */
27
+ regions: z.array(MarkersSchema).optional(),
28
+ })),
29
+ });
30
+ export function sha256(content) {
31
+ return createHash("sha256").update(content).digest("hex");
32
+ }
33
+ export function readLock(root) {
34
+ const path = join(root, LOCK_PATH);
35
+ if (!existsSync(path))
36
+ return undefined;
37
+ return LockSchema.parse(JSON.parse(readFileSync(path, "utf8")));
38
+ }
39
+ export function serializeLock(lock) {
40
+ const files = Object.fromEntries(Object.entries(lock.files).sort(([a], [b]) => a.localeCompare(b)));
41
+ return `${JSON.stringify({ toolfactoryVersion: lock.toolfactoryVersion, files }, null, 2)}\n`;
42
+ }
@@ -0,0 +1,5 @@
1
+ import type { PlannedFile, Project, Surface } from "../model.js";
2
+ export declare const COVERAGE_PATH = "dev.toolfactory/coverage.json";
3
+ export declare const TOOL_SCHEMA_PATH = "dev.toolfactory/tool.schema.json";
4
+ export declare function toolJsonSchema(): Record<string, unknown>;
5
+ export declare function buildPlan(project: Project, surfaces?: Surface[]): PlannedFile[];
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Assemble the full plan for a project: every selected surface's files, deduplicated by
3
+ * path (identical content from two surfaces is fine; conflicting content is a bug), plus
4
+ * the derived coverage files and the published schema for tool.json.
5
+ */
6
+ import { z } from "zod";
7
+ import { getBinding } from "../bindings/index.js";
8
+ import { isInstructionOnly, ToolConfigSchema } from "../model.js";
9
+ import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
10
+ import { assertSurfaceRequirements, getSurface, selectedSurfaces } from "../surfaces/registry.js";
11
+ import { json } from "../surfaces/shared.js";
12
+ import { managedContent } from "./apply.js";
13
+ import { readLock, TOOLFACTORY_DIR } from "./lock.js";
14
+ export const COVERAGE_PATH = `${TOOLFACTORY_DIR}/coverage.json`;
15
+ export const TOOL_SCHEMA_PATH = `${TOOLFACTORY_DIR}/tool.schema.json`;
16
+ export function toolJsonSchema() {
17
+ return z.toJSONSchema(ToolConfigSchema, { io: "input" });
18
+ }
19
+ /**
20
+ * Two projectors may own different regions of one author-owned file (`readme`'s install block and
21
+ * `pypi`'s mcp-name marker both live in README.md), so the plan carries their union and the
22
+ * template grows the marker pairs it is missing. Only the same marker holding different content is
23
+ * a conflict.
24
+ */
25
+ function mergeRegions(base, extra) {
26
+ const regions = [...base.regions];
27
+ for (const region of extra.regions) {
28
+ const owner = regions.find((planned) => planned.begin === region.begin);
29
+ if (!owner)
30
+ regions.push(region);
31
+ else if (owner.content !== region.content) {
32
+ throw new Error(`Two surfaces plan different content for ${base.path} ${region.begin}.`);
33
+ }
34
+ }
35
+ const missing = regions.filter((region) => !base.template.includes(region.begin));
36
+ if (missing.length === 0)
37
+ return { ...base, regions };
38
+ const appended = missing.map((region) => `${region.begin}\n${region.end}`).join("\n");
39
+ return { ...base, regions, template: `${base.template}\n${appended}\n` };
40
+ }
41
+ function dedupe(files) {
42
+ const byPath = new Map();
43
+ for (const file of files) {
44
+ const existing = byPath.get(file.path);
45
+ if (!existing) {
46
+ byPath.set(file.path, file);
47
+ }
48
+ else if (existing.kind === "region" && file.kind === "region") {
49
+ byPath.set(file.path, mergeRegions(existing, file));
50
+ }
51
+ else if (managedContent(existing) !== managedContent(file)) {
52
+ throw new Error(`Two surfaces plan different content for ${file.path}.`);
53
+ }
54
+ }
55
+ return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
56
+ }
57
+ export function buildPlan(project, surfaces = selectedSurfaces(project.tool.surfaces)) {
58
+ // Always the declared selection, never the argument: `eject` plans one surface out of a valid one.
59
+ assertSurfaceRequirements(project.tool.surfaces);
60
+ // Workflows are always generated: ci.yml for every project, release.yml when a registry is selected.
61
+ // COVERAGE.md is always generated too: a one-surface table is still the record of what is excluded.
62
+ const withWorkflows = surfaces.some((s) => s.id === "workflows")
63
+ ? surfaces
64
+ : [...surfaces, getSurface("workflows")];
65
+ // AGENTS.md is always generated too, so an agent developing the tool has it from the first build.
66
+ const withAgents = withWorkflows.some((s) => s.id === "agents")
67
+ ? withWorkflows
68
+ : [...withWorkflows, getSurface("agents")];
69
+ // README.md's install region is always generated too: the one place a human or an agent
70
+ // reads to install what this repo ships.
71
+ const withReadme = withAgents.some((s) => s.id === "readme")
72
+ ? withAgents
73
+ : [...withAgents, getSurface("readme")];
74
+ const files = withReadme.flatMap((surface) => surface.plan(project));
75
+ // Instruction-only plugins are consumed as skill prose by Codex and have no executable payload.
76
+ if (!isInstructionOnly(project.tool)) {
77
+ files.push(...getBinding(project.tool.binding).kernel(project));
78
+ files.push(...getBinding(project.tool.binding).liveTest(project));
79
+ }
80
+ files.push({ kind: "file", path: TOOL_SCHEMA_PATH, content: json(toolJsonSchema()) });
81
+ const coverage = computeCoverage(project, surfaces);
82
+ // The machine-readable half of the coverage report is a build output; COVERAGE.md is the
83
+ // tracked one, so a repo cannot silently drift to majority-manual (§2.2 S5).
84
+ files.push({ kind: "file", path: COVERAGE_PATH, content: json(coverage), output: true });
85
+ const lock = readLock(project.root);
86
+ const manual = Object.entries(lock?.files ?? {})
87
+ .filter(([, entry]) => entry.state === "manual")
88
+ .map(([path]) => path);
89
+ const total = Object.keys(lock?.files ?? {}).length || files.length;
90
+ files.push({
91
+ kind: "file",
92
+ path: "COVERAGE.md",
93
+ content: renderCoverageMarkdown(coverage, manual, total),
94
+ });
95
+ return dedupe(files);
96
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Coverage: one verdict per (operation × selected surface). Degradation is omission plus
3
+ * an explanation, never a stub that fails at call time.
4
+ */
5
+ import { type Operation, type Project, type Surface, type SurfaceId, type ToolConfig, type Verdict } from "../model.js";
6
+ export interface CoverageRow {
7
+ operation: string;
8
+ verdicts: Record<string, Verdict>;
9
+ }
10
+ /** One host registration a plugin contributes beyond its operations (`openclaw.registers`). */
11
+ export type Registration = NonNullable<NonNullable<ToolConfig["openclaw"]>["registers"]>[number];
12
+ export interface Coverage {
13
+ surfaces: SurfaceId[];
14
+ rows: CoverageRow[];
15
+ /** What a plugin contributes besides operations; the whole story when there are none. */
16
+ contributions?: Registration[];
17
+ }
18
+ export declare function defaultVerdict(operation: Operation): Verdict;
19
+ export declare function computeCoverage(project: Project, surfaces: Surface[]): Coverage;
20
+ /** Operations that reach a surface (anything but `excluded`). */
21
+ export declare function includedOperations(project: Project, surface: Surface): Operation[];
22
+ export declare function renderCoverageMarkdown(coverage: Coverage, manualFiles: string[], totalFiles: number): string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Coverage: one verdict per (operation × selected surface). Degradation is omission plus
3
+ * an explanation, never a stub that fails at call time.
4
+ */
5
+ import { isPortable, } from "../model.js";
6
+ export function defaultVerdict(operation) {
7
+ return isPortable(operation)
8
+ ? { kind: "native" }
9
+ : {
10
+ kind: "excluded",
11
+ reason: `excluded:requires-${operation.requires.filter((c) => !["net", "fs", "shell", "secret"].includes(c)).join("+")}`,
12
+ };
13
+ }
14
+ export function computeCoverage(project, surfaces) {
15
+ const rows = project.operations.map((operation) => ({
16
+ operation: operation.name,
17
+ verdicts: Object.fromEntries(surfaces.map((surface) => [
18
+ surface.id,
19
+ surface.verdict ? surface.verdict(operation, project) : defaultVerdict(operation),
20
+ ])),
21
+ }));
22
+ const contributions = project.tool.openclaw?.registers;
23
+ return {
24
+ surfaces: surfaces.map((s) => s.id),
25
+ rows,
26
+ ...(contributions?.length ? { contributions } : {}),
27
+ };
28
+ }
29
+ /** Operations that reach a surface (anything but `excluded`). */
30
+ export function includedOperations(project, surface) {
31
+ return project.operations.filter((operation) => {
32
+ const verdict = surface.verdict
33
+ ? surface.verdict(operation, project)
34
+ : defaultVerdict(operation);
35
+ return verdict.kind !== "excluded";
36
+ });
37
+ }
38
+ export function renderCoverageMarkdown(coverage, manualFiles, totalFiles) {
39
+ const header = `| operation | ${coverage.surfaces.join(" | ")} |`;
40
+ const divider = `|---|${coverage.surfaces.map(() => "---").join("|")}|`;
41
+ const cell = (verdict) => verdict.reason ? `${verdict.kind} (${verdict.reason.replace(/^[a-z]+:/, "")})` : verdict.kind;
42
+ const rows = coverage.rows.map((row) => `| \`${row.operation}\` | ${coverage.surfaces.map((s) => cell(row.verdicts[s] ?? { kind: "excluded" })).join(" | ")} |`);
43
+ // A plugin with no operations has nothing to tabulate; what it contributes is its registrations.
44
+ const table = coverage.rows.length
45
+ ? [header, divider, ...rows]
46
+ : [
47
+ "No operation reaches a surface.",
48
+ ...(coverage.contributions?.length
49
+ ? [
50
+ "",
51
+ "This tool is a host plugin: what it contributes is what it registers with the host.",
52
+ "",
53
+ ...coverage.contributions.map((contribution) => `- \`${contribution.api}\` → \`${contribution.contract}\`: ${contribution.ids
54
+ .map((id) => `\`${id}\``)
55
+ .join(", ")}`),
56
+ ]
57
+ : []),
58
+ ];
59
+ const manual = manualFiles.length === 0
60
+ ? "All generated files are managed by toolfactory."
61
+ : `${manualFiles.length} of ${totalFiles} managed files adopted as manual (${Math.round((100 * manualFiles.length) / Math.max(totalFiles, 1))}%): ${manualFiles.map((f) => `\`${f}\``).join(", ")}.`;
62
+ return [
63
+ "# Coverage",
64
+ "",
65
+ "Generated by `toolfactory build`. Verdicts per operation and surface: `native` (the surface runs it as-is), `bridged` (through a named bridge), `degraded` (present with a limit), `excluded` (omitted, with the reason).",
66
+ "",
67
+ ...table,
68
+ "",
69
+ manual,
70
+ "",
71
+ ].join("\n");
72
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { surface as codex } from "../surfaces/codex.js";
3
+ import { mcp } from "../surfaces/kernel.js";
4
+ import { surface as skill } from "../surfaces/skill.js";
5
+ import { computeCoverage, defaultVerdict, includedOperations, renderCoverageMarkdown, } from "./coverage.js";
6
+ const echo = {
7
+ name: "echo",
8
+ inputSchema: { type: "object", properties: {} },
9
+ requires: ["fs"],
10
+ };
11
+ const screenshot = {
12
+ name: "screenshot",
13
+ inputSchema: { type: "object", properties: {} },
14
+ requires: ["browser"],
15
+ };
16
+ const notify = {
17
+ name: "notify",
18
+ inputSchema: { type: "object", properties: {} },
19
+ requires: ["channel"],
20
+ };
21
+ function project() {
22
+ return {
23
+ root: "/repo",
24
+ tool: {
25
+ schemaVersion: 1,
26
+ identity: "package.json",
27
+ binding: "typescript",
28
+ surfaces: ["skill", "mcp", "codex"],
29
+ bundle: { runtime: "package" },
30
+ tests: { examples: {} },
31
+ },
32
+ identity: { name: "hello", version: "1.0.0" },
33
+ identityExtra: {},
34
+ operations: [echo, screenshot, notify],
35
+ toolfactoryVersion: "0.1.0",
36
+ };
37
+ }
38
+ describe("defaultVerdict", () => {
39
+ it("is native for a portable operation and excluded, reasoned, otherwise", () => {
40
+ expect(defaultVerdict(echo)).toEqual({ kind: "native" });
41
+ expect(defaultVerdict(screenshot)).toEqual({
42
+ kind: "excluded",
43
+ reason: "excluded:requires-browser",
44
+ });
45
+ });
46
+ });
47
+ describe("computeCoverage across skill, mcp, codex", () => {
48
+ const coverage = computeCoverage(project(), [skill, mcp, codex]);
49
+ const verdicts = Object.fromEntries(coverage.rows.map((row) => [row.operation, row.verdicts]));
50
+ it("a portable operation is bridged on skill, native on mcp, degraded (loader-unverified) on codex", () => {
51
+ expect(verdicts.echo.skill).toEqual({ kind: "bridged", reason: "bridged:agent-mediated" });
52
+ expect(verdicts.echo.mcp).toEqual({ kind: "native" });
53
+ expect(verdicts.echo.codex).toEqual({ kind: "degraded", reason: "degraded:loader-unverified" });
54
+ });
55
+ it("a browser operation is bridged on skill, excluded (no host capabilities) on mcp and codex", () => {
56
+ expect(verdicts.screenshot.skill).toEqual({
57
+ kind: "bridged",
58
+ reason: "bridged:agent-mediated",
59
+ });
60
+ expect(verdicts.screenshot.mcp).toEqual({
61
+ kind: "excluded",
62
+ reason: "excluded:mcp-no-host-capabilities",
63
+ });
64
+ expect(verdicts.screenshot.codex).toEqual({
65
+ kind: "excluded",
66
+ reason: "excluded:mcp-no-host-capabilities",
67
+ });
68
+ });
69
+ it("a channel operation is excluded everywhere, with skill naming the missing channel bridge", () => {
70
+ expect(verdicts.notify.skill).toEqual({
71
+ kind: "excluded",
72
+ reason: "excluded:no-channel-bridge",
73
+ });
74
+ expect(verdicts.notify.mcp).toEqual({
75
+ kind: "excluded",
76
+ reason: "excluded:mcp-no-host-capabilities",
77
+ });
78
+ expect(verdicts.notify.codex).toEqual({
79
+ kind: "excluded",
80
+ reason: "excluded:mcp-no-host-capabilities",
81
+ });
82
+ });
83
+ it("includedOperations drops exactly what its surface excludes", () => {
84
+ expect(includedOperations(project(), skill).map((o) => o.name)).toEqual(["echo", "screenshot"]);
85
+ expect(includedOperations(project(), mcp).map((o) => o.name)).toEqual(["echo"]);
86
+ });
87
+ it("renderCoverageMarkdown puts every surface as a column and every verdict's reason in the cell", () => {
88
+ const markdown = renderCoverageMarkdown(coverage, [], 3);
89
+ expect(markdown).toContain("| operation | skill | mcp | codex |");
90
+ expect(markdown).toContain("bridged (agent-mediated)");
91
+ expect(markdown).toContain("All generated files are managed by toolfactory.");
92
+ });
93
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent Plugins 1.0.0 bundle: `plugin.json` is the authored identity file (never written
3
+ * here), `mcp.json` points at the kernel, `skills/` comes from the skill surface.
4
+ * Validation is Ajv against the cached official schemas; the spec forbids fetching at load.
5
+ */
6
+ import type { Project, Surface } from "../model.js";
7
+ export declare const MCP_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
8
+ export declare const PLUGIN_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
9
+ export declare function assertBundleIdentity(project: Project): void;
10
+ export declare const surface: Surface;
@@ -0,0 +1,36 @@
1
+ import { json, kernelLaunch, mcpVerdict } from "./shared.js";
2
+ export const MCP_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
3
+ export const PLUGIN_SCHEMA_ID = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
4
+ export function assertBundleIdentity(project) {
5
+ if (project.tool.identity !== "plugin.json") {
6
+ throw new Error(`Surface "agent-plugins" requires plugin.json as the identity file (tool.json.identity is "${project.tool.identity}").`);
7
+ }
8
+ }
9
+ export const surface = {
10
+ id: "agent-plugins",
11
+ plan(project) {
12
+ assertBundleIdentity(project);
13
+ const launch = kernelLaunch(project, "${PLUGIN_ROOT}");
14
+ return [
15
+ {
16
+ kind: "file",
17
+ path: "mcp.json",
18
+ content: json({
19
+ $schema: MCP_SCHEMA_ID,
20
+ mcpServers: { [project.identity.name]: { type: "stdio", ...launch } },
21
+ }),
22
+ },
23
+ ];
24
+ },
25
+ validate(project) {
26
+ return [
27
+ {
28
+ label: "agent-plugins schema",
29
+ command: "toolfactory",
30
+ args: ["validate", "--surface", "agent-plugins"],
31
+ cwd: project.root,
32
+ },
33
+ ];
34
+ },
35
+ verdict: mcpVerdict,
36
+ };
@@ -0,0 +1,35 @@
1
+ import { type Project, type Surface } from "../model.js";
2
+ export declare const AGENTS_PATH = "AGENTS.md";
3
+ export declare const AGENTS_BEGIN = "<!-- tf:agents -->";
4
+ export declare const AGENTS_END = "<!-- /tf:agents -->";
5
+ export declare const SETUP_PATH = ".agents/setup";
6
+ export declare const SETUP_BEGIN = "# tf:setup";
7
+ export declare const SETUP_END = "# /tf:setup";
8
+ export declare const IGNORE_PATH = ".gitignore";
9
+ export declare const IGNORE_BEGIN = "# tf:ignore";
10
+ export declare const IGNORE_END = "# /tf:ignore";
11
+ export declare const SERVERS_PATH = ".agents/mcp/servers.json";
12
+ /**
13
+ * What it takes for a harness to see a newly registered MCP server, a new skill or an edited
14
+ * `AGENTS.md`. One table: the generated Reload section renders it, and `init` prints the single
15
+ * `line` of whichever harness it is running inside (`reloadLine`). Reload is the one part of
16
+ * registration a repository cannot automate, so the honest replacement for "restart your agent"
17
+ * is one accurate sentence per harness, generated from here.
18
+ */
19
+ export interface Reload {
20
+ harness: string;
21
+ /** Environment variables whose presence means this process is inside that harness. */
22
+ env: string[];
23
+ /** What a new MCP server registration needs before the session can call it. */
24
+ mcp: string;
25
+ /** What a new or edited skill, or an edited `AGENTS.md`, needs. */
26
+ instructions: string;
27
+ /** The line `init` prints when it detects this harness; harnesses with no marker have none. */
28
+ line?: string;
29
+ }
30
+ export declare const RELOAD: Reload[];
31
+ export declare const RELOAD_FALLBACK = "Your harness reads `AGENTS.md` and `.agents/skills/` directly; for MCP config, restart it or use its own reload command \u2014 the Reload table in `AGENTS.md` names one per harness.";
32
+ /** The one reload line that matches the harness this process is running inside. */
33
+ export declare function reloadLine(environment: NodeJS.ProcessEnv): string;
34
+ export declare function agentsContent(project: Project): string;
35
+ export declare const surface: Surface;