@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
@@ -0,0 +1,1179 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Zod schemas for the AI tools shipped in P5. Lives in @caelo-cms/shared so
5
+ * the provider abstraction (which streams tool-call args from the LLM)
6
+ * and the tool dispatcher (which validates + invokes the handler)
7
+ * import from a single source.
8
+ *
9
+ * `.strict()` on every input — the LLM occasionally hallucinates fields
10
+ * and we want a typed rejection at the Validator boundary, not silent
11
+ * silent drops in the handler.
12
+ */
13
+
14
+ import { z } from "zod";
15
+ import {
16
+ contentInstanceCreateSchema,
17
+ contentInstanceDeleteSchema,
18
+ contentInstanceUpdateSchema,
19
+ forkPlacementContentSchema,
20
+ MODULE_CSS_MAX,
21
+ MODULE_HTML_MAX,
22
+ MODULE_JS_MAX,
23
+ moduleFieldSchema,
24
+ setPlacementContentSchema,
25
+ slugSchema,
26
+ } from "./content.js";
27
+
28
+ /**
29
+ * v0.6.2 — `position` argument shared across the three `add_module_to_*`
30
+ * tools. Accepts either the literal string `"top"` / `"bottom"` OR a
31
+ * 0..1000 integer index.
32
+ *
33
+ * The integer branch is `z.preprocess`-wrapped to coerce digit-only
34
+ * strings (`"0"`, `"42"`) to numbers before the int check. The AI
35
+ * recurrently passes JSON-quoted numbers (`"0"`) even though the tool
36
+ * description explicitly says to use bare integers; the coercion
37
+ * removes that ergonomic gotcha while still rejecting non-numeric
38
+ * strings (`"abc"` → still fails the int check cleanly).
39
+ *
40
+ * issue #106 (step-13 round-6) — an OUTER preprocess strips one layer of
41
+ * surrounding matching quotes first. The model occasionally over-quotes the
42
+ * literal: it emitted `position` as the JSON string `"\"bottom\""` (the
43
+ * 8-char value including the quote characters), which matched neither union
44
+ * branch and failed with a bare `Invalid input`. Unwrapping `"bottom"` →
45
+ * `bottom` (and `"\"0\""` → `0`, which the integer branch then coerces) turns
46
+ * that recurring first-call rejection into a clean accept. This is input
47
+ * NORMALISATION at the boundary (decoding a malformed encoding of a value the
48
+ * caller did supply), not a missing-data fallback — `undefined`/`null`/`""`
49
+ * still fall through to a loud rejection, per CLAUDE.md §2.
50
+ */
51
+ function stripSurroundingQuotes(v: unknown): unknown {
52
+ if (typeof v !== "string") return v;
53
+ // Only when BOTH ends are the same quote char and there is at least one
54
+ // inner character — so a bare `"bottom"` / `"0"` is untouched and an empty
55
+ // `""` is not collapsed into a match here.
56
+ const m = v.match(/^(["'])([\s\S]+)\1$/);
57
+ return m ? m[2] : v;
58
+ }
59
+ export const positionInputSchema = z.preprocess(
60
+ stripSurroundingQuotes,
61
+ z.union([
62
+ z.enum(["top", "bottom"]),
63
+ z.preprocess(
64
+ (v) => (typeof v === "string" && /^\d+$/.test(v) ? Number.parseInt(v, 10) : v),
65
+ z.number().int().min(0).max(1000),
66
+ ),
67
+ ]),
68
+ );
69
+
70
+ export const editModuleToolInput = z
71
+ .object({
72
+ moduleId: z.string().uuid(),
73
+ displayName: z.string().min(1).max(128).optional(),
74
+ /**
75
+ * v0.12.0 — rewrite the module's purpose. Optional; passing it
76
+ * updates the `## Modules` block your future self will read.
77
+ */
78
+ description: z.string().max(1000).optional(),
79
+ /** v0.12.0 — re-classify the module's role tag. */
80
+ kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
81
+ /**
82
+ * v0.12.3 (issue #106) — re-classify the module's stable `type` (the
83
+ * class a parent's `allowedModuleTypes` whitelist matches against,
84
+ * e.g. `button`). Rarely needed; usually derived from displayName at
85
+ * create time. Set it to make this module satisfy a parent's allowlist.
86
+ */
87
+ type: slugSchema.optional(),
88
+ html: z.string().max(MODULE_HTML_MAX).optional(),
89
+ css: z.string().max(MODULE_CSS_MAX).optional(),
90
+ js: z.string().max(MODULE_JS_MAX).optional(),
91
+ /**
92
+ * v0.4.0 — module field schema. Each field declares one substitution
93
+ * slot referenced in the module HTML as `{{name}}`. Page placements
94
+ * fill these via `set_page_module_content`. Optional on edits — pass
95
+ * to replace the declared schema.
96
+ */
97
+ fields: z.array(moduleFieldSchema).max(64).optional(),
98
+ /** issue #164 slice 2 — see addModuleToolInput.bindThemeLiterals. */
99
+ bindThemeLiterals: z.boolean().optional(),
100
+ })
101
+ .strict();
102
+
103
+ /**
104
+ * v0.4.0 — `set_page_module_content` AI tool. Fills the content values for
105
+ * a module placement on a specific page. Page-bound + branch-isolated per
106
+ * chat until publish (unlike `edit_module` which is global + immediate).
107
+ */
108
+ export const setPageModuleContentToolInput = z
109
+ .object({
110
+ pageId: z.string().uuid(),
111
+ blockName: z.string().min(1).max(64),
112
+ position: z.number().int().nonnegative(),
113
+ /** Map of module field name → value. Fully replaces existing values. */
114
+ contentValues: z.record(z.string(), z.unknown()),
115
+ })
116
+ .strict();
117
+ export type SetPageModuleContentToolInput = z.infer<typeof setPageModuleContentToolInput>;
118
+
119
+ export const siteMemoryProposeToolInput = z
120
+ .object({
121
+ slot: z.enum(["purpose", "brand-voice", "tone", "banned-phrases", "instructions", "glossary"]),
122
+ body: z.string().min(1).max(4000),
123
+ rationale: z.string().min(1).max(1000),
124
+ })
125
+ .strict();
126
+
127
+ /**
128
+ * Progressive-disclosure skill loading. The `## Skills` system-prompt block
129
+ * lists each active skill's slug + description; when a task matches one or
130
+ * more, the model calls `load_skill({slugs})` to pull their full instructions
131
+ * into the conversation (they persist as a tool result for the rest of the
132
+ * chat, so each skill loads at most once).
133
+ *
134
+ * Takes a LIST, not a single slug, per CLAUDE.md §11: a load is a routine
135
+ * operation and every routine operation ships in bulk form, with n=1 as its
136
+ * smallest case rather than a separate singular tool. Loading three skills was
137
+ * three provider round-trips whose only output was one tool call each.
138
+ *
139
+ * Capped at 5: skills are task-scoped, and a model that wants more than a
140
+ * handful at once has almost certainly matched the index too loosely — each
141
+ * body is a sizeable injection into history.
142
+ */
143
+ export const loadSkillToolInput = z
144
+ .object({
145
+ slugs: z
146
+ .array(
147
+ z
148
+ .string()
149
+ .min(1)
150
+ .max(120)
151
+ .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"),
152
+ )
153
+ .min(1)
154
+ .max(5),
155
+ })
156
+ .strict();
157
+ export type LoadSkillToolInput = z.infer<typeof loadSkillToolInput>;
158
+
159
+ /**
160
+ * The set of tools shipped in P5. Other phases extend by adding a new
161
+ * entry; the dispatcher walks this map at registration time.
162
+ */
163
+ /**
164
+ * The ONE `add_module` input — consolidates add_module_to_{page,layout,template}
165
+ * (audit #2). `target` picks the destination; `targetRef` is a slug OR a uuid
166
+ * (resolved server-side — a slug is friendlier for the AI to hold than a uuid).
167
+ * Same two modes as before: `moduleId` reuses an existing module (now for ALL
168
+ * three targets — layout used to be mint-only), or the authoring fields mint a
169
+ * new one (raw html is fine — moduleize turns it into a proper module). Exactly
170
+ * one mode, enforced by the shared superRefine.
171
+ */
172
+ export const addModuleToolInput = z
173
+ .object({
174
+ target: z.enum(["page", "layout", "template"]),
175
+ /** Slug or uuid of the page / layout / template. Resolved server-side. */
176
+ targetRef: z.string().min(1).max(200),
177
+ blockName: z.string().min(1).max(80),
178
+ position: positionInputSchema,
179
+ /** Reuse an existing module by id (any target). Mutually exclusive with the
180
+ * authoring fields. */
181
+ moduleId: z.string().uuid().optional(),
182
+ displayName: z.string().min(1).max(128).optional(),
183
+ description: z.string().max(1000).optional(),
184
+ kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
185
+ type: slugSchema.optional(),
186
+ html: z.string().min(1).max(50_000).optional(),
187
+ css: z.string().max(50_000).optional(),
188
+ js: z.string().max(50_000).optional(),
189
+ fields: z.array(moduleFieldSchema).max(64).optional(),
190
+ bindThemeLiterals: z.boolean().optional(),
191
+ /**
192
+ * The placement's initial content, applied IN THE SAME CALL (2026-07
193
+ * — a freshly minted module must never land empty and wait for a
194
+ * second round-trip). ONE rule for every target: content goes in
195
+ * `values`. target='page': fills the placement's content_instance.
196
+ * target='template': every fanned-out placement gets these values.
197
+ * target='layout': chrome has no content_instance, so the handler
198
+ * stores the values as the minted module's `fields[].default` —
199
+ * which requires explicit `fields` (moduleize field names are
200
+ * unknowable up front) and is rejected for `moduleId` reuse (a
201
+ * shared module renders its stored defaults; edit_module changes
202
+ * them).
203
+ */
204
+ values: z.record(z.string(), z.unknown()).optional(),
205
+ })
206
+ .strict()
207
+ .superRefine((input, ctx) => {
208
+ if (input.target === "layout" && input.values !== undefined) {
209
+ if (input.moduleId !== undefined) {
210
+ ctx.addIssue({
211
+ code: z.ZodIssueCode.custom,
212
+ message:
213
+ "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.",
214
+ });
215
+ } else if (input.fields === undefined || input.fields.length === 0) {
216
+ ctx.addIssue({
217
+ code: z.ZodIssueCode.custom,
218
+ message:
219
+ "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).",
220
+ });
221
+ } else {
222
+ const known = new Set(input.fields.map((f) => f.name));
223
+ const unknown = Object.keys(input.values).filter((k) => !known.has(k));
224
+ if (unknown.length > 0) {
225
+ ctx.addIssue({
226
+ code: z.ZodIssueCode.custom,
227
+ message: `\`values\` keys [${unknown.join(", ")}] match no declared field — declared: [${[...known].join(", ")}].`,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ // `moduleId` is PLACEMENT-ONLY (see the buildPageModuleSchema note): a
233
+ // placement that CAN succeed must not fail over an extra authoring field
234
+ // (§1A/§11), so we do NOT reject a moduleId call that also carries authoring
235
+ // fields. Those fields are not applied here; the tool HANDLER surfaces an
236
+ // INFO for any ignored field whose value DIFFERS from the module's stored
237
+ // one, pointing at edit_module — so a dropped change is never silent (§2).
238
+ if (input.moduleId !== undefined) return;
239
+ if (input.displayName === undefined || input.html === undefined) {
240
+ ctx.addIssue({
241
+ code: z.ZodIssueCode.custom,
242
+ message:
243
+ "Pass either `moduleId` (place an existing module from ## Modules) " +
244
+ "or `displayName` + `html` (mint a new module).",
245
+ });
246
+ }
247
+ // 2026-07 — a freshly minted module must carry its initial copy IN
248
+ // THIS call, via `values` on every target (`fields[].default` also
249
+ // satisfies it — that's where layout values land anyway). Without
250
+ // either, the placement rendered EMPTY until a second round-trip —
251
+ // the exact two-step build_page was created to kill. Only enforced
252
+ // when the caller authored explicit fields; the raw-HTML path goes
253
+ // through moduleize, which enforces defaults itself.
254
+ if (
255
+ input.fields !== undefined &&
256
+ input.fields.length > 0 &&
257
+ input.values === undefined &&
258
+ !input.fields.some((f) => "default" in f && f.default !== undefined)
259
+ ) {
260
+ ctx.addIssue({
261
+ code: z.ZodIssueCode.custom,
262
+ message:
263
+ "A minted module needs its initial content IN THIS CALL: pass `values` " +
264
+ "({fieldName: value, …}) for this placement, or give the fields `default`s. " +
265
+ "Without either, the module renders empty placeholders until a second call.",
266
+ });
267
+ }
268
+ });
269
+ export type AddModuleToolInput = z.infer<typeof addModuleToolInput>;
270
+
271
+ /**
272
+ * v0.2.16 — `add_plugin_to_page` AI tool. Inserts a plugin's
273
+ * `<div data-caelo-plugin>` placeholder into a page's block via a
274
+ * synthetic module. The placeholder is what the static-generator's
275
+ * plugin pass (apps/static-generator/src/plugin-pass.ts) replaces
276
+ * with the plugin's `staticRender` output at deploy time, and what
277
+ * the plugin's Web Component hydrates against on the client. The
278
+ * tool resolves `plugin-host`'s in-memory registry to confirm the
279
+ * plugin is loaded + active before creating the module.
280
+ */
281
+ export const addPluginToPageToolInput = z
282
+ .object({
283
+ pageId: z.string().uuid(),
284
+ pluginSlug: z.string().min(1).max(80),
285
+ blockName: z.string().min(1).max(80),
286
+ /** "top" | "bottom" | a 0-based integer index. */
287
+ position: positionInputSchema,
288
+ })
289
+ .strict();
290
+
291
+ // ─── v0.12.0 — content_instances + placement AI tools ─────────────────
292
+
293
+ /**
294
+ * `list_content_instances` — browse the content library. The `placementCount`
295
+ * returned per row is a blast-radius affordance: the AI can decide whether
296
+ * to edit a shared instance (propagates everywhere) or fork it first.
297
+ */
298
+ export const listContentInstancesToolInput = z
299
+ .object({
300
+ moduleId: z.string().uuid().optional(),
301
+ slug: z.string().min(1).max(64).optional(),
302
+ /** Free-text matches against displayName + slug. Case-insensitive substring. */
303
+ search: z.string().min(1).max(128).optional(),
304
+ /** Narrow to "instances referenced from this page" for chat-runner planning. */
305
+ pageId: z.string().uuid().optional(),
306
+ })
307
+ .strict();
308
+
309
+ export const getContentInstanceToolInput = z
310
+ .object({
311
+ id: z.string().uuid(),
312
+ })
313
+ .strict();
314
+
315
+ export const createContentInstanceToolInput = contentInstanceCreateSchema;
316
+ export const setContentInstanceValuesToolInput = contentInstanceUpdateSchema;
317
+ export const deleteContentInstanceToolInput = contentInstanceDeleteSchema;
318
+ export const setPlacementContentToolInput = setPlacementContentSchema;
319
+ export const forkPlacementContentToolInput = forkPlacementContentSchema;
320
+
321
+ export const AI_TOOLS = [
322
+ "edit_module",
323
+ "site_memory_propose",
324
+ "add_module",
325
+ "create_page",
326
+ // audit #3 — page metadata (name/title/slug/template/status) is one tool for
327
+ // 1..200 pages: `update_pages_many`. The former rename_page / set_page_title
328
+ // / change_page_slug were single-field wrappers over the same pages.update.
329
+ "update_pages_many",
330
+ // audit #4 — one delete tool for 1..200 pages, disposition per page.
331
+ // The former singular delete_page folded in (n=1 is a one-item array).
332
+ "delete_pages_many",
333
+ // one remove-module tool routed by target (page|layout).
334
+ "remove_module_from",
335
+ "set_structured_set",
336
+ "update_theme",
337
+ "set_template_layout",
338
+ "create_layout",
339
+ "set_site_defaults",
340
+ "duplicate_page",
341
+ "repoint_page_template",
342
+ "move_module",
343
+ "reorder_module",
344
+ "set_nav_menu",
345
+ "add_plugin_to_page",
346
+ // v0.12.0 — content_instances + placement binding
347
+ "list_content_instances",
348
+ "get_content_instance",
349
+ "create_content_instance",
350
+ "set_content_instance_values",
351
+ "delete_content_instance",
352
+ "set_placement_content",
353
+ "fork_placement_content",
354
+ // issue #299 — bulk-first build path (§11)
355
+ "build_page",
356
+ "create_content_instances",
357
+ "set_page_module_content_many",
358
+ ] as const;
359
+ export type AiToolName = (typeof AI_TOOLS)[number];
360
+ export type AddPluginToPageToolInput = z.infer<typeof addPluginToPageToolInput>;
361
+ export type ListContentInstancesToolInput = z.infer<typeof listContentInstancesToolInput>;
362
+ export type GetContentInstanceToolInput = z.infer<typeof getContentInstanceToolInput>;
363
+ export type CreateContentInstanceToolInput = z.infer<typeof createContentInstanceToolInput>;
364
+ export type SetContentInstanceValuesToolInput = z.infer<typeof setContentInstanceValuesToolInput>;
365
+ export type DeleteContentInstanceToolInput = z.infer<typeof deleteContentInstanceToolInput>;
366
+ export type SetPlacementContentToolInput = z.infer<typeof setPlacementContentToolInput>;
367
+ export type ForkPlacementContentToolInput = z.infer<typeof forkPlacementContentToolInput>;
368
+
369
+ /** Chat ops input shapes — used by the SvelteKit form actions. */
370
+ export const chatCreateSessionInput = z
371
+ .object({
372
+ title: z.string().min(1).max(200).optional(),
373
+ /** P6.7.4 — bind the new chat to one page (live-edit surface). */
374
+ pageId: z.string().uuid().optional(),
375
+ /** P6.7.4 — bind the new chat to one template (template editor). */
376
+ templateId: z.string().uuid().optional(),
377
+ /** P10.5 — when set, this is an ephemeral subagent session; sidebar filters it out. */
378
+ subagentRole: z.string().min(1).max(120).optional(),
379
+ /** P10.5 — parent chat session id for subagent attribution (audit trail). */
380
+ parentChatSessionId: z.string().uuid().nullable().optional(),
381
+ })
382
+ .strict();
383
+
384
+ /**
385
+ * issue #190 — an operator-attached image riding a user chat message.
386
+ * References a media_assets row (the upload endpoint owns validation
387
+ * + storage); image/* only in v1 so the provider mapping is always an
388
+ * image part.
389
+ */
390
+ export const chatAttachmentSchema = z
391
+ .object({
392
+ /** Set for an operator upload — a `media_assets` row. */
393
+ assetId: z.string().uuid().optional(),
394
+ /**
395
+ * Set for an AI-produced chat image — an object-store key under
396
+ * `CHAT_IMAGE_PREFIX`. Deliberately not a media asset: see the prefix's
397
+ * doc comment. Exactly one of `assetId` / `storageKey` is present.
398
+ */
399
+ storageKey: z.string().min(1).max(512).optional(),
400
+ mime: z.enum(["image/png", "image/jpeg", "image/webp", "image/gif"]),
401
+ alt: z.string().max(2048).optional(),
402
+ })
403
+ .strict()
404
+ .refine((a) => (a.assetId === undefined) !== (a.storageKey === undefined), {
405
+ message: "exactly one of assetId (operator upload) or storageKey (chat image) must be set",
406
+ });
407
+ export type ChatAttachment = z.infer<typeof chatAttachmentSchema>;
408
+
409
+ /** issue #190 — attachments-per-message cap; bounds provider payload size. */
410
+ export const CHAT_MAX_ATTACHMENTS = 4;
411
+
412
+ export const chatSendMessageInput = z
413
+ .object({
414
+ chatSessionId: z.string().uuid(),
415
+ // Optional as of Plan B: a resume turn (see `resumeApproval`) carries no
416
+ // operator content — it re-runs the paused gated turn. A refine below
417
+ // requires exactly one of content / resumeApproval.
418
+ content: z.string().min(1).max(8000).optional(),
419
+ /** Element-reference chips appended to the message. */
420
+ chips: z
421
+ .array(
422
+ z
423
+ .object({
424
+ moduleId: z.string().uuid(),
425
+ selector: z.string().min(1).max(500),
426
+ label: z.string().min(1).max(200),
427
+ })
428
+ .strict(),
429
+ )
430
+ .default([]),
431
+ /** issue #190 — operator-attached images (see chatAttachmentSchema). */
432
+ attachments: z.array(chatAttachmentSchema).max(CHAT_MAX_ATTACHMENTS).default([]),
433
+ /**
434
+ * issue #29 — provenance of this turn. 'system' marks an auto-injected
435
+ * message the operator did NOT type (crawl-completion nudge, post-
436
+ * approval continuation): the model still receives it as a user turn,
437
+ * but the UI renders it as a muted status note instead of "You:".
438
+ * Omitted / undefined = operator-authored.
439
+ */
440
+ origin: z.enum(["operator", "system"]).optional(),
441
+ /**
442
+ * P6.7.3 — the active /edit page id, threaded so the chat-runner can
443
+ * compose a Current-page volatile chunk in the system prompt and so
444
+ * tools that operate on a page (add_module target='page') know the target
445
+ * without a chip. Optional because the standalone chat editor at
446
+ * /content/chat doesn't have a page context.
447
+ */
448
+ activePageId: z.string().uuid().optional(),
449
+ /**
450
+ * Plan B (SDK approval gate) — production resume of a paused gated turn.
451
+ * When set, this is NOT a new operator message: the runner appends the
452
+ * SDK tool-approval-response (the Owner's in-chat Approve/Reject) to the
453
+ * paused turn's history and re-runs so the SDK either executes the gated
454
+ * tool (approved) or lets the model react to the denial. `approvalId` is
455
+ * the SDK id surfaced on the tool-approval-request ClientEvent.
456
+ */
457
+ resumeApproval: z
458
+ .object({
459
+ approvalId: z.string().min(1).max(200),
460
+ approved: z.boolean(),
461
+ reason: z.string().max(2000).optional(),
462
+ })
463
+ .strict()
464
+ .optional(),
465
+ })
466
+ .strict()
467
+ .refine((d) => d.content !== undefined || d.resumeApproval !== undefined, {
468
+ message: "content or resumeApproval is required",
469
+ });
470
+
471
+ export const chatRenameSessionInput = z
472
+ .object({
473
+ chatSessionId: z.string().uuid(),
474
+ title: z.string().min(1).max(200),
475
+ })
476
+ .strict();
477
+
478
+ export const chatPublishInput = z
479
+ .object({
480
+ chatSessionId: z.string().uuid(),
481
+ /**
482
+ * Optional partial-publish filter (P5.2 #5). When present, only
483
+ * entities listed here are merged into main; the rest stay on the
484
+ * branch for a later publish. Empty array means "publish nothing"
485
+ * and is rejected; omit the field to publish everything.
486
+ */
487
+ entities: z
488
+ .array(
489
+ z
490
+ .object({
491
+ // v0.9.0 — added "layout" + "structuredSet" to the enum so
492
+ // the Stage picker can include/exclude them. Pre-v0.9.0 the
493
+ // helper accepted "structuredSet" via the underlying SQL
494
+ // query but the input schema rejected it — minor gap closed
495
+ // alongside the branched-create work.
496
+ kind: z.enum([
497
+ "module",
498
+ "template",
499
+ "page",
500
+ "pageLayout",
501
+ "pageModuleContent",
502
+ "layout",
503
+ "structuredSet",
504
+ // v0.11.0 (#45) — themes primitive joins the publish/Stage
505
+ // surface so chat-branched theme edits replay into live
506
+ // on chat.publish.
507
+ "theme",
508
+ ]),
509
+ entityId: z.string().uuid(),
510
+ })
511
+ .strict(),
512
+ )
513
+ .min(1)
514
+ .optional(),
515
+ })
516
+ .strict();
517
+
518
+ export const aiMemorySetInput = z
519
+ .object({
520
+ slot: z.enum(["purpose", "brand-voice", "tone", "banned-phrases", "instructions", "glossary"]),
521
+ body: z.string().max(4000),
522
+ })
523
+ .strict();
524
+
525
+ export const aiMemoryReviewInput = z
526
+ .object({
527
+ proposalId: z.string().uuid(),
528
+ decision: z.enum(["accept", "reject"]),
529
+ })
530
+ .strict();
531
+
532
+ export const aiProvidersSetInput = z
533
+ .object({
534
+ name: z.enum(["anthropic", "openai", "google", "local-openai-compat"]),
535
+ displayName: z.string().min(1).max(100),
536
+ config: z.record(z.string(), z.unknown()).default({}),
537
+ isActive: z.boolean().default(true),
538
+ /**
539
+ * Optional plaintext API key. When present the op encrypts it with the
540
+ * project KEK and persists ciphertext + IV + KEK fingerprint. When
541
+ * absent the existing stored key (if any) is preserved untouched —
542
+ * lets the Owner edit displayName / model / baseUrl without re-pasting.
543
+ * Audit logs `apiKeyChanged: boolean`, never the value.
544
+ */
545
+ apiKey: z.string().min(1).max(500).optional(),
546
+ })
547
+ .strict();
548
+
549
+ /**
550
+ * Input for `ai_providers.clear_key` — Owner-only NULLs the encrypted
551
+ * triplet so the resolver falls back to the env-var path for that
552
+ * provider (or returns null if no env is set, which surfaces the
553
+ * "configure AI provider" UI banner).
554
+ */
555
+ export const aiProvidersClearKeyInput = z
556
+ .object({
557
+ name: z.enum(["anthropic", "openai", "google", "local-openai-compat"]),
558
+ })
559
+ .strict();
560
+
561
+ /**
562
+ * P6.7.5 — page-lifecycle tools. Three identifiers, three independent
563
+ * tools so the AI never silently substitutes one for another.
564
+ */
565
+ const slugInputSchema = z
566
+ .string()
567
+ .min(1)
568
+ .max(120)
569
+ .regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/hyphens, leading non-hyphen");
570
+
571
+ /**
572
+ * P18 AI-completeness — `create_template` AI tool input. Wraps
573
+ * `templates.create` (widened to AI in this pass per CLAUDE.md §11
574
+ * default-AI-allowed scope). `layoutId` is optional; the op resolves to
575
+ * `site_defaults.default_layout_id` when omitted.
576
+ */
577
+ export const createTemplateToolInput = z
578
+ .object({
579
+ slug: slugInputSchema,
580
+ displayName: z.string().min(1).max(256),
581
+ html: z.string().min(1).max(2_000_000),
582
+ css: z.string().max(2_000_000).default(""),
583
+ layoutId: z.string().uuid().optional(),
584
+ // Optional block-set metadata, symmetric with create_layout. Omit to
585
+ // auto-derive blocks from <caelo-slot> tags; pass to set displayName /
586
+ // position (each name must match a slot). Enforced op-side in
587
+ // templates.create (loud reject on a slot-less block).
588
+ blocks: z
589
+ .array(
590
+ z.object({
591
+ name: z.string().min(1).max(80),
592
+ displayName: z.string().min(1).max(200),
593
+ position: z.number().int().min(0).max(1000),
594
+ }),
595
+ )
596
+ .optional(),
597
+ })
598
+ .strict();
599
+
600
+ /**
601
+ * The ONE remove-module input — `target` picks page vs layout, `targetRef` is a
602
+ * slug OR a uuid (resolved server-side). Mirrors `add_module` so placement and
603
+ * un-placement share one shape. (No `template` target: there is no
604
+ * remove_module_from_template, matching the add-side history.)
605
+ */
606
+ export const removeModuleFromToolInput = z
607
+ .object({
608
+ target: z.enum(["page", "layout"]),
609
+ targetRef: z.string().min(1).max(200),
610
+ moduleId: z.string().uuid(),
611
+ })
612
+ .strict();
613
+ export type RemoveModuleFromToolInput = z.infer<typeof removeModuleFromToolInput>;
614
+
615
+ export const setStructuredSetToolInput = z
616
+ .object({
617
+ // v0.10.22 — added "language-selector" to match @caelo-cms/shared
618
+ // structuredSetKind (the 6th kind that was previously unreachable
619
+ // from any AI tool — the kind-specific wrappers covered only
620
+ // nav-menu + theme).
621
+ kind: z.enum(["nav-menu", "taxonomy", "theme", "tags", "link-list", "language-selector"]),
622
+ slug: slugInputSchema,
623
+ displayName: z.string().min(1).max(200),
624
+ items: z.array(z.unknown()),
625
+ })
626
+ .strict();
627
+
628
+ // v0.10.22 — `updateThemeToolInput` removed. The kind-specific `update_theme`
629
+ // wrapper was replaced by the unified `set_structured_set` surface.
630
+ // v0.11.0 (#45) — theme moved out of structured_sets entirely into its
631
+ // own `themes` primitive (DTCG-shaped jsonb). Theme edits are now
632
+ // `set_theme_tokens` (loose-name patch) / `propose_create_theme`
633
+ // (gated mint) / `propose_activate_theme` (gated flip) — see
634
+ // packages/admin-core/src/ai/tools/{update-theme-tokens,get-theme,…}.ts.
635
+
636
+ export const setTemplateLayoutToolInput = z
637
+ .object({
638
+ templateId: z.string().uuid(),
639
+ layoutSlug: slugInputSchema,
640
+ })
641
+ .strict();
642
+
643
+ export const createLayoutToolInput = z
644
+ .object({
645
+ slug: slugInputSchema,
646
+ displayName: z.string().min(1).max(200),
647
+ html: z.string().min(1).max(50_000),
648
+ css: z.string().max(50_000).optional(),
649
+ blocks: z
650
+ .array(
651
+ z
652
+ .object({
653
+ name: z.string().min(1).max(80),
654
+ displayName: z.string().min(1).max(200),
655
+ position: z.number().int().min(0).max(1000),
656
+ })
657
+ .strict(),
658
+ )
659
+ .min(1)
660
+ .max(20),
661
+ })
662
+ .strict();
663
+
664
+ export const setSiteDefaultsToolInput = z
665
+ .object({
666
+ defaultLayoutSlug: slugInputSchema.optional(),
667
+ defaultTemplateSlug: slugInputSchema.optional(),
668
+ })
669
+ .strict()
670
+ .refine((v) => v.defaultLayoutSlug !== undefined || v.defaultTemplateSlug !== undefined, {
671
+ message: "must provide at least one of defaultLayoutSlug, defaultTemplateSlug",
672
+ });
673
+
674
+ /**
675
+ * P6.7.7 — content-ops follow-ups: clone a page, swap a page's
676
+ * template, reorder modules within a block, move a module across
677
+ * blocks. All wrap existing or new ops; the tool layer captures the
678
+ * user-facing intent (which the system prompt steers the AI toward).
679
+ */
680
+ export const duplicatePageToolInput = z
681
+ .object({
682
+ sourcePageId: z.string().uuid(),
683
+ newSlug: slugInputSchema,
684
+ newName: z.string().min(1).max(256).optional(),
685
+ newTitle: z.string().min(1).max(256).optional(),
686
+ targetTemplateId: z.string().uuid().optional(),
687
+ locale: z.string().min(2).max(10).optional(),
688
+ })
689
+ .strict();
690
+
691
+ export const repointPageTemplateToolInput = z
692
+ .object({
693
+ pageId: z.string().uuid(),
694
+ newTemplateId: z.string().uuid(),
695
+ /**
696
+ * `drop` discards modules in blocks that don't exist on the new
697
+ * template. `preserve-as-block` reroutes them to a named block on
698
+ * the new template (must exist). The AI should ASK the user when
699
+ * the diff would drop modules; only pass `drop` after explicit
700
+ * confirmation.
701
+ */
702
+ orphanDisposition: z
703
+ .discriminatedUnion("kind", [
704
+ z.object({ kind: z.literal("drop") }).strict(),
705
+ z.object({ kind: z.literal("preserve-as-block"), blockName: slugInputSchema }).strict(),
706
+ ])
707
+ .default({ kind: "drop" }),
708
+ })
709
+ .strict();
710
+
711
+ export const moveModuleToolInput = z
712
+ .object({
713
+ pageId: z.string().uuid(),
714
+ moduleId: z.string().uuid(),
715
+ toBlockName: z.string().min(1).max(80),
716
+ /** "top" | "bottom" | a 0-based index inside the destination block. */
717
+ position: positionInputSchema,
718
+ })
719
+ .strict();
720
+
721
+ export const reorderModuleToolInput = z
722
+ .object({
723
+ pageId: z.string().uuid(),
724
+ moduleId: z.string().uuid(),
725
+ /**
726
+ * `up` / `down` shift one slot. An integer is an absolute 0-based
727
+ * target position within the same block.
728
+ */
729
+ direction: z.union([z.enum(["up", "down"]), z.number().int().min(0).max(1000)]),
730
+ })
731
+ .strict();
732
+
733
+ /**
734
+ * P7 — `find_media`. Searches the media library by alt-text /
735
+ * filename / mime. Returns up to `limit` matches with the WebP-800
736
+ * URL pre-resolved (or `orig` for non-image kinds). The system prompt
737
+ * already lists recent + frequently-used media; this tool covers the
738
+ * "search for an image of a sunlit office" case where the asset isn't
739
+ * in the recent slice.
740
+ */
741
+ export const findMediaToolInput = z
742
+ .object({
743
+ query: z.string().max(256).optional(),
744
+ mime: z
745
+ .enum([
746
+ "image/jpeg",
747
+ "image/png",
748
+ "image/webp",
749
+ "image/avif",
750
+ "image/gif",
751
+ "image/svg+xml",
752
+ "application/pdf",
753
+ "video/mp4",
754
+ ])
755
+ .optional(),
756
+ limit: z.number().int().min(1).max(50).default(15),
757
+ })
758
+ .strict();
759
+ export type FindMediaToolInput = z.infer<typeof findMediaToolInput>;
760
+
761
+ /**
762
+ * run #10 D4 — `regenerate_media_variants`. Recovery tool for the
763
+ * staging generator's "media references unresolved" failure: re-runs
764
+ * the image pipeline for the named assets (or every asset with an
765
+ * incomplete ladder) and reports per-asset what was added or why
766
+ * nothing could be.
767
+ */
768
+ export const regenerateMediaVariantsToolInput = z
769
+ .object({
770
+ assetIds: z.array(z.string().uuid()).min(1).max(100).optional(),
771
+ allMissing: z.boolean().default(false),
772
+ })
773
+ .strict()
774
+ .refine((v) => (v.assetIds !== undefined) !== v.allMissing, {
775
+ message: "pass either assetIds or allMissing: true (exactly one)",
776
+ });
777
+ export type RegenerateMediaVariantsToolInput = z.infer<typeof regenerateMediaVariantsToolInput>;
778
+
779
+ /**
780
+ * P7 — `set_media_alt`. AI may improve a11y on an existing asset
781
+ * without a human round-trip (e.g. when the editor uploaded an image
782
+ * with the default alt and the AI reads its content). Updates only
783
+ * the alt field on `media_assets`.
784
+ */
785
+ export const setMediaAltToolInput = z
786
+ .object({
787
+ assetId: z.string().uuid(),
788
+ alt: z.string().max(2048),
789
+ })
790
+ .strict();
791
+ export type SetMediaAltToolInput = z.infer<typeof setMediaAltToolInput>;
792
+
793
+ /**
794
+ * 0184 — `set_home_page`. AI designates a page as the site homepage (per
795
+ * locale, the locale root). Records `locales.home_page_id`; the page keeps
796
+ * its own slug but resolves to `/`.
797
+ */
798
+ export const setHomePageToolInput = z
799
+ .object({
800
+ pageId: z.string().uuid(),
801
+ locale: z.string().min(2).max(10).optional(),
802
+ })
803
+ .strict();
804
+ export type SetHomePageToolInput = z.infer<typeof setHomePageToolInput>;
805
+
806
+ /**
807
+ * 0181 — `set_media_source`. AI records an asset's provenance (source
808
+ * kind + origin detail) and licence when known. All fields but the
809
+ * assetId are optional; the op COALESCEs so omitted fields stay
810
+ * unchanged.
811
+ */
812
+ export const setMediaSourceToolInput = z
813
+ .object({
814
+ assetId: z.string().uuid(),
815
+ sourceKind: z.enum(["upload", "ai_generated", "imported", "external"]).optional(),
816
+ sourceDetail: z.string().max(2048).optional(),
817
+ license: z.string().max(200).optional(),
818
+ })
819
+ .strict();
820
+ export type SetMediaSourceToolInput = z.infer<typeof setMediaSourceToolInput>;
821
+
822
+ /**
823
+ * P8 — `set_page_seo`. Manual / panel writes to the per-page SEO
824
+ * sidecar. AI calls this only on explicit user intent
825
+ * ("set the home meta description to ..."). Doesn't bump fingerprints
826
+ * (autofilled_at / optimized_at).
827
+ */
828
+ export const setPageSeoToolInput = z
829
+ .object({
830
+ pageId: z.string().uuid(),
831
+ metaDescription: z.string().max(320).optional(),
832
+ ogImageAssetId: z.string().uuid().nullable().optional(),
833
+ canonicalUrl: z.string().max(2048).nullable().optional(),
834
+ noindex: z.boolean().optional(),
835
+ changefreq: z
836
+ .enum(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"])
837
+ .optional(),
838
+ priority: z.number().min(0).max(1).optional(),
839
+ })
840
+ .strict();
841
+ export type SetPageSeoToolInput = z.infer<typeof setPageSeoToolInput>;
842
+
843
+ /**
844
+ * P8 — `autofill_page_seo`. Fill-once. Refuses when the page's SEO
845
+ * was already autofilled. Triggered by the `seo-autofill` skill on
846
+ * the first-publish path.
847
+ */
848
+ export const autofillPageSeoToolInput = z
849
+ .object({
850
+ pageId: z.string().uuid(),
851
+ metaDescription: z.string().min(1).max(320),
852
+ ogImageAssetId: z.string().uuid().nullable().optional(),
853
+ })
854
+ .strict();
855
+ export type AutofillPageSeoToolInput = z.infer<typeof autofillPageSeoToolInput>;
856
+
857
+ /**
858
+ * P8 — `optimize_page_seo`. Explicit re-optimization. Always allowed.
859
+ * Takes optional context (keyword analysis / intent shifts / branding
860
+ * changes). The AI can call this across N pages in one chat turn —
861
+ * the resulting changes batch into one Publish-pill confirm.
862
+ */
863
+ export const optimizePageSeoToolInput = z
864
+ .object({
865
+ pageId: z.string().uuid(),
866
+ metaDescription: z.string().min(1).max(320),
867
+ ogImageAssetId: z.string().uuid().nullable().optional(),
868
+ context: z.string().max(4000).optional(),
869
+ })
870
+ .strict();
871
+ export type OptimizePageSeoToolInput = z.infer<typeof optimizePageSeoToolInput>;
872
+
873
+ /**
874
+ * P8 AI-first review pass — bulk variants. Per CLAUDE.md §11, bulk
875
+ * tools save round-trips when the AI knows it has N changes to make
876
+ * in a single turn. The handlers run inside one transaction so the
877
+ * batch is all-or-nothing.
878
+ */
879
+ export const findRedirectsToolInput = z
880
+ .object({
881
+ query: z.string().max(500).optional(),
882
+ statusCode: z
883
+ .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308), z.literal(410)])
884
+ .optional(),
885
+ limit: z.number().int().min(1).max(200).default(50),
886
+ })
887
+ .strict();
888
+ export type FindRedirectsToolInput = z.infer<typeof findRedirectsToolInput>;
889
+
890
+ export const bulkCreateRedirectsToolInput = z
891
+ .object({
892
+ redirects: z
893
+ .array(
894
+ z
895
+ .object({
896
+ fromPath: z.string().min(1).max(500),
897
+ toPath: z.string().min(1).max(500),
898
+ statusCode: z
899
+ .union([
900
+ z.literal(301),
901
+ z.literal(302),
902
+ z.literal(307),
903
+ z.literal(308),
904
+ z.literal(410),
905
+ ])
906
+ .default(301),
907
+ })
908
+ .strict(),
909
+ )
910
+ .min(1)
911
+ .max(500),
912
+ upsert: z.boolean().default(false),
913
+ })
914
+ .strict();
915
+ export type BulkCreateRedirectsToolInput = z.infer<typeof bulkCreateRedirectsToolInput>;
916
+
917
+ /**
918
+ * v0.9.13 — Singular + bulk status-flip AI tool inputs.
919
+ *
920
+ * Wraps the `pages.set_status` and `pages.set_status_many` ops. Drafts
921
+ * are LIVE-EDIT ONLY; Stage and Production deploys filter to
922
+ * `status='published'`. Use the bulk form for "publish all drafts" or
923
+ * any N>1 flip — saves N round-trips + N snapshot writes + N audit rows.
924
+ */
925
+ export const setPagesStatusManyToolInput = z
926
+ .object({
927
+ pageIds: z.array(z.string().uuid()).min(1).max(200),
928
+ status: z.enum(["draft", "published"]),
929
+ })
930
+ .strict();
931
+ export type SetPagesStatusManyToolInput = z.infer<typeof setPagesStatusManyToolInput>;
932
+
933
+ export const bulkDeleteRedirectsToolInput = z
934
+ .object({
935
+ redirectIds: z.array(z.string().uuid()).max(500).optional(),
936
+ fromPaths: z.array(z.string().min(1).max(500)).max(500).optional(),
937
+ matches: z.string().min(1).max(500).optional(),
938
+ })
939
+ .strict();
940
+ export type BulkDeleteRedirectsToolInput = z.infer<typeof bulkDeleteRedirectsToolInput>;
941
+
942
+ export const bulkOptimizeSeoToolInput = z
943
+ .object({
944
+ updates: z
945
+ .array(
946
+ z
947
+ .object({
948
+ pageId: z.string().uuid(),
949
+ metaDescription: z.string().min(1).max(320),
950
+ ogImageAssetId: z.string().uuid().nullable().optional(),
951
+ })
952
+ .strict(),
953
+ )
954
+ .min(1)
955
+ .max(200),
956
+ context: z.string().max(4000).optional(),
957
+ })
958
+ .strict();
959
+ export type BulkOptimizeSeoToolInput = z.infer<typeof bulkOptimizeSeoToolInput>;
960
+
961
+ export type EditModuleToolInput = z.infer<typeof editModuleToolInput>;
962
+ export type SiteMemoryProposeToolInput = z.infer<typeof siteMemoryProposeToolInput>;
963
+ export type CreateTemplateToolInput = z.infer<typeof createTemplateToolInput>;
964
+ export type SetStructuredSetToolInput = z.infer<typeof setStructuredSetToolInput>;
965
+ // v0.10.22 — `UpdateThemeToolInput` removed alongside `update_theme` tool.
966
+ export type ChatCreateSessionInput = z.infer<typeof chatCreateSessionInput>;
967
+ export type ChatSendMessageInput = z.infer<typeof chatSendMessageInput>;
968
+ export type ChatRenameSessionInput = z.infer<typeof chatRenameSessionInput>;
969
+ export type ChatPublishInput = z.infer<typeof chatPublishInput>;
970
+ export type AiMemorySetInput = z.infer<typeof aiMemorySetInput>;
971
+ export type AiMemoryReviewInput = z.infer<typeof aiMemoryReviewInput>;
972
+ export type AiProvidersSetInput = z.infer<typeof aiProvidersSetInput>;
973
+ export type AiProvidersClearKeyInput = z.infer<typeof aiProvidersClearKeyInput>;
974
+ export type SetTemplateLayoutToolInput = z.infer<typeof setTemplateLayoutToolInput>;
975
+ export type CreateLayoutToolInput = z.infer<typeof createLayoutToolInput>;
976
+ export type SetSiteDefaultsToolInput = z.infer<typeof setSiteDefaultsToolInput>;
977
+ export type DuplicatePageToolInput = z.infer<typeof duplicatePageToolInput>;
978
+ export type RepointPageTemplateToolInput = z.infer<typeof repointPageTemplateToolInput>;
979
+ export type MoveModuleToolInput = z.infer<typeof moveModuleToolInput>;
980
+ export type ReorderModuleToolInput = z.infer<typeof reorderModuleToolInput>;
981
+ // v0.10.22 — `SetNavMenuToolInput` removed alongside `set_nav_menu` tool.
982
+
983
+ /**
984
+ * P9 — locales propose tool schemas. Per CLAUDE.md §11.A all four
985
+ * write paths are TWO-STEP (AI proposes → Owner clicks Approve);
986
+ * the AI cannot bypass the click.
987
+ */
988
+ const localeCodeToolSchema = z
989
+ .string()
990
+ .min(2)
991
+ .max(10)
992
+ .regex(/^[a-z]{2,3}(-[A-Za-z]{2,4})?$/, "BCP-47 like 'en' or 'de-AT'");
993
+ const urlStrategyToolSchema = z.enum(["none", "subdirectory", "subdomain", "domain"]);
994
+
995
+ export const proposeAddLocaleToolInput = z
996
+ .object({
997
+ code: localeCodeToolSchema,
998
+ displayName: z.string().min(1).max(120),
999
+ urlStrategy: urlStrategyToolSchema.default("subdirectory"),
1000
+ urlHost: z.string().min(1).max(253).nullable().optional(),
1001
+ })
1002
+ .strict();
1003
+ export type ProposeAddLocaleToolInput = z.infer<typeof proposeAddLocaleToolInput>;
1004
+
1005
+ export const proposeRemoveLocaleToolInput = z.object({ code: localeCodeToolSchema }).strict();
1006
+ export type ProposeRemoveLocaleToolInput = z.infer<typeof proposeRemoveLocaleToolInput>;
1007
+
1008
+ export const proposeSetDefaultLocaleToolInput = z.object({ code: localeCodeToolSchema }).strict();
1009
+ export type ProposeSetDefaultLocaleToolInput = z.infer<typeof proposeSetDefaultLocaleToolInput>;
1010
+
1011
+ export const proposeUpdateLocaleStrategyToolInput = z
1012
+ .object({
1013
+ code: localeCodeToolSchema,
1014
+ urlStrategy: urlStrategyToolSchema,
1015
+ urlHost: z.string().min(1).max(253).nullable().optional(),
1016
+ })
1017
+ .strict();
1018
+ export type ProposeUpdateLocaleStrategyToolInput = z.infer<
1019
+ typeof proposeUpdateLocaleStrategyToolInput
1020
+ >;
1021
+
1022
+ /**
1023
+ * P10 — translation tool inputs. `translate_page` auto-dispatches
1024
+ * Mode 1 / Mode 2 based on the variant's existing status — the AI
1025
+ * sees one verb regardless of state. `start_translation_job` queues
1026
+ * a bulk run.
1027
+ */
1028
+ export const translatePageToolInput = z
1029
+ .object({
1030
+ pageId: z.string().uuid(),
1031
+ targetLocale: localeCodeToolSchema,
1032
+ })
1033
+ .strict();
1034
+ export type TranslatePageToolInput = z.infer<typeof translatePageToolInput>;
1035
+
1036
+ const translationJobScopeTool = z.discriminatedUnion("kind", [
1037
+ z.object({ kind: z.literal("all-stale") }).strict(),
1038
+ z.object({ kind: z.literal("page"), pageId: z.string().uuid() }).strict(),
1039
+ z.object({ kind: z.literal("locale"), code: localeCodeToolSchema }).strict(),
1040
+ z
1041
+ .object({
1042
+ kind: z.literal("pages"),
1043
+ pageIds: z.array(z.string().uuid()).min(1).max(500),
1044
+ })
1045
+ .strict(),
1046
+ ]);
1047
+
1048
+ export const startTranslationJobToolInput = z
1049
+ .object({
1050
+ scope: translationJobScopeTool,
1051
+ capMicrocents: z.number().int().nonnegative().nullable().optional(),
1052
+ })
1053
+ .strict();
1054
+ export type StartTranslationJobToolInput = z.infer<typeof startTranslationJobToolInput>;
1055
+
1056
+ /**
1057
+ * P10A — `propose_skill`. AI drafts a new skill body (or revision) and
1058
+ * queues it for Owner review. Per CLAUDE.md §2: skills augment the AI's
1059
+ * own system prompt, so site-wide activation requires explicit Owner
1060
+ * confirmation. The proposal lands in skill_proposals; Owner accepts
1061
+ * (creates `skills` row at status='awaiting_activation') and then
1062
+ * activates separately.
1063
+ */
1064
+ export const proposeSkillToolInput = z
1065
+ .object({
1066
+ slug: z
1067
+ .string()
1068
+ .min(1)
1069
+ .max(120)
1070
+ .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"),
1071
+ displayName: z.string().min(1).max(200),
1072
+ description: z.string().max(1000).default(""),
1073
+ body: z.string().min(1).max(20000),
1074
+ rationale: z.string().min(1).max(1000),
1075
+ allowlistedTools: z.array(z.string().min(1).max(120)).default([]),
1076
+ hints: z
1077
+ .object({
1078
+ keywords: z.array(z.string().min(1).max(80)).default([]),
1079
+ chipTrigger: z.boolean().default(false),
1080
+ alwaysOn: z.boolean().default(false),
1081
+ })
1082
+ .strict()
1083
+ .default({ keywords: [], chipTrigger: false, alwaysOn: false }),
1084
+ })
1085
+ .strict();
1086
+ export type ProposeSkillToolInput = z.infer<typeof proposeSkillToolInput>;
1087
+
1088
+ /**
1089
+ * P11 — `submit_plugin`. AI submits a Tier 2 plugin for validation +
1090
+ * Owner approval. CLAUDE.md §2 invariant: AI submits, human Owner
1091
+ * activates. Tier 1 plugins ship via human PR + signed release; the AI
1092
+ * tool surface cannot promote — the manifest field `tier` is forced
1093
+ * to 2 by the handler.
1094
+ */
1095
+ export const submitPluginToolInput = z
1096
+ .object({
1097
+ slug: z
1098
+ .string()
1099
+ .min(1)
1100
+ .max(120)
1101
+ .regex(/^[a-z][a-z0-9-]*$/, "lowercase, dash-separated"),
1102
+ version: z
1103
+ .string()
1104
+ .min(1)
1105
+ .max(40)
1106
+ .regex(/^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/, "semver"),
1107
+ /** Manifest object as written by the plugin author. Tier-1 fields
1108
+ * (`requestedCapabilities`, `workers`, `tools`) and `tier: 1`
1109
+ * are rejected by the validator. */
1110
+ manifest: z.record(z.string(), z.unknown()),
1111
+ /** Full source code of the plugin's compiled JS module. */
1112
+ source: z.string().min(1).max(200_000),
1113
+ })
1114
+ .strict();
1115
+ export type SubmitPluginToolInput = z.infer<typeof submitPluginToolInput>;
1116
+
1117
+ /**
1118
+ * v0.6.0 W4 — composite workflow tool. Bootstraps a fresh install in
1119
+ * one tool call: creates a default layout (header/content/footer), a
1120
+ * default template (single `content` block), and pins both via
1121
+ * `site_defaults.set`. Replaces the 4-5 step bootstrap dance the AI
1122
+ * currently has to orchestrate via the bootstrap-site skill.
1123
+ *
1124
+ * Every field is optional + has a sensible default — the AI can call
1125
+ * with `{}` on the smallest case and get a working scaffold.
1126
+ */
1127
+ export const bootstrapSiteScaffoldToolInput = z
1128
+ .object({
1129
+ /** Slug for the new layout. Defaults to `site-default`. */
1130
+ layoutSlug: z.string().min(1).max(120).optional(),
1131
+ /** Display name for the new layout. Defaults to `Site default`. */
1132
+ layoutDisplayName: z.string().min(1).max(256).optional(),
1133
+ /** Block names for the layout. Defaults to header/content/footer.
1134
+ * The `content` block is REQUIRED (where the template renders);
1135
+ * the validator inserts it if missing. */
1136
+ layoutBlocks: z
1137
+ .array(
1138
+ z.object({
1139
+ name: z
1140
+ .string()
1141
+ .min(1)
1142
+ .max(64)
1143
+ .regex(/^[a-z][a-z0-9-]*$/),
1144
+ displayName: z.string().min(1).max(128),
1145
+ }),
1146
+ )
1147
+ .max(8)
1148
+ .optional(),
1149
+ /** Slug for the new template. Defaults to `home`. */
1150
+ templateSlug: z.string().min(1).max(120).optional(),
1151
+ /** Display name for the new template. Defaults to `Home template`. */
1152
+ templateDisplayName: z.string().min(1).max(256).optional(),
1153
+ /** Whether to pin the new layout+template as site_defaults. Defaults to true. */
1154
+ setAsDefaults: z.boolean().optional(),
1155
+ })
1156
+ .strict();
1157
+ export type BootstrapSiteScaffoldToolInput = z.infer<typeof bootstrapSiteScaffoldToolInput>;
1158
+
1159
+ /**
1160
+ * v0.6.0 W4 (deferred) — composite revert. Undoes every snapshot tagged
1161
+ * with the given chat's branch_id. Per-entity revert ops handle the
1162
+ * fan-out; this composite groups them so a single AI tool call wipes
1163
+ * everything in a chat instead of N tool calls per touched entity.
1164
+ *
1165
+ * Bounded: when the chat touched more than `maxEntities` entities, the
1166
+ * tool refuses and asks the operator to use the per-entity revert UI;
1167
+ * keeps the AI from accidentally reverting a wide-ranging chat in one
1168
+ * click.
1169
+ */
1170
+ export const revertChatChangesToolInput = z
1171
+ .object({
1172
+ chatSessionId: z.string().uuid(),
1173
+ /** Safety cap — when the chat's branch touched more than this many
1174
+ * entities, the composite refuses. Default 20. Set higher
1175
+ * explicitly when the user really wants a wide revert. */
1176
+ maxEntities: z.number().int().min(1).max(500).optional(),
1177
+ })
1178
+ .strict();
1179
+ export type RevertChatChangesToolInput = z.infer<typeof revertChatChangesToolInput>;