@caelo-cms/shared 0.10.21 → 0.10.23

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 (241) hide show
  1. package/dist/ai-tools.d.ts +291 -231
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +349 -281
  4. package/dist/ai-tools.js.map +1 -1
  5. package/dist/auth-forms.d.ts.map +1 -1
  6. package/dist/auth-forms.js +4 -1
  7. package/dist/auth-forms.js.map +1 -1
  8. package/dist/base-css.d.ts +24 -0
  9. package/dist/base-css.d.ts.map +1 -0
  10. package/dist/base-css.js +28 -0
  11. package/dist/base-css.js.map +1 -0
  12. package/dist/build-page.d.ts +330 -0
  13. package/dist/build-page.d.ts.map +1 -0
  14. package/dist/build-page.js +282 -0
  15. package/dist/build-page.js.map +1 -0
  16. package/dist/content.d.ts +322 -9
  17. package/dist/content.d.ts.map +1 -1
  18. package/dist/content.js +354 -11
  19. package/dist/content.js.map +1 -1
  20. package/dist/css-gradient-scan.d.ts +14 -0
  21. package/dist/css-gradient-scan.d.ts.map +1 -0
  22. package/dist/css-gradient-scan.js +81 -0
  23. package/dist/css-gradient-scan.js.map +1 -0
  24. package/dist/css-var-scan.d.ts +56 -0
  25. package/dist/css-var-scan.d.ts.map +1 -0
  26. package/dist/css-var-scan.js +97 -0
  27. package/dist/css-var-scan.js.map +1 -0
  28. package/dist/design-manifest.d.ts +36 -0
  29. package/dist/design-manifest.d.ts.map +1 -0
  30. package/dist/design-manifest.js +90 -0
  31. package/dist/design-manifest.js.map +1 -0
  32. package/dist/fonts.d.ts +89 -0
  33. package/dist/fonts.d.ts.map +1 -0
  34. package/dist/fonts.js +241 -0
  35. package/dist/fonts.js.map +1 -0
  36. package/dist/genesis-inventory.d.ts +32 -0
  37. package/dist/genesis-inventory.d.ts.map +1 -0
  38. package/dist/genesis-inventory.js +186 -0
  39. package/dist/genesis-inventory.js.map +1 -0
  40. package/dist/genesis.d.ts +62 -0
  41. package/dist/genesis.d.ts.map +1 -0
  42. package/dist/genesis.js +78 -0
  43. package/dist/genesis.js.map +1 -0
  44. package/dist/i18n.d.ts +44 -1
  45. package/dist/i18n.d.ts.map +1 -1
  46. package/dist/i18n.js +72 -6
  47. package/dist/i18n.js.map +1 -1
  48. package/dist/index.d.ts +27 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +27 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/interactions.d.ts +23 -0
  53. package/dist/interactions.d.ts.map +1 -0
  54. package/dist/interactions.js +44 -0
  55. package/dist/interactions.js.map +1 -0
  56. package/dist/media.d.ts +101 -16
  57. package/dist/media.d.ts.map +1 -1
  58. package/dist/media.js +126 -15
  59. package/dist/media.js.map +1 -1
  60. package/dist/page-log.d.ts +94 -0
  61. package/dist/page-log.d.ts.map +1 -0
  62. package/dist/page-log.js +111 -0
  63. package/dist/page-log.js.map +1 -0
  64. package/dist/preview-compose.d.ts +79 -0
  65. package/dist/preview-compose.d.ts.map +1 -1
  66. package/dist/preview-compose.js +155 -25
  67. package/dist/preview-compose.js.map +1 -1
  68. package/dist/proposal-status.d.ts +40 -0
  69. package/dist/proposal-status.d.ts.map +1 -0
  70. package/dist/proposal-status.js +34 -0
  71. package/dist/proposal-status.js.map +1 -0
  72. package/dist/responsive-images.d.ts +64 -0
  73. package/dist/responsive-images.d.ts.map +1 -0
  74. package/dist/responsive-images.js +98 -0
  75. package/dist/responsive-images.js.map +1 -0
  76. package/dist/safe-keys.d.ts +9 -0
  77. package/dist/safe-keys.d.ts.map +1 -0
  78. package/dist/safe-keys.js +20 -0
  79. package/dist/safe-keys.js.map +1 -0
  80. package/dist/seo.d.ts +8 -0
  81. package/dist/seo.d.ts.map +1 -1
  82. package/dist/seo.js +3 -1
  83. package/dist/seo.js.map +1 -1
  84. package/dist/skills.d.ts +14 -68
  85. package/dist/skills.d.ts.map +1 -1
  86. package/dist/skills.js +19 -113
  87. package/dist/skills.js.map +1 -1
  88. package/dist/strip-cdata.d.ts +7 -0
  89. package/dist/strip-cdata.d.ts.map +1 -0
  90. package/dist/strip-cdata.js +48 -0
  91. package/dist/strip-cdata.js.map +1 -0
  92. package/dist/structured-sets.d.ts +6 -16
  93. package/dist/structured-sets.d.ts.map +1 -1
  94. package/dist/structured-sets.js +5 -16
  95. package/dist/structured-sets.js.map +1 -1
  96. package/dist/subagents.d.ts +105 -3
  97. package/dist/subagents.d.ts.map +1 -1
  98. package/dist/subagents.js +224 -41
  99. package/dist/subagents.js.map +1 -1
  100. package/dist/template-engine.d.ts +85 -0
  101. package/dist/template-engine.d.ts.map +1 -0
  102. package/dist/template-engine.js +403 -0
  103. package/dist/template-engine.js.map +1 -0
  104. package/dist/theme-importers/auto-detect.d.ts +26 -0
  105. package/dist/theme-importers/auto-detect.d.ts.map +1 -0
  106. package/dist/theme-importers/auto-detect.js +42 -0
  107. package/dist/theme-importers/auto-detect.js.map +1 -0
  108. package/dist/theme-importers/css-comments.d.ts +12 -0
  109. package/dist/theme-importers/css-comments.d.ts.map +1 -0
  110. package/dist/theme-importers/css-comments.js +15 -0
  111. package/dist/theme-importers/css-comments.js.map +1 -0
  112. package/dist/theme-importers/dtcg.d.ts +46 -0
  113. package/dist/theme-importers/dtcg.d.ts.map +1 -0
  114. package/dist/theme-importers/dtcg.js +111 -0
  115. package/dist/theme-importers/dtcg.js.map +1 -0
  116. package/dist/theme-importers/loose.d.ts +3 -0
  117. package/dist/theme-importers/loose.d.ts.map +1 -0
  118. package/dist/theme-importers/loose.js +76 -0
  119. package/dist/theme-importers/loose.js.map +1 -0
  120. package/dist/theme-importers/shadcn.d.ts +24 -0
  121. package/dist/theme-importers/shadcn.d.ts.map +1 -0
  122. package/dist/theme-importers/shadcn.js +135 -0
  123. package/dist/theme-importers/shadcn.js.map +1 -0
  124. package/dist/theme-importers/style-dictionary.d.ts +17 -0
  125. package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
  126. package/dist/theme-importers/style-dictionary.js +125 -0
  127. package/dist/theme-importers/style-dictionary.js.map +1 -0
  128. package/dist/theme-importers/tailwind.d.ts +3 -0
  129. package/dist/theme-importers/tailwind.d.ts.map +1 -0
  130. package/dist/theme-importers/tailwind.js +218 -0
  131. package/dist/theme-importers/tailwind.js.map +1 -0
  132. package/dist/theme-literal-binding.d.ts +37 -0
  133. package/dist/theme-literal-binding.d.ts.map +1 -0
  134. package/dist/theme-literal-binding.js +138 -0
  135. package/dist/theme-literal-binding.js.map +1 -0
  136. package/dist/theme-normalize.d.ts +31 -0
  137. package/dist/theme-normalize.d.ts.map +1 -0
  138. package/dist/theme-normalize.js +587 -0
  139. package/dist/theme-normalize.js.map +1 -0
  140. package/dist/theme-ramp.d.ts +55 -0
  141. package/dist/theme-ramp.d.ts.map +1 -0
  142. package/dist/theme-ramp.js +149 -0
  143. package/dist/theme-ramp.js.map +1 -0
  144. package/dist/theme-render.d.ts +105 -0
  145. package/dist/theme-render.d.ts.map +1 -0
  146. package/dist/theme-render.js +441 -0
  147. package/dist/theme-render.js.map +1 -0
  148. package/dist/themes-errors.d.ts +109 -0
  149. package/dist/themes-errors.d.ts.map +1 -0
  150. package/dist/themes-errors.js +170 -0
  151. package/dist/themes-errors.js.map +1 -0
  152. package/dist/themes.d.ts +343 -0
  153. package/dist/themes.d.ts.map +1 -0
  154. package/dist/themes.js +697 -0
  155. package/dist/themes.js.map +1 -0
  156. package/dist/version.d.ts +7 -4
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +6 -3
  159. package/dist/version.js.map +1 -1
  160. package/package.json +10 -2
  161. package/src/__tests__/redos-hardening.test.ts +160 -0
  162. package/src/ai-tools-add-module-modes.test.ts +106 -0
  163. package/src/ai-tools-position.test.ts +134 -0
  164. package/src/ai-tools.test.ts +81 -0
  165. package/src/ai-tools.ts +1179 -0
  166. package/src/auth-forms.ts +36 -0
  167. package/src/base-css.ts +30 -0
  168. package/src/build-page.test.ts +228 -0
  169. package/src/build-page.ts +319 -0
  170. package/src/cap-failures.ts +67 -0
  171. package/src/content.test.ts +170 -0
  172. package/src/content.ts +620 -0
  173. package/src/context.ts +43 -0
  174. package/src/css-gradient-scan.ts +88 -0
  175. package/src/css-var-scan.test.ts +96 -0
  176. package/src/css-var-scan.ts +144 -0
  177. package/src/derive-module-type.test.ts +80 -0
  178. package/src/design-manifest.ts +93 -0
  179. package/src/fonts.test.ts +157 -0
  180. package/src/fonts.ts +296 -0
  181. package/src/genesis-inventory.test.ts +86 -0
  182. package/src/genesis-inventory.ts +215 -0
  183. package/src/genesis-sanitize.test.ts +35 -0
  184. package/src/genesis.ts +87 -0
  185. package/src/i18n.test.ts +274 -0
  186. package/src/i18n.ts +269 -0
  187. package/src/index.test.ts +10 -0
  188. package/src/index.ts +59 -0
  189. package/src/interactions.ts +48 -0
  190. package/src/logger.ts +147 -0
  191. package/src/media.test.ts +160 -0
  192. package/src/media.ts +355 -0
  193. package/src/page-log.test.ts +163 -0
  194. package/src/page-log.ts +124 -0
  195. package/src/preview-compose.test.ts +602 -0
  196. package/src/preview-compose.ts +709 -0
  197. package/src/preview-scanner.test.ts +96 -0
  198. package/src/preview-scanner.ts +214 -0
  199. package/src/proposal-status.test.ts +69 -0
  200. package/src/proposal-status.ts +40 -0
  201. package/src/responsive-images.test.ts +104 -0
  202. package/src/responsive-images.ts +151 -0
  203. package/src/result.ts +29 -0
  204. package/src/safe-keys.ts +21 -0
  205. package/src/seo.test.ts +234 -0
  206. package/src/seo.ts +261 -0
  207. package/src/skills.ts +48 -0
  208. package/src/snapshots.test.ts +80 -0
  209. package/src/snapshots.ts +81 -0
  210. package/src/strip-cdata.test.ts +41 -0
  211. package/src/strip-cdata.ts +50 -0
  212. package/src/structured-sets.ts +180 -0
  213. package/src/subagents.test.ts +262 -0
  214. package/src/subagents.ts +432 -0
  215. package/src/template-engine.test.ts +379 -0
  216. package/src/template-engine.ts +520 -0
  217. package/src/theme-gradient.test.ts +92 -0
  218. package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
  219. package/src/theme-importers/auto-detect.ts +84 -0
  220. package/src/theme-importers/css-comments.ts +15 -0
  221. package/src/theme-importers/dtcg.ts +106 -0
  222. package/src/theme-importers/loose.ts +76 -0
  223. package/src/theme-importers/shadcn.ts +133 -0
  224. package/src/theme-importers/style-dictionary.ts +125 -0
  225. package/src/theme-importers/tailwind.ts +217 -0
  226. package/src/theme-literal-binding.test.ts +71 -0
  227. package/src/theme-literal-binding.ts +159 -0
  228. package/src/theme-motion.test.ts +115 -0
  229. package/src/theme-normalize-envelope.test.ts +43 -0
  230. package/src/theme-normalize-gradient.test.ts +135 -0
  231. package/src/theme-normalize.ts +661 -0
  232. package/src/theme-ramp.ts +187 -0
  233. package/src/theme-render-sanitize.test.ts +45 -0
  234. package/src/theme-render.test.ts +119 -0
  235. package/src/theme-render.ts +487 -0
  236. package/src/theme-shadow.test.ts +56 -0
  237. package/src/themes-errors.ts +199 -0
  238. package/src/themes.ts +842 -0
  239. package/src/translation.test.ts +160 -0
  240. package/src/translation.ts +295 -0
  241. package/src/version.ts +66 -0
