@avocadostudio-ai/orchestrator-core 0.1.0

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 (196) hide show
  1. package/LICENSE +201 -0
  2. package/dist/agent/agent-context.d.ts +19 -0
  3. package/dist/agent/agent-context.js +67 -0
  4. package/dist/agent/agent-logger.d.ts +5 -0
  5. package/dist/agent/agent-logger.js +22 -0
  6. package/dist/agent/agent-loop-openai.d.ts +8 -0
  7. package/dist/agent/agent-loop-openai.js +172 -0
  8. package/dist/agent/agent-loop.d.ts +56 -0
  9. package/dist/agent/agent-loop.js +167 -0
  10. package/dist/agent/agent-provider.d.ts +28 -0
  11. package/dist/agent/agent-provider.js +63 -0
  12. package/dist/agent/agent-tools.d.ts +28 -0
  13. package/dist/agent/agent-tools.js +899 -0
  14. package/dist/agent/context/editing-guidelines.md +46 -0
  15. package/dist/agent/context/role.md +39 -0
  16. package/dist/agent/integration-prompt.d.ts +9 -0
  17. package/dist/agent/integration-prompt.js +154 -0
  18. package/dist/agent/sites-agent-context.d.ts +12 -0
  19. package/dist/agent/sites-agent-context.js +316 -0
  20. package/dist/agent/sites-agent-shared.d.ts +161 -0
  21. package/dist/agent/sites-agent-shared.js +1101 -0
  22. package/dist/agent/sites-agent-tools.d.ts +18 -0
  23. package/dist/agent/sites-agent-tools.js +1227 -0
  24. package/dist/chat/anthropic-cache.d.ts +20 -0
  25. package/dist/chat/anthropic-cache.js +54 -0
  26. package/dist/chat/anthropic-planner.d.ts +98 -0
  27. package/dist/chat/anthropic-planner.js +1012 -0
  28. package/dist/chat/changelog-coverage-validator.d.ts +37 -0
  29. package/dist/chat/changelog-coverage-validator.js +215 -0
  30. package/dist/chat/chat-pipeline-context.d.ts +211 -0
  31. package/dist/chat/chat-pipeline-context.js +249 -0
  32. package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
  33. package/dist/chat/chat-pipeline-deterministic.js +407 -0
  34. package/dist/chat/chat-pipeline-image.d.ts +86 -0
  35. package/dist/chat/chat-pipeline-image.js +897 -0
  36. package/dist/chat/chat-pipeline-shared.d.ts +69 -0
  37. package/dist/chat/chat-pipeline-shared.js +212 -0
  38. package/dist/chat/chat-pipeline-translation.d.ts +27 -0
  39. package/dist/chat/chat-pipeline-translation.js +417 -0
  40. package/dist/chat/chat-pipeline-ui.d.ts +14 -0
  41. package/dist/chat/chat-pipeline-ui.js +244 -0
  42. package/dist/chat/chat-pipeline.d.ts +99 -0
  43. package/dist/chat/chat-pipeline.js +3999 -0
  44. package/dist/chat/decomposer.d.ts +21 -0
  45. package/dist/chat/decomposer.js +65 -0
  46. package/dist/chat/gemini-planner.d.ts +70 -0
  47. package/dist/chat/gemini-planner.js +541 -0
  48. package/dist/chat/hallucination-validator.d.ts +36 -0
  49. package/dist/chat/hallucination-validator.js +110 -0
  50. package/dist/chat/locale-strings.d.ts +47 -0
  51. package/dist/chat/locale-strings.js +100 -0
  52. package/dist/chat/plan-json-schema.d.ts +133 -0
  53. package/dist/chat/plan-json-schema.js +112 -0
  54. package/dist/chat/planner-types.d.ts +120 -0
  55. package/dist/chat/planner-types.js +66 -0
  56. package/dist/chat/planner.d.ts +148 -0
  57. package/dist/chat/planner.js +1361 -0
  58. package/dist/chat/prompts.d.ts +67 -0
  59. package/dist/chat/prompts.js +356 -0
  60. package/dist/chat/provider-routing.d.ts +14 -0
  61. package/dist/chat/provider-routing.js +27 -0
  62. package/dist/chat/variation-pipeline.d.ts +135 -0
  63. package/dist/chat/variation-pipeline.js +837 -0
  64. package/dist/chat/vision-alt-generator.d.ts +35 -0
  65. package/dist/chat/vision-alt-generator.js +152 -0
  66. package/dist/cms/adapter.d.ts +62 -0
  67. package/dist/cms/adapter.js +1 -0
  68. package/dist/cms/bootstrap.d.ts +17 -0
  69. package/dist/cms/bootstrap.js +85 -0
  70. package/dist/cms/editor-api-adapter.d.ts +23 -0
  71. package/dist/cms/editor-api-adapter.js +71 -0
  72. package/dist/cms/index.d.ts +4 -0
  73. package/dist/cms/index.js +3 -0
  74. package/dist/cms/json-file-adapter.d.ts +11 -0
  75. package/dist/cms/json-file-adapter.js +62 -0
  76. package/dist/demo-mode.d.ts +59 -0
  77. package/dist/demo-mode.js +201 -0
  78. package/dist/errors.d.ts +67 -0
  79. package/dist/errors.js +129 -0
  80. package/dist/http/chat-stream-resumable.d.ts +108 -0
  81. package/dist/http/chat-stream-resumable.js +290 -0
  82. package/dist/http/chat-stream.d.ts +99 -0
  83. package/dist/http/chat-stream.js +92 -0
  84. package/dist/image/gdrive-client.d.ts +22 -0
  85. package/dist/image/gdrive-client.js +215 -0
  86. package/dist/image/image-helpers.d.ts +95 -0
  87. package/dist/image/image-helpers.js +488 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/jira/jira-approval.d.ts +22 -0
  91. package/dist/jira/jira-approval.js +51 -0
  92. package/dist/jira/jira-client.d.ts +44 -0
  93. package/dist/jira/jira-client.js +313 -0
  94. package/dist/jira/jira-poller.d.ts +46 -0
  95. package/dist/jira/jira-poller.js +184 -0
  96. package/dist/jira/jira-processor.d.ts +103 -0
  97. package/dist/jira/jira-processor.js +1085 -0
  98. package/dist/jira/jira-types.d.ts +117 -0
  99. package/dist/jira/jira-types.js +38 -0
  100. package/dist/logger.d.ts +12 -0
  101. package/dist/logger.js +28 -0
  102. package/dist/migration/mcp-server-stdio.d.ts +8 -0
  103. package/dist/migration/mcp-server-stdio.js +672 -0
  104. package/dist/migration/migration-prompt.d.ts +7 -0
  105. package/dist/migration/migration-prompt.js +197 -0
  106. package/dist/migration/migration-tools.d.ts +17 -0
  107. package/dist/migration/migration-tools.js +159 -0
  108. package/dist/migration/scrape-cache.d.ts +9 -0
  109. package/dist/migration/scrape-cache.js +19 -0
  110. package/dist/nlp/deterministic-planner-context.d.ts +141 -0
  111. package/dist/nlp/deterministic-planner-context.js +362 -0
  112. package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
  113. package/dist/nlp/deterministic-planner-pages.js +170 -0
  114. package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
  115. package/dist/nlp/deterministic-planner-patches.js +508 -0
  116. package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
  117. package/dist/nlp/deterministic-planner-refs.js +164 -0
  118. package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
  119. package/dist/nlp/deterministic-planner-suggestions.js +579 -0
  120. package/dist/nlp/deterministic-planner.d.ts +85 -0
  121. package/dist/nlp/deterministic-planner.js +1631 -0
  122. package/dist/nlp/intent-detection.d.ts +309 -0
  123. package/dist/nlp/intent-detection.js +730 -0
  124. package/dist/nlp/intent-helpers.d.ts +15 -0
  125. package/dist/nlp/intent-helpers.js +243 -0
  126. package/dist/nlp/intent-patterns.d.ts +40 -0
  127. package/dist/nlp/intent-patterns.js +223 -0
  128. package/dist/nlp/plan-normalizer.d.ts +41 -0
  129. package/dist/nlp/plan-normalizer.js +1537 -0
  130. package/dist/ops/destructive-action-gate.d.ts +44 -0
  131. package/dist/ops/destructive-action-gate.js +90 -0
  132. package/dist/ops/ops-engine.d.ts +151 -0
  133. package/dist/ops/ops-engine.js +1394 -0
  134. package/dist/publish/diff-engine.d.ts +18 -0
  135. package/dist/publish/diff-engine.js +305 -0
  136. package/dist/publish/publish-helpers.d.ts +87 -0
  137. package/dist/publish/publish-helpers.js +521 -0
  138. package/dist/publish/publish-target-registry.d.ts +7 -0
  139. package/dist/publish/publish-target-registry.js +61 -0
  140. package/dist/publish/publish-target.d.ts +81 -0
  141. package/dist/publish/publish-target.js +1 -0
  142. package/dist/publish/targets/deploy-hook.d.ts +13 -0
  143. package/dist/publish/targets/deploy-hook.js +123 -0
  144. package/dist/publish/targets/git.d.ts +13 -0
  145. package/dist/publish/targets/git.js +55 -0
  146. package/dist/publish/targets/site-contract.d.ts +19 -0
  147. package/dist/publish/targets/site-contract.js +124 -0
  148. package/dist/state/content-source.d.ts +17 -0
  149. package/dist/state/content-source.js +1 -0
  150. package/dist/state/in-memory-content-source.d.ts +27 -0
  151. package/dist/state/in-memory-content-source.js +51 -0
  152. package/dist/state/session-lock.d.ts +13 -0
  153. package/dist/state/session-lock.js +29 -0
  154. package/dist/state/session-state.d.ts +310 -0
  155. package/dist/state/session-state.js +1083 -0
  156. package/dist/state/sqlite-store-singleton.d.ts +31 -0
  157. package/dist/state/sqlite-store-singleton.js +170 -0
  158. package/dist/state/sqlite-store.d.ts +135 -0
  159. package/dist/state/sqlite-store.js +421 -0
  160. package/dist/telemetry/chat-telemetry.d.ts +105 -0
  161. package/dist/telemetry/chat-telemetry.js +247 -0
  162. package/dist/telemetry/eval-candidate-store.d.ts +50 -0
  163. package/dist/telemetry/eval-candidate-store.js +120 -0
  164. package/dist/telemetry/feedback-store.d.ts +34 -0
  165. package/dist/telemetry/feedback-store.js +76 -0
  166. package/dist/telemetry/jira-telemetry.d.ts +57 -0
  167. package/dist/telemetry/jira-telemetry.js +68 -0
  168. package/dist/telemetry/migration-telemetry.d.ts +35 -0
  169. package/dist/telemetry/migration-telemetry.js +40 -0
  170. package/dist/telemetry/usage.d.ts +24 -0
  171. package/dist/telemetry/usage.js +80 -0
  172. package/dist/tools/builtin-registrations.d.ts +12 -0
  173. package/dist/tools/builtin-registrations.js +33 -0
  174. package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
  175. package/dist/tools/builtins/gdrive-browse.js +68 -0
  176. package/dist/tools/builtins/image-generate.d.ts +3 -0
  177. package/dist/tools/builtins/image-generate.js +211 -0
  178. package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
  179. package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
  180. package/dist/tools/builtins/unsplash-search.d.ts +3 -0
  181. package/dist/tools/builtins/unsplash-search.js +74 -0
  182. package/dist/tools/executor.d.ts +23 -0
  183. package/dist/tools/executor.js +169 -0
  184. package/dist/tools/index.d.ts +5 -0
  185. package/dist/tools/index.js +5 -0
  186. package/dist/tools/registry.d.ts +21 -0
  187. package/dist/tools/registry.js +75 -0
  188. package/dist/tools/runtime.d.ts +27 -0
  189. package/dist/tools/runtime.js +48 -0
  190. package/dist/tools/schema-validator.d.ts +24 -0
  191. package/dist/tools/schema-validator.js +88 -0
  192. package/dist/tools/types.d.ts +86 -0
  193. package/dist/tools/types.js +1 -0
  194. package/dist/variation-images.d.ts +19 -0
  195. package/dist/variation-images.js +12 -0
  196. package/package.json +78 -0
