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,112 @@
1
+ /** Helpers shared by surface projectors. Pure. */
2
+ import { projectName } from "../identity/name.js";
3
+ import { PORTABLE_CAPABILITIES } from "../model.js";
4
+ import { defaultVerdict } from "../report/coverage.js";
5
+ export const TOOLFACTORY_DIR = "dev.toolfactory";
6
+ export function json(value) {
7
+ return `${JSON.stringify(value, null, 2)}\n`;
8
+ }
9
+ /** Drop undefined values so projections never emit `"key": undefined`. */
10
+ export function compact(record) {
11
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined));
12
+ }
13
+ export function has(project, surface) {
14
+ return project.tool.surfaces.includes(surface);
15
+ }
16
+ export function npmName(project) {
17
+ return projectName.npm(project.identity.name, project.tool.npm?.scope);
18
+ }
19
+ export function pypiName(project) {
20
+ return projectName.pypi(project.identity.name);
21
+ }
22
+ /** The launch command for the kernel MCP server as an installed package or a committed build. */
23
+ export function kernelLaunch(project, placeholderRoot) {
24
+ const { binding, bundle } = project.tool;
25
+ const version = project.identity.version ?? "latest";
26
+ if (bundle.runtime === "bundled") {
27
+ return binding === "python"
28
+ ? { command: "python3", args: [`${placeholderRoot}/bin/mcp.py`] }
29
+ : { command: "node", args: [`${placeholderRoot}/bin/mcp.js`] };
30
+ }
31
+ return binding === "python"
32
+ ? {
33
+ command: "uvx",
34
+ args: ["--from", `${pypiName(project)}==${version}`, project.identity.name, "mcp"],
35
+ }
36
+ : { command: "npx", args: ["-y", `${npmName(project)}@${version}`, "mcp"] };
37
+ }
38
+ /** Verdict for surfaces that only carry MCP tool calls: no host capabilities at all. */
39
+ export function mcpVerdict(operation) {
40
+ const verdict = defaultVerdict(operation);
41
+ return verdict.kind === "excluded"
42
+ ? { kind: "excluded", reason: "excluded:mcp-no-host-capabilities" }
43
+ : verdict;
44
+ }
45
+ /** Verdict for skills hosts: non-portable operations are bridged by the agent driving the host's own tools. */
46
+ /** A CLI has a human at the terminal, so user-input is native there; other host capabilities are not. */
47
+ export function cliVerdict(operation) {
48
+ const portable = operation.requires.every((capability) => PORTABLE_CAPABILITIES.has(capability) || capability === "user-input");
49
+ return portable
50
+ ? { kind: "native" }
51
+ : { kind: "excluded", reason: "excluded:cli-no-host-capabilities" };
52
+ }
53
+ /** A skill never executes: the agent drives the CLI or MCP server, so everything it can reach is bridged. */
54
+ export function skillVerdict(operation) {
55
+ if (operation.requires.includes("channel"))
56
+ return { kind: "excluded", reason: "excluded:no-channel-bridge" };
57
+ return { kind: "bridged", reason: "bridged:agent-mediated" };
58
+ }
59
+ export function configProperties(project) {
60
+ const properties = project.tool.config?.properties;
61
+ return properties && typeof properties === "object"
62
+ ? properties
63
+ : {};
64
+ }
65
+ export function isSensitive(property) {
66
+ const meta = property["x-toolfactory"];
67
+ return meta?.sensitive === true;
68
+ }
69
+ export function requiredConfig(project) {
70
+ const required = project.tool.config?.required;
71
+ return Array.isArray(required) ? required : [];
72
+ }
73
+ /** The environment variable a config key is read from, on every surface and in every environment. */
74
+ export function envName(key) {
75
+ return key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
76
+ }
77
+ /**
78
+ * Environment variable a host-native shim reads to find the tool's checkout when the host
79
+ * copied the plugin elsewhere (Hermes' doctor, OpenClaw's extensions directory).
80
+ */
81
+ export function rootEnvName(project) {
82
+ return `${project.identity.name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_ROOT`;
83
+ }
84
+ /**
85
+ * The environment variable a host passes the tool's per-instance data directory
86
+ * (caches, session state) through, on every surface. The kernel falls back to an
87
+ * XDG-style default when it is unset.
88
+ */
89
+ export function dataDirEnvName(project) {
90
+ return `${project.identity.name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_DATA_DIR`;
91
+ }
92
+ /**
93
+ * The credentials a live run needs: every config key that is both `required` and sensitive.
94
+ * A tool with none has nothing to gate a live tier on, so no live test and no `live` CI job.
95
+ */
96
+ export function liveCredentials(project) {
97
+ const properties = configProperties(project);
98
+ const required = new Set(requiredConfig(project));
99
+ return Object.keys(properties).filter((key) => required.has(key) && isSensitive(properties[key]));
100
+ }
101
+ /**
102
+ * The operation the generated example test calls: the first one with arguments in
103
+ * `tool.json` `tests.examples`, else the first operation with no arguments.
104
+ */
105
+ export function liveExample(project) {
106
+ const examples = project.tool.tests.examples;
107
+ const withExample = project.operations.find((operation) => examples[operation.name]);
108
+ const operation = withExample ?? project.operations[0];
109
+ if (!operation)
110
+ return undefined;
111
+ return { name: operation.name, args: examples[operation.name] ?? {} };
112
+ }
@@ -0,0 +1,7 @@
1
+ import { type Project, type Surface } from "../model.js";
2
+ export declare const OPERATIONS_BEGIN = "<!-- tf:operations -->";
3
+ export declare const OPERATIONS_END = "<!-- /tf:operations -->";
4
+ export declare function skillPath(project: Project): string;
5
+ export declare function renderOperations(project: Project): string;
6
+ export declare function frontmatter(project: Project): string;
7
+ export declare const surface: Surface;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Agent Skills: one `skills/<N>/SKILL.md` per tool. toolfactory owns the frontmatter and
3
+ * the `<!-- tf:operations -->` block; the body is the author's prose and is never generated.
4
+ * The bundle formats (Agent Plugins, Claude, Codex, Cursor) fix `skills/` at the bundle root,
5
+ * while Copilot, Codex, Hermes and DSH read a project's skills only from `.agents/skills/`
6
+ * (and `npx skills add` reads both): a symbolic link there is the one directory that serves all.
7
+ */
8
+ import { stringify as yaml } from "yaml";
9
+ import { isInstructionOnly, } from "../model.js";
10
+ import { compact, has, skillVerdict } from "./shared.js";
11
+ export const OPERATIONS_BEGIN = "<!-- tf:operations -->";
12
+ export const OPERATIONS_END = "<!-- /tf:operations -->";
13
+ export function skillPath(project) {
14
+ return `skills/${project.identity.name}/SKILL.md`;
15
+ }
16
+ /** The `.agents/` canon's view of the same skill, which `.agents/sync.py` fans out to every harness. */
17
+ function skillLinkPath(project) {
18
+ return `.agents/skills/${project.identity.name}`;
19
+ }
20
+ function invocation(project, operation) {
21
+ const name = project.identity.name;
22
+ const lines = [];
23
+ if (has(project, "cli"))
24
+ lines.push(`\`${name} ${operation.name} --json '<arguments>'\` prints a JSON result.`);
25
+ if (has(project, "mcp") || has(project, "agent-plugins")) {
26
+ lines.push(`MCP tool \`${operation.name}\` on server \`${name}\` returns the same result as \`structuredContent\`.`);
27
+ }
28
+ return lines.join(" ");
29
+ }
30
+ /** How an agent bridges each non-portable capability before calling the operation. */
31
+ const GUIDANCE = {
32
+ browser: "drive this host's own browser tools and pass what they return as arguments",
33
+ model: "do that reasoning yourself and pass the result as an argument",
34
+ "user-input": "ask the user for what it needs before calling it",
35
+ };
36
+ export function renderOperations(project) {
37
+ if (isInstructionOnly(project.tool))
38
+ return "";
39
+ const lines = ["", "## Operations", ""];
40
+ if (project.operations.length === 0) {
41
+ lines.push("_No operations yet: run `toolfactory introspect` after adding one to the kernel._");
42
+ }
43
+ for (const operation of project.operations) {
44
+ const verdict = skillVerdict(operation);
45
+ if (verdict.kind === "excluded")
46
+ continue;
47
+ const required = Object.keys(operation.inputSchema.properties ?? {});
48
+ lines.push(`### ${operation.name}`, "");
49
+ if (operation.description)
50
+ lines.push(operation.description, "");
51
+ if (required.length)
52
+ lines.push(`Arguments: ${required.map((k) => `\`${k}\``).join(", ")}.`, "");
53
+ const how = invocation(project, operation);
54
+ if (how)
55
+ lines.push(how, "");
56
+ const needs = operation.requires.filter((capability) => GUIDANCE[capability]);
57
+ if (verdict.kind === "bridged" && needs.length) {
58
+ lines.push(`This operation needs ${needs.join(", ")}: ${needs.map((capability) => GUIDANCE[capability]).join("; ")}.`, "");
59
+ }
60
+ }
61
+ return `${lines.join("\n")}\n`;
62
+ }
63
+ export function frontmatter(project) {
64
+ const document = compact({
65
+ name: project.identity.name,
66
+ description: project.identity.description ?? `Use the ${project.identity.name} tool.`,
67
+ license: project.identity.license,
68
+ });
69
+ return `\n${yaml(document).trimEnd()}\n`;
70
+ }
71
+ export const surface = {
72
+ id: "skill",
73
+ plan(project) {
74
+ const path = skillPath(project);
75
+ const regions = [
76
+ { begin: "---", end: "---", content: frontmatter(project) },
77
+ { begin: OPERATIONS_BEGIN, end: OPERATIONS_END, content: renderOperations(project) },
78
+ ];
79
+ const template = [
80
+ "---",
81
+ "---",
82
+ "",
83
+ `# ${project.identity.name}`,
84
+ "",
85
+ "Explain here when an agent should reach for this tool and how to combine its operations.",
86
+ "",
87
+ OPERATIONS_BEGIN,
88
+ OPERATIONS_END,
89
+ "",
90
+ ].join("\n");
91
+ return [
92
+ { kind: "region", path, regions, template },
93
+ {
94
+ kind: "file",
95
+ path: skillLinkPath(project),
96
+ content: `../../skills/${project.identity.name}`,
97
+ symlink: true,
98
+ },
99
+ ];
100
+ },
101
+ validate(project) {
102
+ return [
103
+ {
104
+ label: "agentskills validate",
105
+ command: "uvx",
106
+ args: [
107
+ "--from",
108
+ "skills-ref",
109
+ "agentskills",
110
+ "validate",
111
+ `skills/${project.identity.name}`,
112
+ ],
113
+ cwd: project.root,
114
+ },
115
+ ];
116
+ },
117
+ verdict: skillVerdict,
118
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { frontmatter, renderOperations, skillPath, surface } from "./skill.js";
4
+ const echo = {
5
+ name: "echo",
6
+ description: "Return the text you pass in.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: { text: { type: "string" } },
10
+ required: ["text"],
11
+ },
12
+ requires: [],
13
+ };
14
+ const screenshot = {
15
+ name: "screenshot",
16
+ description: "Capture the current page.",
17
+ inputSchema: { type: "object", properties: {} },
18
+ requires: ["browser"],
19
+ };
20
+ const notify = {
21
+ name: "notify",
22
+ description: "Post a message into the live conversation.",
23
+ inputSchema: { type: "object", properties: {} },
24
+ requires: ["channel"],
25
+ };
26
+ function project(overrides = {}) {
27
+ return {
28
+ root: "/repo",
29
+ tool: {
30
+ schemaVersion: 1,
31
+ identity: "package.json",
32
+ binding: "typescript",
33
+ surfaces: ["skill", "cli", "mcp"],
34
+ bundle: { runtime: "package" },
35
+ tests: { examples: {} },
36
+ },
37
+ identity: { name: "hello", version: "1.0.0", description: "Say hello", license: "MIT" },
38
+ identityExtra: {},
39
+ operations: [echo, screenshot, notify],
40
+ toolfactoryVersion: "0.1.0",
41
+ ...overrides,
42
+ };
43
+ }
44
+ describe("skill surface", () => {
45
+ it("names the file after the tool and links it into the .agents canon", () => {
46
+ const target = project();
47
+ expect(skillPath(target)).toBe("skills/hello/SKILL.md");
48
+ expect(surface.plan(target)).toMatchObject([
49
+ { kind: "region", path: "skills/hello/SKILL.md" },
50
+ { kind: "file", path: ".agents/skills/hello", content: "../../skills/hello", symlink: true },
51
+ ]);
52
+ });
53
+ it("frontmatter carries the 6 spec fields, falling back to a description", () => {
54
+ const document = parseYaml(frontmatter(project()));
55
+ expect(document).toEqual({ name: "hello", description: "Say hello", license: "MIT" });
56
+ const noDescription = parseYaml(frontmatter(project({ identity: { name: "hello", description: undefined } })));
57
+ expect(noDescription.description).toBe("Use the hello tool.");
58
+ });
59
+ it("the operations block includes a portable op's CLI/MCP invocation and a bridged op's text, and omits an excluded op", () => {
60
+ const body = renderOperations(project());
61
+ expect(body).toContain("### echo");
62
+ expect(body).toContain("`hello echo --json '<arguments>'` prints a JSON result.");
63
+ expect(body).toContain("MCP tool `echo` on server `hello`");
64
+ expect(body).toContain("Arguments: `text`.");
65
+ expect(body).toContain("### screenshot");
66
+ expect(body).toContain("This operation needs browser: drive this host's own browser tools and pass what they return as arguments.");
67
+ expect(body).not.toContain("### notify");
68
+ });
69
+ it("leaves the operations region empty for an instruction-only skill", () => {
70
+ const target = project({
71
+ tool: { ...project().tool, runtime: "none", surfaces: ["skill", "codex"] },
72
+ operations: [],
73
+ });
74
+ expect(renderOperations(target)).toBe("");
75
+ });
76
+ it("validate runs the upstream agentskills validator against skills/<name>", () => {
77
+ const commands = surface.validate?.(project()) ?? [];
78
+ expect(commands).toEqual([
79
+ {
80
+ label: "agentskills validate",
81
+ command: "uvx",
82
+ args: ["--from", "skills-ref", "agentskills", "validate", "skills/hello"],
83
+ cwd: "/repo",
84
+ },
85
+ ]);
86
+ });
87
+ });
@@ -0,0 +1,97 @@
1
+ import type { Surface } from "../model.js";
2
+ export declare const WEB_DIR = "web";
3
+ /**
4
+ * Everything the upstream generators wrote, transcribed once; `validate()` proves it current.
5
+ * Sources, in the order `src/hosts/web.ts` replays them:
6
+ * npm create vite@latest <dir> -- --template react-ts
7
+ * ui.shadcn.com/docs/installation/vite (Tailwind, the `@/*` alias, vite.config.ts)
8
+ * npx shadcn@latest init -y --base base --preset nova
9
+ * npx shadcn@latest add -y <components>
10
+ */
11
+ export declare const WEB_SCAFFOLD: {
12
+ readonly viteTemplate: "react-ts";
13
+ readonly base: "base";
14
+ readonly preset: "nova";
15
+ /** The shadcn components the page composes; the CLI copies them into src/components/ui. */
16
+ readonly components: readonly ["alert", "badge", "button", "card", "empty", "field", "input", "select", "switch", "tabs", "textarea", "toggle-group"];
17
+ /** toolfactory's own addition: the browser the smoke script drives. */
18
+ readonly playwright: "^1.62.1";
19
+ /** `npm create vite` output, before the shadcn documentation's edits. */
20
+ readonly vite: {
21
+ readonly "vite.config.ts": "import react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vite'\n\n// https://vite.dev/config/\nexport default defineConfig({\n plugins: [react()],\n})\n";
22
+ readonly "tsconfig.json": "{\n \"files\": [],\n \"references\": [\n { \"path\": \"./tsconfig.app.json\" },\n { \"path\": \"./tsconfig.node.json\" }\n ]\n}\n";
23
+ readonly "tsconfig.app.json": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n \"target\": \"es2023\",\n \"lib\": [\"ES2023\", \"DOM\"],\n \"module\": \"esnext\",\n \"types\": [\"vite/client\"],\n \"allowArbitraryExtensions\": true,\n \"skipLibCheck\": true,\n\n /* Bundler mode */\n \"moduleResolution\": \"bundler\",\n \"allowImportingTsExtensions\": true,\n \"verbatimModuleSyntax\": true,\n \"moduleDetection\": \"force\",\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n\n /* Linting */\n \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"erasableSyntaxOnly\": true,\n \"noFallthroughCasesInSwitch\": true\n },\n \"include\": [\"src\"]\n}\n";
24
+ readonly "tsconfig.node.json": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n \"target\": \"es2023\",\n \"lib\": [\"ES2023\"],\n \"types\": [\"node\"],\n \"skipLibCheck\": true,\n\n /* Bundler mode */\n \"module\": \"nodenext\",\n \"allowImportingTsExtensions\": true,\n \"verbatimModuleSyntax\": true,\n \"moduleDetection\": \"force\",\n \"noEmit\": true,\n\n /* Linting */\n \"noUnusedLocals\": true,\n \"noUnusedParameters\": true,\n \"erasableSyntaxOnly\": true,\n \"noFallthroughCasesInSwitch\": true\n },\n \"include\": [\"vite.config.ts\"]\n}\n";
25
+ readonly ".oxlintrc.json": "{\n \"$schema\": \"./node_modules/oxlint/configuration_schema.json\",\n \"plugins\": [\"react\", \"typescript\", \"oxc\"],\n \"rules\": {\n \"react/rules-of-hooks\": \"error\",\n \"react/only-export-components\": [\"warn\", { \"allowConstantExport\": true }]\n }\n}\n";
26
+ };
27
+ readonly packageJson: {
28
+ readonly scripts: {
29
+ readonly dev: "vite";
30
+ readonly build: "tsc -b && vite build";
31
+ readonly lint: "oxlint";
32
+ readonly preview: "vite preview";
33
+ };
34
+ readonly dependencies: {
35
+ readonly "@base-ui/react": "^1.8.0";
36
+ readonly "@fontsource-variable/geist": "^5.3.0";
37
+ readonly "@tailwindcss/vite": "^4.3.3";
38
+ readonly "class-variance-authority": "^0.7.1";
39
+ readonly cn: "^0.2.6";
40
+ readonly "lucide-react": "^1.42.0";
41
+ readonly react: "^19.2.8";
42
+ readonly "react-dom": "^19.2.8";
43
+ readonly shadcn: "^4.21.0";
44
+ readonly tailwindcss: "^4.3.3";
45
+ readonly "tw-animate-css": "^1.4.0";
46
+ };
47
+ readonly devDependencies: {
48
+ readonly "@types/node": "^24.13.3";
49
+ readonly "@types/react": "^19.2.18";
50
+ readonly "@types/react-dom": "^19.2.4";
51
+ readonly "@vitejs/plugin-react": "^6.1.0";
52
+ readonly oxlint: "^1.79.0";
53
+ readonly typescript: "~6.0.2";
54
+ readonly vite: "^8.2.2";
55
+ };
56
+ };
57
+ readonly componentsJson: {
58
+ readonly $schema: "https://ui.shadcn.com/schema.json";
59
+ readonly style: "base-nova";
60
+ readonly rsc: false;
61
+ readonly tsx: true;
62
+ readonly tailwind: {
63
+ readonly config: "";
64
+ readonly css: "src/index.css";
65
+ readonly baseColor: "neutral";
66
+ readonly cssVariables: true;
67
+ readonly prefix: "";
68
+ };
69
+ readonly iconLibrary: "lucide";
70
+ readonly rtl: false;
71
+ readonly aliases: {
72
+ readonly components: "@/components";
73
+ readonly utils: "@/lib/utils";
74
+ readonly ui: "@/components/ui";
75
+ readonly lib: "@/lib";
76
+ readonly hooks: "@/hooks";
77
+ };
78
+ readonly menuColor: "default";
79
+ readonly menuAccent: "subtle";
80
+ readonly registries: {};
81
+ };
82
+ readonly indexCss: "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n@import \"shadcn/tailwind.css\";\n@import \"@fontsource-variable/geist\";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --font-heading: var(--font-sans);\n --font-sans: 'Geist Variable', sans-serif;\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --color-foreground: var(--foreground);\n --color-background: var(--background);\n --radius-sm: calc(var(--radius) * 0.6);\n --radius-md: calc(var(--radius) * 0.8);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) * 1.4);\n --radius-2xl: calc(var(--radius) * 1.8);\n --radius-3xl: calc(var(--radius) * 2.2);\n --radius-4xl: calc(var(--radius) * 2.6);\n}\n\n:root {\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.87 0 0);\n --chart-2: oklch(0.556 0 0);\n --chart-3: oklch(0.439 0 0);\n --chart-4: oklch(0.371 0 0);\n --chart-5: oklch(0.269 0 0);\n --radius: 0.625rem;\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.87 0 0);\n --chart-2: oklch(0.556 0 0);\n --chart-3: oklch(0.439 0 0);\n --chart-4: oklch(0.371 0 0);\n --chart-5: oklch(0.269 0 0);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n html {\n @apply font-sans;\n }\n}";
83
+ readonly utils: "export { cn } from \"cn\"\n";
84
+ };
85
+ /** `src/index.css` after the documentation's Tailwind step and before `shadcn init` rewrites it. */
86
+ export declare const TAILWIND_CSS = "@import \"tailwindcss\";\n";
87
+ /**
88
+ * `src/App.tsx` is a RegionFile: the generated region is the operations page (everything
89
+ * the operator forms need, over `ops.json`) exported as `OperationsPage`; the template's tail —
90
+ * never touched again once the file exists — is the author's `pages` array and the shell that
91
+ * renders whichever one is active. A page an author adds below the end marker (the d3
92
+ * degree-planner graph vutoolkit needs, say) survives every `toolfactory build` because
93
+ * `apply()` only ever replaces the text between the markers (src/project/apply.ts `render()`).
94
+ */
95
+ export declare const APP_BEGIN = "// tf:app";
96
+ export declare const APP_END = "// /tf:app";
97
+ export declare const surface: Surface;