package/dist/ai-tools.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * silent drops in the handler.
11
11
  */
12
12
  import { z } from "zod";
13
- import { MODULE_CSS_MAX, MODULE_HTML_MAX, MODULE_JS_MAX } from "./content.js";
13
+ import { contentInstanceCreateSchema, contentInstanceDeleteSchema, contentInstanceUpdateSchema, forkPlacementContentSchema, MODULE_CSS_MAX, MODULE_HTML_MAX, MODULE_JS_MAX, moduleFieldSchema, setPlacementContentSchema, slugSchema, } from "./content.js";
14
14
  /**
15
15
  * v0.6.2 — `position` argument shared across the three `add_module_to_*`
16
16
  * tools. Accepts either the literal string `"top"` / `"bottom"` OR a
@@ -22,15 +22,49 @@ import { MODULE_CSS_MAX, MODULE_HTML_MAX, MODULE_JS_MAX } from "./content.js";
22
22
  * description explicitly says to use bare integers; the coercion
23
23
  * removes that ergonomic gotcha while still rejecting non-numeric
24
24
  * strings (`"abc"` → still fails the int check cleanly).
25
+ *
26
+ * issue #106 (step-13 round-6) — an OUTER preprocess strips one layer of
27
+ * surrounding matching quotes first. The model occasionally over-quotes the
28
+ * literal: it emitted `position` as the JSON string `"\"bottom\""` (the
29
+ * 8-char value including the quote characters), which matched neither union
30
+ * branch and failed with a bare `Invalid input`. Unwrapping `"bottom"` →
31
+ * `bottom` (and `"\"0\""` → `0`, which the integer branch then coerces) turns
32
+ * that recurring first-call rejection into a clean accept. This is input
33
+ * NORMALISATION at the boundary (decoding a malformed encoding of a value the
34
+ * caller did supply), not a missing-data fallback — `undefined`/`null`/`""`
35
+ * still fall through to a loud rejection, per CLAUDE.md §2.
25
36
  */
