@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
@@ -0,0 +1,1105 @@
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
+ // structuredSetKind (the 6th kind that was previously unreachable
618
+ // from any AI tool — the kind-specific wrappers covered only
619
+ // nav-menu + theme).
620
+ kind: z.enum(["nav-menu", "taxonomy", "tags", "link-list"]),
621
+ slug: slugInputSchema,
622
+ displayName: z.string().min(1).max(200),
623
+ items: z.array(z.unknown()),
624
+ })
625
+ .strict();
626
+
627
+ // v0.10.22 — `updateThemeToolInput` removed. The kind-specific `update_theme`
628
+ // wrapper was replaced by the unified `set_structured_set` surface.
629
+ // v0.11.0 (#45) — theme moved out of structured_sets entirely into its
630
+ // own `themes` primitive (DTCG-shaped jsonb). Theme edits are now
631
+ // `set_theme_tokens` (loose-name patch) / `propose_create_theme`
632
+ // (gated mint) / `propose_activate_theme` (gated flip) — see
633
+ // packages/admin-core/src/ai/tools/{update-theme-tokens,get-theme,…}.ts.
634
+
635
+ export const setTemplateLayoutToolInput = z
636
+ .object({
637
+ templateId: z.string().uuid(),
638
+ layoutSlug: slugInputSchema,
639
+ })
640
+ .strict();
641
+
642
+ export const createLayoutToolInput = z
643
+ .object({
644
+ slug: slugInputSchema,
645
+ displayName: z.string().min(1).max(200),
646
+ html: z.string().min(1).max(50_000),
647
+ css: z.string().max(50_000).optional(),
648
+ blocks: z
649
+ .array(
650
+ z
651
+ .object({
652
+ name: z.string().min(1).max(80),
653
+ displayName: z.string().min(1).max(200),
654
+ position: z.number().int().min(0).max(1000),
655
+ })
656
+ .strict(),
657
+ )
658
+ .min(1)
659
+ .max(20),
660
+ })
661
+ .strict();
662
+
663
+ export const setSiteDefaultsToolInput = z
664
+ .object({
665
+ defaultLayoutSlug: slugInputSchema.optional(),
666
+ defaultTemplateSlug: slugInputSchema.optional(),
667
+ })
668
+ .strict()
669
+ .refine((v) => v.defaultLayoutSlug !== undefined || v.defaultTemplateSlug !== undefined, {
670
+ message: "must provide at least one of defaultLayoutSlug, defaultTemplateSlug",
671
+ });
672
+
673
+ /**
674
+ * P6.7.7 — content-ops follow-ups: clone a page, swap a page's
675
+ * template, reorder modules within a block, move a module across
676
+ * blocks. All wrap existing or new ops; the tool layer captures the
677
+ * user-facing intent (which the system prompt steers the AI toward).
678
+ */
679
+ export const duplicatePageToolInput = z
680
+ .object({
681
+ sourcePageId: z.string().uuid(),
682
+ newSlug: slugInputSchema,
683
+ newName: z.string().min(1).max(256).optional(),
684
+ newTitle: z.string().min(1).max(256).optional(),
685
+ targetTemplateId: z.string().uuid().optional(),
686
+ locale: z.string().min(2).max(10).optional(),
687
+ })
688
+ .strict();
689
+
690
+ export const repointPageTemplateToolInput = z
691
+ .object({
692
+ pageId: z.string().uuid(),
693
+ newTemplateId: z.string().uuid(),
694
+ /**
695
+ * `drop` discards modules in blocks that don't exist on the new
696
+ * template. `preserve-as-block` reroutes them to a named block on
697
+ * the new template (must exist). The AI should ASK the user when
698
+ * the diff would drop modules; only pass `drop` after explicit
699
+ * confirmation.
700
+ */
701
+ orphanDisposition: z
702
+ .discriminatedUnion("kind", [
703
+ z.object({ kind: z.literal("drop") }).strict(),
704
+ z.object({ kind: z.literal("preserve-as-block"), blockName: slugInputSchema }).strict(),
705
+ ])
706
+ .default({ kind: "drop" }),
707
+ })
708
+ .strict();
709
+
710
+ export const moveModuleToolInput = z
711
+ .object({
712
+ pageId: z.string().uuid(),
713
+ moduleId: z.string().uuid(),
714
+ toBlockName: z.string().min(1).max(80),
715
+ /** "top" | "bottom" | a 0-based index inside the destination block. */
716
+ position: positionInputSchema,
717
+ })
718
+ .strict();
719
+
720
+ export const reorderModuleToolInput = z
721
+ .object({
722
+ pageId: z.string().uuid(),
723
+ moduleId: z.string().uuid(),
724
+ /**
725
+ * `up` / `down` shift one slot. An integer is an absolute 0-based
726
+ * target position within the same block.
727
+ */
728
+ direction: z.union([z.enum(["up", "down"]), z.number().int().min(0).max(1000)]),
729
+ })
730
+ .strict();
731
+
732
+ /**
733
+ * P7 — `find_media`. Searches the media library by alt-text /
734
+ * filename / mime. Returns up to `limit` matches with the WebP-800
735
+ * URL pre-resolved (or `orig` for non-image kinds). The system prompt
736
+ * already lists recent + frequently-used media; this tool covers the
737
+ * "search for an image of a sunlit office" case where the asset isn't
738
+ * in the recent slice.
739
+ */
740
+ export const findMediaToolInput = z
741
+ .object({
742
+ query: z.string().max(256).optional(),
743
+ mime: z
744
+ .enum([
745
+ "image/jpeg",
746
+ "image/png",
747
+ "image/webp",
748
+ "image/avif",
749
+ "image/gif",
750
+ "image/svg+xml",
751
+ "application/pdf",
752
+ "video/mp4",
753
+ ])
754
+ .optional(),
755
+ limit: z.number().int().min(1).max(50).default(15),
756
+ })
757
+ .strict();
758
+ export type FindMediaToolInput = z.infer<typeof findMediaToolInput>;
759
+
760
+ /**
761
+ * run #10 D4 — `regenerate_media_variants`. Recovery tool for the
762
+ * staging generator's "media references unresolved" failure: re-runs
763
+ * the image pipeline for the named assets (or every asset with an
764
+ * incomplete ladder) and reports per-asset what was added or why
765
+ * nothing could be.
766
+ */
767
+ export const regenerateMediaVariantsToolInput = z
768
+ .object({
769
+ assetIds: z.array(z.string().uuid()).min(1).max(100).optional(),
770
+ allMissing: z.boolean().default(false),
771
+ })
772
+ .strict()
773
+ .refine((v) => (v.assetIds !== undefined) !== v.allMissing, {
774
+ message: "pass either assetIds or allMissing: true (exactly one)",
775
+ });
776
+ export type RegenerateMediaVariantsToolInput = z.infer<typeof regenerateMediaVariantsToolInput>;
777
+
778
+ /**
779
+ * P7 — `set_media_alt`. AI may improve a11y on an existing asset
780
+ * without a human round-trip (e.g. when the editor uploaded an image
781
+ * with the default alt and the AI reads its content). Updates only
782
+ * the alt field on `media_assets`.
783
+ */
784
+ export const setMediaAltToolInput = z
785
+ .object({
786
+ assetId: z.string().uuid(),
787
+ alt: z.string().max(2048),
788
+ })
789
+ .strict();
790
+ export type SetMediaAltToolInput = z.infer<typeof setMediaAltToolInput>;
791
+
792
+ /**
793
+ * 0184 — `set_home_page`. AI designates a page as the site homepage (per
794
+ * locale, the locale root). Records `locales.home_page_id`; the page keeps
795
+ * its own slug but resolves to `/`.
796
+ */
797
+ export const setHomePageToolInput = z
798
+ .object({
799
+ pageId: z.string().uuid(),
800
+ locale: z.string().min(2).max(10).optional(),
801
+ })
802
+ .strict();
803
+ export type SetHomePageToolInput = z.infer<typeof setHomePageToolInput>;
804
+
805
+ /**
806
+ * 0181 — `set_media_source`. AI records an asset's provenance (source
807
+ * kind + origin detail) and licence when known. All fields but the
808
+ * assetId are optional; the op COALESCEs so omitted fields stay
809
+ * unchanged.
810
+ */
811
+ export const setMediaSourceToolInput = z
812
+ .object({
813
+ assetId: z.string().uuid(),
814
+ sourceKind: z.enum(["upload", "ai_generated", "imported", "external"]).optional(),
815
+ sourceDetail: z.string().max(2048).optional(),
816
+ license: z.string().max(200).optional(),
817
+ })
818
+ .strict();
819
+ export type SetMediaSourceToolInput = z.infer<typeof setMediaSourceToolInput>;
820
+
821
+ /**
822
+ * P8 — `set_page_seo`. Manual / panel writes to the per-page SEO
823
+ * sidecar. AI calls this only on explicit user intent
824
+ * ("set the home meta description to ..."). Doesn't bump fingerprints
825
+ * (autofilled_at / optimized_at).
826
+ */
827
+ export const setPageSeoToolInput = z
828
+ .object({
829
+ pageId: z.string().uuid(),
830
+ metaDescription: z.string().max(320).optional(),
831
+ ogImageAssetId: z.string().uuid().nullable().optional(),
832
+ canonicalUrl: z.string().max(2048).nullable().optional(),
833
+ noindex: z.boolean().optional(),
834
+ changefreq: z
835
+ .enum(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"])
836
+ .optional(),
837
+ priority: z.number().min(0).max(1).optional(),
838
+ })
839
+ .strict();
840
+ export type SetPageSeoToolInput = z.infer<typeof setPageSeoToolInput>;
841
+
842
+ /**
843
+ * P8 — `autofill_page_seo`. Fill-once. Refuses when the page's SEO
844
+ * was already autofilled. Triggered by the `seo-autofill` skill on
845
+ * the first-publish path.
846
+ */
847
+ export const autofillPageSeoToolInput = z
848
+ .object({
849
+ pageId: z.string().uuid(),
850
+ metaDescription: z.string().min(1).max(320),
851
+ ogImageAssetId: z.string().uuid().nullable().optional(),
852
+ })
853
+ .strict();
854
+ export type AutofillPageSeoToolInput = z.infer<typeof autofillPageSeoToolInput>;
855
+
856
+ /**
857
+ * P8 — `optimize_page_seo`. Explicit re-optimization. Always allowed.
858
+ * Takes optional context (keyword analysis / intent shifts / branding
859
+ * changes). The AI can call this across N pages in one chat turn —
860
+ * the resulting changes batch into one Publish-pill confirm.
861
+ */
862
+ export const optimizePageSeoToolInput = z
863
+ .object({
864
+ pageId: z.string().uuid(),
865
+ metaDescription: z.string().min(1).max(320),
866
+ ogImageAssetId: z.string().uuid().nullable().optional(),
867
+ context: z.string().max(4000).optional(),
868
+ })
869
+ .strict();
870
+ export type OptimizePageSeoToolInput = z.infer<typeof optimizePageSeoToolInput>;
871
+
872
+ /**
873
+ * P8 AI-first review pass — bulk variants. Per CLAUDE.md §11, bulk
874
+ * tools save round-trips when the AI knows it has N changes to make
875
+ * in a single turn. The handlers run inside one transaction so the
876
+ * batch is all-or-nothing.
877
+ */
878
+ export const findRedirectsToolInput = z
879
+ .object({
880
+ query: z.string().max(500).optional(),
881
+ statusCode: z
882
+ .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308), z.literal(410)])
883
+ .optional(),
884
+ limit: z.number().int().min(1).max(200).default(50),
885
+ })
886
+ .strict();
887
+ export type FindRedirectsToolInput = z.infer<typeof findRedirectsToolInput>;
888
+
889
+ export const bulkCreateRedirectsToolInput = z
890
+ .object({
891
+ redirects: z
892
+ .array(
893
+ z
894
+ .object({
895
+ fromPath: z.string().min(1).max(500),
896
+ toPath: z.string().min(1).max(500),
897
+ statusCode: z
898
+ .union([
899
+ z.literal(301),
900
+ z.literal(302),
901
+ z.literal(307),
902
+ z.literal(308),
903
+ z.literal(410),
904
+ ])
905
+ .default(301),
906
+ })
907
+ .strict(),
908
+ )
909
+ .min(1)
910
+ .max(500),
911
+ upsert: z.boolean().default(false),
912
+ })
913
+ .strict();
914
+ export type BulkCreateRedirectsToolInput = z.infer<typeof bulkCreateRedirectsToolInput>;
915
+
916
+ /**
917
+ * v0.9.13 — Singular + bulk status-flip AI tool inputs.
918
+ *
919
+ * Wraps the `pages.set_status` and `pages.set_status_many` ops. Drafts
920
+ * are LIVE-EDIT ONLY; Stage and Production deploys filter to
921
+ * `status='published'`. Use the bulk form for "publish all drafts" or
922
+ * any N>1 flip — saves N round-trips + N snapshot writes + N audit rows.
923
+ */
924
+ export const setPagesStatusManyToolInput = z
925
+ .object({
926
+ pageIds: z.array(z.string().uuid()).min(1).max(200),
927
+ status: z.enum(["draft", "published"]),
928
+ })
929
+ .strict();
930
+ export type SetPagesStatusManyToolInput = z.infer<typeof setPagesStatusManyToolInput>;
931
+
932
+ export const bulkDeleteRedirectsToolInput = z
933
+ .object({
934
+ redirectIds: z.array(z.string().uuid()).max(500).optional(),
935
+ fromPaths: z.array(z.string().min(1).max(500)).max(500).optional(),
936
+ matches: z.string().min(1).max(500).optional(),
937
+ })
938
+ .strict();
939
+ export type BulkDeleteRedirectsToolInput = z.infer<typeof bulkDeleteRedirectsToolInput>;
940
+
941
+ export const bulkOptimizeSeoToolInput = z
942
+ .object({
943
+ updates: z
944
+ .array(
945
+ z
946
+ .object({
947
+ pageId: z.string().uuid(),
948
+ metaDescription: z.string().min(1).max(320),
949
+ ogImageAssetId: z.string().uuid().nullable().optional(),
950
+ })
951
+ .strict(),
952
+ )
953
+ .min(1)
954
+ .max(200),
955
+ context: z.string().max(4000).optional(),
956
+ })
957
+ .strict();
958
+ export type BulkOptimizeSeoToolInput = z.infer<typeof bulkOptimizeSeoToolInput>;
959
+
960
+ export type EditModuleToolInput = z.infer<typeof editModuleToolInput>;
961
+ export type SiteMemoryProposeToolInput = z.infer<typeof siteMemoryProposeToolInput>;
962
+ export type CreateTemplateToolInput = z.infer<typeof createTemplateToolInput>;
963
+ export type SetStructuredSetToolInput = z.infer<typeof setStructuredSetToolInput>;
964
+ // v0.10.22 — `UpdateThemeToolInput` removed alongside `update_theme` tool.
965
+ export type ChatCreateSessionInput = z.infer<typeof chatCreateSessionInput>;
966
+ export type ChatSendMessageInput = z.infer<typeof chatSendMessageInput>;
967
+ export type ChatRenameSessionInput = z.infer<typeof chatRenameSessionInput>;
968
+ export type ChatPublishInput = z.infer<typeof chatPublishInput>;
969
+ export type AiMemorySetInput = z.infer<typeof aiMemorySetInput>;
970
+ export type AiMemoryReviewInput = z.infer<typeof aiMemoryReviewInput>;
971
+ export type AiProvidersSetInput = z.infer<typeof aiProvidersSetInput>;
972
+ export type AiProvidersClearKeyInput = z.infer<typeof aiProvidersClearKeyInput>;
973
+ export type SetTemplateLayoutToolInput = z.infer<typeof setTemplateLayoutToolInput>;
974
+ export type CreateLayoutToolInput = z.infer<typeof createLayoutToolInput>;
975
+ export type SetSiteDefaultsToolInput = z.infer<typeof setSiteDefaultsToolInput>;
976
+ export type DuplicatePageToolInput = z.infer<typeof duplicatePageToolInput>;
977
+ export type RepointPageTemplateToolInput = z.infer<typeof repointPageTemplateToolInput>;
978
+ export type MoveModuleToolInput = z.infer<typeof moveModuleToolInput>;
979
+ export type ReorderModuleToolInput = z.infer<typeof reorderModuleToolInput>;
980
+ // v0.10.22 — `SetNavMenuToolInput` removed alongside `set_nav_menu` tool.
981
+
982
+ /**
983
+ * P10A — `propose_skill`. AI drafts a new skill body (or revision) and
984
+ * queues it for Owner review. Per CLAUDE.md §2: skills augment the AI's
985
+ * own system prompt, so site-wide activation requires explicit Owner
986
+ * confirmation. The proposal lands in skill_proposals; Owner accepts
987
+ * (creates `skills` row at status='awaiting_activation') and then
988
+ * activates separately.
989
+ */
990
+ export const proposeSkillToolInput = z
991
+ .object({
992
+ slug: z
993
+ .string()
994
+ .min(1)
995
+ .max(120)
996
+ .regex(/^[a-z0-9-]+$/, "lowercase letters/digits/hyphens"),
997
+ displayName: z.string().min(1).max(200),
998
+ description: z.string().max(1000).default(""),
999
+ body: z.string().min(1).max(20000),
1000
+ rationale: z.string().min(1).max(1000),
1001
+ allowlistedTools: z.array(z.string().min(1).max(120)).default([]),
1002
+ hints: z
1003
+ .object({
1004
+ keywords: z.array(z.string().min(1).max(80)).default([]),
1005
+ chipTrigger: z.boolean().default(false),
1006
+ alwaysOn: z.boolean().default(false),
1007
+ })
1008
+ .strict()
1009
+ .default({ keywords: [], chipTrigger: false, alwaysOn: false }),
1010
+ })
1011
+ .strict();
1012
+ export type ProposeSkillToolInput = z.infer<typeof proposeSkillToolInput>;
1013
+
1014
+ /**
1015
+ * P11 — `submit_plugin`. AI submits a Tier 2 plugin for validation +
1016
+ * Owner approval. CLAUDE.md §2 invariant: AI submits, human Owner
1017
+ * activates. Tier 1 plugins ship via human PR + signed release; the AI
1018
+ * tool surface cannot promote — the manifest field `tier` is forced
1019
+ * to 2 by the handler.
1020
+ */
1021
+ export const submitPluginToolInput = z
1022
+ .object({
1023
+ slug: z
1024
+ .string()
1025
+ .min(1)
1026
+ .max(120)
1027
+ .regex(/^[a-z][a-z0-9-]*$/, "lowercase, dash-separated"),
1028
+ version: z
1029
+ .string()
1030
+ .min(1)
1031
+ .max(40)
1032
+ .regex(/^\d+\.\d+\.\d+(-[a-z0-9.]+)?$/, "semver"),
1033
+ /** Manifest object as written by the plugin author. Tier-1 fields
1034
+ * (`requestedCapabilities`, `workers`, `tools`) and `tier: 1`
1035
+ * are rejected by the validator. */
1036
+ manifest: z.record(z.string(), z.unknown()),
1037
+ /** Full source code of the plugin's compiled JS module. */
1038
+ source: z.string().min(1).max(200_000),
1039
+ })
1040
+ .strict();
1041
+ export type SubmitPluginToolInput = z.infer<typeof submitPluginToolInput>;
1042
+
1043
+ /**
1044
+ * v0.6.0 W4 — composite workflow tool. Bootstraps a fresh install in
1045
+ * one tool call: creates a default layout (header/content/footer), a
1046
+ * default template (single `content` block), and pins both via
1047
+ * `site_defaults.set`. Replaces the 4-5 step bootstrap dance the AI
1048
+ * currently has to orchestrate via the bootstrap-site skill.
1049
+ *
1050
+ * Every field is optional + has a sensible default — the AI can call
1051
+ * with `{}` on the smallest case and get a working scaffold.
1052
+ */
1053
+ export const bootstrapSiteScaffoldToolInput = z
1054
+ .object({
1055
+ /** Slug for the new layout. Defaults to `site-default`. */
1056
+ layoutSlug: z.string().min(1).max(120).optional(),
1057
+ /** Display name for the new layout. Defaults to `Site default`. */
1058
+ layoutDisplayName: z.string().min(1).max(256).optional(),
1059
+ /** Block names for the layout. Defaults to header/content/footer.
1060
+ * The `content` block is REQUIRED (where the template renders);
1061
+ * the validator inserts it if missing. */
1062
+ layoutBlocks: z
1063
+ .array(
1064
+ z.object({
1065
+ name: z
1066
+ .string()
1067
+ .min(1)
1068
+ .max(64)
1069
+ .regex(/^[a-z][a-z0-9-]*$/),
1070
+ displayName: z.string().min(1).max(128),
1071
+ }),
1072
+ )
1073
+ .max(8)
1074
+ .optional(),
1075
+ /** Slug for the new template. Defaults to `home`. */
1076
+ templateSlug: z.string().min(1).max(120).optional(),
1077
+ /** Display name for the new template. Defaults to `Home template`. */
1078
+ templateDisplayName: z.string().min(1).max(256).optional(),
1079
+ /** Whether to pin the new layout+template as site_defaults. Defaults to true. */
1080
+ setAsDefaults: z.boolean().optional(),
1081
+ })
1082
+ .strict();
1083
+ export type BootstrapSiteScaffoldToolInput = z.infer<typeof bootstrapSiteScaffoldToolInput>;
1084
+
1085
+ /**
1086
+ * v0.6.0 W4 (deferred) — composite revert. Undoes every snapshot tagged
1087
+ * with the given chat's branch_id. Per-entity revert ops handle the
1088
+ * fan-out; this composite groups them so a single AI tool call wipes
1089
+ * everything in a chat instead of N tool calls per touched entity.
1090
+ *
1091
+ * Bounded: when the chat touched more than `maxEntities` entities, the
1092
+ * tool refuses and asks the operator to use the per-entity revert UI;
1093
+ * keeps the AI from accidentally reverting a wide-ranging chat in one
1094
+ * click.
1095
+ */
1096
+ export const revertChatChangesToolInput = z
1097
+ .object({
1098
+ chatSessionId: z.string().uuid(),
1099
+ /** Safety cap — when the chat's branch touched more than this many
1100
+ * entities, the composite refuses. Default 20. Set higher
1101
+ * explicitly when the user really wants a wide revert. */
1102
+ maxEntities: z.number().int().min(1).max(500).optional(),
1103
+ })
1104
+ .strict();
1105
+ export type RevertChatChangesToolInput = z.infer<typeof revertChatChangesToolInput>;