@ambitresearch/genie 1.3.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 (301) hide show
  1. package/README.md +15 -0
  2. package/dist/auth/bearer.d.ts +49 -0
  3. package/dist/auth/bearer.d.ts.map +1 -0
  4. package/dist/auth/bearer.js +159 -0
  5. package/dist/auth/bearer.js.map +1 -0
  6. package/dist/auth/oauth/authorize.d.ts +19 -0
  7. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  8. package/dist/auth/oauth/authorize.js +111 -0
  9. package/dist/auth/oauth/authorize.js.map +1 -0
  10. package/dist/auth/oauth/config.d.ts +10 -0
  11. package/dist/auth/oauth/config.d.ts.map +1 -0
  12. package/dist/auth/oauth/config.js +16 -0
  13. package/dist/auth/oauth/config.js.map +1 -0
  14. package/dist/auth/oauth/dcr.d.ts +15 -0
  15. package/dist/auth/oauth/dcr.d.ts.map +1 -0
  16. package/dist/auth/oauth/dcr.js +38 -0
  17. package/dist/auth/oauth/dcr.js.map +1 -0
  18. package/dist/auth/oauth/jwt.d.ts +13 -0
  19. package/dist/auth/oauth/jwt.d.ts.map +1 -0
  20. package/dist/auth/oauth/jwt.js +46 -0
  21. package/dist/auth/oauth/jwt.js.map +1 -0
  22. package/dist/auth/oauth/metadata.d.ts +9 -0
  23. package/dist/auth/oauth/metadata.d.ts.map +1 -0
  24. package/dist/auth/oauth/metadata.js +19 -0
  25. package/dist/auth/oauth/metadata.js.map +1 -0
  26. package/dist/auth/oauth/router.d.ts +32 -0
  27. package/dist/auth/oauth/router.d.ts.map +1 -0
  28. package/dist/auth/oauth/router.js +159 -0
  29. package/dist/auth/oauth/router.js.map +1 -0
  30. package/dist/auth/oauth/store.d.ts +55 -0
  31. package/dist/auth/oauth/store.d.ts.map +1 -0
  32. package/dist/auth/oauth/store.js +63 -0
  33. package/dist/auth/oauth/store.js.map +1 -0
  34. package/dist/auth/oauth/token.d.ts +21 -0
  35. package/dist/auth/oauth/token.d.ts.map +1 -0
  36. package/dist/auth/oauth/token.js +77 -0
  37. package/dist/auth/oauth/token.js.map +1 -0
  38. package/dist/auth/oidc/group-policy.d.ts +43 -0
  39. package/dist/auth/oidc/group-policy.d.ts.map +1 -0
  40. package/dist/auth/oidc/group-policy.js +54 -0
  41. package/dist/auth/oidc/group-policy.js.map +1 -0
  42. package/dist/auth/oidc/verifier.d.ts +42 -0
  43. package/dist/auth/oidc/verifier.d.ts.map +1 -0
  44. package/dist/auth/oidc/verifier.js +145 -0
  45. package/dist/auth/oidc/verifier.js.map +1 -0
  46. package/dist/auth/token-cli.d.ts +9 -0
  47. package/dist/auth/token-cli.d.ts.map +1 -0
  48. package/dist/auth/token-cli.js +98 -0
  49. package/dist/auth/token-cli.js.map +1 -0
  50. package/dist/cli.d.ts +3 -0
  51. package/dist/cli.d.ts.map +1 -0
  52. package/dist/cli.js +149 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config/redact.d.ts +20 -0
  55. package/dist/config/redact.d.ts.map +1 -0
  56. package/dist/config/redact.js +77 -0
  57. package/dist/config/redact.js.map +1 -0
  58. package/dist/config/secrets.d.ts +100 -0
  59. package/dist/config/secrets.d.ts.map +1 -0
  60. package/dist/config/secrets.js +130 -0
  61. package/dist/config/secrets.js.map +1 -0
  62. package/dist/framework/html.d.ts +103 -0
  63. package/dist/framework/html.d.ts.map +1 -0
  64. package/dist/framework/html.js +271 -0
  65. package/dist/framework/html.js.map +1 -0
  66. package/dist/framework/interface.d.ts +130 -0
  67. package/dist/framework/interface.d.ts.map +1 -0
  68. package/dist/framework/interface.js +112 -0
  69. package/dist/framework/interface.js.map +1 -0
  70. package/dist/framework/react.d.ts +133 -0
  71. package/dist/framework/react.d.ts.map +1 -0
  72. package/dist/framework/react.js +243 -0
  73. package/dist/framework/react.js.map +1 -0
  74. package/dist/framework/vue.d.ts +99 -0
  75. package/dist/framework/vue.d.ts.map +1 -0
  76. package/dist/framework/vue.js +375 -0
  77. package/dist/framework/vue.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +3 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/llm/client.d.ts +81 -0
  83. package/dist/llm/client.d.ts.map +1 -0
  84. package/dist/llm/client.js +141 -0
  85. package/dist/llm/client.js.map +1 -0
  86. package/dist/llm/component-response.d.ts +111 -0
  87. package/dist/llm/component-response.d.ts.map +1 -0
  88. package/dist/llm/component-response.js +167 -0
  89. package/dist/llm/component-response.js.map +1 -0
  90. package/dist/llm/generated-files.d.ts +10 -0
  91. package/dist/llm/generated-files.d.ts.map +1 -0
  92. package/dist/llm/generated-files.js +26 -0
  93. package/dist/llm/generated-files.js.map +1 -0
  94. package/dist/llm/prompts/CHANGELOG.md +70 -0
  95. package/dist/llm/prompts/generate-component.system.md +99 -0
  96. package/dist/llm/prompts/refine-component.system.md +112 -0
  97. package/dist/llm/prompts.d.ts +66 -0
  98. package/dist/llm/prompts.d.ts.map +1 -0
  99. package/dist/llm/prompts.js +78 -0
  100. package/dist/llm/prompts.js.map +1 -0
  101. package/dist/llm/retry.d.ts +183 -0
  102. package/dist/llm/retry.d.ts.map +1 -0
  103. package/dist/llm/retry.js +475 -0
  104. package/dist/llm/retry.js.map +1 -0
  105. package/dist/llm/schema.d.ts +129 -0
  106. package/dist/llm/schema.d.ts.map +1 -0
  107. package/dist/llm/schema.js +147 -0
  108. package/dist/llm/schema.js.map +1 -0
  109. package/dist/llm/validate.d.ts +107 -0
  110. package/dist/llm/validate.d.ts.map +1 -0
  111. package/dist/llm/validate.js +174 -0
  112. package/dist/llm/validate.js.map +1 -0
  113. package/dist/manifest/compiler.d.ts +112 -0
  114. package/dist/manifest/compiler.d.ts.map +1 -0
  115. package/dist/manifest/compiler.js +306 -0
  116. package/dist/manifest/compiler.js.map +1 -0
  117. package/dist/manifest/ensure.d.ts +46 -0
  118. package/dist/manifest/ensure.d.ts.map +1 -0
  119. package/dist/manifest/ensure.js +49 -0
  120. package/dist/manifest/ensure.js.map +1 -0
  121. package/dist/manifest/index.d.ts +13 -0
  122. package/dist/manifest/index.d.ts.map +1 -0
  123. package/dist/manifest/index.js +13 -0
  124. package/dist/manifest/index.js.map +1 -0
  125. package/dist/middleware/plan-guard.d.ts +167 -0
  126. package/dist/middleware/plan-guard.d.ts.map +1 -0
  127. package/dist/middleware/plan-guard.js +190 -0
  128. package/dist/middleware/plan-guard.js.map +1 -0
  129. package/dist/plans/index.d.ts +71 -0
  130. package/dist/plans/index.d.ts.map +1 -0
  131. package/dist/plans/index.js +252 -0
  132. package/dist/plans/index.js.map +1 -0
  133. package/dist/schemas/component.schema.json +114 -0
  134. package/dist/server.d.ts +96 -0
  135. package/dist/server.d.ts.map +1 -0
  136. package/dist/server.js +258 -0
  137. package/dist/server.js.map +1 -0
  138. package/dist/store/empty-manifest.d.ts +58 -0
  139. package/dist/store/empty-manifest.d.ts.map +1 -0
  140. package/dist/store/empty-manifest.js +59 -0
  141. package/dist/store/empty-manifest.js.map +1 -0
  142. package/dist/store/git-host.d.ts +120 -0
  143. package/dist/store/git-host.d.ts.map +1 -0
  144. package/dist/store/git-host.js +796 -0
  145. package/dist/store/git-host.js.map +1 -0
  146. package/dist/store/index.d.ts +5 -0
  147. package/dist/store/index.d.ts.map +1 -0
  148. package/dist/store/index.js +4 -0
  149. package/dist/store/index.js.map +1 -0
  150. package/dist/store/interface.d.ts +322 -0
  151. package/dist/store/interface.d.ts.map +1 -0
  152. package/dist/store/interface.js +100 -0
  153. package/dist/store/interface.js.map +1 -0
  154. package/dist/store/kit-files.d.ts +88 -0
  155. package/dist/store/kit-files.d.ts.map +1 -0
  156. package/dist/store/kit-files.js +199 -0
  157. package/dist/store/kit-files.js.map +1 -0
  158. package/dist/store/local.d.ts +70 -0
  159. package/dist/store/local.d.ts.map +1 -0
  160. package/dist/store/local.js +651 -0
  161. package/dist/store/local.js.map +1 -0
  162. package/dist/store/manifest.d.ts +44 -0
  163. package/dist/store/manifest.d.ts.map +1 -0
  164. package/dist/store/manifest.js +116 -0
  165. package/dist/store/manifest.js.map +1 -0
  166. package/dist/store/viewer-assets.d.ts +20 -0
  167. package/dist/store/viewer-assets.d.ts.map +1 -0
  168. package/dist/store/viewer-assets.js +118 -0
  169. package/dist/store/viewer-assets.js.map +1 -0
  170. package/dist/sync/anchor.d.ts +125 -0
  171. package/dist/sync/anchor.d.ts.map +1 -0
  172. package/dist/sync/anchor.js +210 -0
  173. package/dist/sync/anchor.js.map +1 -0
  174. package/dist/sync/index.d.ts +15 -0
  175. package/dist/sync/index.d.ts.map +1 -0
  176. package/dist/sync/index.js +15 -0
  177. package/dist/sync/index.js.map +1 -0
  178. package/dist/sync/orchestrator.d.ts +160 -0
  179. package/dist/sync/orchestrator.d.ts.map +1 -0
  180. package/dist/sync/orchestrator.js +288 -0
  181. package/dist/sync/orchestrator.js.map +1 -0
  182. package/dist/tools/bind_kit.d.ts +29 -0
  183. package/dist/tools/bind_kit.d.ts.map +1 -0
  184. package/dist/tools/bind_kit.js +70 -0
  185. package/dist/tools/bind_kit.js.map +1 -0
  186. package/dist/tools/conjure.d.ts +141 -0
  187. package/dist/tools/conjure.d.ts.map +1 -0
  188. package/dist/tools/conjure.js +655 -0
  189. package/dist/tools/conjure.js.map +1 -0
  190. package/dist/tools/conjure_screen.d.ts +174 -0
  191. package/dist/tools/conjure_screen.d.ts.map +1 -0
  192. package/dist/tools/conjure_screen.js +458 -0
  193. package/dist/tools/conjure_screen.js.map +1 -0
  194. package/dist/tools/create_kit.d.ts +32 -0
  195. package/dist/tools/create_kit.d.ts.map +1 -0
  196. package/dist/tools/create_kit.js +131 -0
  197. package/dist/tools/create_kit.js.map +1 -0
  198. package/dist/tools/create_project.d.ts +267 -0
  199. package/dist/tools/create_project.d.ts.map +1 -0
  200. package/dist/tools/create_project.js +546 -0
  201. package/dist/tools/create_project.js.map +1 -0
  202. package/dist/tools/delete_files.d.ts +93 -0
  203. package/dist/tools/delete_files.d.ts.map +1 -0
  204. package/dist/tools/delete_files.js +276 -0
  205. package/dist/tools/delete_files.js.map +1 -0
  206. package/dist/tools/delete_project.d.ts +43 -0
  207. package/dist/tools/delete_project.d.ts.map +1 -0
  208. package/dist/tools/delete_project.js +118 -0
  209. package/dist/tools/delete_project.js.map +1 -0
  210. package/dist/tools/get_kit.d.ts +28 -0
  211. package/dist/tools/get_kit.d.ts.map +1 -0
  212. package/dist/tools/get_kit.js +95 -0
  213. package/dist/tools/get_kit.js.map +1 -0
  214. package/dist/tools/get_project.d.ts +37 -0
  215. package/dist/tools/get_project.d.ts.map +1 -0
  216. package/dist/tools/get_project.js +71 -0
  217. package/dist/tools/get_project.js.map +1 -0
  218. package/dist/tools/list_components.d.ts +42 -0
  219. package/dist/tools/list_components.d.ts.map +1 -0
  220. package/dist/tools/list_components.js +125 -0
  221. package/dist/tools/list_components.js.map +1 -0
  222. package/dist/tools/list_files.d.ts +32 -0
  223. package/dist/tools/list_files.d.ts.map +1 -0
  224. package/dist/tools/list_files.js +109 -0
  225. package/dist/tools/list_files.js.map +1 -0
  226. package/dist/tools/list_kits.d.ts +14 -0
  227. package/dist/tools/list_kits.d.ts.map +1 -0
  228. package/dist/tools/list_kits.js +41 -0
  229. package/dist/tools/list_kits.js.map +1 -0
  230. package/dist/tools/list_projects.d.ts +23 -0
  231. package/dist/tools/list_projects.d.ts.map +1 -0
  232. package/dist/tools/list_projects.js +66 -0
  233. package/dist/tools/list_projects.js.map +1 -0
  234. package/dist/tools/plan.d.ts +16 -0
  235. package/dist/tools/plan.d.ts.map +1 -0
  236. package/dist/tools/plan.js +146 -0
  237. package/dist/tools/plan.js.map +1 -0
  238. package/dist/tools/preview.d.ts +289 -0
  239. package/dist/tools/preview.d.ts.map +1 -0
  240. package/dist/tools/preview.js +672 -0
  241. package/dist/tools/preview.js.map +1 -0
  242. package/dist/tools/read_file.d.ts +40 -0
  243. package/dist/tools/read_file.d.ts.map +1 -0
  244. package/dist/tools/read_file.js +114 -0
  245. package/dist/tools/read_file.js.map +1 -0
  246. package/dist/tools/refine.d.ts +220 -0
  247. package/dist/tools/refine.d.ts.map +1 -0
  248. package/dist/tools/refine.js +501 -0
  249. package/dist/tools/refine.js.map +1 -0
  250. package/dist/tools/validate.d.ts +74 -0
  251. package/dist/tools/validate.d.ts.map +1 -0
  252. package/dist/tools/validate.js +224 -0
  253. package/dist/tools/validate.js.map +1 -0
  254. package/dist/tools/write_files.d.ts +158 -0
  255. package/dist/tools/write_files.d.ts.map +1 -0
  256. package/dist/tools/write_files.js +541 -0
  257. package/dist/tools/write_files.js.map +1 -0
  258. package/dist/transport.d.ts +77 -0
  259. package/dist/transport.d.ts.map +1 -0
  260. package/dist/transport.js +448 -0
  261. package/dist/transport.js.map +1 -0
  262. package/dist/ui/card-asset-broker.d.ts +42 -0
  263. package/dist/ui/card-asset-broker.d.ts.map +1 -0
  264. package/dist/ui/card-asset-broker.js +479 -0
  265. package/dist/ui/card-asset-broker.js.map +1 -0
  266. package/dist/ui/grid-resource.d.ts +252 -0
  267. package/dist/ui/grid-resource.d.ts.map +1 -0
  268. package/dist/ui/grid-resource.js +680 -0
  269. package/dist/ui/grid-resource.js.map +1 -0
  270. package/dist/ui/viewer-static/index.html +95 -0
  271. package/dist/ui/viewer-static/viewer.css +451 -0
  272. package/dist/ui/viewer-static/viewer.js +1405 -0
  273. package/dist/validate/full-scan.d.ts +87 -0
  274. package/dist/validate/full-scan.d.ts.map +1 -0
  275. package/dist/validate/full-scan.js +299 -0
  276. package/dist/validate/full-scan.js.map +1 -0
  277. package/dist/validate/index.d.ts +15 -0
  278. package/dist/validate/index.d.ts.map +1 -0
  279. package/dist/validate/index.js +18 -0
  280. package/dist/validate/index.js.map +1 -0
  281. package/dist/validate/marker.d.ts +89 -0
  282. package/dist/validate/marker.d.ts.map +1 -0
  283. package/dist/validate/marker.js +87 -0
  284. package/dist/validate/marker.js.map +1 -0
  285. package/dist/validate/phash.d.ts +135 -0
  286. package/dist/validate/phash.d.ts.map +1 -0
  287. package/dist/validate/phash.js +307 -0
  288. package/dist/validate/phash.js.map +1 -0
  289. package/dist/validate/render.d.ts +38 -0
  290. package/dist/validate/render.d.ts.map +1 -0
  291. package/dist/validate/render.js +155 -0
  292. package/dist/validate/render.js.map +1 -0
  293. package/dist/watch/index.d.ts +12 -0
  294. package/dist/watch/index.d.ts.map +1 -0
  295. package/dist/watch/index.js +12 -0
  296. package/dist/watch/index.js.map +1 -0
  297. package/dist/watch/watcher.d.ts +65 -0
  298. package/dist/watch/watcher.d.ts.map +1 -0
  299. package/dist/watch/watcher.js +238 -0
  300. package/dist/watch/watcher.js.map +1 -0
  301. package/package.json +70 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * M3-05 (DRO-261) — genie's 5-step atomic write-sequence orchestrator.