26
- export const positionInputSchema = z.union([
37
+ function stripSurroundingQuotes(v) {
38
+ if (typeof v !== "string")
39
+ return v;
40
+ // Only when BOTH ends are the same quote char and there is at least one
41
+ // inner character — so a bare `"bottom"` / `"0"` is untouched and an empty
42
+ // `""` is not collapsed into a match here.
43
+ const m = v.match(/^(["'])([\s\S]+)\1$/);
44
+ return m ? m[2] : v;
45
+ }
46
+ export const positionInputSchema = z.preprocess(stripSurroundingQuotes, z.union([
27
47
  z.enum(["top", "bottom"]),
28
48
  z.preprocess((v) => (typeof v === "string" && /^\d+$/.test(v) ? Number.parseInt(v, 10) : v), z.number().int().min(0).max(1000)),
29
- ]);
49
+ ]));
30
50
  export const editModuleToolInput = z
31
51
  .object({
32
52
  moduleId: z.string().uuid(),
33
53
  displayName: z.string().min(1).max(128).optional(),
54
+ /**
55
+ * v0.12.0 — rewrite the module's purpose. Optional; passing it
56
+ * updates the `## Modules` block your future self will read.
57
+ */
58
+ description: z.string().max(1000).optional(),
59
+ /** v0.12.0 — re-classify the module's role tag. */
60
+ kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
61
+ /**
62
+ * v0.12.3 (issue #106) — re-classify the module's stable `type` (the
63
+ * class a parent's `allowedModuleTypes` whitelist matches against,
64
+ * e.g. `button`). Rarely needed; usually derived from displayName at
65
+ * create time. Set it to make this module satisfy a parent's allowlist.
66
+ */
67
+ type: slugSchema.optional(),
34
68
  html: z.string().max(MODULE_HTML_MAX).optional(),
35
69
  css: z.string().max(MODULE_CSS_MAX).optional(),
36
70
  js: z.string().max(MODULE_JS_MAX).optional(),
@@ -40,17 +74,9 @@ export const editModuleToolInput = z
40
74
  * fill these via `set_page_module_content`. Optional on edits — pass
41
75
  * to replace the declared schema.
42
76
  */
43
- fields: z
44
- .array(z
45
- .object({
46
- name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
47
- kind: z.enum(["text", "richtext", "url", "image", "number", "boolean", "link"]),
48
- label: z.string().min(1).max(128),
49
- default: z.unknown().optional(),
50
- })
51
- .strict())
52
- .max(64)
53
- .optional(),
77
+ fields: z.array(moduleFieldSchema).max(64).optional(),
78
+ /** issue #164 slice 2 — see addModuleToolInput.bindThemeLiterals. */
79
+ bindThemeLiterals: z.boolean().optional(),
54
80
  })
55
81
  .strict();
56
82
  /**
@@ -75,74 +101,140 @@ export const siteMemoryProposeToolInput = z
75
101
  })
76
102
  .strict();
77
103
  /**
78
- * The set of tools shipped in P5. Other phases extend by adding a new
79
- * entry; the dispatcher walks this map at registration time.
80
- */
81
- /**
82
- * P6.7.3 — `add_module_to_page` AI tool. Creates a new module and
83
- * inserts it into a target page's block at the requested position. The
84
- * AI passes html (and optionally css/js) and a sluggable displayName;
85
- * the tool generates a unique slug.
104
+ * Progressive-disclosure skill loading. The `## Skills` system-prompt block
105
+ * lists each active skill's slug + description; when a task matches one or
106
+ * more, the model calls `load_skill({slugs})` to pull their full instructions
107
+ * into the conversation (they persist as a tool result for the rest of the
108
+ * chat, so each skill loads at most once).
109
+ *
110
+ * Takes a LIST, not a single slug, per CLAUDE.md §11: a load is a routine
111
+ * operation and every routine operation ships in bulk form, with n=1 as its
112
+ * smallest case rather than a separate singular tool. Loading three skills was
113
+ * three provider round-trips whose only output was one tool call each.
114
+ *
115
+ * Capped at 5: skills are task-scoped, and a model that wants more than a
116
+ * handful at once has almost certainly matched the index too loosely — each
117
+ * body is a sizeable injection into history.
86
118
  */
87
- export const addModuleToPageToolInput = z
119
+ export const loadSkillToolInput = z
88
120
  .object({
89
- pageId: z.string().uuid(),
90
- blockName: z.string().min(1).max(80),
91
- /** "top" | "bottom" | a 0-based integer index. */
92
- position: positionInputSchema,
93
- displayName: z.string().min(1).max(128),
94
- html: z.string().min(1).max(50_000),
95
- css: z.string().max(50_000).optional(),
96
- js: z.string().max(50_000).optional(),
97
- /**
98
- * v0.5.21 — module field schema (v0.4.0 module/content split). The
99
- * AI declares fields here when creating a module that uses `{{name}}`
100
- * substitutions; per-page content gets filled via
101
- * `set_page_module_content` later. Optional — modules with no
102
- * field schema are static HTML.
103
- */
104
- fields: z
121
+ slugs: z
105
122
  .array(z
106
- .object({
107
- name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
108
- kind: z.enum(["text", "richtext", "url", "image", "number", "boolean", "link"]),
109
- label: z.string().min(1).max(128),
110
- default: z.unknown().optional(),
111
- })
112
- .strict())
113
- .max(64)
114
- .optional(),
123
+ .string()
124
+ .min(1)
125
+ .max(120)
126
+ .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"))
127
+ .min(1)
128
+ .max(5),
115
129
  })
116
130
  .strict();
117
131
  /**
118
- * P6.7.3 — `add_module_to_template` AI tool. Same shape as
119
- * `add_module_to_page` but fans the new module out to every page using
120
- * the target template, inserting at the same block + position. Used
121
- * for "site-wide" content (a global footer, a header banner, etc.).
132
+ * The set of tools shipped in P5. Other phases extend by adding a new
133
+ * entry; the dispatcher walks this map at registration time.
134
+ */
135
+ /**
136
+ * The ONE `add_module` input — consolidates add_module_to_{page,layout,template}
137
+ * (audit #2). `target` picks the destination; `targetRef` is a slug OR a uuid
138
+ * (resolved server-side — a slug is friendlier for the AI to hold than a uuid).
139
+ * Same two modes as before: `moduleId` reuses an existing module (now for ALL
140
+ * three targets — layout used to be mint-only), or the authoring fields mint a
141
+ * new one (raw html is fine — moduleize turns it into a proper module). Exactly
142
+ * one mode, enforced by the shared superRefine.
122
143
  */
123
- export const addModuleToTemplateToolInput = z
144
+ export const addModuleToolInput = z
124
145
  .object({
125
- templateId: z.string().uuid(),
146
+ target: z.enum(["page", "layout", "template"]),
147
+ /** Slug or uuid of the page / layout / template. Resolved server-side. */
148
+ targetRef: z.string().min(1).max(200),
126
149
  blockName: z.string().min(1).max(80),
127
150
  position: positionInputSchema,
128
- displayName: z.string().min(1).max(128),
129
- html: z.string().min(1).max(50_000),
151
+ /** Reuse an existing module by id (any target). Mutually exclusive with the
152
+ * authoring fields. */
153
+ moduleId: z.string().uuid().optional(),
154
+ displayName: z.string().min(1).max(128).optional(),
155
+ description: z.string().max(1000).optional(),
156
+ kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
157
+ type: slugSchema.optional(),
158
+ html: z.string().min(1).max(50_000).optional(),
130
159
  css: z.string().max(50_000).optional(),
131
160
  js: z.string().max(50_000).optional(),
132
- /** v0.5.21 — see addModuleToPageToolInput.fields for context. */
133
- fields: z
134
- .array(z
135
- .object({
136
- name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
137
- kind: z.enum(["text", "richtext", "url", "image", "number", "boolean", "link"]),
138
- label: z.string().min(1).max(128),
139
- default: z.unknown().optional(),
140
- })
141
- .strict())
142
- .max(64)
143
- .optional(),
161
+ fields: z.array(moduleFieldSchema).max(64).optional(),
162
+ bindThemeLiterals: z.boolean().optional(),
163
+ /**
164
+ * The placement's initial content, applied IN THE SAME CALL (2026-07
165
+ * — a freshly minted module must never land empty and wait for a
166
+ * second round-trip). ONE rule for every target: content goes in
167
+ * `values`. target='page': fills the placement's content_instance.
168
+ * target='template': every fanned-out placement gets these values.
169
+ * target='layout': chrome has no content_instance, so the handler
170
+ * stores the values as the minted module's `fields[].default` —
171
+ * which requires explicit `fields` (moduleize field names are
172
+ * unknowable up front) and is rejected for `moduleId` reuse (a
173
+ * shared module renders its stored defaults; edit_module changes
174
+ * them).
175
+ */
176
+ values: z.record(z.string(), z.unknown()).optional(),
144
177
  })
145
- .strict();
178
+ .strict()
179
+ .superRefine((input, ctx) => {
180
+ if (input.target === "layout" && input.values !== undefined) {
181
+ if (input.moduleId !== undefined) {
182
+ ctx.addIssue({
183
+ code: z.ZodIssueCode.custom,
184
+ message: "Reusing a module on a layout renders the module's STORED field defaults — `values` has nothing to bind to. Drop `values` (the chrome shows the module as-is), or change the shared copy via edit_module.",
185
+ });
186
+ }
187
+ else if (input.fields === undefined || input.fields.length === 0) {
188
+ ctx.addIssue({
189
+ code: z.ZodIssueCode.custom,
190
+ message: "target='layout' with `values` needs explicit `fields` — chrome stores the values as field defaults, so the field names must be declared in this call (raw-HTML field inference can't know your value keys).",
191
+ });
192
+ }
193
+ else {
194
+ const known = new Set(input.fields.map((f) => f.name));
195
+ const unknown = Object.keys(input.values).filter((k) => !known.has(k));
196
+ if (unknown.length > 0) {
197
+ ctx.addIssue({
198
+ code: z.ZodIssueCode.custom,
199
+ message: `\`values\` keys [${unknown.join(", ")}] match no declared field — declared: [${[...known].join(", ")}].`,
200
+ });
201
+ }
202
+ }
203
+ }
204
+ // `moduleId` is PLACEMENT-ONLY (see the buildPageModuleSchema note): a
205
+ // placement that CAN succeed must not fail over an extra authoring field
206
+ // (§1A/§11), so we do NOT reject a moduleId call that also carries authoring
207
+ // fields. Those fields are not applied here; the tool HANDLER surfaces an
208
+ // INFO for any ignored field whose value DIFFERS from the module's stored
209
+ // one, pointing at edit_module — so a dropped change is never silent (§2).
210
+ if (input.moduleId !== undefined)
211
+ return;
212
+ if (input.displayName === undefined || input.html === undefined) {
213
+ ctx.addIssue({
214
+ code: z.ZodIssueCode.custom,
215
+ message: "Pass either `moduleId` (place an existing module from ## Modules) " +
216
+ "or `displayName` + `html` (mint a new module).",
217
+ });
218
+ }
219
+ // 2026-07 — a freshly minted module must carry its initial copy IN
220
+ // THIS call, via `values` on every target (`fields[].default` also
221
+ // satisfies it — that's where layout values land anyway). Without
222
+ // either, the placement rendered EMPTY until a second round-trip —
223
+ // the exact two-step build_page was created to kill. Only enforced
224
+ // when the caller authored explicit fields; the raw-HTML path goes
225
+ // through moduleize, which enforces defaults itself.
226
+ if (input.fields !== undefined &&
227
+ input.fields.length > 0 &&
228
+ input.values === undefined &&
229
+ !input.fields.some((f) => "default" in f && f.default !== undefined)) {
230
+ ctx.addIssue({
231
+ code: z.ZodIssueCode.custom,
232
+ message: "A minted module needs its initial content IN THIS CALL: pass `values` " +
233
+ "({fieldName: value, …}) for this placement, or give the fields `default`s. " +
234
+ "Without either, the module renders empty placeholders until a second call.",
235
+ });
236
+ }
237
+ });
146
238
  /**
147
239
  * v0.2.16 — `add_plugin_to_page` AI tool. Inserts a plugin's
148
240
  * `<div data-caelo-plugin>` placeholder into a page's block via a
@@ -162,30 +254,69 @@ export const addPluginToPageToolInput = z
162
254
  position: positionInputSchema,
163
255
  })
164
256
  .strict();
257
+ // ─── v0.12.0 — content_instances + placement AI tools ─────────────────
258
+ /**
259
+ * `list_content_instances` — browse the content library. The `placementCount`
260
+ * returned per row is a blast-radius affordance: the AI can decide whether
261
+ * to edit a shared instance (propagates everywhere) or fork it first.
262
+ */
263
+ export const listContentInstancesToolInput = z
264
+ .object({
265
+ moduleId: z.string().uuid().optional(),
266
+ slug: z.string().min(1).max(64).optional(),
267
+ /** Free-text matches against displayName + slug. Case-insensitive substring. */
268
+ search: z.string().min(1).max(128).optional(),
269
+ /** Narrow to "instances referenced from this page" for chat-runner planning. */
270
+ pageId: z.string().uuid().optional(),
271
+ })
272
+ .strict();
273
+ export const getContentInstanceToolInput = z
274
+ .object({
275
+ id: z.string().uuid(),
276
+ })
277
+ .strict();
278
+ export const createContentInstanceToolInput = contentInstanceCreateSchema;
279
+ export const setContentInstanceValuesToolInput = contentInstanceUpdateSchema;
280
+ export const deleteContentInstanceToolInput = contentInstanceDeleteSchema;
281
+ export const setPlacementContentToolInput = setPlacementContentSchema;
282
+ export const forkPlacementContentToolInput = forkPlacementContentSchema;
165
283
  export const AI_TOOLS = [
166
284
  "edit_module",
167
285
  "site_memory_propose",
168
- "add_module_to_page",
169
- "add_module_to_template",
286
+ "add_module",
170
287
  "create_page",
171
- "rename_page",
172
- "set_page_title",
173
- "change_page_slug",
174
- "delete_page",
175
- "remove_module_from_page",
288
+ // audit #3 — page metadata (name/title/slug/template/status) is one tool for
289
+ // 1..200 pages: `update_pages_many`. The former rename_page / set_page_title
290
+ // / change_page_slug were single-field wrappers over the same pages.update.
291
+ "update_pages_many",
292
+ // audit #4 — one delete tool for 1..200 pages, disposition per page.
293
+ // The former singular delete_page folded in (n=1 is a one-item array).
294
+ "delete_pages_many",
295
+ // one remove-module tool routed by target (page|layout).
296
+ "remove_module_from",
176
297
  "set_structured_set",
177
298
  "update_theme",
178
- "add_module_to_layout",
179
- "remove_module_from_layout",
180
299
  "set_template_layout",
181
300
  "create_layout",
182
301
  "set_site_defaults",
183
302
  "duplicate_page",
184
- "change_template",
303
+ "repoint_page_template",
185
304
  "move_module",
186
305
  "reorder_module",
187
306
  "set_nav_menu",
188
307
  "add_plugin_to_page",
308
+ // v0.12.0 — content_instances + placement binding
309
+ "list_content_instances",
310
+ "get_content_instance",
311
+ "create_content_instance",
312
+ "set_content_instance_values",
313
+ "delete_content_instance",
314
+ "set_placement_content",
315
+ "fork_placement_content",
316
+ // issue #299 — bulk-first build path (§11)
317
+ "build_page",
318
+ "create_content_instances",
319
+ "set_page_module_content_many",
189
320
  ];
190
321
  /** Chat ops input shapes — used by the SvelteKit form actions. */
191
322
  export const chatCreateSessionInput = z
@@ -201,10 +332,38 @@ export const chatCreateSessionInput = z
201
332
  parentChatSessionId: z.string().uuid().nullable().optional(),
202
333
  })
203
334
  .strict();
335
+ /**
336
+ * issue #190 — an operator-attached image riding a user chat message.
337
+ * References a media_assets row (the upload endpoint owns validation
338
+ * + storage); image/* only in v1 so the provider mapping is always an
339
+ * image part.
340
+ */
341
+ export const chatAttachmentSchema = z
342
+ .object({
343
+ /** Set for an operator upload — a `media_assets` row. */
344
+ assetId: z.string().uuid().optional(),
345
+ /**
346
+ * Set for an AI-produced chat image — an object-store key under
347
+ * `CHAT_IMAGE_PREFIX`. Deliberately not a media asset: see the prefix's
348
+ * doc comment. Exactly one of `assetId` / `storageKey` is present.
349
+ */
350
+ storageKey: z.string().min(1).max(512).optional(),
351
+ mime: z.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]),
352
+ alt: z.string().max(2048).optional(),
353
+ })
354
+ .strict()
355
+ .refine((a) => (a.assetId === undefined) !== (a.storageKey === undefined), {
356
+ message: "exactly one of assetId (operator upload) or storageKey (chat image) must be set",
357
+ });
358
+ /** issue #190 — attachments-per-message cap; bounds provider payload size. */
359
+ export const CHAT_MAX_ATTACHMENTS = 4;
204
360
  export const chatSendMessageInput = z
