@caelo-cms/shared 0.10.22 → 0.10.24

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 (249) hide show
  1. package/dist/ai-tools.d.ts +289 -273
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +342 -323
  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 +35 -0
  9. package/dist/base-css.d.ts.map +1 -0
  10. package/dist/base-css.js +40 -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-draft-shell.d.ts +21 -0
  29. package/dist/design-draft-shell.d.ts.map +1 -0
  30. package/dist/design-draft-shell.js +81 -0
  31. package/dist/design-draft-shell.js.map +1 -0
  32. package/dist/design-manifest.d.ts +36 -0
  33. package/dist/design-manifest.d.ts.map +1 -0
  34. package/dist/design-manifest.js +90 -0
  35. package/dist/design-manifest.js.map +1 -0
  36. package/dist/fonts.d.ts +89 -0
  37. package/dist/fonts.d.ts.map +1 -0
  38. package/dist/fonts.js +241 -0
  39. package/dist/fonts.js.map +1 -0
  40. package/dist/genesis-inventory.d.ts +32 -0
  41. package/dist/genesis-inventory.d.ts.map +1 -0
  42. package/dist/genesis-inventory.js +186 -0
  43. package/dist/genesis-inventory.js.map +1 -0
  44. package/dist/genesis.d.ts +102 -0
  45. package/dist/genesis.d.ts.map +1 -0
  46. package/dist/genesis.js +145 -0
  47. package/dist/genesis.js.map +1 -0
  48. package/dist/i18n.d.ts +29 -36
  49. package/dist/i18n.d.ts.map +1 -1
  50. package/dist/i18n.js +53 -128
  51. package/dist/i18n.js.map +1 -1
  52. package/dist/index.d.ts +28 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +28 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/interactions.d.ts +23 -0
  57. package/dist/interactions.d.ts.map +1 -0
  58. package/dist/interactions.js +44 -0
  59. package/dist/interactions.js.map +1 -0
  60. package/dist/media.d.ts +101 -16
  61. package/dist/media.d.ts.map +1 -1
  62. package/dist/media.js +126 -15
  63. package/dist/media.js.map +1 -1
  64. package/dist/page-log.d.ts +94 -0
  65. package/dist/page-log.d.ts.map +1 -0
  66. package/dist/page-log.js +111 -0
  67. package/dist/page-log.js.map +1 -0
  68. package/dist/preview-compose.d.ts +85 -12
  69. package/dist/preview-compose.d.ts.map +1 -1
  70. package/dist/preview-compose.js +157 -67
  71. package/dist/preview-compose.js.map +1 -1
  72. package/dist/proposal-status.d.ts +40 -0
  73. package/dist/proposal-status.d.ts.map +1 -0
  74. package/dist/proposal-status.js +34 -0
  75. package/dist/proposal-status.js.map +1 -0
  76. package/dist/responsive-images.d.ts +64 -0
  77. package/dist/responsive-images.d.ts.map +1 -0
  78. package/dist/responsive-images.js +98 -0
  79. package/dist/responsive-images.js.map +1 -0
  80. package/dist/safe-keys.d.ts +9 -0
  81. package/dist/safe-keys.d.ts.map +1 -0
  82. package/dist/safe-keys.js +20 -0
  83. package/dist/safe-keys.js.map +1 -0
  84. package/dist/seo.d.ts +11 -16
  85. package/dist/seo.d.ts.map +1 -1
  86. package/dist/seo.js +7 -23
  87. package/dist/seo.js.map +1 -1
  88. package/dist/skills.d.ts +14 -68
  89. package/dist/skills.d.ts.map +1 -1
  90. package/dist/skills.js +19 -113
  91. package/dist/skills.js.map +1 -1
  92. package/dist/strip-cdata.d.ts +7 -0
  93. package/dist/strip-cdata.d.ts.map +1 -0
  94. package/dist/strip-cdata.js +48 -0
  95. package/dist/strip-cdata.js.map +1 -0
  96. package/dist/structured-sets.d.ts +6 -52
  97. package/dist/structured-sets.d.ts.map +1 -1
  98. package/dist/structured-sets.js +6 -68
  99. package/dist/structured-sets.js.map +1 -1
  100. package/dist/subagents.d.ts +105 -3
  101. package/dist/subagents.d.ts.map +1 -1
  102. package/dist/subagents.js +224 -41
  103. package/dist/subagents.js.map +1 -1
  104. package/dist/template-engine.d.ts +85 -0
  105. package/dist/template-engine.d.ts.map +1 -0
  106. package/dist/template-engine.js +403 -0
  107. package/dist/template-engine.js.map +1 -0
  108. package/dist/theme-importers/auto-detect.d.ts +26 -0
  109. package/dist/theme-importers/auto-detect.d.ts.map +1 -0
  110. package/dist/theme-importers/auto-detect.js +42 -0
  111. package/dist/theme-importers/auto-detect.js.map +1 -0
  112. package/dist/theme-importers/css-comments.d.ts +12 -0
  113. package/dist/theme-importers/css-comments.d.ts.map +1 -0
  114. package/dist/theme-importers/css-comments.js +15 -0
  115. package/dist/theme-importers/css-comments.js.map +1 -0
  116. package/dist/theme-importers/dtcg.d.ts +46 -0
  117. package/dist/theme-importers/dtcg.d.ts.map +1 -0
  118. package/dist/theme-importers/dtcg.js +111 -0
  119. package/dist/theme-importers/dtcg.js.map +1 -0
  120. package/dist/theme-importers/loose.d.ts +3 -0
  121. package/dist/theme-importers/loose.d.ts.map +1 -0
  122. package/dist/theme-importers/loose.js +76 -0
  123. package/dist/theme-importers/loose.js.map +1 -0
  124. package/dist/theme-importers/shadcn.d.ts +24 -0
  125. package/dist/theme-importers/shadcn.d.ts.map +1 -0
  126. package/dist/theme-importers/shadcn.js +135 -0
  127. package/dist/theme-importers/shadcn.js.map +1 -0
  128. package/dist/theme-importers/style-dictionary.d.ts +17 -0
  129. package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
  130. package/dist/theme-importers/style-dictionary.js +125 -0
  131. package/dist/theme-importers/style-dictionary.js.map +1 -0
  132. package/dist/theme-importers/tailwind.d.ts +3 -0
  133. package/dist/theme-importers/tailwind.d.ts.map +1 -0
  134. package/dist/theme-importers/tailwind.js +218 -0
  135. package/dist/theme-importers/tailwind.js.map +1 -0
  136. package/dist/theme-literal-binding.d.ts +37 -0
  137. package/dist/theme-literal-binding.d.ts.map +1 -0
  138. package/dist/theme-literal-binding.js +138 -0
  139. package/dist/theme-literal-binding.js.map +1 -0
  140. package/dist/theme-normalize.d.ts +31 -0
  141. package/dist/theme-normalize.d.ts.map +1 -0
  142. package/dist/theme-normalize.js +587 -0
  143. package/dist/theme-normalize.js.map +1 -0
  144. package/dist/theme-ramp.d.ts +55 -0
  145. package/dist/theme-ramp.d.ts.map +1 -0
  146. package/dist/theme-ramp.js +149 -0
  147. package/dist/theme-ramp.js.map +1 -0
  148. package/dist/theme-render.d.ts +105 -0
  149. package/dist/theme-render.d.ts.map +1 -0
  150. package/dist/theme-render.js +441 -0
  151. package/dist/theme-render.js.map +1 -0
  152. package/dist/themes-errors.d.ts +109 -0
  153. package/dist/themes-errors.d.ts.map +1 -0
  154. package/dist/themes-errors.js +170 -0
  155. package/dist/themes-errors.js.map +1 -0
  156. package/dist/themes.d.ts +343 -0
  157. package/dist/themes.d.ts.map +1 -0
  158. package/dist/themes.js +697 -0
  159. package/dist/themes.js.map +1 -0
  160. package/dist/version.d.ts +7 -4
  161. package/dist/version.d.ts.map +1 -1
  162. package/dist/version.js +6 -3
  163. package/dist/version.js.map +1 -1
  164. package/package.json +10 -2
  165. package/src/__tests__/redos-hardening.test.ts +160 -0
  166. package/src/ai-tools-add-module-modes.test.ts +106 -0
  167. package/src/ai-tools-position.test.ts +134 -0
  168. package/src/ai-tools.test.ts +81 -0
  169. package/src/ai-tools.ts +1105 -0
  170. package/src/auth-forms.ts +36 -0
  171. package/src/base-css.ts +42 -0
  172. package/src/build-page.test.ts +228 -0
  173. package/src/build-page.ts +319 -0
  174. package/src/cap-failures.ts +67 -0
  175. package/src/content.test.ts +170 -0
  176. package/src/content.ts +620 -0
  177. package/src/context.ts +43 -0
  178. package/src/css-gradient-scan.ts +88 -0
  179. package/src/css-var-scan.test.ts +96 -0
  180. package/src/css-var-scan.ts +144 -0
  181. package/src/derive-module-type.test.ts +80 -0
  182. package/src/design-draft-shell.test.ts +85 -0
  183. package/src/design-draft-shell.ts +109 -0
  184. package/src/design-manifest.ts +93 -0
  185. package/src/fonts.test.ts +157 -0
  186. package/src/fonts.ts +296 -0
  187. package/src/genesis-inventory.test.ts +86 -0
  188. package/src/genesis-inventory.ts +215 -0
  189. package/src/genesis-sanitize.test.ts +35 -0
  190. package/src/genesis.ts +158 -0
  191. package/src/i18n.test.ts +58 -0
  192. package/src/i18n.ts +91 -0
  193. package/src/index.test.ts +10 -0
  194. package/src/index.ts +59 -0
  195. package/src/interactions.ts +48 -0
  196. package/src/logger.ts +147 -0
  197. package/src/media.test.ts +160 -0
  198. package/src/media.ts +355 -0
  199. package/src/page-log.test.ts +163 -0
  200. package/src/page-log.ts +124 -0
  201. package/src/preview-compose.test.ts +637 -0
  202. package/src/preview-compose.ts +656 -0
  203. package/src/preview-scanner.test.ts +96 -0
  204. package/src/preview-scanner.ts +214 -0
  205. package/src/proposal-status.test.ts +69 -0
  206. package/src/proposal-status.ts +40 -0
  207. package/src/responsive-images.test.ts +104 -0
  208. package/src/responsive-images.ts +151 -0
  209. package/src/result.ts +29 -0
  210. package/src/safe-keys.ts +21 -0
  211. package/src/seo.test.ts +194 -0
  212. package/src/seo.ts +233 -0
  213. package/src/skills.ts +48 -0
  214. package/src/snapshots.test.ts +80 -0
  215. package/src/snapshots.ts +81 -0
  216. package/src/strip-cdata.test.ts +41 -0
  217. package/src/strip-cdata.ts +50 -0
  218. package/src/structured-sets.ts +114 -0
  219. package/src/subagents.test.ts +262 -0
  220. package/src/subagents.ts +432 -0
  221. package/src/template-engine.test.ts +379 -0
  222. package/src/template-engine.ts +520 -0
  223. package/src/theme-gradient.test.ts +92 -0
  224. package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
  225. package/src/theme-importers/auto-detect.ts +84 -0
  226. package/src/theme-importers/css-comments.ts +15 -0
  227. package/src/theme-importers/dtcg.ts +106 -0
  228. package/src/theme-importers/loose.ts +76 -0
  229. package/src/theme-importers/shadcn.ts +133 -0
  230. package/src/theme-importers/style-dictionary.ts +125 -0
  231. package/src/theme-importers/tailwind.ts +217 -0
  232. package/src/theme-literal-binding.test.ts +71 -0
  233. package/src/theme-literal-binding.ts +159 -0
  234. package/src/theme-motion.test.ts +115 -0
  235. package/src/theme-normalize-envelope.test.ts +43 -0
  236. package/src/theme-normalize-gradient.test.ts +135 -0
  237. package/src/theme-normalize.ts +661 -0
  238. package/src/theme-ramp.ts +187 -0
  239. package/src/theme-render-sanitize.test.ts +45 -0
  240. package/src/theme-render.test.ts +119 -0
  241. package/src/theme-render.ts +487 -0
  242. package/src/theme-shadow.test.ts +56 -0
  243. package/src/themes-errors.ts +199 -0
  244. package/src/themes.ts +842 -0
  245. package/src/version.ts +66 -0
  246. package/dist/translation.d.ts +0 -127
  247. package/dist/translation.d.ts.map +0 -1
  248. package/dist/translation.js +0 -208
  249. package/dist/translation.js.map +0 -1
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,57 +517,38 @@ 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
- // v0.10.22 — added "language-selector" to match @caelo-cms/shared
410
548
  // structuredSetKind (the 6th kind that was previously unreachable