3
+ *
4
+ * `runAtomicSync` sequences one sync commit as five ordered, stop-on-first-
5
+ * failure steps. The atomic write sequence is load-bearing and unchanged in
6
+ * shape:
7
+ *
8
+ * 1. Write the `.genie/recompile` sentinel FIRST, to fence the manifest/copy
9
+ * machinery — a consumer that sees the sentinel knows a sync is mid-flight
10
+ * and the tree may be inconsistent until the anchor lands.
11
+ * 2. Apply every content write, chunked into batches of ≤ 256 per
12
+ * `write_files` call (the M1-08 `MAX_FILES_PER_CALL` ceiling — a 257-file
13
+ * call would be rejected as `TooManyFilesError`).
14
+ * 3. Apply every delete via one `delete_files` call. A path that is
15
+ * authorized-but-absent is NOT a failure (M1-09 AC5 returns it in
16
+ * `notFoundPaths`) — only a real error (out-of-plan path, unlink failure)
17
+ * trips stop-on-first-failure.
18
+ * 4. RE-ARM the sentinel (write it again), so it still fences the tree after
19
+ * the writes/deletes landed and right up until the anchor is committed.
20
+ * 5. Write `.genie/sync.json` LAST, via M3-06's {@link writeAnchor}. It is the
21
+ * completion proof: its presence means every prior step succeeded.
22
+ *
23
+ * ── Why the anchor is last (AC7) ─────────────────────────────────────────────
24
+ * A mid-plan failure (steps 1–4) returns before step 5, so `.genie/sync.json`
25
+ * is never written for an incomplete sync. The NEXT sync sees "sentinel present,
26
+ * anchor absent" ({@link detectResumeStep} → 2) and knows the tree is a
27
+ * half-write to repair from step 2 — rather than trusting a stale anchor that
28
+ * would mask the incomplete state. This is the whole reason the write order is
29
+ * load-bearing.
30
+ *
31
+ * ── Native surface (CLAUDE.md hard rule 1) ───────────────────────────────────
32
+ * `.genie/recompile` and `.genie/sync.json` are genie-NATIVE artifacts. They are
33
+ * written fs-native to `projectRoot` (NOT through the plan-gated `write_files`
34
+ * verb — they would never match a user plan's `writes` globs, and they are
35
+ * genie's own bookkeeping, not user content). They must NOT be renamed to the
36
+ * Anthropic `_ds_recompile` / `_ds_sync.json` interop shapes; an interop bridge
37
+ * that maps them is a separate, opt-in concern.
38
+ *
39
+ * ── Composition (Kiln's plan, DRO-696) ───────────────────────────────────────
40
+ * Steps 2–3 call the in-process `writeFiles` / `deleteFiles` CORE functions
41
+ * directly (not over the MCP transport) — the same way `conjure_screen.ts`
42
+ * composes tool-layer logic — so the orchestrator reuses their full plan-gating,
43
+ * atomicity, and rollback contracts without re-implementing any of it.
44
+ */
45
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
46
+ import { join } from "node:path";
47
+ import { performance } from "node:perf_hooks";
48
+ import { deleteFiles } from "../tools/delete_files.js";
49
+ import { MAX_FILES_PER_CALL, writeFiles } from "../tools/write_files.js";
50
+ import { readAnchor, writeAnchor } from "./anchor.js";
51
+ // ─── Native sentinel (AC2) ───────────────────────────────────────────────────
52
+ /** Kit-root-relative path of genie's recompile fence sentinel (native surface). */
53
+ export const RECOMPILE_SENTINEL_PATH = ".genie/recompile";
54
+ /**
55
+ * The exact sentinel body (AC2). genie's native `{"by":"genie"}` marker — NOT
56
+ * the Anthropic `_ds_recompile` interop shape (CLAUDE.md hard rule 1). Kept as
57
+ * a compact single-line literal so a byte-for-byte assertion is trivial and the
58
+ * re-arm in step 4 reproduces it exactly.
59
+ */
60
+ export const RECOMPILE_SENTINEL_BODY = '{"by":"genie"}';
61
+ // ─── runAtomicSync (AC1–AC7, AC9) ────────────────────────────────────────────
62
+ /**
63
+ * Run the 5-step atomic sync sequence. Stops on the first failing step (AC7),
64
+ * leaving `.genie/sync.json` unwritten so the next sync's diff repairs the
65
+ * half-write; a not-found delete inside step 3 is NOT a failure (AC4). Every
66
+ * attempted step emits a {@link StepEvent} (AC9).
67
+ */
68
+ export async function runAtomicSync(deps, args) {
69
+ const { store, projectRoot } = deps;
70
+ const events = [];
71
+ try {
72
+ // Step 1 — fence the tree with the recompile sentinel FIRST (AC2).
73
+ await runStep(1, events, () => writeSentinel(projectRoot));
74
+ // Step 2 — content writes, chunked ≤ 256/call (AC3).
75
+ await runStep(2, events, () => runWrites(store, args.planId, args.writes));
76
+ // Step 3 — deletes (AC4); a not-found delete does not throw (M1-09 AC5).
77
+ await runStep(3, events, () => runDeletes(store, args.planId, args.deletes));
78
+ // Step 4 — re-arm the sentinel so it fences right up to the anchor (AC5).
79
+ await runStep(4, events, () => writeSentinel(projectRoot));
80
+ // Step 5 — write `.genie/sync.json` LAST as the completion proof (AC6).
81
+ await runStep(5, events, () => writeFinalAnchor(projectRoot, args));
82
+ return { ok: true, events };
83
+ }
84
+ catch (error) {
85
+ // The failing step pushed its own `ok: false` event before rethrowing, so
86
+ // the last event's step IS the failed step (AC7). `?? 1` is unreachable
87
+ // (a throw always follows at least one pushed event) but keeps the type
88
+ // total without a non-null assertion.
89
+ const failedStep = events[events.length - 1]?.step ?? 1;
90
+ return {
91
+ ok: false,
92
+ failedStep,
93
+ error: error instanceof Error ? error : new Error(String(error)),
94
+ events,
95
+ };
96
+ }
97
+ }
98
+ // ─── detectResumeStep (AC8) ──────────────────────────────────────────────────
99
+ /**
100
+ * Inspect a kit root and report where the NEXT sync should resume from (AC8):
101
+ *
102
+ * - `2` — sentinel present, anchor ABSENT: a prior sync fenced the tree
103
+ * (step 1) then crashed before the anchor landed (steps 2–4). The
104
+ * tree is a half-write; resume from step 2 to re-apply
105
+ * writes/deletes and re-arm before writing the anchor.
106
+ * - `1` — sentinel AND anchor both absent: a fresh, never-synced kit.
107
+ * Start a normal sync from step 1.
108
+ * - `null` — the anchor is present (a prior sync completed cleanly): there is
109
+ * no partial-write to resume; the next sync is a NEW plan from step
110
+ * 1 with fresh inputs, not a resume.
111
+ *
112
+ * Never mutates the tree. Propagates {@link readAnchor}'s `AnchorParseError`
113
+ * for a corrupt-but-present anchor — a corrupt anchor is a real operability
114
+ * problem, not a "nothing to resume" signal (same reasoning `readAnchor` itself
115
+ * applies). Likewise propagates any non-"missing" error from the sentinel
116
+ * `stat` (e.g. `EACCES` on `.genie/`) rather than treating it as "sentinel
117
+ * absent" — see {@link pathExists}.
118
+ */
119
+ export async function detectResumeStep(projectRoot) {
120
+ const sentinelPresent = await pathExists(join(projectRoot, RECOMPILE_SENTINEL_PATH));
121
+ const anchor = await readAnchor(projectRoot); // null when absent (AC8 of M3-06)
122
+ if (anchor !== null)
123
+ return null; // clean — last sync completed
124
+ return sentinelPresent ? 2 : 1; // half-write → step 2; fresh → step 1
125
+ }
126
+ // ─── Step bodies ─────────────────────────────────────────────────────────────
127
+ /**
128
+ * Write (or re-arm) the native recompile sentinel fs-directly under
129
+ * `projectRoot`. Idempotent — steps 1 and 4 both call it and the second write
130
+ * simply reproduces the same bytes. Not plan-gated: `.genie/recompile` is
131
+ * genie's own bookkeeping, never user content.
132
+ */
133
+ async function writeSentinel(projectRoot) {
134
+ await mkdir(join(projectRoot, ".genie"), { recursive: true });
135
+ await writeFile(join(projectRoot, RECOMPILE_SENTINEL_PATH), RECOMPILE_SENTINEL_BODY, "utf-8");
136
+ }
137
+ /**
138
+ * Step 2 — apply all content writes, chunked into ≤ `MAX_FILES_PER_CALL`
139
+ * batches, one plan-gated `writeFiles` call each (AC3). An empty `writes` array
140
+ * yields zero batches → zero calls → a clean no-op (the step still emits its
141
+ * event). A `WriteFailedError` / `PathOutsidePlanError` from any batch
142
+ * propagates and fails the step.
143
+ */
144
+ async function runWrites(store, planId, writes) {
145
+ for (const batch of chunk(writes, MAX_FILES_PER_CALL)) {
146
+ await writeFiles(store, { planId, files: batch });
147
+ }
148
+ }
149
+ /**
150
+ * Step 3 — apply all deletes in one plan-gated `delete_files` call (AC4).
151
+ *
152
+ * Skipped entirely when there is nothing to delete: `delete_files`' args schema
153
+ * requires `paths` to be non-empty (`.min(1)`), so calling it with `[]` would
154
+ * be an `InvalidArguments` error rather than a no-op. A not-found path is
155
+ * handled INSIDE `deleteFiles` (returned in `notFoundPaths`, never thrown), so
156
+ * it does not fail this step (AC7's not-found exception). Only a real error
157
+ * (out-of-plan path, unlink failure) propagates.
158
+ */
159
+ async function runDeletes(store, planId, deletes) {
160
+ if (deletes.length === 0)
161
+ return;
162
+ await deleteFiles(store, { planId, paths: deletes });
163
+ }
164
+ /**
165
+ * Step 5 — assemble M3-06's `PlanResult` from the sync inputs and write
166
+ * `.genie/sync.json` LAST via {@link writeAnchor} (AC6). `writeAnchor` hashes
167
+ * only source/render-extension paths and does its own temp-file + rename commit,
168
+ * so this step is the atomic completion proof.
169
+ */
170
+ async function writeFinalAnchor(projectRoot, args) {
171
+ const planResult = {
172
+ writes: await resolveAnchorWrites(projectRoot, args.writes),
173
+ verified: args.verified ?? [],
174
+ };
175
+ await writeAnchor(projectRoot, planResult);
176
+ }
177
+ /**
178
+ * Resolve each {@link WriteInput} to the `{ path, content }` shape
179
+ * `writeAnchor` hashes.
180
+ *
181
+ * ── Committed bytes, not the plan's local source (Copilot review) ───────────
182
+ * Inline `data` resolves for free — utf-8 decodes to the string, base64 to a
183
+ * `Buffer` — the exact bytes already handed to `writeFiles` in step 2, no disk
184
+ * read at all.
185
+ *
186
+ * A `localPath` write is different: step 2 STREAMS it straight from the
187
+ * plan's `localDir` into the kit tree (`store/local.ts`'s `streamCopy`, via
188
+ * `createReadStream` — never buffered here, by design, so a large upload
189
+ * never fully loads into memory). This function used to re-read that SAME
190
+ * `localDir` source file a second time, during step 5, to compute its hash —
191
+ * a TOCTOU hazard, since the plan's `localDir` is a directory the CALLER
192
+ * controls and may keep writing to for the lifetime of a sync, so the bytes
193
+ * step 5 would hash could silently diverge from the bytes step 2 actually
194
+ * streamed into the kit. It also contradicted `sync/anchor.ts`'s own
195
+ * contract that hashes must come from "the exact bytes that landed, not
196
+ * re-read from disk after the fact."
197
+ *
198
+ * The fix: read the file back from `projectRoot` — the KIT destination step
199
+ * 2 just committed it to (`join(projectRoot, write.path)`) — never from the
200
+ * plan's `localDir` source. `projectRoot` is guaranteed (by `SyncDeps`'s own
201
+ * contract, see above) to be the exact directory `store.writeFiles` resolved
202
+ * the write to, so this hashes precisely the bytes the kit tree now holds —
203
+ * the same tree `writeAnchor` is meant to vouch for — rather than a second,
204
+ * independently-timed read of a directory outside genie's control.
205
+ *
206
+ * A write with neither `data` nor `localPath` is unreachable here (step 2's
207
+ * `write_files` would have rejected it, and step 5 only runs once step 2
208
+ * passed); it is defensively hashed as empty rather than throwing.
209
+ */
210
+ async function resolveAnchorWrites(projectRoot, writes) {
211
+ const resolved = [];
212
+ for (const write of writes) {
213
+ let content;
214
+ if (write.data !== undefined) {
215
+ content = write.encoding === "base64" ? Buffer.from(write.data, "base64") : write.data;
216
+ }
217
+ else if (write.localPath !== undefined) {
218
+ content = await readFile(join(projectRoot, write.path));
219
+ }
220
+ else {
221
+ content = "";
222
+ }
223
+ resolved.push({ path: write.path, content });
224
+ }
225
+ return resolved;
226
+ }
227
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
228
+ /**
229
+ * Time one step, push its {@link StepEvent}, and either return the step's value
230
+ * or re-throw after recording the failure (AC9). The re-throw is what
231
+ * `runAtomicSync`'s outer catch turns into the stop-on-first-failure result — so
232
+ * the failing step's event is always recorded before the sequence unwinds.
233
+ */
234
+ async function runStep(step, events, fn) {
235
+ const start = performance.now();
236
+ try {
237
+ const value = await fn();
238
+ events.push({ step, ok: true, ms: performance.now() - start });
239
+ return value;
240
+ }
241
+ catch (error) {
242
+ events.push({ step, ok: false, ms: performance.now() - start });
243
+ throw error;
244
+ }
245
+ }
246
+ /** Split `items` into consecutive slices of at most `size` (AC3 chunking). */
247
+ function chunk(items, size) {
248
+ const batches = [];
249
+ for (let index = 0; index < items.length; index += size) {
250
+ batches.push(items.slice(index, index + size));
251
+ }
252
+ return batches;
253
+ }
254
+ /**
255
+ * True if `path` exists (any node type); false if it (or a parent directory
256
+ * component) genuinely does not exist. Any OTHER `stat` failure — EACCES,
257
+ * EPERM, a transient I/O error — propagates rather than being misread as
258
+ * "absent" (Copilot review on this PR): silently mapping every error to
259
+ * `false` could make {@link detectResumeStep} report "fresh, start from step
260
+ * 1" for a sentinel that is actually present-but-unreadable, masking a real
261
+ * operability problem instead of surfacing it. Mirrors `./anchor.ts`'s
262
+ * `readAnchor` (and `store/local.ts`'s identical helper), which apply the
263
+ * same ENOENT/ENOTDIR-only rule for the same reason.
264
+ */
265
+ async function pathExists(path) {
266
+ try {
267
+ await stat(path);
268
+ return true;
269
+ }
270
+ catch (error) {
271
+ if (isMissingPathError(error))
272
+ return false;
273
+ throw error;
274
+ }
275
+ }
276
+ /**
277
+ * ENOENT (missing file) and ENOTDIR (a parent path component is a file, not a
278
+ * directory) both mean "not there". Duplicated locally rather than imported
279
+ * from `./anchor.ts` / `store/local.ts` — each of those already carries its
280
+ * own copy of this exact one-liner, matching this repo's established
281
+ * per-module-owns-its-guard convention for this helper.
282
+ */
283
+ function isMissingPathError(error) {
284
+ return (error instanceof Error &&
285
+ "code" in error &&
286
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
287
+ }
288
+ //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/sync/orchestrator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP tool: bind_kit (M1-20).
3
+ *
4
+ * Attaches a UI kit to a project and optionally marks it the default for
5
+ * screen generation (M1-21, `conjure_screen`, resolves against this).
6
+ *
7
+ * Input: { projectId: string, kitId: string, default?: boolean }
8
+ * Output: ProjectSummary (same shape `list_projects`/`get_project` return)
9
+ */
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import type { BindKitArgs, ProjectSummary } from "./create_project.js";
12
+ export declare const BIND_KIT_TOOL_NAME = "mcp__genie__bind_kit";
13
+ /**
14
+ * Narrow port `bind_kit` depends on — anything that can bind a kit to a
15
+ * project and return its updated summary. `ProjectStore` (from
16
+ * `create_project.ts`) satisfies this structurally, matching the
17
+ * `ProjectGetStore`/`ProjectListStore` pattern already used by sibling tools.
18
+ */
19
+ export interface ProjectBindKitStore {
20
+ bindKit(args: BindKitArgs): Promise<ProjectSummary>;
21
+ }
22
+ /**
23
+ * Validate args and bind the kit. Exported standalone (bypassing the MCP
24
+ * transport) so programmatic/direct callers get the same validation
25
+ * defense-in-depth as `get_project`'s `getProject()` and `get_kit`'s `getKit()`.
26
+ */
27
+ export declare function bindKit(store: ProjectBindKitStore, args: unknown): Promise<ProjectSummary>;
28
+ export declare function registerBindKitTool(server: McpServer, store: ProjectBindKitStore): void;
29
+ //# sourceMappingURL=bind_kit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind_kit.d.ts","sourceRoot":"","sources":["../../src/tools/bind_kit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGvE,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAwBzD;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAGhG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI,CA8CvF"}
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { PROJECT_ID_PATTERN, ProjectStoreError, projectSummaryShape } from "./create_project.js";
3
+ import { KIT_ID_PATTERN } from "./get_kit.js";
4
+ export const BIND_KIT_TOOL_NAME = "mcp__genie__bind_kit";
5
+ const projectIdSchema = z
6
+ .string()
7
+ .regex(PROJECT_ID_PATTERN, "projectId must be a 3-64 character slug containing only lowercase letters, numbers, and hyphens.");
8
+ const kitIdSchema = z
9
+ .string()
10
+ .regex(KIT_ID_PATTERN, "kitId must be a 3-64 character slug containing only lowercase letters, numbers, and hyphens.");
11
+ const bindKitArgsSchema = z
12
+ .object({
13
+ projectId: projectIdSchema,
14
+ kitId: kitIdSchema,
15
+ default: z.boolean().optional(),
16
+ })
17
+ .strict();
18
+ /**
19
+ * Validate args and bind the kit. Exported standalone (bypassing the MCP
20
+ * transport) so programmatic/direct callers get the same validation
21
+ * defense-in-depth as `get_project`'s `getProject()` and `get_kit`'s `getKit()`.
22
+ */
23
+ export async function bindKit(store, args) {
24
+ const parsed = bindKitArgsSchema.parse(args);
25
+ return store.bindKit(parsed);
26
+ }
27
+ export function registerBindKitTool(server, store) {
28
+ server.registerTool(BIND_KIT_TOOL_NAME, {
29
+ title: "Bind kit",
30
+ description: "Attach a UI kit to a workspace or blueprint project, optionally marking it the " +
31
+ "default kit for screen generation. Binding the same kit twice updates the existing " +
32
+ "binding in place. Setting default: true clears default status from every other " +
33
+ "binding on the project. Typically precedes conjure_screen, which resolves its kit " +
34
+ "from these bindings when no explicit kitId is given.",
35
+ inputSchema: {
36
+ projectId: projectIdSchema,
37
+ kitId: kitIdSchema,
38
+ default: z.boolean().optional(),
39
+ },
40
+ outputSchema: projectSummaryShape,
41
+ }, async (args) => {
42
+ try {
43
+ const result = await bindKit(store, args);
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify(result) }],
46
+ structuredContent: result,
47
+ };
48
+ }
49
+ catch (error) {
50
+ if (error instanceof ProjectStoreError) {
51
+ return {
52
+ isError: true,
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: JSON.stringify({
57
+ code: error.code,
58
+ message: error.message,
59
+ ...(error.projectId ? { projectId: error.projectId } : {}),
60
+ ...(error.kitId ? { kitId: error.kitId } : {}),
61
+ }),
62
+ },
63
+ ],
64
+ };
65
+ }
66
+ throw error;
67
+ }
68
+ });
69
+ }
70
+ //# sourceMappingURL=bind_kit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind_kit.js","sourceRoot":"","sources":["../../src/tools/bind_kit.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEjG,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD,MAAM,eAAe,GAAG,CAAC;KACtB,MAAM,EAAE;KACR,KAAK,CACJ,kBAAkB,EAClB,kGAAkG,CACnG,CAAC;AAEJ,MAAM,WAAW,GAAG,CAAC;KAClB,MAAM,EAAE;KACR,KAAK,CACJ,cAAc,EACd,8FAA8F,CAC/F,CAAC;AAEJ,MAAM,iBAAiB,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,SAAS,EAAE,eAAe;IAC1B,KAAK,EAAE,WAAW;IAClB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC;KACD,MAAM,EAAE,CAAC;AAYZ;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAA0B,EAAE,IAAa;IACrE,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAiB,EAAE,KAA0B;IAC/E,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,KAAK,EAAE,UAAU;QACjB,WAAW,EACT,iFAAiF;YACjF,qFAAqF;YACrF,iFAAiF;YACjF,oFAAoF;YACpF,sDAAsD;QACxD,WAAW,EAAE;YACX,SAAS,EAAE,eAAe;YAC1B,KAAK,EAAE,WAAW;YAClB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;SAChC;QACD,YAAY,EAAE,mBAAmB;KAClC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;QACb,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC1C,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClE,iBAAiB,EAAE,MAAM;aAC1B,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;gBACvC,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAe;4BACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gCACnB,IAAI,EAAE,KAAK,CAAC,IAAI;gCAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gCACtB,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gCAC1D,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;6BAC/C,CAAC;yBACH;qBACF;iBACF,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,141 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { type ValidatedComponent } from "../llm/schema.js";
4
+ import { type LoadedPrompt } from "../llm/prompts.js";
5
+ import { type ChatCompletionFn, type UsageInfo } from "../llm/component-response.js";
6
+ import { type GeneratedFileWithEncoding } from "../llm/generated-files.js";
7
+ import { type Framework } from "../framework/interface.js";
8
+ export type { ChatCompletionFn, UsageInfo } from "../llm/component-response.js";
9
+ export declare const CONJURE_TOOL_NAME = "mcp__genie__conjure";
10
+ /** Target framework for the generated component (AC2/AC3). Re-exported from the
11
+ * M2-08 adapter module so the tool contract and the adapter registry share one
12
+ * source of truth (`framework/interface.ts#FRAMEWORKS`). */
13
+ export declare const CONJURE_FRAMEWORKS: readonly ["react", "vue", "html"];
14
+ export type ConjureFramework = Framework;
15
+ /** Default framework + model routing alias (AC3). `design-default` is resolved
16
+ * to a concrete provider model by the configured endpoint/gateway (M2-05). */
17
+ export declare const DEFAULT_FRAMEWORK: ConjureFramework;
18
+ export declare const DEFAULT_MODEL = "design-default";
19
+ /** `refUrl` bodies larger than this are warned about (AC7) and truncated before
20
+ * inlining, so a giant page can't blow the request's token budget. */
21
+ export declare const REF_URL_WARN_BYTES: number;
22
+ export type AddressLookup = (hostname: string) => Promise<readonly {
23
+ address: string;
24
+ family: number;
25
+ }[]>;
26
+ export declare function isSafeRefUrl(raw: string): boolean;
27
+ /**
28
+ * Public boolean form of {@link resolveSafeAddress} kept for the existing
29
+ * test surface / any external reuse (M6-03 follow-up). A hostname is "safe"
30
+ * if at least one resolved address is not private/loopback/link-local.
31
+ */
32
+ export declare function isSafeResolvedAddress(hostname: string, lookupAddresses?: AddressLookup): Promise<boolean>;
33
+ /**
34
+ * Fetch a single URL whose hostname has already been resolved to a
35
+ * known-safe `address`/`family` (M6-03 follow-up, TOCTOU fix): rather than
36
+ * re-validating a hostname and then letting the HTTP client perform its own,
37
+ * second, independent DNS resolution at connect time — the exact gap an
38
+ * attacker-controlled DNS name (a "rebinding" host that answers safely to
39
+ * the guard and privately to the real connection) can exploit — this pins
40
+ * the *already-validated* address into the connection via undici's
41
+ * `Agent({ connect: { lookup } })` override, so validation and connection
42
+ * are guaranteed to use the identical address. Redirects are fetched with
43
+ * `redirect: "manual"` and returned to the caller un-followed; the caller
44
+ * (`safeFetchFollowingRedirects`) re-runs full validation (schema-level
45
+ * `isSafeRefUrl` + resolved-address check) on every redirect target before
46
+ * following it, so a public URL cannot use a redirect to smuggle a request to
47
+ * a private target past the guard.
48
+ */
49
+ export declare function fetchWithPinnedAddress(url: string, address: string, family: 4 | 6, timeoutMs?: number): Promise<ReferenceFetchResponse & {
50
+ bodyTruncated: boolean;
51
+ observedBodyBytes: number;
52
+ }>;
53
+ declare const conjureArgsSchema: z.ZodObject<{
54
+ kitId: z.ZodString;
55
+ kit: z.ZodString;
56
+ prompt: z.ZodString;
57
+ group: z.ZodOptional<z.ZodString>;
58
+ refImageDataUrl: z.ZodOptional<z.ZodString>;
59
+ refUrl: z.ZodOptional<z.ZodString>;
60
+ framework: z.ZodDefault<z.ZodEnum<{
61
+ react: "react";
62
+ vue: "vue";
63
+ html: "html";
64
+ }>>;
65
+ model: z.ZodDefault<z.ZodString>;
66
+ }, z.core.$strict>;
67
+ export type ConjureArgs = z.infer<typeof conjureArgsSchema>;
68
+ /**
69
+ * What `conjure` returns (AC9): the validated component — its name, resolved
70
+ * group, file set, and manifest metadata — plus token `usage` for the caller's
71
+ * accounting. Deliberately does NOT include anything about having *written* the
72
+ * files: generation is pure (AC9).
73
+ */
74
+ export interface ConjureResult extends Record<string, unknown> {
75
+ componentName: string;
76
+ group: string;
77
+ files: GeneratedFileWithEncoding[];
78
+ manifestEntry: ValidatedComponent["manifestEntry"];
79
+ usage: UsageInfo;
80
+ }
81
+ /** Injectable URL-fetch seam for tests (AC7). When omitted, production uses the
82
+ * DNS-validated, address-pinned Undici path rather than global `fetch`.
83
+ * `headers` is optional so existing test doubles that only stub `{ ok, status, text }`
84
+ * keep compiling; the redirect-following logic treats a missing `headers` on a
85
+ * 3xx response as "no Location to follow" and stops rather than throwing. */
86
+ export interface ReferenceFetchResponse {
87
+ ok: boolean;
88
+ status: number;
89
+ headers?: {
90
+ get(name: string): string | null;
91
+ };
92
+ bodyTruncated?: boolean;
93
+ observedBodyBytes?: number;
94
+ text(): Promise<string>;
95
+ }
96
+ export type FetchFn = (url: string) => Promise<ReferenceFetchResponse>;
97
+ export type PinnedFetchFn = typeof fetchWithPinnedAddress;
98
+ export interface ConjureDeps {
99
+ chat?: ChatCompletionFn;
100
+ fetchImpl?: FetchFn;
101
+ /** Pinned socket seam for deterministic production-path tests. */
102
+ fetchPinnedAddress?: PinnedFetchFn;
103
+ /** DNS seam for deterministic SSRF tests. Production uses `dns.lookup`. */
104
+ lookupAddresses?: AddressLookup;
105
+ /** Overall DNS + redirect/fetch deadline seam. Production defaults to 30 seconds. */
106
+ refUrlTimeoutMs?: number;
107
+ /** Prompt loader override (tests). Defaults to the real versioned loader (AC5). */
108
+ loadSystemPrompt?: () => LoadedPrompt;
109
+ }
110
+ /** Typed failure surfaced to the tool boundary (mapped to an MCP error result).
111
+ * One code: an empty reply is just one way output can be invalid, so it folds
112
+ * into `ERR_LLM_OUTPUT_INVALID` rather than being its own code (Copilot review —
113
+ * keep the public error surface to codes this file actually throws). */
114
+ export declare class ConjureError extends Error {
115
+ readonly code: "ERR_LLM_OUTPUT_INVALID";
116
+ readonly details?: Record<string, unknown> | undefined;
117
+ constructor(code: "ERR_LLM_OUTPUT_INVALID", message: string, details?: Record<string, unknown> | undefined);
118
+ }
119
+ /**
120
+ * Truncate `text` so its UTF-8 encoding is at most `maxBytes` bytes, cutting on
121
+ * a codepoint boundary (Copilot review: `String.slice` cuts UTF-16 code units,
122
+ * so a non-ASCII page could still blow the byte cap). We encode, slice the byte
123
+ * buffer at the cap, then drop any trailing bytes that form a partial multi-byte
124
+ * sequence so the decoded result carries no U+FFFD replacement char and is
125
+ * guaranteed ≤ `maxBytes`.
126
+ */
127
+ export declare function truncateUtf8(text: string, maxBytes: number): string;
128
+ /**
129
+ * Generate one component. Pure (AC9): validates + returns, never writes.
130
+ *
131
+ * Flow: resolve args → optionally fetch `refUrl` (AC7) → run the shared
132
+ * request/validate/retry loop (build messages +vision AC6, call the endpoint
133
+ * with the json_schema response_format AC4, validate against COMPONENT_SCHEMA
134
+ * AC8, retry ONCE on failure) → return
135
+ * `{ componentName, group, files, manifestEntry, usage }` (AC9) and log a
136
+ * per-call line (AC10). The loop itself lives in `component-response.ts`, shared
137
+ * with `refine` (M2-04).
138
+ */
139
+ export declare function conjure(deps: ConjureDeps, args: unknown): Promise<ConjureResult>;
140
+ export declare function registerConjureTool(server: McpServer, deps?: ConjureDeps): void;
141
+ //# sourceMappingURL=conjure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conjure.d.ts","sourceRoot":"","sources":["../../src/tools/conjure.ts"],"names":[],"mappings":"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"}