@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,655 @@
1
+ /**
2
+ * MCP tool: conjure (M2-03 · DRO-250) — genie's headline verb.
3
+ *
4
+ * Takes a natural-language prompt (plus an optional reference image / page) and
5
+ * returns a single UI component as the file set defined by `COMPONENT_SCHEMA`
6
+ * (M2-02). This is the real generation path: it calls the operator's configured
7
+ * OpenAI-compatible endpoint through the M2-01 client, demanding structured
8
+ * output via `response_format: { type: "json_schema", … }`, validates the reply
9
+ * against `COMPONENT_SCHEMA` with Ajv, and retries once with the validation
10
+ * error fed back on failure.
11
+ *
12
+ * `conjure` is deliberately **pure generation** (AC9): it never calls
13
+ * `write_files`. It hands the caller a validated component; committing it to a
14
+ * kit is a separate, plan-gated step the caller owns. That keeps generation
15
+ * free of side effects and testable without a store.
16
+ *
17
+ * Two DISTINCT retry mechanisms are in play here, at different layers — don't
18
+ * conflate them when reading this file:
19
+ * 1. The "retries once on validation failure" above is the shared
20
+ * `component-response.ts` harness's schema-repair loop (AC8): a
21
+ * structurally-valid-but-wrong-shape reply gets one more attempt with the
22
+ * Ajv error fed back into the prompt.
23
+ * 2. Each of those (up to 2) calls individually goes through M2-06's
24
+ * `withRetry(createChatCompletion)` (DRO-253, see `defaultChatCompletion`
25
+ * below) for transient network/429/5xx failures — invisible to this
26
+ * file's own logic and to the shared harness, handled entirely inside the
27
+ * `chat` seam.
28
+ *
29
+ * ── Import-time safety ────────────────────────────────────────────────────────
30
+ * `../llm/client.js` constructs its `llmClient` singleton eagerly at module load
31
+ * and throws `MissingLLMConfigError` when `GENIE_LLM_*` env vars are unset (M2-01
32
+ * AC1/AC2). If this module imported it *statically*, merely building the server
33
+ * (`createServer()`) — which CI does with no LLM endpoint configured — would
34
+ * throw. So the client is a **type-only** import here (erased by
35
+ * `verbatimModuleSyntax`), and the default runtime path reaches it via a lazy
36
+ * `await import(...)` inside `defaultChatCompletion`, touched only when an actual
37
+ * `conjure` call runs. Tests inject their own `chat` and never load the client.
38
+ * (`../llm/retry.js`'s `withRetry`, by contrast, has no such eager side effect —
39
+ * it's a plain higher-order function — so it's imported statically below and
40
+ * applied inside that same lazy seam.)
41
+ *
42
+ * §6 honest uncertainty (from the issue): the exact prompt shape / generation
43
+ * loop is unspecified R&D. The system prompt (`prompts/generate-component.system.md`,
44
+ * versioned by git-blob hash, AC5) is where that iteration lives; this file is the
45
+ * stable request/validate/retry harness around it.
46
+ */
47
+ import { lookup as dnsLookup } from "node:dns/promises";
48
+ import ipaddr from "ipaddr.js";
49
+ import { Agent, fetch as undiciFetch } from "undici";
50
+ import { z } from "zod";
51
+ import {} from "../llm/schema.js";
52
+ import { GENERATE_COMPONENT_SYSTEM_PROMPT_FILE, loadPrompt, } from "../llm/prompts.js";
53
+ // The shared request/validate/retry harness (M2-03/M2-04). The Ajv compile,
54
+ // fence-stripper, response_format envelope, retry-feedback wording, and the
55
+ // two-attempt loop all live here so `conjure` and `refine` share ONE copy —
56
+ // see llm/component-response.ts's header.
57
+ import { appendRetryFeedback, logStderr, runComponentGeneration, } from "../llm/component-response.js";
58
+ // Value import (not type-only): `retry.ts` has no import-time side effects —
59
+ // unlike `client.ts`, it never touches `GENIE_LLM_*` env or constructs
60
+ // anything eagerly at module load — so wrapping `defaultChatCompletion`
61
+ // below in `withRetry` is safe to do statically (Copilot review on PR #126 /
62
+ // DRO-253: this file's docstring claimed conjure "calls the endpoint through
63
+ // the M2-01 client" via `withRetry(createChatCompletion)`, but no production
64
+ // call site actually applied the wrapper — every future generation verb
65
+ // would have silently needed to remember to wrap it manually).
66
+ import { withRetry } from "../llm/retry.js";
67
+ import { normalizeGeneratedFiles, validateGeneratedBinaryContent, } from "../llm/generated-files.js";
68
+ // Framework adapter seam (M2-08 · DRO-255). `conjure` picks the adapter from its
69
+ // `framework` input (AC4) and reads the adapter's `promptDirective` — the one
70
+ // framework-specific bit generation carries. `interface.js` has no heavy imports
71
+ // at module top (esbuild/ts-morph load lazily inside the React adapter, reached
72
+ // only via `getAdapter`'s dynamic import), so this static import is server-build
73
+ // safe. The framework enum + default live in the adapter module as the single
74
+ // source of truth; `conjure` re-exports them under their established names.
75
+ import { FRAMEWORKS, DEFAULT_FRAMEWORK as ADAPTER_DEFAULT_FRAMEWORK, getAdapter, } from "../framework/interface.js";
76
+ import { KIT_ID_PATTERN } from "./get_kit.js";
77
+ export const CONJURE_TOOL_NAME = "mcp__genie__conjure";
78
+ /** Target framework for the generated component (AC2/AC3). Re-exported from the
79
+ * M2-08 adapter module so the tool contract and the adapter registry share one
80
+ * source of truth (`framework/interface.ts#FRAMEWORKS`). */
81
+ export const CONJURE_FRAMEWORKS = FRAMEWORKS;
82
+ /** Default framework + model routing alias (AC3). `design-default` is resolved
83
+ * to a concrete provider model by the configured endpoint/gateway (M2-05). */
84
+ export const DEFAULT_FRAMEWORK = ADAPTER_DEFAULT_FRAMEWORK;
85
+ export const DEFAULT_MODEL = "design-default";
86
+ /** `refUrl` bodies larger than this are warned about (AC7) and truncated before
87
+ * inlining, so a giant page can't blow the request's token budget. */
88
+ export const REF_URL_WARN_BYTES = 1024 * 1024; // 1 MB
89
+ const REF_URL_FETCH_TIMEOUT_MS = 30_000;
90
+ const defaultAddressLookup = (hostname) => dnsLookup(hostname, { all: true, verbatim: true });
91
+ function parseIpLiteral(host) {
92
+ const unbracketed = host.startsWith("[") ? host.replace(/^\[|\]$/g, "") : host;
93
+ try {
94
+ return ipaddr.process(unbracketed);
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ /** Return a canonical address only for globally routable unicast IPs. Using a
101
+ * real IP parser avoids textual-prefix gaps such as IPv4-mapped loopback
102
+ * (`::ffff:127.0.0.1`), the full IPv6 link-local `/10`, and multicast or
103
+ * unspecified addresses. */
104
+ function asSafeAddress(host) {
105
+ const parsed = parseIpLiteral(host);
106
+ if (parsed === undefined || parsed.range() !== "unicast")
107
+ return undefined;
108
+ return {
109
+ address: parsed.toString(),
110
+ family: parsed.kind() === "ipv4" ? 4 : 6,
111
+ };
112
+ }
113
+ export function isSafeRefUrl(raw) {
114
+ let url;
115
+ try {
116
+ url = new URL(raw);
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ if (url.protocol !== "http:" && url.protocol !== "https:")
122
+ return false;
123
+ if (url.username !== "" || url.password !== "")
124
+ return false;
125
+ const host = url.hostname.toLowerCase();
126
+ // Named internal hosts.
127
+ if (host === "localhost" || host === "ip6-localhost" || host.endsWith(".localhost"))
128
+ return false;
129
+ if (host === "" || host === "[::1]" || host === "::1")
130
+ return false;
131
+ // Any literal must be globally routable unicast. Hostnames are resolved and
132
+ // subjected to the same rule immediately before the pinned connection.
133
+ if (parseIpLiteral(host) !== undefined && asSafeAddress(host) === undefined)
134
+ return false;
135
+ return true;
136
+ }
137
+ /**
138
+ * Resolve `hostname` and return only the addresses that pass the
139
+ * private/loopback/link-local/CGNAT check, or `undefined` if none do (all
140
+ * addresses unsafe, or the lookup failed). Exported so both the fetch-time
141
+ * guard and its regression tests share one resolution path.
142
+ *
143
+ * DNS-rebinding guard: {@link isSafeRefUrl} is a *syntactic*
144
+ * pre-filter on the hostname as typed — it cannot catch an attacker-controlled
145
+ * DNS name that resolves to a private/loopback/link-local address (the
146
+ * classic SSRF/DNS-rebinding bypass: `evil.example.com` passes the hostname
147
+ * check today and answers `127.0.0.1` at fetch time).
148
+ */
149
+ async function resolveSafeAddress(hostname, lookupAddresses = defaultAddressLookup) {
150
+ const literal = parseIpLiteral(hostname);
151
+ if (literal !== undefined)
152
+ return asSafeAddress(hostname);
153
+ try {
154
+ const results = await lookupAddresses(hostname);
155
+ for (const { address } of results) {
156
+ const safe = asSafeAddress(address);
157
+ if (safe !== undefined)
158
+ return safe;
159
+ }
160
+ return undefined;
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
166
+ /**
167
+ * Public boolean form of {@link resolveSafeAddress} kept for the existing
168
+ * test surface / any external reuse (M6-03 follow-up). A hostname is "safe"
169
+ * if at least one resolved address is not private/loopback/link-local.
170
+ */
171
+ export async function isSafeResolvedAddress(hostname, lookupAddresses = defaultAddressLookup) {
172
+ return (await resolveSafeAddress(hostname, lookupAddresses)) !== undefined;
173
+ }
174
+ /**
175
+ * Fetch a single URL whose hostname has already been resolved to a
176
+ * known-safe `address`/`family` (M6-03 follow-up, TOCTOU fix): rather than
177
+ * re-validating a hostname and then letting the HTTP client perform its own,
178
+ * second, independent DNS resolution at connect time — the exact gap an
179
+ * attacker-controlled DNS name (a "rebinding" host that answers safely to
180
+ * the guard and privately to the real connection) can exploit — this pins
181
+ * the *already-validated* address into the connection via undici's
182
+ * `Agent({ connect: { lookup } })` override, so validation and connection
183
+ * are guaranteed to use the identical address. Redirects are fetched with
184
+ * `redirect: "manual"` and returned to the caller un-followed; the caller
185
+ * (`safeFetchFollowingRedirects`) re-runs full validation (schema-level
186
+ * `isSafeRefUrl` + resolved-address check) on every redirect target before
187
+ * following it, so a public URL cannot use a redirect to smuggle a request to
188
+ * a private target past the guard.
189
+ */
190
+ export async function fetchWithPinnedAddress(url, address, family, timeoutMs = REF_URL_FETCH_TIMEOUT_MS) {
191
+ const pinnedAgent = new Agent({
192
+ connect: {
193
+ lookup: (_hostname, options, callback) => {
194
+ if (options.all) {
195
+ callback(null, [{ address, family }]);
196
+ }
197
+ else {
198
+ callback(null, address, family);
199
+ }
200
+ },
201
+ },
202
+ });
203
+ try {
204
+ const response = await undiciFetch(url, {
205
+ redirect: "manual",
206
+ dispatcher: pinnedAgent,
207
+ signal: AbortSignal.timeout(timeoutMs),
208
+ headers: { "accept-encoding": "identity" },
209
+ });
210
+ const contentEncoding = response.headers.get("content-encoding")?.trim().toLowerCase();
211
+ if (contentEncoding && contentEncoding !== "identity") {
212
+ await response.body?.cancel();
213
+ throw new Error("Encoded reference responses are not supported");
214
+ }
215
+ const isRedirect = response.status >= 300 && response.status < 400;
216
+ if (isRedirect || !response.ok) {
217
+ await response.body?.cancel();
218
+ return {
219
+ status: response.status,
220
+ ok: response.ok,
221
+ headers: response.headers,
222
+ bodyTruncated: false,
223
+ observedBodyBytes: 0,
224
+ text: async () => "",
225
+ };
226
+ }
227
+ const reader = response.body?.getReader();
228
+ const chunks = [];
229
+ let observedBodyBytes = 0;
230
+ let bodyTruncated = false;
231
+ if (reader) {
232
+ try {
233
+ while (observedBodyBytes <= REF_URL_WARN_BYTES) {
234
+ const { done, value } = await reader.read();
235
+ if (done)
236
+ break;
237
+ const remaining = REF_URL_WARN_BYTES + 1 - observedBodyBytes;
238
+ chunks.push(Buffer.from(value.subarray(0, remaining)));
239
+ observedBodyBytes += Math.min(value.byteLength, remaining);
240
+ if (value.byteLength > remaining || observedBodyBytes > REF_URL_WARN_BYTES) {
241
+ bodyTruncated = true;
242
+ await reader.cancel();
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ finally {
248
+ reader.releaseLock();
249
+ }
250
+ }
251
+ const body = truncateUtf8Buffer(Buffer.concat(chunks), REF_URL_WARN_BYTES);
252
+ return {
253
+ status: response.status,
254
+ ok: response.ok,
255
+ headers: response.headers,
256
+ bodyTruncated,
257
+ observedBodyBytes,
258
+ text: async () => body,
259
+ };
260
+ }
261
+ finally {
262
+ await pinnedAgent.close();
263
+ }
264
+ }
265
+ const MAX_REF_URL_REDIRECTS = 5;
266
+ async function runBeforeDeadline(deadlineMs, operation) {
267
+ const remainingMs = deadlineMs - Date.now();
268
+ if (remainingMs <= 0)
269
+ throw new Error("Reference URL fetch timed out");
270
+ let timeout;
271
+ try {
272
+ return await Promise.race([
273
+ operation(remainingMs),
274
+ new Promise((_resolve, reject) => {
275
+ timeout = setTimeout(() => reject(new Error("Reference URL fetch timed out")), remainingMs);
276
+ }),
277
+ ]);
278
+ }
279
+ finally {
280
+ clearTimeout(timeout);
281
+ }
282
+ }
283
+ /**
284
+ * Fetch `refUrl`, following redirects manually and re-validating every hop
285
+ * (M6-03 follow-up — closes both blocking gaps from the security-audit
286
+ * re-review): each hop must (1) pass the schema-level `isSafeRefUrl` check —
287
+ * scheme + syntactic private-range rules — and (2) resolve to a non-private
288
+ * address, which is then pinned into the actual connection via
289
+ * {@link fetchWithPinnedAddress} so the validated and connected addresses can
290
+ * never diverge. A public URL that redirects to a private target (loopback,
291
+ * link-local, cloud metadata, or another rebinding host) is rejected at the
292
+ * first unsafe hop rather than silently followed by the HTTP client's own
293
+ * redirect handling.
294
+ */
295
+ async function safeFetchFollowingRedirects(fetchImpl, fetchPinnedAddress, startUrl, lookupAddresses, timeoutMs) {
296
+ const deadlineMs = Date.now() + timeoutMs;
297
+ let currentUrl = startUrl;
298
+ for (let hop = 0; hop <= MAX_REF_URL_REDIRECTS; hop++) {
299
+ if (!isSafeRefUrl(currentUrl)) {
300
+ logStderr({ event: "conjure.ref_url.ssrf_blocked", startUrl, hop });
301
+ return undefined;
302
+ }
303
+ const hostname = new URL(currentUrl).hostname;
304
+ const resolved = await runBeforeDeadline(deadlineMs, () => resolveSafeAddress(hostname, lookupAddresses));
305
+ if (!resolved) {
306
+ logStderr({ event: "conjure.ref_url.ssrf_blocked", refUrl: currentUrl, startUrl, hop });
307
+ return undefined;
308
+ }
309
+ const res = await runBeforeDeadline(deadlineMs, (remainingMs) => fetchImpl
310
+ ? fetchImpl(currentUrl)
311
+ : fetchPinnedAddress(currentUrl, resolved.address, resolved.family, remainingMs));
312
+ const status = res.status;
313
+ if (status >= 300 && status < 400) {
314
+ const location = res.headers?.get("location") ?? null;
315
+ if (!location) {
316
+ logStderr({ event: "conjure.ref_url.skip", refUrl: currentUrl, status });
317
+ return undefined;
318
+ }
319
+ currentUrl = new URL(location, currentUrl).toString();
320
+ continue;
321
+ }
322
+ return res;
323
+ }
324
+ logStderr({ event: "conjure.ref_url.skip", refUrl: startUrl, reason: "too_many_redirects" });
325
+ return undefined;
326
+ }
327
+ /** Reusable Zod schema for `refUrl`: a bounded http(s) URL that passes the SSRF
328
+ * guard. Shared by `conjureArgsSchema` and the MCP tool `inputSchema` so the
329
+ * declared tool contract and runtime validation cannot drift (Copilot review). */
330
+ const refUrlSchema = z
331
+ .string()
332
+ .url()
333
+ .max(2048)
334
+ .refine(isSafeRefUrl, {
335
+ message: "refUrl must be an http(s) URL to a public host — file:/ftp:/data: schemes and " +
336
+ "localhost/private/link-local addresses are rejected (SSRF guard).",
337
+ });
338
+ // ── Input schema (AC2) ────────────────────────────────────────────────────────
339
+ //
340
+ // { kitId, kit, prompt, group?, refImageDataUrl?, refUrl?, framework?, model? }.
341
+ // `kit` is the kit *description* the model matches against (tokens, primitives,
342
+ // house style) — a free-form string, generously bounded. `refImageDataUrl` must
343
+ // be an actual `data:` URL (AC6 attaches it as a vision input); a bare http URL
344
+ // belongs in `refUrl` (AC7) instead.
345
+ //
346
+ // Defined as ONE field map reused by both `conjureArgsSchema` (runtime parse)
347
+ // and the MCP tool `inputSchema` (declared contract), so the two can never drift
348
+ // (Copilot review: keep the tool-boundary schema aligned with runtime validation).
349
+ const conjureInputShape = {
350
+ kitId: z
351
+ .string()
352
+ .regex(KIT_ID_PATTERN, "kitId must be a 3-64 character slug of lowercase letters, numbers, and hyphens."),
353
+ kit: z.string().min(1).max(100_000),
354
+ prompt: z.string().min(3).max(8192),
355
+ group: z
356
+ .string()
357
+ .regex(/^[a-z0-9-]{1,32}$/, "group must be kebab-case: 1-32 chars of [a-z0-9-].")
358
+ .optional(),
359
+ refImageDataUrl: z
360
+ .string()
361
+ .max(8_000_000)
362
+ .regex(/^data:/, "refImageDataUrl must be a data: URL; use refUrl for an http(s) reference.")
363
+ .optional(),
364
+ refUrl: refUrlSchema.optional(),
365
+ framework: z.enum(CONJURE_FRAMEWORKS).default(DEFAULT_FRAMEWORK),
366
+ model: z.string().min(1).max(128).default(DEFAULT_MODEL),
367
+ };
368
+ const conjureArgsSchema = z.object(conjureInputShape).strict();
369
+ /** Typed failure surfaced to the tool boundary (mapped to an MCP error result).
370
+ * One code: an empty reply is just one way output can be invalid, so it folds
371
+ * into `ERR_LLM_OUTPUT_INVALID` rather than being its own code (Copilot review —
372
+ * keep the public error surface to codes this file actually throws). */
373
+ export class ConjureError extends Error {
374
+ code;
375
+ details;
376
+ constructor(code, message, details) {
377
+ super(message);
378
+ this.code = code;
379
+ this.details = details;
380
+ this.name = "ConjureError";
381
+ }
382
+ }
383
+ // ── Reference handling ────────────────────────────────────────────────────────
384
+ /**
385
+ * Truncate `text` so its UTF-8 encoding is at most `maxBytes` bytes, cutting on
386
+ * a codepoint boundary (Copilot review: `String.slice` cuts UTF-16 code units,
387
+ * so a non-ASCII page could still blow the byte cap). We encode, slice the byte
388
+ * buffer at the cap, then drop any trailing bytes that form a partial multi-byte
389
+ * sequence so the decoded result carries no U+FFFD replacement char and is
390
+ * guaranteed ≤ `maxBytes`.
391
+ */
392
+ export function truncateUtf8(text, maxBytes) {
393
+ return truncateUtf8Buffer(Buffer.from(text, "utf-8"), maxBytes);
394
+ }
395
+ function truncateUtf8Buffer(buf, maxBytes) {
396
+ if (buf.length <= maxBytes)
397
+ return buf.toString("utf-8");
398
+ let end = maxBytes;
399
+ // A UTF-8 continuation byte matches 0b10xxxxxx (0x80–0xBF); back up off any
400
+ // partial sequence at the cut so we never split a codepoint.
401
+ while (end > 0 && (buf[end] & 0xc0) === 0x80)
402
+ end -= 1;
403
+ return buf.subarray(0, end).toString("utf-8");
404
+ }
405
+ /**
406
+ * Fetch `refUrl` and return its body for inlining (AC7). Warns (does not throw)
407
+ * when the body exceeds 1 MB and truncates it, so a huge page degrades to a
408
+ * bounded excerpt rather than a runaway request. A network/HTTP failure is
409
+ * warned and skipped — generation proceeds without the reference rather than
410
+ * dying on a flaky link.
411
+ */
412
+ async function fetchReference(fetchImpl, fetchPinnedAddress, refUrl, lookupAddresses, timeoutMs) {
413
+ try {
414
+ // Re-validate at fetch time against the *resolved* address, pin that exact
415
+ // address into the connection, and re-validate every redirect hop the same
416
+ // way (M6-03 follow-up — closes both the TOCTOU and redirect-bypass gaps
417
+ // from the security-audit re-review). See `safeFetchFollowingRedirects`.
418
+ const res = await safeFetchFollowingRedirects(fetchImpl, fetchPinnedAddress, refUrl, lookupAddresses, timeoutMs);
419
+ if (!res)
420
+ return undefined;
421
+ if (!res.ok) {
422
+ logStderr({ event: "conjure.ref_url.skip", refUrl, status: res.status });
423
+ return undefined;
424
+ }
425
+ const body = await res.text();
426
+ const bytes = Math.max(res.observedBodyBytes ?? 0, Buffer.byteLength(body, "utf-8"));
427
+ if (res.bodyTruncated || bytes > REF_URL_WARN_BYTES) {
428
+ logStderr({ event: "conjure.ref_url.oversize", refUrl, bytes, capBytes: REF_URL_WARN_BYTES });
429
+ return (truncateUtf8(body, REF_URL_WARN_BYTES) + "\n<!-- …reference truncated by genie (>1 MB) -->");
430
+ }
431
+ return body;
432
+ }
433
+ catch (err) {
434
+ logStderr({ event: "conjure.ref_url.error", refUrl, error: String(err) });
435
+ return undefined;
436
+ }
437
+ }
438
+ // ── Prompt / message assembly ─────────────────────────────────────────────────
439
+ /** Build the natural-language user instruction block (everything except the
440
+ * optional vision image, which is attached as a separate content part).
441
+ * `frameworkDirective` is the target-framework instruction the selected adapter
442
+ * owns (AC4) — it begins with `Target framework: <framework>` and adds the
443
+ * per-framework source-shape guidance. */
444
+ function buildUserText(args, frameworkDirective, referenceHtml) {
445
+ const lines = [
446
+ frameworkDirective,
447
+ args.group
448
+ ? `Kit category (group): ${args.group}`
449
+ : `Kit category (group): (choose the best fit)`,
450
+ "",
451
+ "## UI kit",
452
+ args.kit,
453
+ "",
454
+ "## Component to build",
455
+ args.prompt,
456
+ ];
457
+ if (args.refImageDataUrl) {
458
+ lines.push("", "## Reference image", "A reference image is attached — match its visual intent.");
459
+ }
460
+ if (referenceHtml !== undefined) {
461
+ lines.push("", `## Reference page (fetched from ${args.refUrl})`, referenceHtml);
462
+ }
463
+ return lines.join("\n");
464
+ }
465
+ /**
466
+ * Assemble the messages for one attempt. The system prompt is always message 0
467
+ * (AC5). The user message carries the instruction text; if `refImageDataUrl` is
468
+ * set it becomes a content-parts array with a vision `image_url` part alongside
469
+ * the text (AC6 — the real OpenAI/Anthropic vision shape; the AC's
470
+ * `[{type:"image", …}]` sketch is the intent). `retryReason`, when present,
471
+ * appends the prior validation failure so the model can self-correct (AC8).
472
+ */
473
+ function buildMessages(systemPrompt, args, frameworkDirective, referenceHtml, retry) {
474
+ let userText = buildUserText(args, frameworkDirective, referenceHtml);
475
+ if (retry) {
476
+ userText = appendRetryFeedback(userText, retry);
477
+ }
478
+ const userContent = args.refImageDataUrl
479
+ ? [
480
+ { type: "text", text: userText },
481
+ { type: "image_url", image_url: { url: args.refImageDataUrl } },
482
+ ]
483
+ : userText;
484
+ return [
485
+ { role: "system", content: systemPrompt },
486
+ { role: "user", content: userContent },
487
+ ];
488
+ }
489
+ // ── Default (production) chat impl: lazy client import ─────────────────────────
490
+ /**
491
+ * Default `chat` seam: dynamically imports the M2-01 client on first call, so
492
+ * building the server never eagerly triggers `MissingLLMConfigError` (see module
493
+ * header). A real `conjure` call in a properly-configured deployment resolves
494
+ * this to `withRetry(createChatCompletion)` (M2-06, DRO-253) — every
495
+ * production LLM call this tool makes is retry/backoff-wrapped by default;
496
+ * callers (and tests, via `deps.chat`) never need to remember to apply the
497
+ * wrapper themselves.
498
+ *
499
+ * `withRetry` is called fresh on every invocation rather than wrapped once at
500
+ * module scope: the module-scope `createChatCompletion` binding doesn't exist
501
+ * until the dynamic import resolves, and re-wrapping a plain async function
502
+ * is cheap (no state beyond reading `GENIE_LLM_RETRY_MAX` from `process.env`
503
+ * per call, matching every other env read in this codebase's request path).
504
+ */
505
+ const defaultChatCompletion = async (input) => {
506
+ const { createChatCompletion } = await import("../llm/client.js");
507
+ return withRetry(createChatCompletion)(input);
508
+ };
509
+ // ── Core ──────────────────────────────────────────────────────────────────────
510
+ /**
511
+ * Generate one component. Pure (AC9): validates + returns, never writes.
512
+ *
513
+ * Flow: resolve args → optionally fetch `refUrl` (AC7) → run the shared
514
+ * request/validate/retry loop (build messages +vision AC6, call the endpoint
515
+ * with the json_schema response_format AC4, validate against COMPONENT_SCHEMA
516
+ * AC8, retry ONCE on failure) → return
517
+ * `{ componentName, group, files, manifestEntry, usage }` (AC9) and log a
518
+ * per-call line (AC10). The loop itself lives in `component-response.ts`, shared
519
+ * with `refine` (M2-04).
520
+ */
521
+ export async function conjure(deps, args) {
522
+ const parsed = conjureArgsSchema.parse(args);
523
+ const chat = deps.chat ?? defaultChatCompletion;
524
+ const fetchImpl = deps.fetchImpl;
525
+ const fetchPinnedAddress = deps.fetchPinnedAddress ?? fetchWithPinnedAddress;
526
+ const lookupAddresses = deps.lookupAddresses ?? defaultAddressLookup;
527
+ const refUrlTimeoutMs = deps.refUrlTimeoutMs ?? REF_URL_FETCH_TIMEOUT_MS;
528
+ const systemPrompt = (deps.loadSystemPrompt ?? (() => loadPrompt(GENERATE_COMPONENT_SYSTEM_PROMPT_FILE)))();
529
+ const startedAt = performance.now();
530
+ // AC4 — pick the framework adapter from the validated `framework` input. The
531
+ // adapter owns the framework-specific prompt directive (the one such bit
532
+ // `conjure` carried inline). Selection works for every framework, including
533
+ // the Vue/HTML stubs: their codegen is stubbed, but `promptDirective` is
534
+ // metadata the model reads, so pure generation still targets them.
535
+ const adapter = await getAdapter(parsed.framework);
536
+ const frameworkDirective = adapter.promptDirective;
537
+ const referenceHtml = parsed.refUrl
538
+ ? await fetchReference(fetchImpl, fetchPinnedAddress, parsed.refUrl, lookupAddresses, refUrlTimeoutMs)
539
+ : undefined;
540
+ const { outcome, usage, attempts } = await runComponentGeneration({
541
+ chat,
542
+ model: parsed.model,
543
+ validateGeneratedComponent: validateGeneratedBinaryContent,
544
+ buildMessages: (retry) => buildMessages(systemPrompt.text, parsed, frameworkDirective, referenceHtml, retry),
545
+ });
546
+ const latencyMs = Math.round(performance.now() - startedAt);
547
+ if (!outcome.ok) {
548
+ // Per-call log even on failure — keeps AC10's accounting honest.
549
+ logStderr({
550
+ event: "conjure",
551
+ ok: false,
552
+ model: parsed.model,
553
+ promptTokens: usage.promptTokens,
554
+ completionTokens: usage.completionTokens,
555
+ latencyMs,
556
+ componentName: null,
557
+ promptVersion: systemPrompt.version,
558
+ attempts,
559
+ });
560
+ throw new ConjureError("ERR_LLM_OUTPUT_INVALID", `The model did not return a schema-valid component after ${attempts} attempt(s). ` +
561
+ `Last validation error:\n${outcome.reason}`, { attempts, reason: outcome.reason });
562
+ }
563
+ const component = outcome.component;
564
+ // AC10 — per-call structured log, including componentName + prompt version.
565
+ logStderr({
566
+ event: "conjure",
567
+ ok: true,
568
+ model: parsed.model,
569
+ promptTokens: usage.promptTokens,
570
+ completionTokens: usage.completionTokens,
571
+ latencyMs,
572
+ componentName: component.componentName,
573
+ promptVersion: systemPrompt.version,
574
+ attempts,
575
+ });
576
+ return {
577
+ componentName: component.componentName,
578
+ group: component.group,
579
+ files: normalizeGeneratedFiles(component.files),
580
+ manifestEntry: component.manifestEntry,
581
+ usage,
582
+ };
583
+ }
584
+ // ── MCP registration ──────────────────────────────────────────────────────────
585
+ const conjureOutputShape = {
586
+ componentName: z.string(),
587
+ group: z.string(),
588
+ files: z.array(z
589
+ .object({
590
+ path: z.string(),
591
+ content: z.string(),
592
+ mimeType: z.string(),
593
+ encoding: z.enum(["utf-8", "base64"]),
594
+ })
595
+ .strict()),
596
+ manifestEntry: z
597
+ .object({
598
+ viewport: z.object({ width: z.number(), height: z.number() }).strict(),
599
+ subtitle: z.string().optional(),
600
+ tags: z.array(z.string()).optional(),
601
+ })
602
+ .strict(),
603
+ usage: z
604
+ .object({
605
+ promptTokens: z.number().int().min(0),
606
+ completionTokens: z.number().int().min(0),
607
+ totalTokens: z.number().int().min(0),
608
+ })
609
+ .strict(),
610
+ };
611
+ export function registerConjureTool(server, deps = {}) {
612
+ server.registerTool(CONJURE_TOOL_NAME, {
613
+ title: "Conjure component",
614
+ description: "Generate a single UI component against your UI kit from a natural-language prompt " +
615
+ "(with an optional reference image or page). Returns the COMPONENT_SCHEMA file set — " +
616
+ "{ componentName, group, files, manifestEntry } — validated against the schema (retried " +
617
+ "once on a validation failure). Pure generation: it does NOT write the files; committing " +
618
+ "them to a kit is the caller's separate, plan-gated step: plan the returned paths, map " +
619
+ "each {path, content, mimeType, encoding} to write_files " +
620
+ "{path, data: content, mimeType, encoding}, then preview to show the result. " +
621
+ "`design-default` is the valid gateway routing alias; " +
622
+ "override it only with a concrete model id exposed by the configured endpoint.",
623
+ // Reuse the exact same field map the runtime parser uses (incl. the
624
+ // SSRF-guarded refUrl) — no second, drift-prone copy (Copilot review).
625
+ inputSchema: conjureInputShape,
626
+ outputSchema: conjureOutputShape,
627
+ }, async (args) => {
628
+ try {
629
+ const result = await conjure(deps, args);
630
+ return {
631
+ content: [{ type: "text", text: JSON.stringify(result) }],
632
+ structuredContent: result,
633
+ };
634
+ }
635
+ catch (error) {
636
+ if (error instanceof ConjureError) {
637
+ return {
638
+ isError: true,
639
+ content: [
640
+ {
641
+ type: "text",
642
+ text: JSON.stringify({
643
+ code: error.code,
644
+ message: error.message,
645
+ ...(error.details ? { details: error.details } : {}),
646
+ }),
647
+ },
648
+ ],
649
+ };
650
+ }
651
+ throw error;
652
+ }
653
+ });
654
+ }
655
+ //# sourceMappingURL=conjure.js.map