411
549
  // from any AI tool — the kind-specific wrappers covered only
412
550
  // nav-menu + theme).
413
- kind: z.enum(["nav-menu", "taxonomy", "theme", "tags", "link-list", "language-selector"]),
551
+ kind: z.enum(["nav-menu", "taxonomy", "tags", "link-list"]),
414
552
  slug: slugInputSchema,
415
553
  displayName: z.string().min(1).max(200),
416
554
  items: z.array(z.unknown()),
@@ -418,48 +556,11 @@ export const setStructuredSetToolInput = z
418
556
  .strict();
419
557
  // v0.10.22 — `updateThemeToolInput` removed. The kind-specific `update_theme`
420
558
  // wrapper was replaced by the unified `set_structured_set` surface.
421
- // Partial token updates (merge by token) are now expressed as the
422
- // two-step AI workflow: `get_structured_set({kind:'theme', slug:'site'})`
423
- // to read existing items, mutate in JS, then `set_structured_set` with
424
- // the merged array.
425
- /**
426
- * P6.7.6 — layout-layer tools. Layouts are site-wide chrome (header /
427
- * footer / nav) that wraps every page on every template bound to the
428
- * layout. `add_module_to_layout` reaches every page across the site
429
- * with one call; `set_template_layout` re-points a template's chrome.
430
- * `create_layout` and `set_site_defaults` are Owner-only at the op
431
- * level — AI calls reject with ActorScopeRejected and the chat surfaces
432
- * the permission requirement.
433
- */
434
- export const addModuleToLayoutToolInput = z
435
- .object({
436
- layoutSlug: slugInputSchema,
437
- blockName: z.string().min(1).max(80),
438
- position: positionInputSchema,
439
- displayName: z.string().min(1).max(128),
440
- html: z.string().min(1).max(50_000),
441
- css: z.string().max(50_000).optional(),
442
- js: z.string().max(50_000).optional(),
443
- /** v0.5.21 — see addModuleToPageToolInput.fields for context. */
444
- fields: z
445
- .array(z
446
- .object({
447
- name: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/),
448
- kind: z.enum(["text", "richtext", "url", "image", "number", "boolean", "link"]),
449
- label: z.string().min(1).max(128),
450
- default: z.unknown().optional(),
451
- })
452
- .strict())
453
- .max(64)
454
- .optional(),
455
- })
456
- .strict();
457
- export const removeModuleFromLayoutToolInput = z
458
- .object({
459
- layoutSlug: slugInputSchema,
460
- moduleId: z.string().uuid(),
461
- })
462
- .strict();
559
+ // v0.11.0 (#45) — theme moved out of structured_sets entirely into its
560
+ // own `themes` primitive (DTCG-shaped jsonb). Theme edits are now
561
+ // `set_theme_tokens` (loose-name patch) / `propose_create_theme`
562
+ // (gated mint) / `propose_activate_theme` (gated flip) — see
563
+ // packages/admin-core/src/ai/tools/{update-theme-tokens,get-theme,…}.ts.
463
564
  export const setTemplateLayoutToolInput = z