205
361
  .object({
206
362
  chatSessionId: z.string().uuid(),
207
- content: z.string().min(1).max(8000),
363
+ // Optional as of Plan B: a resume turn (see `resumeApproval`) carries no
364
+ // operator content — it re-runs the paused gated turn. A refine below
365
+ // requires exactly one of content / resumeApproval.
366
+ content: z.string().min(1).max(8000).optional(),
208
367
  /** Element-reference chips appended to the message. */
209
368
  chips: z
210
369
  .array(z
@@ -215,16 +374,45 @@ export const chatSendMessageInput = z
215
374
  })
216
375
  .strict())
217
376
  .default([]),
377
+ /** issue #190 — operator-attached images (see chatAttachmentSchema). */
378
+ attachments: z.array(chatAttachmentSchema).max(CHAT_MAX_ATTACHMENTS).default([]),
379
+ /**
380
+ * issue #29 — provenance of this turn. 'system' marks an auto-injected
381
+ * message the operator did NOT type (crawl-completion nudge, post-
382
+ * approval continuation): the model still receives it as a user turn,
383
+ * but the UI renders it as a muted status note instead of "You:".
384
+ * Omitted / undefined = operator-authored.
385
+ */
386
+ origin: z.enum(["operator", "system"]).optional(),
218
387
  /**
219
388
  * P6.7.3 — the active /edit page id, threaded so the chat-runner can
220
389
  * compose a Current-page volatile chunk in the system prompt and so
221
- * tools that operate on a page (add_module_to_page) know the target
390
+ * tools that operate on a page (add_module target='page') know the target
222
391
  * without a chip. Optional because the standalone chat editor at
223
392
  * /content/chat doesn't have a page context.
224
393
  */