@@ -0,0 +1,899 @@
1
+ /**
2
+ * Agent tool definitions for Anthropic tool-use.
3
+ * These are the tools Claude can call to modify the website.
4
+ *
5
+ * Three categories:
6
+ * 1. Compound tools (edit_page, batch_update_props, add_block_with_content)
7
+ * 2. Atomic tools (remove_block, move_block, create_page, etc.)
8
+ * 3. Read-only context tools (get_page, list_pages, get_block_schema)
9
+ */
10
+ import { defaultPropsForType } from "@avocadostudio-ai/shared";
11
+ import { applyOpsAtomically, pickFocusBlockId } from "../ops/ops-engine.js";
12
+ import { getPage, getSessionDraft, bumpVersion, orderSlugsHomeFirst, getSiteConfig } from "../state/session-state.js";
13
+ import { unsplashSearchHandler, unsplashSearchManifest } from "../tools/builtins/unsplash-search.js";
14
+ import { unsplashGetByIdHandler, unsplashGetByIdManifest } from "../tools/builtins/unsplash-get-by-id.js";
15
+ import { imageGenerateHandler, imageGenerateManifest } from "../tools/builtins/image-generate.js";
16
+ import { consoleAgentLogger } from "./agent-loop.js";
17
+ /** "ctaText" / "cta_text" → "Cta text" for human-readable change lines. */
18
+ function humanizePropKey(key) {
19
+ const spaced = key
20
+ .replace(/[_-]+/g, " ")
21
+ .replace(/([a-z\d])([A-Z])/g, "$1 $2")
22
+ .trim();
23
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
24
+ }
25
+ /** Keep change lines short — long body copy gets an ellipsis. */
26
+ function clampText(value, max = 80) {
27
+ const v = value.replace(/\s+/g, " ").trim();
28
+ return v.length > max ? `${v.slice(0, max - 1)}…` : v;
29
+ }
30
+ /**
31
+ * Create all agent tools bound to a specific session.
32
+ */
33
+ export function createAgentTools(session, options) {
34
+ const applyOpts = options?.manifest ? { componentsManifest: options.manifest } : {};
35
+ const log = options?.logger ?? consoleAgentLogger;
36
+ // Helper: apply ops and return result
37
+ async function applyOps(ops) {
38
+ try {
39
+ log.info(`[agent-tools] Applying ops: ${JSON.stringify(ops.map(o => o.op))}`);
40
+ // Snapshot before-values so we can describe text edits as "old → new"
41
+ // in the chat. Must run before applyOpsAtomically mutates the draft.
42
+ const changeLines = describeTextDeltas(ops);
43
+ const result = await applyOpsAtomically(session, ops, applyOpts);
44
+ log.info(`[agent-tools] Applied successfully: ${result.appliedCount}`);
45
+ const version = bumpVersion(session);
46
+ const focusBlockId = pickFocusBlockId(ops);
47
+ return {
48
+ result: JSON.stringify({
49
+ status: "applied",
50
+ appliedCount: result.appliedCount,
51
+ previewVersion: version,
52
+ focusBlockId,
53
+ skippedOps: result.skippedOps,
54
+ slugs: extractSlugsFromOps(ops),
55
+ ...(changeLines.length > 0 ? { changeLines } : {}),
56
+ })
57
+ };
58
+ }
59
+ catch (e) {
60
+ const msg = e instanceof Error ? e.message : String(e);
61
+ return { result: `Error: ${msg}`, isError: true };
62
+ }
63
+ }
64
+ // Build "Heading: "old" → "new"" lines for update_props text edits so the
65
+ // user can see exactly what an edit changed without diffing from memory.
66
+ // Reads the current draft (pre-apply) for the old values; the patch holds
67
+ // the new ones. Non-string / unchanged / oversized values are skipped.
68
+ function describeTextDeltas(ops) {
69
+ const lines = [];
70
+ for (const op of ops) {
71
+ if (op.op !== "update_props")
72
+ continue;
73
+ const slug = typeof op.pageSlug === "string" ? op.pageSlug : null;
74
+ const blockId = typeof op.blockId === "string" ? op.blockId : null;
75
+ const patch = op.patch && typeof op.patch === "object" ? op.patch : null;
76
+ if (!slug || !blockId || !patch)
77
+ continue;
78
+ const page = getPage(session, slug);
79
+ const block = page?.blocks.find((b) => b.id === blockId);
80
+ const props = (block?.props ?? {});
81
+ for (const [key, nextRaw] of Object.entries(patch)) {
82
+ if (typeof nextRaw !== "string")
83
+ continue;
84
+ const prevRaw = props[key];
85
+ if (typeof prevRaw !== "string")
86
+ continue;
87
+ const prev = prevRaw.trim();
88
+ const next = nextRaw.trim();
89
+ if (!next || prev === next)
90
+ continue;
91
+ const field = humanizePropKey(key);
92
+ lines.push(prev
93
+ ? `${field}: “${clampText(prev)}” → “${clampText(next)}”`
94
+ : `${field}: “${clampText(next)}”`);
95
+ }
96
+ }
97
+ return lines;
98
+ }
99
+ function extractSlugsFromOps(ops) {
100
+ const slugs = new Set();
101
+ for (const op of ops) {
102
+ if (typeof op.pageSlug === "string")
103
+ slugs.add(op.pageSlug);
104
+ if (typeof op.newPageSlug === "string")
105
+ slugs.add(op.newPageSlug);
106
+ if (typeof op.toPageSlug === "string")
107
+ slugs.add(op.toPageSlug);
108
+ const page = op.page;
109
+ if (page && typeof page === "object" && typeof page.slug === "string") {
110
+ slugs.add(page.slug);
111
+ }
112
+ }
113
+ return Array.from(slugs);
114
+ }
115
+ return [
116
+ // ================================================================
117
+ // COMPOUND TOOLS
118
+ // ================================================================
119
+ {
120
+ definition: {
121
+ name: "edit_page",
122
+ description: "Apply multiple operations to a page atomically. All operations succeed or all roll back. Use this for multi-step edits like rewriting multiple blocks, adding and configuring a block, or restructuring a page.",
123
+ input_schema: {
124
+ type: "object",
125
+ properties: {
126
+ pageSlug: { type: "string", description: "Page slug, e.g. '/' or '/pricing'" },
127
+ operations: {
128
+ type: "array",
129
+ description: "Array of operations to apply atomically",
130
+ items: {
131
+ type: "object",
132
+ properties: {
133
+ op: { type: "string", enum: ["update_props", "add_block", "remove_block", "move_block", "add_item", "update_item", "remove_item", "move_item", "reorder_items", "reorder_blocks"] },
134
+ blockId: { type: "string" },
135
+ patch: { type: "object", description: "For update_props: key-value pairs to update" },
136
+ block: { type: "object", description: "For add_block: { id, type, props }", properties: { id: { type: "string" }, type: { type: "string" }, props: { type: "object" } } },
137
+ afterBlockId: { type: "string", description: "For add_block/move_block: insert after this block" },
138
+ listKey: { type: "string", description: "For item ops: array prop name" },
139
+ itemId: { type: "string", description: "For update_item/remove_item/move_item: stable id of the target item (preferred over index — robust when other ops in this batch mutate the list)" },
140
+ index: { type: "number", description: "For item ops: 0-based item index (used only when itemId is omitted)" },
141
+ item: { type: "object", description: "For add_item: item data (a stable id is assigned automatically)" },
142
+ afterItemId: { type: "string", description: "For add_item/move_item: position after the item with this id (preferred over afterIndex)" },
143
+ afterIndex: { type: "number", description: "For add_item/move_item: target position by 0-based index" },
144
+ order: { type: "array", items: { type: "string" }, description: "For reorder_items/reorder_blocks: the FINAL order as item ids / block ids, covering every (non-chrome) entry exactly once" },
145
+ },
146
+ required: ["op"],
147
+ },
148
+ },
149
+ },
150
+ required: ["pageSlug", "operations"],
151
+ },
152
+ },
153
+ handler: async (input) => {
154
+ const pageSlug = input.pageSlug;
155
+ const operations = input.operations.map((op) => ({
156
+ ...op,
157
+ pageSlug,
158
+ }));
159
+ return applyOps(operations);
160
+ },
161
+ },
162
+ {
163
+ definition: {
164
+ name: "batch_update_props",
165
+ description: "Update multiple properties on a single block. Use this for text edits — e.g. updating heading, subheading, and CTA text on a Hero block in one call.",
166
+ input_schema: {
167
+ type: "object",
168
+ properties: {
169
+ pageSlug: { type: "string", description: "Page slug" },
170
+ blockId: { type: "string", description: "Block ID to update" },
171
+ patch: { type: "object", description: "Key-value pairs of properties to update. E.g. { heading: 'New Title', subheading: 'New subtitle' }" },
172
+ },
173
+ required: ["pageSlug", "blockId", "patch"],
174
+ },
175
+ },
176
+ handler: async (input) => {
177
+ const op = {
178
+ op: "update_props",
179
+ pageSlug: input.pageSlug,
180
+ blockId: input.blockId,
181
+ patch: input.patch,
182
+ };
183
+ return applyOps([op]);
184
+ },
185
+ },
186
+ {
187
+ definition: {
188
+ name: "add_block_with_content",
189
+ description: "Add a new block to a page with populated content. Generates a unique block ID automatically. Available block types: Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, Stats, TwoColumn, Banner, Carousel, Gallery, Tabs, Table, Quote, Video, Embed.",
190
+ input_schema: {
191
+ type: "object",
192
+ properties: {
193
+ pageSlug: { type: "string", description: "Page slug" },
194
+ blockType: { type: "string", description: "Block type (PascalCase), e.g. 'FAQAccordion', 'Hero', 'FeatureGrid'" },
195
+ props: { type: "object", description: "Block properties. Use get_block_schema to see available props for each type." },
196
+ afterBlockId: { type: "string", description: "Insert after this block. Omit to append at end." },
197
+ },
198
+ required: ["pageSlug", "blockType", "props"],
199
+ },
200
+ },
201
+ handler: async (input) => {
202
+ const blockType = input.blockType;
203
+ const safeType = blockType.toLowerCase().replace(/[^a-z0-9]+/g, "_");
204
+ const blockId = `b_${safeType}_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
205
+ const props = input.props;
206
+ const defaults = defaultPropsForType(blockType);
207
+ const mergedProps = { ...defaults, ...props };
208
+ const op = {
209
+ op: "add_block",
210
+ pageSlug: input.pageSlug,
211
+ block: { id: blockId, type: blockType, props: mergedProps },
212
+ };
213
+ if (input.afterBlockId)
214
+ op.afterBlockId = input.afterBlockId;
215
+ return applyOps([op]);
216
+ },
217
+ },
218
+ // ================================================================
219
+ // ATOMIC TOOLS
220
+ // ================================================================
221
+ {
222
+ definition: {
223
+ name: "remove_block",
224
+ description: "Remove a block from a page.",
225
+ input_schema: {
226
+ type: "object",
227
+ properties: {
228
+ pageSlug: { type: "string" },
229
+ blockId: { type: "string" },
230
+ },
231
+ required: ["pageSlug", "blockId"],
232
+ },
233
+ },
234
+ handler: async (input) => applyOps([{ op: "remove_block", pageSlug: input.pageSlug, blockId: input.blockId }]),
235
+ },
236
+ {
237
+ definition: {
238
+ name: "move_block",
239
+ description: "Move a block to a new position on the page. Set afterBlockId to place it after a specific block, or omit to move to the top.",
240
+ input_schema: {
241
+ type: "object",
242
+ properties: {
243
+ pageSlug: { type: "string" },
244
+ blockId: { type: "string" },
245
+ afterBlockId: { type: "string", description: "Place after this block. Omit to move to top." },
246
+ },
247
+ required: ["pageSlug", "blockId"],
248
+ },
249
+ },
250
+ handler: async (input) => {
251
+ const op = { op: "move_block", pageSlug: input.pageSlug, blockId: input.blockId };
252
+ if (input.afterBlockId)
253
+ op.afterBlockId = input.afterBlockId;
254
+ return applyOps([op]);
255
+ },
256
+ },
257
+ {
258
+ definition: {
259
+ name: "create_page",
260
+ description: "Create a new page with blocks.",
261
+ input_schema: {
262
+ type: "object",
263
+ properties: {
264
+ slug: { type: "string", description: "Page slug, e.g. '/pricing'" },
265
+ title: { type: "string" },
266
+ blocks: {
267
+ type: "array",
268
+ items: {
269
+ type: "object",
270
+ properties: {
271
+ id: { type: "string" },
272
+ type: { type: "string" },
273
+ props: { type: "object" },
274
+ },
275
+ required: ["id", "type", "props"],
276
+ },
277
+ },
278
+ },
279
+ required: ["slug", "title", "blocks"],
280
+ },
281
+ },
282
+ handler: async (input) => {
283
+ const page = {
284
+ slug: input.slug,
285
+ title: input.title,
286
+ blocks: input.blocks,
287
+ };
288
+ return applyOps([{ op: "create_page", page }]);
289
+ },
290
+ },
291
+ {
292
+ definition: {
293
+ name: "rename_page",
294
+ description: "Rename a page (change its slug and/or title).",
295
+ input_schema: {
296
+ type: "object",
297
+ properties: {
298
+ pageSlug: { type: "string" },
299
+ newPageSlug: { type: "string" },
300
+ newTitle: { type: "string" },
301
+ },
302
+ required: ["pageSlug", "newPageSlug"],
303
+ },
304
+ },
305
+ handler: async (input) => applyOps([{
306
+ op: "rename_page",
307
+ pageSlug: input.pageSlug,
308
+ newPageSlug: input.newPageSlug,
309
+ ...(input.newTitle ? { newTitle: input.newTitle } : {}),
310
+ }]),
311
+ },
312
+ {
313
+ definition: {
314
+ name: "remove_page",
315
+ description: "Delete a page.",
316
+ input_schema: {
317
+ type: "object",
318
+ properties: { pageSlug: { type: "string" } },
319
+ required: ["pageSlug"],
320
+ },
321
+ },
322
+ handler: async (input) => applyOps([{ op: "remove_page", pageSlug: input.pageSlug }]),
323
+ },
324
+ {
325
+ definition: {
326
+ name: "duplicate_block",
327
+ description: "Clone a block. Optionally place the copy on a different page or after a specific block.",
328
+ input_schema: {
329
+ type: "object",
330
+ properties: {
331
+ pageSlug: { type: "string" },
332
+ blockId: { type: "string", description: "Block to duplicate" },
333
+ toPageSlug: { type: "string", description: "Target page (omit for same page)" },
334
+ afterBlockId: { type: "string", description: "Place copy after this block" },
335
+ },
336
+ required: ["pageSlug", "blockId"],
337
+ },
338
+ },
339
+ handler: async (input) => {
340
+ const op = { op: "duplicate_block", pageSlug: input.pageSlug, blockId: input.blockId };
341
+ if (input.toPageSlug)
342
+ op.toPageSlug = input.toPageSlug;
343
+ if (input.afterBlockId)
344
+ op.afterBlockId = input.afterBlockId;
345
+ return applyOps([op]);
346
+ },
347
+ },
348
+ {
349
+ definition: {
350
+ name: "duplicate_page",
351
+ description: "Clone a page with all its blocks.",
352
+ input_schema: {
353
+ type: "object",
354
+ properties: {
355
+ pageSlug: { type: "string", description: "Page to duplicate" },
356
+ newPageSlug: { type: "string", description: "Slug for the copy (e.g. '/pricing-v2')" },
357
+ newTitle: { type: "string", description: "Title for the copy" },
358
+ },
359
+ required: ["pageSlug"],
360
+ },
361
+ },
362
+ handler: async (input) => {
363
+ const op = { op: "duplicate_page", pageSlug: input.pageSlug };
364
+ if (input.newPageSlug)
365
+ op.newPageSlug = input.newPageSlug;
366
+ if (input.newTitle)
367
+ op.newTitle = input.newTitle;
368
+ return applyOps([op]);
369
+ },
370
+ },
371
+ {
372
+ definition: {
373
+ name: "move_page",
374
+ description: "Reorder a page in the site navigation. Set afterPageSlug to place it after a specific page, or omit to move to the top.",
375
+ input_schema: {
376
+ type: "object",
377
+ properties: {
378
+ pageSlug: { type: "string" },
379
+ afterPageSlug: { type: "string", description: "Place after this page. Omit to move to top." },
380
+ },
381
+ required: ["pageSlug"],
382
+ },
383
+ },
384
+ handler: async (input) => {
385
+ const op = { op: "move_page", pageSlug: input.pageSlug };
386
+ if (input.afterPageSlug)
387
+ op.afterPageSlug = input.afterPageSlug;
388
+ return applyOps([op]);
389
+ },
390
+ },
391
+ {
392
+ definition: {
393
+ name: "update_page_meta",
394
+ description: "Update page SEO metadata — title, description, and Open Graph image.",
395
+ input_schema: {
396
+ type: "object",
397
+ properties: {
398
+ pageSlug: { type: "string" },
399
+ patch: {
400
+ type: "object",
401
+ description: "SEO fields to update",
402
+ properties: {
403
+ title: { type: "string", description: "Page title (browser tab, search results)" },
404
+ description: { type: "string", description: "Meta description for search engines" },
405
+ ogImage: { type: "string", description: "Open Graph image URL for social sharing" },
406
+ },
407
+ },
408
+ },
409
+ required: ["pageSlug", "patch"],
410
+ },
411
+ },
412
+ handler: async (input) => applyOps([{
413
+ op: "update_page_meta",
414
+ pageSlug: input.pageSlug,
415
+ patch: input.patch,
416
+ }]),
417
+ },
418
+ {
419
+ definition: {
420
+ name: "add_item",
421
+ description: "Add an item to an array property on a block (e.g. FAQ items, feature cards, testimonials). The new item is given a stable id automatically — do not supply one.",
422
+ input_schema: {
423
+ type: "object",
424
+ properties: {
425
+ pageSlug: { type: "string" },
426
+ blockId: { type: "string" },
427
+ listKey: { type: "string", description: "Array property name (e.g. 'items', 'features', 'testimonials')" },
428
+ item: { type: "object", description: "Item data to add" },
429
+ afterItemId: { type: "string", description: "Insert after the item with this id (preferred over afterIndex). Omit to append." },
430
+ afterIndex: { type: "number", description: "Insert after this 0-based index. Omit to append." },
431
+ },
432
+ required: ["pageSlug", "blockId", "listKey", "item"],
433
+ },
434
+ },
435
+ handler: async (input) => {
436
+ const op = { op: "add_item", pageSlug: input.pageSlug, blockId: input.blockId, listKey: input.listKey, item: input.item };
437
+ if (input.afterItemId !== undefined)
438
+ op.afterItemId = input.afterItemId;
439
+ if (input.afterIndex !== undefined)
440
+ op.afterIndex = input.afterIndex;
441
+ return applyOps([op]);
442
+ },
443
+ },
444
+ {
445
+ definition: {
446
+ name: "update_item",
447
+ description: "Update a single item in an array property on a block. Target it by itemId (preferred — the item's stable `id`, robust when other ops mutate the list) or by 0-based index.",
448
+ input_schema: {
449
+ type: "object",
450
+ properties: {
451
+ pageSlug: { type: "string" },
452
+ blockId: { type: "string" },
453
+ listKey: { type: "string" },
454
+ itemId: { type: "string", description: "Stable id of the item to update (preferred over index)" },
455
+ index: { type: "number", description: "Item index (0-based). Used only when itemId is omitted." },
456
+ patch: { type: "object", description: "Key-value pairs to update on the item" },
457
+ },
458
+ required: ["pageSlug", "blockId", "listKey", "patch"],
459
+ },
460
+ },
461
+ handler: async (input) => {
462
+ const op = {
463
+ op: "update_item",
464
+ pageSlug: input.pageSlug,
465
+ blockId: input.blockId,
466
+ listKey: input.listKey,
467
+ patch: input.patch,
468
+ };
469
+ if (input.itemId !== undefined)
470
+ op.itemId = input.itemId;
471
+ if (input.index !== undefined)
472
+ op.index = input.index;
473
+ return applyOps([op]);
474
+ },
475
+ },
476
+ {
477
+ definition: {
478
+ name: "remove_item",
479
+ description: "Remove an item from an array property on a block. Target it by itemId (preferred — the item's stable `id`) or by 0-based index.",
480
+ input_schema: {
481
+ type: "object",
482
+ properties: {
483
+ pageSlug: { type: "string" },
484
+ blockId: { type: "string" },
485
+ listKey: { type: "string" },
486
+ itemId: { type: "string", description: "Stable id of the item to remove (preferred over index)" },
487
+ index: { type: "number", description: "Item index to remove (0-based). Used only when itemId is omitted." },
488
+ },
489
+ required: ["pageSlug", "blockId", "listKey"],
490
+ },
491
+ },
492
+ handler: async (input) => {
493
+ const op = {
494
+ op: "remove_item",
495
+ pageSlug: input.pageSlug,
496
+ blockId: input.blockId,
497
+ listKey: input.listKey,
498
+ };
499
+ if (input.itemId !== undefined)
500
+ op.itemId = input.itemId;
501
+ if (input.index !== undefined)
502
+ op.index = input.index;
503
+ return applyOps([op]);
504
+ },
505
+ },
506
+ {
507
+ definition: {
508
+ name: "move_item",
509
+ description: "Reorder an item within an array property on a block. Identify the item and its destination by itemId/afterItemId (preferred — stable `id`s) or by 0-based index/afterIndex. Omit the destination to move it to the front.",
510
+ input_schema: {
511
+ type: "object",
512
+ properties: {
513
+ pageSlug: { type: "string" },
514
+ blockId: { type: "string" },
515
+ listKey: { type: "string" },
516
+ itemId: { type: "string", description: "Stable id of the item to move (preferred over index)" },
517
+ index: { type: "number", description: "Current item index (0-based). Used only when itemId is omitted." },
518
+ afterItemId: { type: "string", description: "Move to after the item with this id (preferred over afterIndex). Omit ⇒ move to front." },
519
+ afterIndex: { type: "number", description: "Move to after this 0-based index. Omit ⇒ move to front." },
520
+ },
521
+ required: ["pageSlug", "blockId", "listKey"],
522
+ },
523
+ },
524
+ handler: async (input) => {
525
+ const op = {
526
+ op: "move_item",
527
+ pageSlug: input.pageSlug,
528
+ blockId: input.blockId,
529
+ listKey: input.listKey,
530
+ };
531
+ if (input.itemId !== undefined)
532
+ op.itemId = input.itemId;
533
+ if (input.index !== undefined)
534
+ op.index = input.index;
535
+ if (input.afterItemId !== undefined)
536
+ op.afterItemId = input.afterItemId;
537
+ if (input.afterIndex !== undefined)
538
+ op.afterIndex = input.afterIndex;
539
+ return applyOps([op]);
540
+ },
541
+ },
542
+ {
543
+ definition: {
544
+ name: "reorder_items",
545
+ description: "Reorder ALL items of a block's array property in one atomic operation by stating the final order. Prefer this over repeated move_item calls whenever more than one item changes position (sort, reverse, full rearrangement) — sequential moves use stale positions and scramble the list.",
546
+ input_schema: {
547
+ type: "object",
548
+ properties: {
549
+ pageSlug: { type: "string" },
550
+ blockId: { type: "string" },
551
+ listKey: { type: "string" },
552
+ order: { type: "array", items: { type: "string" }, description: "The FINAL order: every item's stable id, each exactly once (get ids from get_page)" },
553
+ },
554
+ required: ["pageSlug", "blockId", "listKey", "order"],
555
+ },
556
+ },
557
+ handler: async (input) => applyOps([{
558
+ op: "reorder_items",
559
+ pageSlug: input.pageSlug,
560
+ blockId: input.blockId,
561
+ listKey: input.listKey,
562
+ order: input.order,
563
+ }]),
564
+ },
565
+ {
566
+ definition: {
567
+ name: "reorder_blocks",
568
+ description: "Reorder a page's sections in one atomic operation by stating the final block order. Prefer this over repeated move_block calls whenever more than one block changes position. Omit chrome blocks (SiteHeader/Footer) — they are pinned.",
569
+ input_schema: {
570
+ type: "object",
571
+ properties: {
572
+ pageSlug: { type: "string" },
573
+ order: { type: "array", items: { type: "string" }, description: "The FINAL order: every non-chrome block id on the page, each exactly once" },
574
+ },
575
+ required: ["pageSlug", "order"],
576
+ },
577
+ },
578
+ handler: async (input) => applyOps([{
579
+ op: "reorder_blocks",
580
+ pageSlug: input.pageSlug,
581
+ order: input.order,
582
+ }]),
583
+ },
584
+ // ================================================================
585
+ // READ-ONLY CONTEXT TOOLS
586
+ // ================================================================
587
+ {
588
+ definition: {
589
+ name: "get_page",
590
+ description: "Get the full content of a page — all blocks with their properties. Use this to inspect the current state before making edits.",
591
+ input_schema: {
592
+ type: "object",
593
+ properties: {
594
+ pageSlug: { type: "string", description: "Page slug, e.g. '/' or '/pricing'" },
595
+ },
596
+ required: ["pageSlug"],
597
+ },
598
+ },
599
+ handler: async (input) => {
600
+ const page = getPage(session, input.pageSlug);
601
+ if (!page)
602
+ return { result: `Page "${input.pageSlug}" not found`, isError: true };
603
+ return {
604
+ result: JSON.stringify({
605
+ slug: page.slug,
606
+ title: page.title,
607
+ blocks: page.blocks.map((b) => ({
608
+ id: b.id,
609
+ type: b.type,
610
+ props: b.props,
611
+ })),
612
+ }, null, 2),
613
+ };
614
+ },
615
+ },
616
+ {
617
+ definition: {
618
+ name: "list_pages",
619
+ description: "List all pages in the site with their titles and block counts.",
620
+ input_schema: {
621
+ type: "object",
622
+ properties: {},
623
+ },
624
+ },
625
+ handler: async () => {
626
+ const draft = getSessionDraft(session);
627
+ const slugs = orderSlugsHomeFirst(Array.from(draft.keys()));
628
+ const pages = slugs.map((slug) => {
629
+ const page = getPage(session, slug);
630
+ return {
631
+ slug,
632
+ title: page?.title ?? "(untitled)",
633
+ blockCount: page?.blocks.length ?? 0,
634
+ blockTypes: page?.blocks.map((b) => b.type) ?? [],
635
+ };
636
+ });
637
+ return { result: JSON.stringify(pages, null, 2) };
638
+ },
639
+ },
640
+ {
641
+ definition: {
642
+ name: "get_block_schema",
643
+ description: "Get the property schema for a block type. Shows all available properties, their types, and which are required.",
644
+ input_schema: {
645
+ type: "object",
646
+ properties: {
647
+ blockType: { type: "string", description: "Block type name (PascalCase), e.g. 'Hero', 'FAQAccordion'" },
648
+ },
649
+ required: ["blockType"],
650
+ },
651
+ },
652
+ handler: async (input) => {
653
+ const blockType = input.blockType;
654
+ const defaults = defaultPropsForType(blockType);
655
+ return {
656
+ result: JSON.stringify({
657
+ type: blockType,
658
+ defaultProps: defaults,
659
+ hint: "These are the default props. The actual schema may accept additional fields. Use the default props structure as a guide.",
660
+ }, null, 2),
661
+ };
662
+ },
663
+ },
664
+ // ================================================================
665
+ // SITE CONFIG TOOLS
666
+ // ================================================================
667
+ {
668
+ definition: {
669
+ name: "get_site_config",
670
+ description: "Get the site configuration — site name, logo URL, navigation labels, and navigation groups.",
671
+ input_schema: {
672
+ type: "object",
673
+ properties: {},
674
+ },
675
+ },
676
+ handler: async () => {
677
+ const config = getSiteConfig(session);
678
+ return { result: JSON.stringify(config, null, 2) };
679
+ },
680
+ },
681
+ {
682
+ definition: {
683
+ name: "update_site_config",
684
+ description: "Update site configuration — site name, logo, navigation labels (custom text for nav links), and navigation groups (dropdown menus). Use this to edit the site header, rename the site, or reorganize navigation.",
685
+ input_schema: {
686
+ type: "object",
687
+ properties: {
688
+ name: { type: "string", description: "Site name displayed in the header" },
689
+ logo: { type: "string", description: "Logo URL" },
690
+ navLabels: {
691
+ type: "object",
692
+ description: "Custom navigation labels. Keys are page slugs, values are display text. E.g. { \"/pricing\": \"Plans & Pricing\" }",
693
+ },
694
+ navGroups: {
695
+ type: "object",
696
+ description: "Navigation dropdown groups. Keys are group labels, values are arrays of page slugs. E.g. { \"Products\": [\"/bananas\", \"/cherries\"] }",
697
+ },
698
+ },
699
+ },
700
+ },
701
+ handler: async (input) => {
702
+ const patch = {};
703
+ if (input.name !== undefined)
704
+ patch.name = input.name;
705
+ if (input.logo !== undefined)
706
+ patch.logo = input.logo;
707
+ if (input.navLabels !== undefined)
708
+ patch.navLabels = input.navLabels;
709
+ if (input.navGroups !== undefined)
710
+ patch.navGroups = input.navGroups;
711
+ return applyOps([{ op: "update_site_config", patch }]);
712
+ },
713
+ },
714
+ {
715
+ definition: {
716
+ name: "update_theme",
717
+ description: "Change the site-wide visual theme — colors, fonts, and corner radius applied to every page and block. Uses semantic tokens (not raw CSS variable names). Merge-patch: only provided tokens change; pass an empty string to reset a token to the theme default. Use for requests like 'make the brand blue', 'use a serif heading font', or 'round all the corners'.",
718
+ input_schema: {
719
+ type: "object",
720
+ properties: {
721
+ brandColor: { type: "string", description: "Primary brand color (CSS color). Sets --brand, --brand-hover, --link." },
722
+ accentColor: { type: "string", description: "Secondary accent color. Sets --accent, --accent-hover." },
723
+ backgroundColor: { type: "string", description: "Page background color. Sets --bg-0, --bg-000." },
724
+ surfaceColor: { type: "string", description: "Card/panel surface color. Sets --surface, --card-bg, --bg-100." },
725
+ headingColor: { type: "string", description: "Heading text color. Sets --heading, --text-100." },
726
+ textColor: { type: "string", description: "Body text color. Sets --body, --text-200." },
727
+ mutedTextColor: { type: "string", description: "Muted/secondary text color. Sets --body-secondary, --text-300, --caption." },
728
+ headingFont: { type: "string", description: "Heading font stack, e.g. 'Georgia, serif'. Sets --font-heading." },
729
+ bodyFont: { type: "string", description: "Body font stack. Sets --font-body." },
730
+ radius: { type: "string", description: "Corner radius across buttons/cards/inputs, e.g. '12px'. Sets --radius and friends." },
731
+ cssVars: { type: "object", description: "Raw --var → value escape hatch, merged after the semantic tokens (raw wins)." },
732
+ },
733
+ },
734
+ },
735
+ handler: async (input) => {
736
+ const { cssVars, ...tokens } = input;
737
+ const patch = {};
738
+ for (const [k, v] of Object.entries(tokens)) {
739
+ if (v !== undefined)
740
+ patch[k] = v;
741
+ }
742
+ return applyOps([{ op: "update_theme", patch, ...(cssVars ? { cssVars } : {}) }]);
743
+ },
744
+ },
745
+ // ================================================================
746
+ // IMAGE/MEDIA TOOLS
747
+ // ================================================================
748
+ {
749
+ definition: {
750
+ name: "unsplash_get_by_id",
751
+ description: "Resolve an Unsplash photo page URL (e.g. https://unsplash.com/photos/slug-PHOTOID) or a bare photo ID to a direct image asset URL. ALWAYS call this when the user or reporter provides an unsplash.com/photos/... link — that link is an HTML page, not an image, and assigning it to imageUrl will break rendering. The returned imageUrl is safe to pass straight into batch_update_props.",
752
+ input_schema: {
753
+ type: "object",
754
+ properties: {
755
+ url: { type: "string", description: "Unsplash photo page URL" },
756
+ photoId: { type: "string", description: "Bare Unsplash photo ID (alternative to url)" },
757
+ },
758
+ },
759
+ },
760
+ handler: async (input) => {
761
+ try {
762
+ const ctx = { siteId: "", sessionId: session, traceId: "agent", plannerProvider: "anthropic" };
763
+ const result = await unsplashGetByIdHandler({ input, context: ctx, manifest: unsplashGetByIdManifest });
764
+ return { result: JSON.stringify(result) };
765
+ }
766
+ catch (e) {
767
+ return { result: `Error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
768
+ }
769
+ },
770
+ },
771
+ {
772
+ definition: {
773
+ name: "unsplash_search",
774
+ description: "Search Unsplash for stock photos. Returns a list of image candidates with URLs. Use the imageUrl from the result to update a block's image property via batch_update_props.",
775
+ input_schema: {
776
+ type: "object",
777
+ properties: {
778
+ query: { type: "string", description: "Natural-language photo search query, e.g. 'modern office workspace' or 'tropical beach sunset'" },
779
+ limit: { type: "number", description: "Number of candidates to return (1-5, default 3)" },
780
+ },
781
+ required: ["query"],
782
+ },
783
+ },
784
+ handler: async (input) => {
785
+ try {
786
+ const ctx = { siteId: "", sessionId: session, traceId: "agent", plannerProvider: "anthropic" };
787
+ const result = await unsplashSearchHandler({ input, context: ctx, manifest: unsplashSearchManifest });
788
+ return { result: JSON.stringify(result) };
789
+ }
790
+ catch (e) {
791
+ return { result: `Error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
792
+ }
793
+ },
794
+ },
795
+ {
796
+ definition: {
797
+ name: "image_generate",
798
+ description: "Generate an AI image from a text prompt using DALL-E or Gemini. Returns an image URL. Use the imageUrl from the result to update a block's image property via batch_update_props. Default to 'draft' quality unless the user asks for high quality. Always provide blockType, blockId, and pageSlug for context-aware generation.",
799
+ input_schema: {
800
+ type: "object",
801
+ properties: {
802
+ prompt: { type: "string", description: "Detailed text prompt describing the image to generate" },
803
+ aspectRatio: { type: "string", enum: ["landscape", "square", "portrait"], description: "Aspect ratio (default: landscape)" },
804
+ quality: { type: "string", enum: ["draft", "final"], description: "Quality: 'draft' for fast, 'final' for production" },
805
+ style: { type: "string", description: "Optional style guidance, e.g. 'photorealistic', 'illustration', 'flat design'" },
806
+ background: { type: "string", enum: ["transparent", "opaque", "auto"], description: "Background: 'transparent' for logos/icons/cutouts, 'opaque' for full scenes, 'auto' lets the model decide (default: auto)" },
807
+ outputFormat: { type: "string", enum: ["png", "webp", "jpeg"], description: "Output format (default: png). Use png for transparency." },
808
+ blockType: { type: "string", description: "Block type (e.g. 'Hero', 'Card') — helps match image composition to layout" },
809
+ blockId: { type: "string", description: "Block ID — enables context-aware generation using block content" },
810
+ pageSlug: { type: "string", description: "Page slug — used with blockId to look up page context" },
811
+ },
812
+ required: ["prompt"],
813
+ },
814
+ },
815
+ handler: async (input) => {
816
+ try {
817
+ const ctx = { siteId: "", sessionId: session, traceId: "agent", plannerProvider: "anthropic" };
818
+ const result = await imageGenerateHandler({ input, context: ctx, manifest: imageGenerateManifest });
819
+ return { result: JSON.stringify(result) };
820
+ }
821
+ catch (e) {
822
+ return { result: `Error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
823
+ }
824
+ },
825
+ },
826
+ // ================================================================
827
+ // VARIATION TOOLS
828
+ // ================================================================
829
+ {
830
+ definition: {
831
+ name: "generate_variations",
832
+ description: "Present content variations for a block to the user. YOU generate the alternative content — provide 2-4 variations with different tones, styles, or approaches. Each variation needs a title, summary, and patch (key-value pairs to change on the block). The user sees these as clickable cards and picks their favorite. Always call get_page first to see current block props before generating variations. Do NOT apply changes after calling this — let the user pick.",
833
+ input_schema: {
834
+ type: "object",
835
+ properties: {
836
+ pageSlug: { type: "string", description: "Page slug containing the block" },
837
+ blockId: { type: "string", description: "Block ID to generate variations for" },
838
+ variations: {
839
+ type: "array",
840
+ description: "2-4 alternative content options. Each must be materially different.",
841
+ items: {
842
+ type: "object",
843
+ properties: {
844
+ title: { type: "string", description: "Short label, e.g. 'Crisp & Direct', 'Benefit-Led', 'Action-Driven'" },
845
+ summary: { type: "string", description: "One-line explanation of the approach" },
846
+ patch: { type: "object", description: "Key-value pairs to change on the block props. Only include props that differ from current values." },
847
+ },
848
+ required: ["title", "summary", "patch"],
849
+ },
850
+ },
851
+ },
852
+ required: ["pageSlug", "blockId", "variations"],
853
+ },
854
+ },
855
+ handler: async (input) => {
856
+ const page = getPage(session, input.pageSlug);
857
+ if (!page)
858
+ return { result: `Page "${input.pageSlug}" not found`, isError: true };
859
+ const block = page.blocks.find(b => b.id === input.blockId);
860
+ if (!block)
861
+ return { result: `Block "${input.blockId}" not found`, isError: true };
862
+ const rawVariations = input.variations;
863
+ if (!Array.isArray(rawVariations) || rawVariations.length === 0) {
864
+ return { result: "At least one variation is required", isError: true };
865
+ }
866
+ const blockProps = block.props;
867
+ const validatedVariations = rawVariations.slice(0, 6).map((v, idx) => {
868
+ // Filter patch to only keys that exist on the block and differ from current value
869
+ const cleanPatch = {};
870
+ const changedKeys = [];
871
+ for (const [key, value] of Object.entries(v.patch)) {
872
+ if (key in blockProps && JSON.stringify(blockProps[key]) !== JSON.stringify(value)) {
873
+ cleanPatch[key] = value;
874
+ changedKeys.push(key);
875
+ }
876
+ }
877
+ return {
878
+ id: `var_${idx}_${Date.now()}`,
879
+ title: v.title || `Option ${String.fromCharCode(65 + idx)}`,
880
+ summary: v.summary || "",
881
+ patch: Object.keys(cleanPatch).length > 0 ? cleanPatch : v.patch,
882
+ changedKeys: changedKeys.length > 0 ? changedKeys : Object.keys(v.patch),
883
+ };
884
+ });
885
+ return {
886
+ result: JSON.stringify({
887
+ status: "ok",
888
+ summary: `Generated ${validatedVariations.length} variations for ${block.type}.`,
889
+ blockId: block.id,
890
+ blockType: block.type,
891
+ pageSlug: input.pageSlug,
892
+ baseProps: blockProps,
893
+ variations: validatedVariations,
894
+ })
895
+ };
896
+ },
897
+ },
898
+ ];
899
+ }