464
565
  .object({
465
566
  templateId: z.string().uuid(),
@@ -509,7 +610,7 @@ export const duplicatePageToolInput = z
509
610
  locale: z.string().min(2).max(10).optional(),
510
611
  })
511
612
  .strict();
512
- export const changeTemplateToolInput = z
613
+ export const repointPageTemplateToolInput = z
513
614
  .object({
514
615
  pageId: z.string().uuid(),
515
616
  newTemplateId: z.string().uuid(),
@@ -574,6 +675,22 @@ export const findMediaToolInput = z
574
675
  limit: z.number().int().min(1).max(50).default(15),
575
676
  })
576
677
  .strict();
678
+ /**
679
+ * run #10 D4 — `regenerate_media_variants`. Recovery tool for the
680
+ * staging generator's "media references unresolved" failure: re-runs
681
+ * the image pipeline for the named assets (or every asset with an
682
+ * incomplete ladder) and reports per-asset what was added or why
683
+ * nothing could be.
684
+ */
685
+ export const regenerateMediaVariantsToolInput = z
686
+ .object({
687
+ assetIds: z.array(z.string().uuid()).min(1).max(100).optional(),
688
+ allMissing: z.boolean().default(false),
689
+ })
690
+ .strict()
691
+ .refine((v) => (v.assetIds !== undefined) !== v.allMissing, {
692
+ message: "pass either assetIds or allMissing: true (exactly one)",
693
+ });
577
694
  /**
578
695
  * P7 — `set_media_alt`. AI may improve a11y on an existing asset
579
696
  * without a human round-trip (e.g. when the editor uploaded an image
@@ -586,6 +703,31 @@ export const setMediaAltToolInput = z
586
703
  alt: z.string().max(2048),
587
704
  })
588
705
  .strict();
706
+ /**
707
+ * 0184 — `set_home_page`. AI designates a page as the site homepage (per
708
+ * locale, the locale root). Records `locales.home_page_id`; the page keeps
709
+ * its own slug but resolves to `/`.
710
+ */
711
+ export const setHomePageToolInput = z
712
+ .object({
713
+ pageId: z.string().uuid(),
714
+ locale: z.string().min(2).max(10).optional(),
715
+ })
716
+ .strict();
717
+ /**
718
+ * 0181 — `set_media_source`. AI records an asset's provenance (source
719
+ * kind + origin detail) and licence when known. All fields but the
720
+ * assetId are optional; the op COALESCEs so omitted fields stay
721
+ * unchanged.
722
+ */
723
+ export const setMediaSourceToolInput = z
724
+ .object({
725
+ assetId: z.string().uuid(),
726
+ sourceKind: z.enum(["upload", "ai_generated", "imported", "external"]).optional(),
727
+ sourceDetail: z.string().max(2048).optional(),
728
+ license: z.string().max(200).optional(),
729
+ })
730
+ .strict();
589
731
  /**
590
732
  * P8 — `set_page_seo`. Manual / panel writes to the per-page SEO
591
733
  * sidecar. AI calls this only on explicit user intent
@@ -677,12 +819,6 @@ export const bulkCreateRedirectsToolInput = z
677
819
  * `status='published'`. Use the bulk form for "publish all drafts" or
678
820
  * any N>1 flip — saves N round-trips + N snapshot writes + N audit rows.
679
821
  */
