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,609 @@
1
+ /**
2
+ * Surface "agents": the agent-config machinery every generated project carries, always on
3
+ * (never in `tool.json.surfaces`; `project/plan.ts` adds it unconditionally).
4
+ *
5
+ * It projects two things. The first is `AGENTS.md`, read natively by OpenClaw, Hermes, Codex,
6
+ * Cursor, Gemini and Claude Code, so an agent working inside any of them can develop the tool;
7
+ * toolfactory owns the `<!-- tf:agents -->` region and the author owns the prose around it. The
8
+ * second is the `.agents/` canon from github.com/GoatInAHat/template — `sync.py`, the per-harness
9
+ * hook carriers, the agent-config workflow — whose bytes are vendored in `agents.template.ts` and
10
+ * proved against the real repository by `hosts/template.ts`. Whole file where the template has no
11
+ * per-project content, merge where another tool writes into the same document, region where the
12
+ * author has legitimate lines of their own.
13
+ */
14
+ import { execArgv } from "node:process";
15
+ import { getBinding } from "../bindings/index.js";
16
+ import { DRIFT_ENTRY } from "../hosts/template.js";
17
+ import { projectName } from "../identity/name.js";
18
+ import { isInstructionOnly } from "../model.js";
19
+ import { TEMPLATE_FILES } from "./agents.template.js";
20
+ import { HOST_DIR as BROWSER_HOST_DIR } from "./browser-extension.js";
21
+ import { HOST_DIR as DSH_HOST_DIR, PROFILE as DSH_PROFILE, rowId as dshRowId, toolName as dshToolName, LOCAL_PATCH_FILE, PATCH_FILE, } from "./dsh.js";
22
+ import { pluginDir as hermesPluginDir } from "./hermes-native.js";
23
+ import { INSPECTOR_CONFIG_PATH } from "./kernel.js";
24
+ import { displayName, HOST_DIR as OPENCLAW_HOST_DIR } from "./openclaw-native.js";
25
+ import { envName, has, liveCredentials, TOOLFACTORY_DIR } from "./shared.js";
26
+ import { WEB_DIR } from "./web.js";
27
+ export const AGENTS_PATH = "AGENTS.md";
28
+ export const AGENTS_BEGIN = "<!-- tf:agents -->";
29
+ export const AGENTS_END = "<!-- /tf:agents -->";
30
+ export const SETUP_PATH = ".agents/setup";
31
+ export const SETUP_BEGIN = "# tf:setup";
32
+ export const SETUP_END = "# /tf:setup";
33
+ export const IGNORE_PATH = ".gitignore";
34
+ export const IGNORE_BEGIN = "# tf:ignore";
35
+ export const IGNORE_END = "# /tf:ignore";
36
+ export const SERVERS_PATH = ".agents/mcp/servers.json";
37
+ /** Where the template's `.agents/setup` hands the project its own setup steps. */
38
+ const SETUP_SENTINEL = "# Project setup goes here";
39
+ export const RELOAD = [
40
+ {
41
+ harness: "Claude Code",
42
+ env: ["CLAUDECODE", "CLAUDE_CODE_REMOTE"],
43
+ mcp: "Reconnect from `/mcp`, or start a new session: stdio servers are not reconnected automatically. A server's own `list_changed` refreshes its tool list without one.",
44
+ instructions: "`.claude/skills/`, symlinked by `sync.py`; no documented mid-session reload.",
45
+ line: "Claude Code will not pick up the new MCP servers in this session: stdio servers are not reconnected automatically — reconnect from `/mcp`, or start a new session. Skills are read through `.claude/skills`, which `sync.py` has just linked.",
46
+ },
47
+ {
48
+ harness: "OpenClaw",
49
+ env: ["OPENCLAW_CLI", "OPENCLAW_SHELL"],
50
+ mcp: "`openclaw gateway restart`: plugins and MCP config load at Gateway start, and `openclaw mcp reload` only refreshes the current CLI process.",
51
+ instructions: "Skills refresh mid-session; the watcher's list is picked up on the next agent turn. `AGENTS.md` is read at session start.",
52
+ line: "Run `openclaw gateway restart` — OpenClaw loads plugins and MCP config at Gateway start, and `openclaw mcp reload` only refreshes the current CLI process. Skills refresh on your next turn (the skills watcher).",
53
+ },
54
+ {
55
+ harness: "Hermes",
56
+ env: ["HERMES_HOME", "TERMINAL_CWD"],
57
+ mcp: "A new invocation: every run is a fresh process. `/reload-mcp` inside an open session; `hermes gateway restart` is the messaging gateway only.",
58
+ instructions: "`hermes skills trust` once in this repo, then a new conversation: the resolved skill directories are stable for a conversation.",
59
+ line: "Run `hermes skills trust` once in this repo, then start a new Hermes conversation — every invocation re-scans plugins, skills and AGENTS.md. `/reload-mcp` only affects an already-open session; `hermes gateway restart` is only for the messaging gateway.",
60
+ },
61
+ {
62
+ harness: "Gemini CLI",
63
+ env: [],
64
+ mcp: "`/mcp reload`",
65
+ instructions: "`/memory refresh`; `/skills reload` for skills",
66
+ },
67
+ {
68
+ harness: "Codex",
69
+ env: [],
70
+ mcp: "Restart: `sync.py install-codex` writes the user-level `~/.codex/config.toml`, read at startup, because a project config applies only once the project is trusted.",
71
+ instructions: "Reads `AGENTS.md` and `.agents/skills/` natively.",
72
+ },
73
+ {
74
+ harness: "Factory (`droid`)",
75
+ env: [],
76
+ mcp: "None: it reloads when `.factory/mcp.json` changes.",
77
+ instructions: "Reads `AGENTS.md` natively.",
78
+ },
79
+ {
80
+ harness: "VS Code",
81
+ env: [],
82
+ mcp: "The per-server Restart control, or `chat.mcp.autostart` (experimental).",
83
+ instructions: "Reads `AGENTS.md` and `.agents/skills/` natively.",
84
+ },
85
+ {
86
+ harness: "Cursor, Qwen, OpenCode, Kilo, Amp, CodeBuddy",
87
+ env: [],
88
+ mcp: "No primary-source reload documentation: assume a restart.",
89
+ instructions: "Reads `AGENTS.md` and `.agents/skills/` natively.",
90
+ },
91
+ ];
92
+ export const RELOAD_FALLBACK = "Your harness reads `AGENTS.md` and `.agents/skills/` directly; for MCP config, restart it or use its own reload command — the Reload table in `AGENTS.md` names one per harness.";
93
+ /** The one reload line that matches the harness this process is running inside. */
94
+ export function reloadLine(environment) {
95
+ const match = RELOAD.find((entry) => entry.env.some((name) => environment[name]));
96
+ return match?.line ?? RELOAD_FALLBACK;
97
+ }
98
+ function commandsSection(project) {
99
+ if (isInstructionOnly(project.tool)) {
100
+ return [
101
+ "## Commands",
102
+ "",
103
+ "- `npx toolfactory build` — regenerate the plugin metadata after changing `dev.toolfactory/tool.json`.",
104
+ "- `npx toolfactory check` — fail when generated plugin metadata drifted.",
105
+ "- `npx toolfactory validate` — validate the Skill and Codex plugin installation.",
106
+ "",
107
+ ];
108
+ }
109
+ const typescript = project.tool.binding === "typescript";
110
+ const pm = project.packageManager ?? "npm";
111
+ const testCmd = typescript ? `${pm} test` : "uv run --with pytest pytest -q";
112
+ const live = liveCredentials(project).length > 0 && project.operations.length > 0;
113
+ const liveCmd = typescript
114
+ ? pm === "pnpm"
115
+ ? "pnpm test:live"
116
+ : "npm run test:live"
117
+ : "uv run --with pytest pytest -q tests/test_live.py";
118
+ return [
119
+ "## Commands",
120
+ "",
121
+ "- `npx toolfactory introspect` — resnapshot `dev.toolfactory/ops.json` after editing the operations.",
122
+ "- `npx toolfactory build` — regenerate every generated file after a `dev.toolfactory/tool.json` change.",
123
+ "- `npx toolfactory check` — the drift gate: fails when a generated file or the operation snapshot is stale.",
124
+ "- `npx toolfactory validate` — runs every selected surface's upstream validator.",
125
+ "- `npx toolfactory coverage` — operation × surface verdicts.",
126
+ `- Tests: \`${testCmd}\`${live ? `; live tests against the real service (needs \`.env\`, copied from \`.env.example\`): \`${liveCmd}\`` : ""}.`,
127
+ `- Package manager: ${typescript ? pm : "uv"}.`,
128
+ "",
129
+ ];
130
+ }
131
+ function layoutSection(project) {
132
+ if (isInstructionOnly(project.tool)) {
133
+ return [
134
+ "## Layout",
135
+ "",
136
+ `- \`skills/${project.identity.name}/SKILL.md\` — the author-owned instructions Codex follows; only its frontmatter is generated.`,
137
+ "- `dev.toolfactory/` — toolfactory configuration, generated metadata and the generation lock; never hand-edit generated files.",
138
+ "",
139
+ ];
140
+ }
141
+ const typescript = project.tool.binding === "typescript";
142
+ const opsPath = typescript
143
+ ? "src/ops.ts"
144
+ : `src/${projectName.pythonPackage(project.identity.name)}/ops.py`;
145
+ const lines = [
146
+ "## Layout",
147
+ "",
148
+ `- \`${opsPath}\` — your operations, the only hand-written source. Everything else here is a`,
149
+ " generated projection of it plus `dev.toolfactory/tool.json`.",
150
+ "- `dev.toolfactory/` — the operation snapshot, coverage verdicts and the generation lock;",
151
+ " never hand-edit (`toolfactory adopt <path>` first if you must).",
152
+ "- `.agents/` — the agent-config canon: `skills/`, `mcp/servers.json`, `setup`, `sync.py`.",
153
+ ];
154
+ if (has(project, "openclaw-native") ||
155
+ has(project, "hermes-native") ||
156
+ has(project, "dsh") ||
157
+ has(project, "browser-extension")) {
158
+ lines.push("- `hosts/<id>/` — the host-native escape hatch for a selected host; nothing else creates it.");
159
+ }
160
+ if (has(project, "web"))
161
+ lines.push("- `web/` — the shadcn/ui app.");
162
+ lines.push("");
163
+ return lines;
164
+ }
165
+ function boundarySection() {
166
+ return [
167
+ "## The boundary",
168
+ "",
169
+ "Core logic is a pure function of (JSON arguments, environment/config, filesystem). Anything a",
170
+ "host provides that is not one of those three is not available to core; a host-native shim",
171
+ "converts it into one of them before the call.",
172
+ "",
173
+ ];
174
+ }
175
+ function agentConfigSection(project) {
176
+ const own = kernelServerName(project);
177
+ return [
178
+ "## Agent config",
179
+ "",
180
+ "Skills and MCP servers live once in `.agents/` — `skills/` and `mcp/servers.json` — and sync",
181
+ "to every harness automatically, in both directions; `CLAUDE.md`, `GEMINI.md` and the",
182
+ "per-harness configs are rendered from there by `.agents/sync.py`. `bash .agents/setup` runs it",
183
+ "and installs the git hooks, so pulls, branch switches and commits re-sync on their own, and a",
184
+ "commit cannot leave a generated file stale (pre-commit also runs `npx toolfactory check`).",
185
+ "Personal-only config: gitignored `.agents/local/`, same shape. Details: `.agents/README.md`.",
186
+ "",
187
+ ...(own
188
+ ? [
189
+ `This tool's own kernel is registered there as \`${own}\`, so an agent developing it can call`,
190
+ "the operations it is writing; register nothing by hand.",
191
+ "",
192
+ ]
193
+ : []),
194
+ ];
195
+ }
196
+ function installSection(project) {
197
+ const lines = ["## Installing into the host you are developing in", ""];
198
+ const openclaw = has(project, "openclaw-native");
199
+ const hermes = has(project, "hermes-native");
200
+ const gemini = has(project, "gemini");
201
+ if (gemini) {
202
+ lines.push("- **Gemini CLI**: `gemini extensions link .` links the checkout in place; a new `gemini`", " session picks it up (there is no reload command — start a fresh session).");
203
+ }
204
+ if (openclaw) {
205
+ lines.push(`- **OpenClaw**: \`openclaw plugins install --link ${OPENCLAW_HOST_DIR} --force\` links the`, ` checkout in place; \`openclaw plugins inspect ${project.identity.name} --runtime --json\` confirms it loaded.`, ` \`openclaw plugins uninstall ${project.identity.name} --keep-files\` removes the registration and leaves the checkout.`);
206
+ }
207
+ if (hermes) {
208
+ lines.push("- **Hermes**: commit, then", ` \`hermes plugins install file://<absolute path to this repo>#${hermesPluginDir(project)}\``, " (a `file://` URL installs, with a security warning at install time). Every `hermes` run is a", " fresh process, so nothing needs restarting; `hermes gateway restart` is only for the", " messaging gateway daemon.");
209
+ }
210
+ if (has(project, "dsh")) {
211
+ lines.push(`- **DSH** (experimental): \`dsh plugin --profile ${DSH_PROFILE} add ./${DSH_HOST_DIR}\` installs the`, " bundle (`dsh plugin` forwards to pnpm, so pnpm must be on `PATH`); the operations reach the", ` model as \`${dshToolName(project, "<operation>")}\`, and \`dsh --profile ${DSH_PROFILE} --dump-config\` shows`, ` the composed \`id: ${dshRowId(project)}\` row. Bundle patches are read once per boot and never watched,`, ` so restart DSH after editing \`${DSH_HOST_DIR}/${PATCH_FILE}\` or reinstalling. Boot with`, ` \`--patch ${DSH_HOST_DIR}/${LOCAL_PATCH_FILE}\` and no bundle installed to drive this checkout`, " instead. Every config variable is restated in the row's `env:` because DSH scrubs", " KEY/PASSWORD/SECRET/TOKEN names before spawning an MCP server.");
212
+ }
213
+ if (has(project, "browser-extension")) {
214
+ const { command, args } = getBinding(project.tool.binding).cliCommand(project);
215
+ lines.push(`- **Browser extension**: \`npm --prefix ${BROWSER_HOST_DIR} install && npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt build\`,`, ` then \`chrome://extensions\` → developer mode → Load unpacked → \`${BROWSER_HOST_DIR}/.output/chrome-mv3\`;`, ` \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- web-ext run\` does the same for Firefox. Pair it with this`, ` checkout's kernel: \`${[command, ...args].join(" ")} mcp --http --pair\` mints a token under the data`, ` directory and prints the \`<url>#<token>\` the extension's options page accepts (with no token minted`, ` and no \`${envName(project.identity.name)}_MCP_TOKEN\` in the environment the endpoint stays open on loopback). The content`, " script's selectors are yours to maintain: it is authored escape-hatch code against a page that", " changes on its own schedule, and nothing here can version-proof it.");
216
+ }
217
+ if (!gemini &&
218
+ !openclaw &&
219
+ !hermes &&
220
+ !has(project, "dsh") &&
221
+ !has(project, "browser-extension")) {
222
+ lines.push("_No host-native surface (`openclaw-native`, `hermes-native`, `dsh`, `browser-extension`) or", "`gemini` extension is selected; nothing installs into a running host._");
223
+ }
224
+ lines.push("", "Codex and Cursor read this file as is; Claude Code and Gemini read `CLAUDE.md` / `GEMINI.md`,", "each of which `.agents/sync.py` renders as the one line `@AGENTS.md`.", "");
225
+ return lines;
226
+ }
227
+ /**
228
+ * The curated directories and marketplaces that *list* a tool rather than merely install it —
229
+ * each a one-time portal submission or PR, reviewed by humans on the other end, so none of them
230
+ * is (or should be) automated. One line per directory, gated on the surface that makes the tool
231
+ * a fit for it; a project selecting none of those surfaces gets no section at all.
232
+ */
233
+ function listingSection(project) {
234
+ const items = [];
235
+ if (has(project, "mcp")) {
236
+ items.push(`- **Docker MCP Catalog** — PR to \`docker/mcp-registry\` adding \`servers/${project.identity.name}/server.yaml\` (\`--image ghcr.io/<owner>/${project.identity.name}\`): https://github.com/docker/mcp-registry`, "- **GitHub MCP registry** — manual curation, not automated by publishing to the official registry: https://github.com/github/github-mcp-server/discussions/1257", "- **Cline marketplace** — issue on https://github.com/cline/mcp-marketplace", "- **mcp.so** — issue on https://github.com/chatmcp/mcpso", "- **awesome-mcp-servers** — PR to https://github.com/punkpeye/awesome-mcp-servers");
237
+ }
238
+ if (has(project, "claude")) {
239
+ items.push("- **Anthropic plugin directory** — https://claude.com/docs/plugins/submit");
240
+ }
241
+ if (has(project, "agent-plugins")) {
242
+ items.push("- **Kiro Powers** — https://kiro.dev/powers/submit (needs `keywords` in the manifest and a privacy-policy link in the README)");
243
+ }
244
+ if (has(project, "codex")) {
245
+ items.push("- **OpenAI plugin directory** — https://developers.openai.com/plugins/deploy/submission (needs a hosted HTTPS MCP server; a repo-only `codex plugin marketplace add` install, above, does not qualify)");
246
+ }
247
+ if (!items.length)
248
+ return [];
249
+ return [
250
+ "## Listing",
251
+ "",
252
+ "Every install line above already works from this repository alone; these are the curated",
253
+ "directories and marketplaces that additionally *list* it. Each is a one-time human-reviewed",
254
+ "portal step or pull request, run at the author's discretion — never generated, never",
255
+ "automated.",
256
+ "",
257
+ ...items,
258
+ "",
259
+ ];
260
+ }
261
+ function reloadSection(project) {
262
+ return [
263
+ "## Reload",
264
+ "",
265
+ "Registration is automated; reload is not, and only some harnesses have one.",
266
+ "",
267
+ "| Harness | MCP config | Skills and `AGENTS.md` |",
268
+ "|---|---|---|",
269
+ ...RELOAD.map((entry) => `| ${entry.harness} | ${entry.mcp} | ${entry.instructions} |`),
270
+ "",
271
+ // The extension is not a harness, so it gets a line rather than a row in a table whose two
272
+ // columns are MCP config and skills; its reload is the browser's, one per engine.
273
+ ...(has(project, "browser-extension")
274
+ ? [
275
+ `The browser extension reloads on the browser's terms: \`npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt dev\``,
276
+ "hot-reloads it as you edit, a `wxt build` output needs the Reload button on `chrome://extensions`,",
277
+ "and Firefox's `web-ext run` reloads on change. Re-pair only after minting a new token.",
278
+ "",
279
+ ]
280
+ : []),
281
+ ];
282
+ }
283
+ function worktreesSection() {
284
+ return [
285
+ "## Worktrees",
286
+ "",
287
+ "Use the host's own worktree system; toolfactory creates none and writes nothing outside this",
288
+ "repository.",
289
+ "",
290
+ "- **OpenClaw**: `openclaw worktrees create <repo> --name <name>` (state-dir owned, outside the repo).",
291
+ "- **Hermes**: `hermes chat -w <name>` (repo-local `.worktrees/`, gitignored for you).",
292
+ "- **Claude Code**: `claude --worktree <name>` (`.claude/worktrees/`).",
293
+ "- **Cursor**: `agent -w <name>` (`~/.cursor/worktrees/`).",
294
+ "",
295
+ "Calling toolfactory over MCP from inside one: pass `root` explicitly. A stdio MCP server gets a",
296
+ "static, config-time working directory, so it cannot tell which worktree you are in.",
297
+ "",
298
+ ];
299
+ }
300
+ /** The content of the generated region — everything between the markers, not the markers themselves. */
301
+ /**
302
+ * Where the web app comes up, per surface. One page, one listener: the kernel serves `web/dist`
303
+ * beside its own `/mcp`, so every row below is a way of starting or reaching that one process —
304
+ * never a second server, and never a build step at launch time.
305
+ */
306
+ function launchSection(project) {
307
+ if (!has(project, "web"))
308
+ return [];
309
+ const { command, args } = getBinding(project.tool.binding).cliCommand(project);
310
+ const cli = `${[command, ...args].join(" ")} mcp --http --open`;
311
+ const lines = [
312
+ "## Launch the web app",
313
+ "",
314
+ `- **This checkout**: \`${cli}\` — serves the operations page and \`/mcp\` on one port, opens it,`,
315
+ " and (once the project declares a secret) carries the Secrets panel that writes `.env`.",
316
+ "- **From an agent**: call the `web` operation — it starts the same listener detached, opens the",
317
+ " browser on this machine, and returns the URL. Over MCP, a skill, the CLI, or the page itself.",
318
+ ];
319
+ if (has(project, "openclaw-native") && project.tool.binding === "typescript") {
320
+ lines.push(`- **OpenClaw**: a **${displayName(project.identity.name)}** tab in the Control UI once the plugin is enabled — the`, ` plugin serves the same page at \`/plugins/${project.identity.name}/web\` through the gateway, so there is`, " nothing to start.");
321
+ }
322
+ if (has(project, "browser-extension")) {
323
+ lines.push("- **Browser extension**: click the toolbar icon, then **Open full page**; the options page is the", " same tree, paired to a kernel over the relay.");
324
+ }
325
+ lines.push("- **Hermes, DSH, Gemini CLI, Claude Code**: no native surface for a page — run the line above, or", " ask the agent to call `web`.", "");
326
+ return lines;
327
+ }
328
+ export function agentsContent(project) {
329
+ const lines = [
330
+ "",
331
+ ...commandsSection(project),
332
+ ...layoutSection(project),
333
+ ...boundarySection(),
334
+ ...agentConfigSection(project),
335
+ ...launchSection(project),
336
+ ...installSection(project),
337
+ ...listingSection(project),
338
+ ...reloadSection(project),
339
+ ...worktreesSection(),
340
+ ];
341
+ return `${lines.join("\n")}\n`;
342
+ }
343
+ /**
344
+ * The one-time bootstrap notice is the author's, above the region: `.agents/setup` deletes it on
345
+ * its first successful run, and a self-deleting block inside a toolfactory-owned region would be
346
+ * permanent drift. It ships without the template's `keep=` slug, which is what makes the template
347
+ * repository itself keep the notice forever.
348
+ */
349
+ function agentsTemplate(project) {
350
+ return [
351
+ `# ${project.identity.name}`,
352
+ "",
353
+ "<!-- setup -->",
354
+ "One-time bootstrap: if no environment hook ran it yet, run",
355
+ "`bash .agents/setup`. Its first successful run makes everything in Agent",
356
+ "config automatic and deletes this notice.",
357
+ "<!-- /setup -->",
358
+ "",
359
+ `One paragraph: what an agent working in this repo should know before ${isInstructionOnly(project.tool) ? "editing the Skill instructions" : `touching \`${project.tool.binding === "typescript" ? "src/ops.ts" : "the operation module"}\``} — the tool's domain, and anything not obvious from the layout below.`,
360
+ "",
361
+ AGENTS_BEGIN,
362
+ AGENTS_END,
363
+ "",
364
+ ].join("\n");
365
+ }
366
+ /** The name the tool's own kernel is registered under, when a surface makes it worth running. */
367
+ function kernelServerName(project) {
368
+ return has(project, "mcp") || has(project, "openclaw-native") || has(project, "hermes-native")
369
+ ? project.identity.name
370
+ : undefined;
371
+ }
372
+ /**
373
+ * `.agents/mcp/servers.json` is a merge file keyed by server name: `sync.py absorb()` writes
374
+ * servers a teammate added through their own harness into this same document, so toolfactory owns
375
+ * exactly the names it puts there and every entry whole (`owned`), never field by field.
376
+ */
377
+ function servers(project) {
378
+ if (isInstructionOnly(project.tool)) {
379
+ return { kind: "merge", path: SERVERS_PATH, format: "json", patch: {} };
380
+ }
381
+ const own = kernelServerName(project);
382
+ const patch = {};
383
+ if (own)
384
+ patch[own] = getBinding(project.tool.binding).kernelCommand(project);
385
+ // The generator that maintains the repo — except in toolfactory's own, where the published
386
+ // package would shadow the checkout entry above under the same name.
387
+ if (project.identity.name !== "toolfactory") {
388
+ patch.toolfactory = { command: "npx", args: ["toolfactory", "mcp"] };
389
+ }
390
+ if (has(project, "web")) {
391
+ patch.shadcn = { command: "npx", args: ["shadcn@latest", "mcp"] };
392
+ patch.playwright = { command: "npx", args: ["@playwright/mcp@latest"] };
393
+ }
394
+ return {
395
+ kind: "merge",
396
+ path: SERVERS_PATH,
397
+ format: "json",
398
+ patch,
399
+ owned: Object.keys(patch),
400
+ };
401
+ }
402
+ /** The vendored `.gitignore`, split on its `# ─── Title ───` headers. */
403
+ function ignoreBlocks() {
404
+ const blocks = new Map();
405
+ let title = "";
406
+ let lines = [];
407
+ for (const line of TEMPLATE_FILES[IGNORE_PATH]?.split("\n") ?? []) {
408
+ const header = line.match(/^# ─+ (.+?) ─+$/);
409
+ if (header) {
410
+ if (title)
411
+ blocks.set(title, lines.join("\n"));
412
+ title = header[1];
413
+ lines = [line];
414
+ }
415
+ else if (title && line !== "") {
416
+ lines.push(line);
417
+ }
418
+ }
419
+ if (title)
420
+ blocks.set(title, lines.join("\n"));
421
+ return blocks;
422
+ }
423
+ /** The build products toolfactory generates and locks but rebuilds rather than tracks (§2.2 S4). */
424
+ function outputPaths(project) {
425
+ return [
426
+ `${TOOLFACTORY_DIR}/coverage.json`,
427
+ INSPECTOR_CONFIG_PATH,
428
+ ...(has(project, "web") ? [`${WEB_DIR}/src/ops.json`] : []),
429
+ ];
430
+ }
431
+ /**
432
+ * The `.gitignore` region: the template's two agent blocks — `sync.py check` fails unless every
433
+ * renderer output is ignored — plus toolfactory's own. Everything else the template ignores is
434
+ * written once into the author's half of the file.
435
+ */
436
+ function ignoreRegion(project) {
437
+ const blocks = ignoreBlocks();
438
+ const binding = isInstructionOnly(project.tool)
439
+ ? []
440
+ : project.tool.binding === "python"
441
+ ? [".venv/", "__pycache__/"]
442
+ : ["node_modules/"];
443
+ return [
444
+ "",
445
+ blocks.get("Agent local state") ?? "",
446
+ "",
447
+ blocks.get("Generated agent adapters") ?? "",
448
+ "# The `skills` CLI symlinks straight into Factory's own skills directory, which",
449
+ "# `sync.py` does not manage.",
450
+ "/.factory/skills/",
451
+ "",
452
+ "# ─── toolfactory ──────────────────────────────────────────",
453
+ ...binding,
454
+ "dist/",
455
+ ".env",
456
+ ".env.*",
457
+ "!.env.example",
458
+ ...(has(project, "openclaw-native")
459
+ ? [
460
+ "# npm writes this when the host plugin is validated; its only dependency on this repo is file:../..",
461
+ "hosts/*/package-lock.json",
462
+ "hosts/*/uv.lock",
463
+ ]
464
+ : []),
465
+ "# Build products: generated and locked like every other file, rebuilt rather than tracked.",
466
+ ...outputPaths(project),
467
+ "",
468
+ ].join("\n");
469
+ }
470
+ /** The author's half of a fresh `.gitignore`: the template's remaining blocks, minus duplicates. */
471
+ function ignoreTemplate(project) {
472
+ const region = ignoreRegion(project);
473
+ const owned = new Set(region.split("\n"));
474
+ const rest = [...ignoreBlocks()]
475
+ .filter(([title]) => title !== "Agent local state" && title !== "Generated agent adapters")
476
+ .map(([, block]) => block.split("\n").filter((line, index) => index === 0 || !owned.has(line)))
477
+ .filter((lines) => lines.length > 1)
478
+ .map((lines) => lines.join("\n"));
479
+ return `${[IGNORE_BEGIN + region + IGNORE_END, ...rest].join("\n\n")}\n`;
480
+ }
481
+ /**
482
+ * `.agents/setup` is the template's machinery verbatim — isolation detection, the git hooks, the
483
+ * bootstrap-notice deletion — plus the dependency install and drift gate toolfactory generates for
484
+ * the binding. Both are toolfactory's; the author's codegen and migrations go after the marker,
485
+ * where the template's own sentinel comment puts them.
486
+ */
487
+ function setupTail(project) {
488
+ const install = isInstructionOnly(project.tool)
489
+ ? []
490
+ : project.tool.binding === "python"
491
+ ? [
492
+ "if command -v uv >/dev/null 2>&1; then",
493
+ " uv sync",
494
+ "else",
495
+ ' echo "uv not found: https://docs.astral.sh/uv/getting-started/installation/, then rerun .agents/setup" >&2',
496
+ " exit 1",
497
+ "fi",
498
+ ]
499
+ : (project.packageManager ?? "npm") === "pnpm"
500
+ ? [
501
+ "if ! command -v pnpm >/dev/null 2>&1; then",
502
+ " corepack enable >/dev/null 2>&1 || true",
503
+ "fi",
504
+ "if command -v pnpm >/dev/null 2>&1; then",
505
+ " pnpm install --prefer-offline",
506
+ "else",
507
+ ' echo "pnpm not found: install Node >= 22.12 (corepack provides pnpm), then rerun .agents/setup" >&2',
508
+ " exit 1",
509
+ "fi",
510
+ ]
511
+ : ["npm install --no-audit --no-fund"];
512
+ return [
513
+ "",
514
+ "# The pre-commit hook installed above converges agent config; toolfactory's drift gate goes in",
515
+ "# front of its final `exec` so a commit cannot leave a generated file stale. That hook is",
516
+ "# rewritten on every setup run, so the gate is re-inserted here every time, and before the",
517
+ "# dependency install below, which is the step most likely to fail on a fresh machine.",
518
+ "python3 - <<'PY'",
519
+ "import pathlib, subprocess",
520
+ "",
521
+ 'hooks = subprocess.run(["git", "rev-parse", "--git-path", "hooks"],',
522
+ " capture_output=True, text=True).stdout.strip()",
523
+ 'hook = pathlib.Path(hooks) / "pre-commit"',
524
+ "gate = 'npx toolfactory check >/dev/null || { echo \"toolfactory: generated files are stale; run npx toolfactory build\" >&2; exit 1; }'",
525
+ "if hook.is_file():",
526
+ ' lines = hook.read_text(encoding="utf-8").splitlines()',
527
+ " if gate not in lines:",
528
+ ' at = next((i for i, line in enumerate(lines) if line.startswith("exec ")), len(lines))',
529
+ " lines.insert(at, gate)",
530
+ ' hook.write_text("\\n".join(lines) + "\\n", encoding="utf-8")',
531
+ "PY",
532
+ "",
533
+ "# Project setup: the dependencies this project's toolchain needs.",
534
+ ...install,
535
+ "",
536
+ ].join("\n");
537
+ }
538
+ function setupFile(project) {
539
+ const vendored = TEMPLATE_FILES[SETUP_PATH] ?? "";
540
+ const sentinel = vendored.indexOf(SETUP_SENTINEL);
541
+ const head = `${vendored.slice(vendored.indexOf("\n") + 1, sentinel).trimEnd()}\n`;
542
+ const shebang = vendored.slice(0, vendored.indexOf("\n"));
543
+ const content = `\n${head}${setupTail(project)}`;
544
+ return {
545
+ kind: "region",
546
+ path: SETUP_PATH,
547
+ regions: [{ begin: SETUP_BEGIN, end: SETUP_END, content }],
548
+ template: [
549
+ shebang,
550
+ `${SETUP_BEGIN}${content}${SETUP_END}`,
551
+ "",
552
+ "# Anything else this project needs at setup time (codegen, migrations) goes",
553
+ "# here. Keep it idempotent and fast when there is nothing to do.",
554
+ "",
555
+ ].join("\n"),
556
+ };
557
+ }
558
+ /** The one template file with a per-project key: the binding's devcontainer feature. */
559
+ function devcontainer(project) {
560
+ const path = ".devcontainer/devcontainer.json";
561
+ const document = JSON.parse(TEMPLATE_FILES[path] ?? "{}");
562
+ const features = !isInstructionOnly(project.tool) && project.tool.binding === "typescript"
563
+ ? { "ghcr.io/devcontainers/features/node:1": { version: "22" }, ...document.features }
564
+ : document.features;
565
+ return `${JSON.stringify({ ...document, features }, null, 2)}\n`;
566
+ }
567
+ /** Whole-file carriers: no per-project content, `adopt` is the escape hatch. */
568
+ function vendored(project) {
569
+ const skip = new Set([SETUP_PATH, IGNORE_PATH, ".devcontainer/devcontainer.json"]);
570
+ return [
571
+ ...Object.entries(TEMPLATE_FILES)
572
+ .filter(([path]) => !skip.has(path))
573
+ .map(([path, content]) => ({
574
+ kind: "file",
575
+ path,
576
+ content,
577
+ ...(path.endsWith("sync.py") ? { mode: 0o755 } : {}),
578
+ })),
579
+ { kind: "file", path: ".devcontainer/devcontainer.json", content: devcontainer(project) },
580
+ ];
581
+ }
582
+ export const surface = {
583
+ id: "agents",
584
+ plan(project) {
585
+ return [
586
+ {
587
+ kind: "region",
588
+ path: AGENTS_PATH,
589
+ regions: [{ begin: AGENTS_BEGIN, end: AGENTS_END, content: agentsContent(project) }],
590
+ template: agentsTemplate(project),
591
+ },
592
+ {
593
+ kind: "region",
594
+ path: IGNORE_PATH,
595
+ regions: [{ begin: IGNORE_BEGIN, end: IGNORE_END, content: ignoreRegion(project) }],
596
+ template: ignoreTemplate(project),
597
+ },
598
+ setupFile(project),
599
+ servers(project),
600
+ ...vendored(project),
601
+ ];
602
+ },
603
+ validate() {
604
+ // C2: the vendored template is proved against the repository that owns it, never trusted.
605
+ return [
606
+ { label: "agent-config template drift", command: "node", args: [...execArgv, DRIFT_ENTRY] },
607
+ ];
608
+ },
609
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Vendored from https://github.com/GoatInAHat/template at d63bc8cede8c432fc2d9345e2e3057bbe3b3f4d2.
3
+ *
4
+ * Do not edit: written by `pnpm vendor:template`, proved by `src/hosts/template.ts`'s
5
+ * scaffoldDrift, which the `agents` surface's validate() runs against the real repository.
6
+ */
7
+ export declare const TEMPLATE_COMMIT = "d63bc8cede8c432fc2d9345e2e3057bbe3b3f4d2";
8
+ export declare const TEMPLATE_FILES: Record<string, string>;