225
394
  activePageId: z.string().uuid().optional(),
395
+ /**
396
+ * Plan B (SDK approval gate) — production resume of a paused gated turn.
397
+ * When set, this is NOT a new operator message: the runner appends the
398
+ * SDK tool-approval-response (the Owner's in-chat Approve/Reject) to the
399
+ * paused turn's history and re-runs so the SDK either executes the gated
400
+ * tool (approved) or lets the model react to the denial. `approvalId` is
401
+ * the SDK id surfaced on the tool-approval-request ClientEvent.
402
+ */
403
+ resumeApproval: z
404
+ .object({
405
+ approvalId: z.string().min(1).max(200),
406
+ approved: z.boolean(),
407
+ reason: z.string().max(2000).optional(),
408
+ })
409
+ .strict()
410
+ .optional(),
226
411
  })
227
- .strict();
412
+ .strict()
413
+ .refine((d) => d.content !== undefined || d.resumeApproval !== undefined, {
414
+ message: "content or resumeApproval is required",
415
+ });
228
416
  export const chatRenameSessionInput = z
229
417
  .object({
230
418
  chatSessionId: z.string().uuid(),
@@ -256,6 +444,10 @@ export const chatPublishInput = z
256
444
  "pageModuleContent",
257
445
  "layout",
258
446
  "structuredSet",
447
+ // v0.11.0 (#45) — themes primitive joins the publish/Stage
448
+ // surface so chat-branched theme edits replay into live
449
+ // on chat.publish.
450
+ "theme",
259
451
  ]),