680
- export const setPageStatusToolInput = z
681
- .object({
682
- pageId: z.string().uuid(),
683
- status: z.enum(["draft", "published"]),
684
- })
685
- .strict();
686
822
  export const setPagesStatusManyToolInput = z
687
823
  .object({
688
824
  pageIds: z.array(z.string().uuid()).min(1).max(200),
@@ -712,63 +848,6 @@ export const bulkOptimizeSeoToolInput = z
712
848
  })
713
849
  .strict();
714
850
  // v0.10.22 — `SetNavMenuToolInput` removed alongside `set_nav_menu` tool.
715
- /**
716
- * P9 — locales propose tool schemas. Per CLAUDE.md §11.A all four
717
- * write paths are TWO-STEP (AI proposes → Owner clicks Approve);
718
- * the AI cannot bypass the click.
719
- */
720
- const localeCodeToolSchema = z
721
- .string()
722
- .min(2)
723
- .max(10)
724
- .regex(/^[a-z]{2,3}(-[A-Za-z]{2,4})?$/, "BCP-47 like 'en' or 'de-AT'");
725
- const urlStrategyToolSchema = z.enum(["none", "subdirectory", "subdomain", "domain"]);
726
- export const proposeAddLocaleToolInput = z
727
- .object({
728
- code: localeCodeToolSchema,
729
- displayName: z.string().min(1).max(120),
730
- urlStrategy: urlStrategyToolSchema.default("subdirectory"),
731
- urlHost: z.string().min(1).max(253).nullable().optional(),
732
- })
733
- .strict();
734
- export const proposeRemoveLocaleToolInput = z.object({ code: localeCodeToolSchema }).strict();
735
- export const proposeSetDefaultLocaleToolInput = z.object({ code: localeCodeToolSchema }).strict();
736
- export const proposeUpdateLocaleStrategyToolInput = z
737
- .object({
738
- code: localeCodeToolSchema,
739
- urlStrategy: urlStrategyToolSchema,
740
- urlHost: z.string().min(1).max(253).nullable().optional(),
741
- })
742
- .strict();
743
- /**
744
- * P10 — translation tool inputs. `translate_page` auto-dispatches
745
- * Mode 1 / Mode 2 based on the variant's existing status — the AI
746
- * sees one verb regardless of state. `start_translation_job` queues
747
- * a bulk run.
748
- */
749
- export const translatePageToolInput = z
750
- .object({
751
- pageId: z.string().uuid(),
752
- targetLocale: localeCodeToolSchema,
753
- })
754
- .strict();
755
- const translationJobScopeTool = z.discriminatedUnion("kind", [
756
- z.object({ kind: z.literal("all-stale") }).strict(),
757
- z.object({ kind: z.literal("page"), pageId: z.string().uuid() }).strict(),
758
- z.object({ kind: z.literal("locale"), code: localeCodeToolSchema }).strict(),
759
- z
760
- .object({
761
- kind: z.literal("pages"),
762
- pageIds: z.array(z.string().uuid()).min(1).max(500),
763
- })
764
- .strict(),
765
- ]);
766
- export const startTranslationJobToolInput = z
767
- .object({
768
- scope: translationJobScopeTool,
769
- capMicrocents: z.number().int().nonnegative().nullable().optional(),
770
- })
771
- .strict();
772
851
  /**
773
852
  * P10A — `propose_skill`. AI drafts a new skill body (or revision) and
774
853
  * queues it for Owner review. Per CLAUDE.md §2: skills augment the AI's
@@ -864,66 +943,6 @@ export const bootstrapSiteScaffoldToolInput = z
864
943
  setAsDefaults: z.boolean().optional(),
865
944
  })
866
945
  .strict();
867
- /**
868
- * v0.6.0 W4 (deferred) — composite tool. Creates a page and attaches N
869
- * modules to its content block in one tool call, mirroring the way the
870
- * AI naturally describes a multi-section page: a title + a series of
871
- * sections, each with its own HTML/CSS. The handler runs the chain
872
- * server-side so the AI doesn't have to orchestrate create_page +
873
- * N×add_module_to_page across N+1 round-trips.
874
- *
875
- * `sections[]` are placed on the block named in `blockName` (default
876
- * `content`) in the order given.
877
- */
878
- export const composePageFromSpecToolInput = z
879
- .object({
880
- /** Page identifiers (mirror create_page). */
881
- slug: z
882
- .string()
883
- .min(1)
884
- .max(120)
885
- .regex(/^[a-z0-9][a-z0-9-]*$/),
886
- name: z.string().min(1).max(256),
887
- title: z.string().min(1).max(256),
888
- locale: z.string().min(2).max(10).optional(),
889
- /** Template UUID. Optional — resolves to site_defaults if absent,
890
- * yielding the same nextAction recovery as create_page when missing. */
891
- templateId: z.string().uuid().optional(),
892
- status: z.enum(["draft", "published"]).optional(),
893
- /** Block to place the sections in. Defaults to `content`. */
894
- blockName: z.string().min(1).max(80).optional(),
895
- /** Sections to place, in order. Each becomes a freshly-created module. */
896
- sections: z
897
- .array(z.object({
898
- displayName: z.string().min(1).max(128),
899
- html: z.string().min(1).max(50_000),
900
- css: z.string().max(50_000).optional(),
901
- js: z.string().max(50_000).optional(),
902
- }))
903
- .min(1)
904
- .max(32),
905
- /**
906
- * v0.6.1 — optional SEO. When supplied, the composite calls
907
- * `pages_seo.autofill` after the page is created. When omitted,
908
- * the composite auto-derives a meta description from the page
909
- * title + first section's displayName (capped at the recommended
910
- * length). This is the "invisible-by-default" SEO step — caller
911
- * never needs a separate set_page_seo round-trip.
912
- *
913
- * `metaDescription` overrides the auto-derived fallback. Set
914
- * `skipSeo: true` to opt out entirely (e.g., stub pages where
915
- * SEO would be noise).
916
- */
917
- seo: z
918
- .object({
919
- metaDescription: z.string().min(1).max(320).optional(),
920
- ogImageAssetId: z.string().uuid().optional(),
921
- skipSeo: z.boolean().optional(),
922
- })
923
- .strict()
924
- .optional(),
925
- })
926
- .strict();
927
946
  /**
928
947
  * v0.6.0 W4 (deferred) — composite revert. Undoes every snapshot tagged
929
948
  * with the given chat's branch_id. Per-entity revert ops handle the