260
452
  entityId: z.string().uuid(),
261
453
  })
@@ -292,25 +484,6 @@ export const aiProvidersSetInput = z
292
484
  apiKey: z.string().min(1).max(500).optional(),
293
485
  })
294
486
  .strict();
295
- /**
296
- * P19 — `compose_from_import` AI tool input. Wraps
297
- * `imports.compose_from_run`. Single transaction synthesis: aggregates
298
- * theme tokens, creates one template bound to the default layout,
299
- * materialises every staged import_pages row into a draft page +
300
- * modules. Idempotent — pages already accepted skip cleanly.
301
- */
302
- export const composeFromImportToolInput = z
303
- .object({
304
- runId: z.string().uuid(),
305
- templateSlug: z
306
- .string()
307
- .min(1)
308
- .max(120)
309
- .regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/hyphens, leading non-hyphen")
310
- .optional(),
311
- includeImportPageIds: z.array(z.string().uuid()).optional(),
312
- })
313
- .strict();
314
487
  /**
315
488
  * Input for `ai_providers.clear_key` — Owner-only NULLs the encrypted
316
489
  * triplet so the resolver falls back to the env-var path for that
@@ -331,22 +504,6 @@ const slugInputSchema = z
331
504
  .min(1)
332
505
  .max(120)
333
506
  .regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/hyphens, leading non-hyphen");
334
- export const createPageToolInput = z
335
- .object({
336
- name: z.string().min(1).max(256),
337
- title: z.string().min(1).max(256),
338
- slug: slugInputSchema,
339
- locale: z.string().min(2).max(10).default("en"),
340
- /**
341
- * Optional. When omitted, the underlying `pages.create` op resolves
342
- * to `site_defaults.default_template_id` (P6.7.6). The AI should pass
343
- * a UUID from `## Site defaults` / `## Templates → layouts` only when
344
- * the user asks for a non-default template.
345
- */
346
- templateId: z.string().uuid().optional(),
347
- status: z.enum(["draft", "published"]).default("draft"),
348
- })
349
- .strict();
350
507
  /**
351
508
  * P18 AI-completeness — `create_template` AI tool input. Wraps
352
509
  * `templates.create` (widened to AI in this pass per CLAUDE.md §11
@@ -360,102 +517,51 @@ export const createTemplateToolInput = z
360
517
  html: z.string().min(1).max(2_000_000),
361
518
  css: z.string().max(2_000_000).default(""),
362
519
  layoutId: z.string().uuid().optional(),
520
+ // Optional block-set metadata, symmetric with create_layout. Omit to
521
+ // auto-derive blocks from <caelo-slot> tags; pass to set displayName /
522
+ // position (each name must match a slot). Enforced op-side in
523
+ // templates.create (loud reject on a slot-less block).
524
+ blocks: z
525
+ .array(z.object({
526
+ name: z.string().min(1).max(80),
527
+ displayName: z.string().min(1).max(200),
528
+ position: z.number().int().min(0).max(1000),
529
+ }))
530
+ .optional(),
363
531
  })
364
532
  .strict();
365
- export const renamePageToolInput = z
366
- .object({
367
- pageId: z.string().uuid(),
368
- newName: z.string().min(1).max(256),
369
- })
370
- .strict();
371
- export const setPageTitleToolInput = z
372
- .object({
373
- pageId: z.string().uuid(),
374
- newTitle: z.string().min(1).max(256),
375
- })
376
- .strict();
377
- export const changePageSlugToolInput = z
378
- .object({
379
- pageId: z.string().uuid(),
380
- newSlug: slugInputSchema,
381
- /**
382
- * `auto` (default): create a 301 from the old slug → new slug.
383
- * `skip`: only choose when the user explicitly says they don't
384
- * want existing inbound links to redirect.
385
- */
386
- redirectFromOld: z.enum(["auto", "skip"]).default("auto"),
387
- })
388
- .strict();
389
- export const deletePageToolInput = z
390
- .object({
391
- pageId: z.string().uuid(),
392
- /** '404' returns a not-found; 'redirect' creates a 301 to redirectTo. */
393
- disposition: z.enum(["404", "redirect"]),
394
- redirectTo: z.string().min(1).max(500).optional(),
395
- })
396
- .strict()
397
- .refine((v) => v.disposition === "404" || (v.redirectTo !== undefined && v.redirectTo.length > 0), {
398
- message: "redirectTo is required when disposition='redirect'",
399
- path: ["redirectTo"],
400
- });
401
- export const removeModuleFromPageToolInput = z
533
+ /**
534
+ * The ONE remove-module input — `target` picks page vs layout, `targetRef` is a
535
+ * slug OR a uuid (resolved server-side). Mirrors `add_module` so placement and
536
+ * un-placement share one shape. (No `template` target: there is no
537
+ * remove_module_from_template, matching the add-side history.)
538
+ */
539
+ export const removeModuleFromToolInput = z
402
540
  .object({
403
- pageId: z.string().uuid(),
541
+ target: z.enum(["page", "layout"]),
542
+ targetRef: z.string().min(1).max(200),
404
543
  moduleId: z.string().uuid(),
405
544
  })
406
545
  .strict();
407
546
  export const setStructuredSetToolInput = z
408
547
  .object({
409
- kind: z.enum(["nav-menu", "taxonomy", "theme", "tags", "link-list"]),
548
+ // v0.10.22 — added "language-selector" to match @caelo-cms/shared
549
+ // structuredSetKind (the 6th kind that was previously unreachable
550
+ // from any AI tool — the kind-specific wrappers covered only
551
+ // nav-menu + theme).
552
+ kind: z.enum(["nav-menu", "taxonomy", "theme", "tags", "link-list", "language-selector"]),
410
553
  slug: slugInputSchema,
411
554
  displayName: z.string().min(1).max(200),
412
555
  items: z.array(z.unknown()),
413
556
  })
414
557
  .strict();
415
- export const updateThemeToolInput = z
416
- .object({
417
- /** Map of token name to value. Merges into the existing theme/site set. */
418
- tokens: z.record(z.string(), z.string()),
419
- })
420
- .strict();
421
- /**
422
- * P6.7.6 — layout-layer tools. Layouts are site-wide chrome (header /
423
- * footer / nav) that wraps every page on every template bound to the
424
- * layout. `add_module_to_layout` reaches every page across the site
425
- * with one call; `set_template_layout` re-points a template's chrome.
426
- * `create_layout` and `set_site_defaults` are Owner-only at the op
427
- * level — AI calls reject with ActorScopeRejected and the chat surfaces
428
- * the permission requirement.
429
- */
430
- export const addModuleToLayoutToolInput = z
431
- .object({
432
- layoutSlug: slugInputSchema,
433
- blockName: z.string().min(1).max(80),
434
- position: positionInputSchema,
435
- displayName: z.string().min(1).max(128),
436
- html: z.string().min(1).max(50_000),
437
- css: z.string().max(50_000).optional(),
438
- js: z.string().max(50_000).optional(),
439
- /** v0.5.21 — see addModuleToPageToolInput.fields for context. */
440
- fields: z
441
- .array(z
442
- .object({
443
- name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
444
- kind: z.enum(["text", "richtext", "url", "image", "number", "boolean", "link"]),
445
- label: z.string().min(1).max(128),
446
- default: z.unknown().optional(),
447
- })
448
- .strict())
449
- .max(64)
450
- .optional(),
451
- })
452
- .strict();
453
- export const removeModuleFromLayoutToolInput = z
454
- .object({
455
- layoutSlug: slugInputSchema,
456
- moduleId: z.string().uuid(),
457
- })
458
- .strict();
558
+ // v0.10.22 — `updateThemeToolInput` removed. The kind-specific `update_theme`
559
+ // wrapper was replaced by the unified `set_structured_set` surface.
560
+ // v0.11.0 (#45) — theme moved out of structured_sets entirely into its
561
+ // own `themes` primitive (DTCG-shaped jsonb). Theme edits are now
562
+ // `set_theme_tokens` (loose-name patch) / `propose_create_theme`
563
+ // (gated mint) / `propose_activate_theme` (gated flip) — see
564
+ // packages/admin-core/src/ai/tools/{update-theme-tokens,get-theme,…}.ts.
459
565
  export const setTemplateLayoutToolInput = z
460
566
  .object({
461
567
  templateId: z.string().uuid(),
@@ -505,7 +611,7 @@ export const duplicatePageToolInput = z
505
611
  locale: z.string().min(2).max(10).optional(),
506
612
  })
507
613
  .strict();
508
- export const changeTemplateToolInput = z
614
+ export const repointPageTemplateToolInput = z
509
615
  .object({
510
616
  pageId: z.string().uuid(),
511
617
  newTemplateId: z.string().uuid(),
@@ -544,20 +650,6 @@ export const reorderModuleToolInput = z
544
650
  direction: z.union([z.enum(["up", "down"]), z.number().int().min(0).max(1000)]),
545
651
  })
546
652
  .strict();
547
- /**
548
- * Convenience wrapper over `set_structured_set` for the `nav-menu`
549
- * kind specifically. Users say "edit the menu", not "set the
550
- * structured set kind=nav-menu" — this maps natural language to the
551
- * right tool. Items shape matches `navMenuItem` from
552
- * @caelo-cms/shared/structured-sets.
553
- */
554
- export const setNavMenuToolInput = z
555
- .object({
556
- slug: slugInputSchema,
557
- displayName: z.string().min(1).max(200),
558
- items: z.array(z.unknown()),
559
- })
560
- .strict();
561
653
  /**
562
654
  * P7 — `find_media`. Searches the media library by alt-text /
563
655
  * filename / mime. Returns up to `limit` matches with the WebP-800
@@ -584,6 +676,22 @@ export const findMediaToolInput = z
584
676
  limit: z.number().int().min(1).max(50).default(15),
585
677
  })
586
678
  .strict();
679
+ /**
680
+ * run #10 D4 — `regenerate_media_variants`. Recovery tool for the
681
+ * staging generator's "media references unresolved" failure: re-runs
682
+ * the image pipeline for the named assets (or every asset with an
683
+ * incomplete ladder) and reports per-asset what was added or why
684
+ * nothing could be.
685
+ */
686
+ export const regenerateMediaVariantsToolInput = z
687
+ .object({
688
+ assetIds: z.array(z.string().uuid()).min(1).max(100).optional(),
689
+ allMissing: z.boolean().default(false),
690
+ })
691
+ .strict()
692
+ .refine((v) => (v.assetIds !== undefined) !== v.allMissing, {
693
+ message: "pass either assetIds or allMissing: true (exactly one)",
694
+ });
587
695
  /**
588
696
  * P7 — `set_media_alt`. AI may improve a11y on an existing asset
589
697
  * without a human round-trip (e.g. when the editor uploaded an image
@@ -596,6 +704,31 @@ export const setMediaAltToolInput = z
596
704
  alt: z.string().max(2048),
597
705
  })
598
706
  .strict();
707
+ /**
708
+ * 0184 — `set_home_page`. AI designates a page as the site homepage (per
709
+ * locale, the locale root). Records `locales.home_page_id`; the page keeps
710
+ * its own slug but resolves to `/`.
711
+ */
712
+ export const setHomePageToolInput = z
713
+ .object({
714
+ pageId: z.string().uuid(),
715
+ locale: z.string().min(2).max(10).optional(),
716
+ })
717
+ .strict();
718
+ /**
719
+ * 0181 — `set_media_source`. AI records an asset's provenance (source
720
+ * kind + origin detail) and licence when known. All fields but the
721
+ * assetId are optional; the op COALESCEs so omitted fields stay
722
+ * unchanged.
723
+ */
724
+ export const setMediaSourceToolInput = z
725
+ .object({
726
+ assetId: z.string().uuid(),
727
+ sourceKind: z.enum(["upload", "ai_generated", "imported", "external"]).optional(),
728
+ sourceDetail: z.string().max(2048).optional(),
729
+ license: z.string().max(200).optional(),
730
+ })
731
+ .strict();
599
732
  /**
600
733
  * P8 — `set_page_seo`. Manual / panel writes to the per-page SEO
601
734
  * sidecar. AI calls this only on explicit user intent
@@ -687,12 +820,6 @@ export const bulkCreateRedirectsToolInput = z
687
820
  * `status='published'`. Use the bulk form for "publish all drafts" or
688
821
  * any N>1 flip — saves N round-trips + N snapshot writes + N audit rows.
689
822
  */
690
- export const setPageStatusToolInput = z
691
- .object({
692
- pageId: z.string().uuid(),
693
- status: z.enum(["draft", "published"]),
694
- })
695
- .strict();
696
823
  export const setPagesStatusManyToolInput = z
697
824
  .object({
698
825
  pageIds: z.array(z.string().uuid()).min(1).max(200),
@@ -721,6 +848,7 @@ export const bulkOptimizeSeoToolInput = z
721
848
  context: z.string().max(4000).optional(),
722
849
  })
723
850
  .strict();
851
+ // v0.10.22 — `SetNavMenuToolInput` removed alongside `set_nav_menu` tool.
724
852
  /**
725
853
  * P9 — locales propose tool schemas. Per CLAUDE.md §11.A all four
726
854
  * write paths are TWO-STEP (AI proposes → Owner clicks Approve);
@@ -873,66 +1001,6 @@ export const bootstrapSiteScaffoldToolInput = z
873
1001
  setAsDefaults: z.boolean().optional(),
874
1002
  })
875
1003
  .strict();
876
- /**
877
- * v0.6.0 W4 (deferred) — composite tool. Creates a page and attaches N
878
- * modules to its content block in one tool call, mirroring the way the
879
- * AI naturally describes a multi-section page: a title + a series of
880
- * sections, each with its own HTML/CSS. The handler runs the chain
881
- * server-side so the AI doesn't have to orchestrate create_page +
882
- * N×add_module_to_page across N+1 round-trips.
883
- *
884
- * `sections[]` are placed on the block named in `blockName` (default
885
- * `content`) in the order given.
886
- */
887
- export const composePageFromSpecToolInput = z
888
- .object({
889
- /** Page identifiers (mirror create_page). */
890
- slug: z
891
- .string()
892
- .min(1)
893
- .max(120)
894
- .regex(/^[a-z0-9][a-z0-9-]*$/),
895
- name: z.string().min(1).max(256),
896
- title: z.string().min(1).max(256),
897
- locale: z.string().min(2).max(10).optional(),
898
- /** Template UUID. Optional — resolves to site_defaults if absent,
899
- * yielding the same nextAction recovery as create_page when missing. */
900
- templateId: z.string().uuid().optional(),
901
- status: z.enum(["draft", "published"]).optional(),
902
- /** Block to place the sections in. Defaults to `content`. */
903
- blockName: z.string().min(1).max(80).optional(),
904
- /** Sections to place, in order. Each becomes a freshly-created module. */
905
- sections: z
906
- .array(z.object({
907
- displayName: z.string().min(1).max(128),
908
- html: z.string().min(1).max(50_000),
909
- css: z.string().max(50_000).optional(),
910
- js: z.string().max(50_000).optional(),
911
- }))
912
- .min(1)
913
- .max(32),
914
- /**
915
- * v0.6.1 — optional SEO. When supplied, the composite calls
916
- * `pages_seo.autofill` after the page is created. When omitted,
917
- * the composite auto-derives a meta description from the page
918
- * title + first section's displayName (capped at the recommended
919
- * length). This is the "invisible-by-default" SEO step — caller
920
- * never needs a separate set_page_seo round-trip.
921
- *
922
- * `metaDescription` overrides the auto-derived fallback. Set
923
- * `skipSeo: true` to opt out entirely (e.g., stub pages where
924
- * SEO would be noise).
925
- */
926
- seo: z
927
- .object({
928
- metaDescription: z.string().min(1).max(320).optional(),
929
- ogImageAssetId: z.string().uuid().optional(),
930
- skipSeo: z.boolean().optional(),
931
- })
932
- .strict()
933
- .optional(),
934
- })
935
- .strict();
936
1004
  /**
937
1005
  * v0.6.0 W4 (deferred) — composite revert. Undoes every snapshot tagged
938
1006
  * with the given chat's branch_id. Per-entity revert ops handle the