@caelo-cms/mcp-server 0.10.64 → 0.10.66
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.
- package/package.json +1 -1
- package/power-catalogue.json +110 -58
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@caelo-cms/mcp-server",
|
|
3
3
|
"mcpName": "io.github.caelo-cms/caelo",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.66",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MPL-2.0",
|
|
7
7
|
"description": "MCP servers for Caelo CMS: `caelo_chat` (talk to Caelo's own AI) and the admin-scoped Power-MCP (the full chat-runner tool catalogue, driven by your own agent). Lets Claude Code (or any MCP-aware client) drive a Caelo install from outside the browser.",
|
package/power-catalogue.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.66",
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "edit_module",
|
|
6
|
-
"description": "Edit ONE module's structure: HTML template, fields, CSS, JS, displayName, description, or kind. **AUTHOR EXPLICITLY (v0.12.0+):** pass `html` with `{{fieldName}}` placeholders + an explicit `fields[]` array with semantic snake_case names (`hero_title`, `primary_cta_href`, `nav_items`), NOT a literal-content fallback. The operator says 'fix the homepage hero copy'; YOU translate that to the right field names. Field names like `cta2label2` or `spanText` are wrong — they leak the extractor heuristic. **Field kinds (v0.12.0):** `text`, `richtext`, `url`, `image`, `number`, `boolean`, `link` (primitives); `text-list` (array of strings, slot `{{#field}}…{{.}}…{{/field}}`); `link-list` (array of `{label, href}`, slot `{{#field}}…{{label}}…{{href}}…{{/field}}` — use for menus, footer columns); `module` (single nested module, slot `{{>field}}`); `module-list` (array of nested modules, slot `{{#field}}…{{/field}}`). **Nested fields may declare `allowedModuleTypes`** — a whitelist of stable module `type`s (e.g. `[\"button\"]`), NOT slugs. It matches the referenced module's `type`; set it to constrain what can nest in a `module`/`module-list` slot, and widen it here if a valid module is being wrongly rejected. **Lists are lists, not numbered scalars.** A menu with 10 items is ONE `link-list` field with 10 elements — never `label1`, `label2`, …, `label10`. A tag cloud is ONE `text-list`. Cards with rich per-item structure use `module-list` pointing at a sub-module. **Update description + kind when the module's purpose drifts.** The `## Modules` block exposes them to your future self; stale descriptions hurt your own decision-making. **Legacy fallback only:** if you pass HTML with literal content and NO `fields[]`, a server-side extractor mints heuristic field names — useful for one-shot drafts but the result hurts the `## Modules` block. Treat it as a fallback, not the default path. **Scope module CSS under the module's own root class** - bare global selectors (`body`, `h1`, `.card`) bleed into every other module on the page (issue #158); the tool result flags unscoped selectors. **Module JS shares ONE page-level script** - no implicit `root`/`el` binding exists; wrap in an IIFE and query via document.querySelectorAll scoped to your module's root class (see the `js` input description). **Accordion, tabs, carousel need NO module JS** - mark the markup with `data-caelo-ui=\"accordion|tabs|carousel\"` and Caelo ships vetted, accessible behaviour (markup contract in the `js` input description). Edits are CHAT-BRANCHED until publish. **For a TARGETED change to existing html/css/js, PREFER `edit_content`** (surgical string-replace: far cheaper than re-emitting the whole body here, and the diff is reviewable). Use `edit_module` for a WHOLESALE rewrite, a brand-new module body, or `fields`/`displayName`/`kind`/`type`/`description` changes. Use this for structure, styling, fields list, or `description`/`kind` updates. DO NOT use this to change what one page shows — use `set_page_module_content` (per-page content) or `set_content_instance_values` (shared content) for that. Prefer `update_modules_many` when targeting > 1 module. DO NOT use for page metadata (`update_pages_many`) or template-level edits (`propose_update_template`). **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
6
|
+
"description": "Edit ONE module's structure: HTML template, fields, CSS, JS, displayName, description, or kind. **AUTHOR EXPLICITLY (v0.12.0+):** pass `html` with `{{fieldName}}` placeholders + an explicit `fields[]` array with semantic snake_case names (`hero_title`, `primary_cta_href`, `nav_items`), NOT a literal-content fallback. The operator says 'fix the homepage hero copy'; YOU translate that to the right field names. Field names like `cta2label2` or `spanText` are wrong — they leak the extractor heuristic. **Field kinds (v0.12.0):** `text`, `richtext`, `url`, `image`, `number`, `boolean`, `link` (primitives); `text-list` (array of strings, slot `{{#field}}…{{.}}…{{/field}}`); `link-list` (array of `{label, href}`, slot `{{#field}}…{{label}}…{{href}}…{{/field}}` — use for menus, footer columns); `module` (single nested module, slot `{{>field}}`); `module-list` (array of nested modules, slot `{{#field}}…{{/field}}`). **Nested fields may declare `allowedModuleTypes`** — a whitelist of stable module `type`s (e.g. `[\"button\"]`), NOT slugs. It matches the referenced module's `type`; set it to constrain what can nest in a `module`/`module-list` slot, and widen it here if a valid module is being wrongly rejected. **Lists are lists, not numbered scalars.** A menu with 10 items is ONE `link-list` field with 10 elements — never `label1`, `label2`, …, `label10`. A tag cloud is ONE `text-list`. Cards with rich per-item structure use `module-list` pointing at a sub-module. **Update description + kind when the module's purpose drifts.** The `## Modules` block exposes them to your future self; stale descriptions hurt your own decision-making. **Legacy fallback only:** if you pass HTML with literal content and NO `fields[]`, a server-side extractor mints heuristic field names — useful for one-shot drafts but the result hurts the `## Modules` block. Treat it as a fallback, not the default path. **Scope module CSS under the module's own root class** - bare global selectors (`body`, `h1`, `.card`) bleed into every other module on the page (issue #158); the tool result flags unscoped selectors. **Module JS shares ONE page-level script** - no implicit `root`/`el` binding exists; wrap in an IIFE and query via document.querySelectorAll scoped to your module's root class (see the `js` input description). **Accordion, tabs, carousel need NO module JS** - mark the markup with `data-caelo-ui=\"accordion|tabs|carousel\"` and Caelo ships vetted, accessible behaviour (markup contract in the `js` input description). Edits are CHAT-BRANCHED until publish. **For a TARGETED change to existing html/css/js, PREFER `edit_content`** (surgical string-replace: far cheaper than re-emitting the whole body here, and the diff is reviewable). Use `edit_module` for a WHOLESALE rewrite, a brand-new module body, or `fields`/`displayName`/`kind`/`type`/`description` changes. Use this for structure, styling, fields list, or `description`/`kind` updates. DO NOT use this to change what one page shows — use `set_page_module_content` (per-page content) or `set_content_instance_values` (shared content) for that. Prefer `update_modules_many` when targeting > 1 module. DO NOT use for page metadata (`update_pages_many`) or template-level edits (`propose_update_template`). **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
7
7
|
"inputSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
@@ -41,14 +41,16 @@
|
|
|
41
41
|
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
|
|
42
42
|
},
|
|
43
43
|
"html": {
|
|
44
|
-
"type": "string"
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Module HTML. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
45
46
|
},
|
|
46
47
|
"css": {
|
|
47
|
-
"type": "string"
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Module CSS. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
48
50
|
},
|
|
49
51
|
"js": {
|
|
50
52
|
"type": "string",
|
|
51
|
-
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
53
|
+
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping). **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
52
54
|
},
|
|
53
55
|
"fields": {
|
|
54
56
|
"type": "array",
|
|
@@ -171,7 +173,7 @@
|
|
|
171
173
|
},
|
|
172
174
|
{
|
|
173
175
|
"name": "read_content",
|
|
174
|
-
"description": "Read a module's or
|
|
176
|
+
"description": "Read a module's, template's or layout's body (html/css/js) with line numbers — the Read step before a surgical edit_content. Pass `entityKind` ('module' | 'template' | 'layout' — a layout is the chrome shell its templates render in; list_templates shows each template's layoutId) + `entityId`, and optionally `field` ('html'/'css'/'js'; templates and layouts have html/css). Omit `field` to see every body; name a `field` to window a large one via `offset` (1-based line) + `limit`. Each block prints a `sha=` token you can hand back to edit_content as `expectedSha` to guard against a concurrent change. Prefer this over dumping a whole module through list_modules (which returns metadata only). Use edit_content (NOT edit_module) to change what you read here surgically.",
|
|
175
177
|
"inputSchema": {
|
|
176
178
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
177
179
|
"type": "object",
|
|
@@ -180,7 +182,8 @@
|
|
|
180
182
|
"type": "string",
|
|
181
183
|
"enum": [
|
|
182
184
|
"module",
|
|
183
|
-
"template"
|
|
185
|
+
"template",
|
|
186
|
+
"layout"
|
|
184
187
|
]
|
|
185
188
|
},
|
|
186
189
|
"entityId": {
|
|
@@ -213,7 +216,7 @@
|
|
|
213
216
|
},
|
|
214
217
|
{
|
|
215
218
|
"name": "edit_content",
|
|
216
|
-
"description": "Surgically edit a module's or
|
|
219
|
+
"description": "Surgically edit a module's, template's or layout's body (html/css/js) by string replacement — the cheap, minimal-diff way to change existing code. Pass `entityKind` + `entityId` + `field` + an `edits` array of {oldString, newString, replaceAll?}. Each `oldString` must be UNIQUE in the body (add surrounding context if not) unless `replaceAll:true`; edits apply in order, atomically (all-or-nothing). Read the body first with read_content and copy exact text (incl. whitespace) into `oldString`; optionally pass its `sha=` as `expectedSha` to guard against a concurrent change. On success it returns the NEW `sha=` plus a line-numbered snippet of each changed region — chain another edit_content in the same turn using that sha and the fresh line numbers, WITHOUT re-reading. PREFER this over edit_module for targeted changes to existing html/css/js — it is far cheaper and the diff is reviewable. Use edit_module instead for a wholesale rewrite, a new module, or field-schema / displayName / kind changes. TEMPLATES AND LAYOUTS ARE GATED: an edit_content on a template or layout is a TWO-STEP §11.A flow — you propose, the Owner clicks Approve on the proposal card. Do not claim such an edit was applied. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
217
220
|
"inputSchema": {
|
|
218
221
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
219
222
|
"type": "object",
|
|
@@ -222,7 +225,8 @@
|
|
|
222
225
|
"type": "string",
|
|
223
226
|
"enum": [
|
|
224
227
|
"module",
|
|
225
|
-
"template"
|
|
228
|
+
"template",
|
|
229
|
+
"layout"
|
|
226
230
|
]
|
|
227
231
|
},
|
|
228
232
|
"entityId": {
|
|
@@ -277,7 +281,7 @@
|
|
|
277
281
|
},
|
|
278
282
|
{
|
|
279
283
|
"name": "grep_content",
|
|
280
|
-
"description": "Search every module
|
|
284
|
+
"description": "Search every module, template and layout body for a string or regex — find WHERE to edit before calling edit_content. Pass `pattern` (literal substring, or a JS regex with `isRegex:true`), optional `ignoreCase`, and narrow with `entityKind` and/or `field` (html/css/js). Returns matching lines as `<kind> <slug> (<id>) <field>:<lineNo>: <text>` so you can jump straight to read_content + edit_content on the right entity. Use when the operator says 'change X everywhere it appears' or you need to locate a class / string across the catalog.",
|
|
281
285
|
"inputSchema": {
|
|
282
286
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
283
287
|
"type": "object",
|
|
@@ -291,7 +295,8 @@
|
|
|
291
295
|
"type": "string",
|
|
292
296
|
"enum": [
|
|
293
297
|
"module",
|
|
294
|
-
"template"
|
|
298
|
+
"template",
|
|
299
|
+
"layout"
|
|
295
300
|
]
|
|
296
301
|
},
|
|
297
302
|
"field": {
|
|
@@ -319,7 +324,7 @@
|
|
|
319
324
|
},
|
|
320
325
|
{
|
|
321
326
|
"name": "set_page_module_content",
|
|
322
|
-
"description": "Set the content values for ONE module placement on a specific page. **Prefer `set_page_module_content_many` when a content pass touches more than one placement** (a whole page's text in one transaction — §11 bulk-first), and `build_page` when the modules don't exist yet (it places modules WITH content in one call). v0.12.0 — page CONTENT lives in a content_instance bound to the placement. For UNSYNCED placements (the default) this tool routes through content_instances.set_values and edits stay local to this page. For SYNCED placements (sharing content with other pages) this tool refuses and points you at fork_placement_content (to detach first) or set_content_instance_values (to commit to the propagate-everywhere blast radius). Use this when the operator says 'change the hero heading on /home to X' or 'set the about page subtitle to Y'. Use `edit_module` (NOT this tool) when the change should affect every page using the module (structural / styling / field-schema change). `contentValues` is keyed by the module's declared field names (`{{fieldName}}` placeholders). For nested fields (kind `module` / `module-list`) the value is `{ moduleId, contentInstanceId }` (or an array of them); the referenced module's stable `type` must be in the field's `allowedModuleTypes` when set (reuse a module of an allowed type — see `## Modules`). Missing fields fall back to the module field's `default`. Pass an empty object to reset a placement to all defaults. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
327
|
+
"description": "Set the content values for ONE module placement on a specific page. **Prefer `set_page_module_content_many` when a content pass touches more than one placement** (a whole page's text in one transaction — §11 bulk-first), and `build_page` when the modules don't exist yet (it places modules WITH content in one call). v0.12.0 — page CONTENT lives in a content_instance bound to the placement. For UNSYNCED placements (the default) this tool routes through content_instances.set_values and edits stay local to this page. For SYNCED placements (sharing content with other pages) this tool refuses and points you at fork_placement_content (to detach first) or set_content_instance_values (to commit to the propagate-everywhere blast radius). Use this when the operator says 'change the hero heading on /home to X' or 'set the about page subtitle to Y'. Use `edit_module` (NOT this tool) when the change should affect every page using the module (structural / styling / field-schema change). `contentValues` is keyed by the module's declared field names (`{{fieldName}}` placeholders). For nested fields (kind `module` / `module-list`) the value is `{ moduleId, contentInstanceId }` (or an array of them); the referenced module's stable `type` must be in the field's `allowedModuleTypes` when set (reuse a module of an allowed type — see `## Modules`). Missing fields fall back to the module field's `default`. Pass an empty object to reset a placement to all defaults. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
323
328
|
"inputSchema": {
|
|
324
329
|
"type": "object",
|
|
325
330
|
"additionalProperties": false,
|
|
@@ -422,7 +427,7 @@
|
|
|
422
427
|
},
|
|
423
428
|
{
|
|
424
429
|
"name": "build_page",
|
|
425
|
-
"description": "Build a WHOLE page in ONE call: the page (create new via `page: {slug, title}` or target existing via `page: {pageId}`) plus the FULL ordered module list — each entry places one module into a template block, top to bottom. ONE transaction: any invalid entry aborts the whole call (the error names `modules[i]` and the failing field) and nothing is written. This is THE page-creation tool: `modules: []` (empty) creates an intentionally empty page shell; a populated `modules` array builds the whole page in one transaction. **Prefer it over add_module / create_content_instance chains whenever a page needs more than one module** — one build_page call replaces that whole round-trip chain (§11 bulk-first). Use add_module (target='page') only for a single incremental module on an existing page. **Per module, two modes** (same contract as add_module): pass `moduleId` to place an EXISTING module from `## Modules` (reuse first — shared modules keep pages consistent), or `displayName` + `html` (+ required `description`, `kind`, explicit `fields[]`) to mint a new one. Field names are semantic snake_case (`hero_title`, `primary_cta_href`); repeats are LIST fields (`text-list`, `link-list`, `module-list`), never numbered scalars. **Per module, `content` fills the placement**: `{source:'inline', values:{hero_title:'…'}}` for page-local content (the routine case); `{source:'shared', purpose:'…', values:{…}}` to mint a REUSABLE instance other pages can bind (synced by default); `{source:'existing', contentInstanceId}` to bind a row from `## Content Library`. Omit `content` for an empty private instance you fill later. Typical call: `{page:{slug:'pricing', title:'Pricing'}, modules:[{blockName:'content', displayName:'Pricing Hero', kind:'hero', description:'…', html:'<section><h1>{{hero_title}}</h1></section>', fields:[{name:'hero_title', kind:'text', label:'Hero title'}], content:{source:'inline', values:{hero_title:'Fair pricing'}}}, …]}`. **Nested modules in the SAME call**: give the inner module `ref` and NO blockName (detached — minted + content-bound but not placed), then reference it from the outer module's `module`/`module-list` field value as `{\"$ref\": \"<ref>\"}` — the server resolves it to the real {moduleId, contentInstanceId}
|
|
430
|
+
"description": "Build a WHOLE page in ONE call: the page (create new via `page: {slug, title}` or target existing via `page: {pageId}`) plus the FULL ordered module list — each entry places one module into a template block, top to bottom (on an existing page entries append; an entry's optional `position` — 'top' or an index, as in add_module — inserts it instead). ONE transaction: any invalid entry aborts the whole call (the error names `modules[i]` and the failing field) and nothing is written. This is THE page-creation tool: when creating, pass `page.metaDescription` (and `status:'published'` for a page that belongs on the site) — a Stage refuses published pages without a description, and builds only published ones; the result says when something still blocks a Stage. `modules: []` (empty) creates an intentionally empty page shell; a populated `modules` array builds the whole page in one transaction. **Prefer it over add_module / create_content_instance chains whenever a page needs more than one module** — one build_page call replaces that whole round-trip chain (§11 bulk-first). Use add_module (target='page') only for a single incremental module on an existing page. **Per module, two modes** (same contract as add_module): pass `moduleId` to place an EXISTING module from `## Modules` (reuse first — shared modules keep pages consistent), or `displayName` + `html` (+ required `description`, `kind`, explicit `fields[]`) to mint a new one. Field names are semantic snake_case (`hero_title`, `primary_cta_href`); repeats are LIST fields (`text-list`, `link-list`, `module-list`), never numbered scalars. **Per module, `content` fills the placement**: `{source:'inline', values:{hero_title:'…'}}` for page-local content (the routine case); `{source:'shared', purpose:'…', values:{…}}` to mint a REUSABLE instance other pages can bind (synced by default); `{source:'existing', contentInstanceId}` to bind a row from `## Content Library`. Omit `content` for an empty private instance you fill later. Typical call: `{page:{slug:'pricing', title:'Pricing', status:'published', metaDescription:'…'}, modules:[{blockName:'content', displayName:'Pricing Hero', kind:'hero', description:'…', html:'<section><h1>{{hero_title}}</h1></section>', fields:[{name:'hero_title', kind:'text', label:'Hero title'}], content:{source:'inline', values:{hero_title:'Fair pricing'}}}, …]}`. **Nested modules in the SAME call**: give the inner module `ref` and NO blockName (detached — minted + content-bound but not placed), then reference it from the outer module's `module`/`module-list` field value as `{\"$ref\": \"<ref>\"}` — the server resolves it to the real {moduleId, contentInstanceId}, in any array order. Example: `modules:[{ref:'btn', displayName:'Button', html:'<button>{{label}}</button>', fields:[…], content:{source:'inline', values:{label:'Go'}}}, {blockName:'content', displayName:'CTA', html:'<section>{{>cta}}</section>', fields:[{name:'cta', kind:'module', label:'CTA', allowedModuleTypes:['button']}], content:{source:'inline', values:{cta:{'$ref':'btn'}}}}]` — the whole nested composition is ONE call, never an add_module → remove_module_from → create_content_instance chain. For site-wide chrome (header/footer) use add_module (target='layout') — chrome is layout-owned and never rides in a page body. Each entry's `blockName` must be a block on the page's template — see `# Current page` (exhaustive list for the focused page); a mismatch error names the valid set. A block name is a template slot, NOT a module `kind`. With zero templates on the site, bootstrap first (create_layout → create_template). **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
426
431
|
"inputSchema": {
|
|
427
432
|
"type": "object",
|
|
428
433
|
"additionalProperties": false,
|
|
@@ -434,7 +439,7 @@
|
|
|
434
439
|
"page": {
|
|
435
440
|
"type": "object",
|
|
436
441
|
"additionalProperties": false,
|
|
437
|
-
"description": "EITHER { pageId } to build onto an existing page (modules are appended), OR { slug, title, name?, templateId?, status?, parentPageId? } to create the page in the same call. Never both.",
|
|
442
|
+
"description": "EITHER { pageId } to build onto an existing page (modules are appended), OR { slug, title, metaDescription, name?, templateId?, status?, parentPageId? } to create the page in the same call. Never both.",
|
|
438
443
|
"properties": {
|
|
439
444
|
"pageId": {
|
|
440
445
|
"type": "string",
|
|
@@ -443,7 +448,8 @@
|
|
|
443
448
|
"slug": {
|
|
444
449
|
"type": "string",
|
|
445
450
|
"minLength": 1,
|
|
446
|
-
"maxLength": 120
|
|
451
|
+
"maxLength": 120,
|
|
452
|
+
"description": "Lowercase letters, digits and hyphens only — never a path or a slash. The HOMEPAGE (served at `/`) uses slug 'home'."
|
|
447
453
|
},
|
|
448
454
|
"title": {
|
|
449
455
|
"type": "string",
|
|
@@ -464,7 +470,14 @@
|
|
|
464
470
|
"enum": [
|
|
465
471
|
"draft",
|
|
466
472
|
"published"
|
|
467
|
-
]
|
|
473
|
+
],
|
|
474
|
+
"description": "Only 'published' pages are built on staging. Omitted: published on a site with no published page yet, else draft. Pass 'published' for a page that belongs on the site now."
|
|
475
|
+
},
|
|
476
|
+
"metaDescription": {
|
|
477
|
+
"type": "string",
|
|
478
|
+
"minLength": 1,
|
|
479
|
+
"maxLength": 320,
|
|
480
|
+
"description": "Create mode: the new page's meta description (150–160 characters, written from the page's content; in a migration, the source page's). A Stage refuses a published page without one — pass it here so the page is Stage-ready from this call (fill-once, same as autofill_page_seo)."
|
|
468
481
|
},
|
|
469
482
|
"parentPageId": {
|
|
470
483
|
"type": "string",
|
|
@@ -490,15 +503,32 @@
|
|
|
490
503
|
"type": "string",
|
|
491
504
|
"minLength": 1,
|
|
492
505
|
"maxLength": 80,
|
|
493
|
-
"description": "
|
|
506
|
+
"description": "Page-TEMPLATE block to place into (usually 'content'; see `# Current page`). Never 'header' / 'footer': site chrome lives on the layout (add_module target='layout'). OMIT for a DETACHED nested-only module (requires `ref`; another entry embeds it via {\"$ref\": …})."
|
|
507
|
+
},
|
|
508
|
+
"position": {
|
|
509
|
+
"description": "Where in its block this entry lands: \"top\", \"bottom\" (default — entries append in array order, so a new page needs none) or a 0-based index. Same grammar as add_module. Not for detached entries.",
|
|
510
|
+
"oneOf": [
|
|
511
|
+
{
|
|
512
|
+
"type": "string",
|
|
513
|
+
"enum": [
|
|
514
|
+
"top",
|
|
515
|
+
"bottom"
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"type": "integer",
|
|
520
|
+
"minimum": 0,
|
|
521
|
+
"maximum": 1000
|
|
522
|
+
}
|
|
523
|
+
]
|
|
494
524
|
},
|
|
495
525
|
"ref": {
|
|
496
526
|
"type": "string",
|
|
497
527
|
"pattern": "^[a-z][a-z0-9_-]{0,31}$",
|
|
498
|
-
"description": "Local handle:
|
|
528
|
+
"description": "Local handle: another entry's module/module-list field value (or moduleId) {\"$ref\": \"<ref>\"} resolves to this entry's {moduleId, contentInstanceId}. Array order does not matter — referenced entries are created first automatically."
|
|
499
529
|
},
|
|
500
530
|
"moduleId": {
|
|
501
|
-
"description": "EXISTING module UUID from ## Modules — or {\"$ref\": \"<handle>\"} to RE-PLACE a module minted
|
|
531
|
+
"description": "EXISTING module UUID from ## Modules — or {\"$ref\": \"<handle>\"} to RE-PLACE a module minted by another entry of THIS call (e.g. one card module, three placements; each entry still gets its own content).",
|
|
502
532
|
"oneOf": [
|
|
503
533
|
{
|
|
504
534
|
"type": "string",
|
|
@@ -546,16 +576,18 @@
|
|
|
546
576
|
"html": {
|
|
547
577
|
"type": "string",
|
|
548
578
|
"minLength": 1,
|
|
549
|
-
"maxLength": 50000
|
|
579
|
+
"maxLength": 50000,
|
|
580
|
+
"description": "Module HTML. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
550
581
|
},
|
|
551
582
|
"css": {
|
|
552
583
|
"type": "string",
|
|
553
|
-
"maxLength": 50000
|
|
584
|
+
"maxLength": 50000,
|
|
585
|
+
"description": "Module CSS. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
554
586
|
},
|
|
555
587
|
"js": {
|
|
556
588
|
"type": "string",
|
|
557
589
|
"maxLength": 50000,
|
|
558
|
-
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
590
|
+
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping). **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
559
591
|
},
|
|
560
592
|
"fields": {
|
|
561
593
|
"type": "array",
|
|
@@ -774,7 +806,7 @@
|
|
|
774
806
|
},
|
|
775
807
|
{
|
|
776
808
|
"name": "set_page_module_content_many",
|
|
777
|
-
"description": "Set content values for SEVERAL placements in ONE transaction — prefer this over multiple `set_page_module_content` calls whenever a content pass touches more than one placement (a whole page's text, the same fix across pages; §11 bulk-first). All-or-nothing: any invalid item aborts the whole batch (the error names `items[i]`, the placement, and for value problems the failing field) and nothing is written. Each item is the exact `set_page_module_content` shape: `{pageId, blockName, position, contentValues}` with `contentValues` keyed by the module's declared field names; each fully replaces that placement's values. Items may span multiple pages. Same UNSYNCED-only contract as the singular tool: a SYNCED placement in the batch aborts it with the fork/commit guidance — fork_placement_content that placement (or set_content_instance_values deliberately) and resend. **When NOT to use:** when the page and its modules don't exist yet, `build_page` places modules WITH their content in one call — never build a page as add_module + set_page_module_content_many when one build_page call does both. For one placement the singular tool is fine. Typical call: `{items: [{pageId: \"…\", blockName: \"content\", position: 0, contentValues: {hero_title: \"…\"}}, {pageId: \"…\", blockName: \"content\", position: 1, contentValues: {body: \"…\"}}]}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
809
|
+
"description": "Set content values for SEVERAL placements in ONE transaction — prefer this over multiple `set_page_module_content` calls whenever a content pass touches more than one placement (a whole page's text, the same fix across pages; §11 bulk-first). All-or-nothing: any invalid item aborts the whole batch (the error names `items[i]`, the placement, and for value problems the failing field) and nothing is written. Each item is the exact `set_page_module_content` shape: `{pageId, blockName, position, contentValues}` with `contentValues` keyed by the module's declared field names; each fully replaces that placement's values. Items may span multiple pages. Same UNSYNCED-only contract as the singular tool: a SYNCED placement in the batch aborts it with the fork/commit guidance — fork_placement_content that placement (or set_content_instance_values deliberately) and resend. **When NOT to use:** when the page and its modules don't exist yet, `build_page` places modules WITH their content in one call — never build a page as add_module + set_page_module_content_many when one build_page call does both. For one placement the singular tool is fine. Typical call: `{items: [{pageId: \"…\", blockName: \"content\", position: 0, contentValues: {hero_title: \"…\"}}, {pageId: \"…\", blockName: \"content\", position: 1, contentValues: {body: \"…\"}}]}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
778
810
|
"inputSchema": {
|
|
779
811
|
"type": "object",
|
|
780
812
|
"additionalProperties": false,
|
|
@@ -821,7 +853,7 @@
|
|
|
821
853
|
},
|
|
822
854
|
{
|
|
823
855
|
"name": "create_content_instances",
|
|
824
|
-
"description": "Mint SEVERAL content_instances in ONE transaction — prefer this over multiple `create_content_instance` calls whenever you need more than one instance (§11 bulk-first: one round-trip instead of N). All-or-nothing: any invalid item aborts the whole batch (the error names `instances[i]` and, for value problems, the failing field) and nothing is written. Each item is the exact `create_content_instance` shape: `{moduleId, values, purpose?, slug?, displayName?}` — `purpose` required for SHARED rows (why this row exists as a reusable instance; surfaced in `## Content Library`), `values` keyed by the module's declared field names. **When NOT to use:** if you're also creating the page and its modules, `build_page` does the whole assembly (modules + instances + placements) in one call — use that instead. For a single instance the singular tool is fine. After minting, bind rows to placements via `set_placement_content` (or let `build_page` content.source='existing' bind them). Typical call: `{instances: [{moduleId: \"…\", purpose: \"Footer CTA shared across product pages\", values: {cta_label: \"Start free\", cta_href: \"/signup\"}}, {moduleId: \"…\", values: {…}}]}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
856
|
+
"description": "Mint SEVERAL content_instances in ONE transaction — prefer this over multiple `create_content_instance` calls whenever you need more than one instance (§11 bulk-first: one round-trip instead of N). All-or-nothing: any invalid item aborts the whole batch (the error names `instances[i]` and, for value problems, the failing field) and nothing is written. Each item is the exact `create_content_instance` shape: `{moduleId, values, purpose?, slug?, displayName?}` — `purpose` required for SHARED rows (why this row exists as a reusable instance; surfaced in `## Content Library`), `values` keyed by the module's declared field names. **When NOT to use:** if you're also creating the page and its modules, `build_page` does the whole assembly (modules + instances + placements) in one call — use that instead. For a single instance the singular tool is fine. After minting, bind rows to placements via `set_placement_content` (or let `build_page` content.source='existing' bind them). Typical call: `{instances: [{moduleId: \"…\", purpose: \"Footer CTA shared across product pages\", values: {cta_label: \"Start free\", cta_href: \"/signup\"}}, {moduleId: \"…\", values: {…}}]}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
825
857
|
"inputSchema": {
|
|
826
858
|
"type": "object",
|
|
827
859
|
"additionalProperties": false,
|
|
@@ -935,7 +967,7 @@
|
|
|
935
967
|
},
|
|
936
968
|
{
|
|
937
969
|
"name": "create_content_instance",
|
|
938
|
-
"description": "Mint ONE shared, reusable content_instance for a module — used when the same content should appear identically on N pages (the site footer's copyright, a brand banner across product pages, a repeated CTA in the blog). **Prefer `create_content_instances` (plural) when minting more than one instance** — one transaction instead of N round-trips (§11 bulk-first) — and `build_page` when you're also creating the page/modules (its per-module `content` payload mints and binds in the same call). The new row is then bindable to N placements via `set_placement_content({syncMode:'synced'})` so editing it propagates everywhere bound. **Required v0.12.0 input:** `purpose` — a one-line rationale for why this is a shared row (\"The brand footer used across the whole marketing site\", \"Pricing CTA — appears at the bottom of every pricing-adjacent page\"). The `## Content Library` block surfaces this so your future self can decide reuse vs fork without asking the operator. **When NOT to use:** for one-off per-page content, prefer `set_page_module_content` — that path auto-mints an unsynced (private) instance per placement, no shared semantics, no decision needed. Only mint a shared instance when reuse is the actual intent. **Decision rule before calling:** check `## Content Library` first. If a row with the matching purpose + module + appropriate placement pattern already exists, BIND to it via `set_placement_content` instead of minting a new one. Duplicate shared rows defeat the point. `values` keys are the module's declared field names (`{{fieldName}}` placeholders); `slug` is an optional kebab-case handle (must be unique per module) for easy AI-side reference. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
970
|
+
"description": "Mint ONE shared, reusable content_instance for a module — used when the same content should appear identically on N pages (the site footer's copyright, a brand banner across product pages, a repeated CTA in the blog). **Prefer `create_content_instances` (plural) when minting more than one instance** — one transaction instead of N round-trips (§11 bulk-first) — and `build_page` when you're also creating the page/modules (its per-module `content` payload mints and binds in the same call). The new row is then bindable to N placements via `set_placement_content({syncMode:'synced'})` so editing it propagates everywhere bound. **Required v0.12.0 input:** `purpose` — a one-line rationale for why this is a shared row (\"The brand footer used across the whole marketing site\", \"Pricing CTA — appears at the bottom of every pricing-adjacent page\"). The `## Content Library` block surfaces this so your future self can decide reuse vs fork without asking the operator. **When NOT to use:** for one-off per-page content, prefer `set_page_module_content` — that path auto-mints an unsynced (private) instance per placement, no shared semantics, no decision needed. Only mint a shared instance when reuse is the actual intent. **Decision rule before calling:** check `## Content Library` first. If a row with the matching purpose + module + appropriate placement pattern already exists, BIND to it via `set_placement_content` instead of minting a new one. Duplicate shared rows defeat the point. `values` keys are the module's declared field names (`{{fieldName}}` placeholders); `slug` is an optional kebab-case handle (must be unique per module) for easy AI-side reference. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
939
971
|
"inputSchema": {
|
|
940
972
|
"type": "object",
|
|
941
973
|
"additionalProperties": false,
|
|
@@ -969,7 +1001,7 @@
|
|
|
969
1001
|
},
|
|
970
1002
|
{
|
|
971
1003
|
"name": "set_content_instance_values",
|
|
972
|
-
"description": "Edit a content_instance's values. **BLAST RADIUS = placementCount in `## Content Library`** — this propagates to EVERY placement bound with sync_mode='synced'. **Read `## Content Library` first** to see the row's `purpose` + sample pages + placement count before editing. If the operator described an edit that should ONLY affect one page, this is the WRONG tool — use `set_page_module_content` (unsynced placements) or `fork_placement_content` first (synced placements that need to be detached). `values` fully replaces existing values (zero-merge); read first via `get_content_instance` if you need to preserve other fields. **Nested fields (kind `module` / `module-list`):** the value is `{ moduleId, contentInstanceId }` (single) or an array of them (list). The referenced module's stable `type` MUST be in the field's `allowedModuleTypes` when that whitelist is set — see each module's `type` in `## Modules`. Reuse an existing module of an allowed type rather than minting a duplicate; if none fits, create one with `type` set to an allowed value. Optional metadata edits in the same write: `slug` + `displayName` (pass `null` to clear), and v0.12.0 `purpose` (rewrite the rationale when the operator's intent for this shared row has shifted — keeps `## Content Library` accurate for your future self). Prefer `set_content_instance_values_many` over multiple `set_content_instance_values` calls when changing more than one. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
1004
|
+
"description": "Edit a content_instance's values. **BLAST RADIUS = placementCount in `## Content Library`** — this propagates to EVERY placement bound with sync_mode='synced'. **Read `## Content Library` first** to see the row's `purpose` + sample pages + placement count before editing. If the operator described an edit that should ONLY affect one page, this is the WRONG tool — use `set_page_module_content` (unsynced placements) or `fork_placement_content` first (synced placements that need to be detached). `values` fully replaces existing values (zero-merge); read first via `get_content_instance` if you need to preserve other fields. **Nested fields (kind `module` / `module-list`):** the value is `{ moduleId, contentInstanceId }` (single) or an array of them (list). The referenced module's stable `type` MUST be in the field's `allowedModuleTypes` when that whitelist is set — see each module's `type` in `## Modules`. Reuse an existing module of an allowed type rather than minting a duplicate; if none fits, create one with `type` set to an allowed value. Optional metadata edits in the same write: `slug` + `displayName` (pass `null` to clear), and v0.12.0 `purpose` (rewrite the rationale when the operator's intent for this shared row has shifted — keeps `## Content Library` accurate for your future self). Prefer `set_content_instance_values_many` over multiple `set_content_instance_values` calls when changing more than one. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
973
1005
|
"inputSchema": {
|
|
974
1006
|
"type": "object",
|
|
975
1007
|
"properties": {
|
|
@@ -1034,7 +1066,7 @@
|
|
|
1034
1066
|
},
|
|
1035
1067
|
{
|
|
1036
1068
|
"name": "delete_content_instance",
|
|
1037
|
-
"description": "Soft-delete a content_instance. Only allowed when ZERO placements still bind to it (orphan instance). When placements exist, the op returns an error
|
|
1069
|
+
"description": "Soft-delete a content_instance. Only allowed when ZERO placements still bind to it (orphan instance). When placements exist, the op returns an error listing each placement (page path, pageId, blockName, position): detach each one via fork_placement_content with those values (the page keeps its content as a private copy), then retry. In a chat this all happens on the chat's draft; the live site changes when the chat is staged. Use list_content_instances to find orphan instances (placementCount=0) and prune them. Prefer `delete_content_instances` over multiple `delete_content_instance` calls when deleting more than one.",
|
|
1038
1070
|
"inputSchema": {
|
|
1039
1071
|
"type": "object",
|
|
1040
1072
|
"additionalProperties": false,
|
|
@@ -1051,7 +1083,7 @@
|
|
|
1051
1083
|
},
|
|
1052
1084
|
{
|
|
1053
1085
|
"name": "set_content_instance_values_many",
|
|
1054
|
-
"description": "Edit SEVERAL content_instances in ONE transaction — prefer this over multiple `set_content_instance_values` calls whenever you change more than one (§11 bulk-first: one round-trip + one snapshot instead of N). All-or-nothing: any invalid item aborts the whole batch (the error names `items[i]`) and nothing is written. **Same per-item BLAST RADIUS caveat as the singular tool:** each item propagates to EVERY placement bound to that instance with sync_mode='synced' — read `## Content Library` first. `values` fully replaces existing values (zero-merge) per item. Each item is the exact `set_content_instance_values` shape: `{id, values, expectedVersion?, slug?, displayName?, purpose?}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
1086
|
+
"description": "Edit SEVERAL content_instances in ONE transaction — prefer this over multiple `set_content_instance_values` calls whenever you change more than one (§11 bulk-first: one round-trip + one snapshot instead of N). All-or-nothing: any invalid item aborts the whole batch (the error names `items[i]`) and nothing is written. **Same per-item BLAST RADIUS caveat as the singular tool:** each item propagates to EVERY placement bound to that instance with sync_mode='synced' — read `## Content Library` first. `values` fully replaces existing values (zero-merge) per item. Each item is the exact `set_content_instance_values` shape: `{id, values, expectedVersion?, slug?, displayName?, purpose?}`. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
1055
1087
|
"inputSchema": {
|
|
1056
1088
|
"type": "object",
|
|
1057
1089
|
"additionalProperties": false,
|
|
@@ -1866,7 +1898,7 @@
|
|
|
1866
1898
|
},
|
|
1867
1899
|
{
|
|
1868
1900
|
"name": "add_import_page_notes",
|
|
1869
|
-
"description": "Record findings you made while rebuilding an imported page: typos (fix obvious ones in the rebuilt content and set applied: true), dead links, missing image alt texts, thin content, improvement ideas (applied: false — the operator decides). BATCH all of a page's notes in ONE call. `importPageId` accepts EITHER id: the staging import_pages id OR the
|
|
1901
|
+
"description": "Record findings you made while rebuilding an imported page: typos (fix obvious ones in the rebuilt content and set applied: true), dead links, missing image alt texts, thin content, improvement ideas (applied: false — the operator decides). BATCH all of a page's notes in ONE call. `importPageId` accepts EITHER id: the staging import_pages id OR the CMS page id you just built (from build_page / accept_import_pages) — both work, no need to look one up from the other. These feed the migration report the operator receives at the end — record honestly, including what you could not fix.",
|
|
1870
1902
|
"inputSchema": {
|
|
1871
1903
|
"type": "object",
|
|
1872
1904
|
"additionalProperties": false,
|
|
@@ -1878,7 +1910,7 @@
|
|
|
1878
1910
|
"importPageId": {
|
|
1879
1911
|
"type": "string",
|
|
1880
1912
|
"format": "uuid",
|
|
1881
|
-
"description": "The page to attach notes to — pass EITHER the staging import_pages id OR the
|
|
1913
|
+
"description": "The page to attach notes to — pass EITHER the staging import_pages id OR the built CMS page id (accepted_page_id). Both resolve to the same import page."
|
|
1882
1914
|
},
|
|
1883
1915
|
"notes": {
|
|
1884
1916
|
"type": "array",
|
|
@@ -2013,7 +2045,7 @@
|
|
|
2013
2045
|
},
|
|
2014
2046
|
{
|
|
2015
2047
|
"name": "check_page_content_inventory",
|
|
2016
|
-
"description": "Verify a rebuilt imported page lost NO content from the source. Call it right AFTER you rebuild a page's modules with `build_page`. It compares the source page's crawled content against the rebuilt modules and returns what is covered plus a LOUD list of every missing heading, paragraph, list item, image, link, or CTA (with the source context so you can find it). Content is sacred: restore anything genuinely dropped, or record WHY you dropped it (add_import_page_notes) so the operator sees it — never let content vanish silently. Chrome (header/footer) is excluded by default because it is layout-owned. `importPageId` accepts the staging import_pages id (from list_import_pages / get_import_page — this ALWAYS resolves)
|
|
2048
|
+
"description": "Verify a rebuilt imported page lost NO content from the source. Call it right AFTER you rebuild a page's modules with `build_page`. It compares the source page's crawled content against the rebuilt modules and returns what is covered plus a LOUD list of every missing heading, paragraph, list item, image, link, or CTA (with the source context so you can find it). Content is sacred: restore anything genuinely dropped, or record WHY you dropped it (add_import_page_notes) so the operator sees it — never let content vanish silently. Chrome (header/footer) is excluded by default because it is layout-owned. `importPageId` accepts the staging import_pages id (from list_import_pages / get_import_page — this ALWAYS resolves) or the built CMS page id (matched by the build_page importPageId link, by slug, or — for the homepage built at a custom slug — by its locale home-page designation). If a built page can't be found (you gave it a custom/translated slug), pass the staging import_pages id instead. Import data does not expire, so a miss is an id/slug mismatch, not staleness.",
|
|
2017
2049
|
"inputSchema": {
|
|
2018
2050
|
"type": "object",
|
|
2019
2051
|
"additionalProperties": false,
|
|
@@ -2024,7 +2056,7 @@
|
|
|
2024
2056
|
"importPageId": {
|
|
2025
2057
|
"type": "string",
|
|
2026
2058
|
"format": "uuid",
|
|
2027
|
-
"description": "The page to check — pass EITHER the staging import_pages id OR the
|
|
2059
|
+
"description": "The page to check — pass EITHER the staging import_pages id OR the built CMS page id (accepted_page_id)."
|
|
2028
2060
|
},
|
|
2029
2061
|
"includeChrome": {
|
|
2030
2062
|
"type": "boolean",
|
|
@@ -2488,7 +2520,7 @@
|
|
|
2488
2520
|
},
|
|
2489
2521
|
{
|
|
2490
2522
|
"name": "add_module",
|
|
2491
|
-
"description": "Add ONE module to a page, a layout (site-wide chrome on every page), or a template (fans out to every page of that page-type). **Prefer `build_page` when a PAGE needs more than one module** — it places the whole ordered list WITH content in one transaction. `target` = 'page' | 'layout' | 'template'. `targetRef` is that page's id or URL path ('/docs/setup'), or the layout's / template's slug or id. A bare page slug works only while it names one page — nested pages can share a slug. Two modes, on ALL three targets. **Reuse (check the catalog first):** pass `moduleId` of an existing module from `## Modules` to place/fan-out a SHARED module without minting a duplicate — a later edit_module then updates it everywhere at once. **Mint:** pass `html` (+ `displayName`, `kind`, and semantic snake_case `fields`) to author a new module and place it; raw HTML is fine, it is parametrised into {{fields}} automatically. `moduleId` is placement-only: if you also pass authoring fields (a different label, new html, …) the call still succeeds but those fields are NOT applied and the result says so — to actually change a shared module, place it by `moduleId` and then call edit_module. **The initial content comes IN THIS CALL, always via `values`** ({fieldName: value, …}): on 'page' it fills the placement's content_instance, on 'template' every fanned-out placement gets it, on 'layout' it is stored as the minted module's field defaults (chrome has no content_instance — so layout `values` require explicit `fields`, and are rejected with `moduleId` reuse: a shared module renders its stored defaults; edit_module changes them). A mint with fields but neither `values` nor field defaults is REJECTED — it would render empty placeholders until a second call. `blockName` must be a real block on the target; the handler returns the available block set if it isn't. NOTE on `position`: pass the literal string \"top\" or \"bottom\", OR a bare integer (0, 1, 2…). Prefer a bare integer (`0`, not `\"0\"`). **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.\n\nOver this connection the install's AI is not used: the AI part comes back to you — a work item to answer with your own model and send back through the tool it names, or an `AiDelegated:` answer that says what to do instead (see caelo_get_context).",
|
|
2523
|
+
"description": "Add ONE module to a page, a layout (site-wide chrome on every page), or a template (fans out to every page of that page-type). **Prefer `build_page` when a PAGE needs more than one module** — it places the whole ordered list WITH content in one transaction. `target` = 'page' | 'layout' | 'template'. `targetRef` is that page's id or URL path ('/docs/setup'), or the layout's / template's slug or id. A bare page slug works only while it names one page — nested pages can share a slug. Two modes, on ALL three targets. **Reuse (check the catalog first):** pass `moduleId` of an existing module from `## Modules` to place/fan-out a SHARED module without minting a duplicate — a later edit_module then updates it everywhere at once. **Mint:** pass `html` (+ `displayName`, `kind`, and semantic snake_case `fields`) to author a new module and place it; raw HTML is fine, it is parametrised into {{fields}} automatically. `moduleId` is placement-only: if you also pass authoring fields (a different label, new html, …) the call still succeeds but those fields are NOT applied and the result says so — to actually change a shared module, place it by `moduleId` and then call edit_module. **The initial content comes IN THIS CALL, always via `values`** ({fieldName: value, …}): on 'page' it fills the placement's content_instance, on 'template' every fanned-out placement gets it, on 'layout' it is stored as the minted module's field defaults (chrome has no content_instance — so layout `values` require explicit `fields`, and are rejected with `moduleId` reuse: a shared module renders its stored defaults; edit_module changes them). A mint with fields but neither `values` nor field defaults is REJECTED — it would render empty placeholders until a second call. `blockName` must be a real block on the target; the handler returns the available block set if it isn't. NOTE on `position`: pass the literal string \"top\" or \"bottom\", OR a bare integer (0, 1, 2…). Prefer a bare integer (`0`, not `\"0\"`). **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.\n\nOver this connection the install's AI is not used: the AI part comes back to you — a work item to answer with your own model and send back through the tool it names, or an `AiDelegated:` answer that says what to do instead (see caelo_get_context).",
|
|
2492
2524
|
"inputSchema": {
|
|
2493
2525
|
"type": "object",
|
|
2494
2526
|
"additionalProperties": false,
|
|
@@ -2566,16 +2598,18 @@
|
|
|
2566
2598
|
"html": {
|
|
2567
2599
|
"type": "string",
|
|
2568
2600
|
"minLength": 1,
|
|
2569
|
-
"maxLength": 50000
|
|
2601
|
+
"maxLength": 50000,
|
|
2602
|
+
"description": "Module HTML. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
2570
2603
|
},
|
|
2571
2604
|
"css": {
|
|
2572
2605
|
"type": "string",
|
|
2573
|
-
"maxLength": 50000
|
|
2606
|
+
"maxLength": 50000,
|
|
2607
|
+
"description": "Module CSS. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
2574
2608
|
},
|
|
2575
2609
|
"js": {
|
|
2576
2610
|
"type": "string",
|
|
2577
2611
|
"maxLength": 50000,
|
|
2578
|
-
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
2612
|
+
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping). **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
2579
2613
|
},
|
|
2580
2614
|
"bindThemeLiterals": {
|
|
2581
2615
|
"type": "boolean",
|
|
@@ -2919,7 +2953,7 @@
|
|
|
2919
2953
|
},
|
|
2920
2954
|
{
|
|
2921
2955
|
"name": "set_structured_set",
|
|
2922
|
-
"description": "Upsert a structured-data set. Creates the set if `(kind, slug)` doesn't exist yet; REPLACES the items array if it does (NOT append — pass the full desired list). Kinds: nav-menu, tags, taxonomy, link-list. The per-item shape is enforced by Zod at the tool boundary; a mismatch is rejected with a structured error naming the offending field. Read before you write: sets are NOT inlined in the system prompt — call `get_structured_set({kind, slug})` first and modify its items. Visibility: writing a set does NOT by itself change the site. The only built-in binding renders a set through a module whose slug is exactly `nav-menu-<set-slug>` placed directly in a layout/page block — that module's own HTML is replaced by the built-in renderer (`children` render as nested submenus). No AI tool can mint such a slug today (admin UI only — issue #414); if no bound module exists, render nav in the chrome module via a `link-list` field instead. Other kinds render only where module HTML/JS consumes them. Common slugs: nav-menu/header-main, nav-menu/footer-main, tags/blog. Theme tokens are NOT structured sets anymore (v0.11.0) — use `set_theme_tokens` for theme edits and `propose_create_theme` to mint a new theme. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first, or add the link after. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
2956
|
+
"description": "Upsert a structured-data set. Creates the set if `(kind, slug)` doesn't exist yet; REPLACES the items array if it does (NOT append — pass the full desired list). Kinds: nav-menu, tags, taxonomy, link-list. The per-item shape is enforced by Zod at the tool boundary; a mismatch is rejected with a structured error naming the offending field. Read before you write: sets are NOT inlined in the system prompt — call `get_structured_set({kind, slug})` first and modify its items. Visibility: writing a set does NOT by itself change the site. The only built-in binding renders a set through a module whose slug is exactly `nav-menu-<set-slug>` placed directly in a layout/page block — that module's own HTML is replaced by the built-in renderer (`children` render as nested submenus). No AI tool can mint such a slug today (admin UI only — issue #414); if no bound module exists, render nav in the chrome module via a `link-list` field instead. Other kinds render only where module HTML/JS consumes them. Common slugs: nav-menu/header-main, nav-menu/footer-main, tags/blog. Theme tokens are NOT structured sets anymore (v0.11.0) — use `set_theme_tokens` for theme edits and `propose_create_theme` to mint a new theme. **Internal links** (`href`s in HTML, `url`/`link` fields, link-list and nav items): write the page's path, e.g. `/about` — it must be an EXISTING page (list_pages); a link to a page that does not exist yet is refused, so create the target first (`build_page({page:{slug, title}, modules: []})` makes an empty page shell in one call), or add the link after. The site root `/` (also `/#section`) is always valid, even before the homepage is built. Exception, a site import you are rebuilding in this chat: a link to a page of that import run is kept and resolved by resolve_import_links at the end. Images/files: their media URL (find_media). Other sites: the full https:// URL.",
|
|
2923
2957
|
"inputSchema": {
|
|
2924
2958
|
"type": "object",
|
|
2925
2959
|
"additionalProperties": false,
|
|
@@ -4058,7 +4092,7 @@
|
|
|
4058
4092
|
},
|
|
4059
4093
|
{
|
|
4060
4094
|
"name": "autofill_page_seo",
|
|
4061
|
-
"description": "Write a page's meta description (and optional og:image) for the FIRST time. Every new page needs one before it is Staged — a Stage refuses pages without one. Write it from the page's actual content (or carry over the source page's description when migrating a site). Prefer `autofill_page_seo_many` when more than one page needs it.
|
|
4095
|
+
"description": "Write a page's meta description (and optional og:image) for the FIRST time. Every new page needs one before it is Staged — a Stage refuses pages without one. Write it from the page's actual content (or carry over the source page's description when migrating a site). Prefer `autofill_page_seo_many` when more than one page needs it. For a page you create with build_page, pass `page.metaDescription` there instead — same fill-once write, no extra call. Fill-once: sending the SAME text again is a harmless no-op; a DIFFERENT text for an already-filled page returns AlreadyAutofilled with the stored description (the page is not missing one) — change it on purpose only via `optimize_page_seo`, and only when the user asks. Aim for 50–60 char title (set via `update_pages_many`) and 150–160 char meta description.",
|
|
4062
4096
|
"inputSchema": {
|
|
4063
4097
|
"type": "object",
|
|
4064
4098
|
"additionalProperties": false,
|
|
@@ -4088,7 +4122,7 @@
|
|
|
4088
4122
|
},
|
|
4089
4123
|
{
|
|
4090
4124
|
"name": "autofill_page_seo_many",
|
|
4091
|
-
"description": "Write the FIRST meta description for SEVERAL pages in ONE transaction — prefer this over multiple `autofill_page_seo` calls (§11 bulk-first). Use it after building pages and before Staging them: a Stage refuses pages without a meta description and names them. Write each description from that page's actual content (150–160 characters, for a human reading a search result), or carry over the source page's description when migrating a site. All-or-nothing: a page
|
|
4125
|
+
"description": "Write the FIRST meta description for SEVERAL pages in ONE transaction — prefer this over multiple `autofill_page_seo` calls (§11 bulk-first). Use it after building pages and before Staging them: a Stage refuses pages without a meta description and names them. Write each description from that page's actual content (150–160 characters, for a human reading a search result), or carry over the source page's description when migrating a site. All-or-nothing: a page already autofilled with a DIFFERENT text (AlreadyAutofilled) aborts the whole batch — leave such pages out (the same text again is a no-op); their descriptions change through `optimize_page_seo` / `bulk_optimize_seo` (re-optimization) or `set_page_seo_many` (an exact value the user dictated). Each item is the `autofill_page_seo` shape: `{pageId, metaDescription, ogImageAssetId?}`.",
|
|
4092
4126
|
"inputSchema": {
|
|
4093
4127
|
"type": "object",
|
|
4094
4128
|
"additionalProperties": false,
|
|
@@ -4254,7 +4288,7 @@
|
|
|
4254
4288
|
},
|
|
4255
4289
|
{
|
|
4256
4290
|
"name": "get_quality_audit",
|
|
4257
|
-
"description": "Read the quality check of the staged site: the page checks plugins add (such as SEO checks; when an active plugin adds any) that read EVERY page on every Stage, plus a Lighthouse audit (Performance, Accessibility, Best Practices, SEO; mobile) when the Stage changed how pages render. With no arguments: the newest audit of THIS chat's Stages. Shows per page the scores (the target is 100 in every category), the Performance metrics behind the Performance score (FCP, LCP, TBT, CLS, Speed Index: the best run's value with Lighthouse's rating, plus the LCP element, the LCP breakdown and the elements that shifted with their causes), every problem with its Lighthouse audit id (e.g. `image-alt`, `color-contrast`, `meta-description`, `render-blocking-insight`) and the elements it flagged (CSS selector, HTML snippet, why — e.g. the measured contrast and colours), every page-check problem with its rule id (`<check>/<rule>`, e.g. `seo-check/title-missing` — rule ids contain a slash) and a fix hint naming the tool to use, site-wide findings, non-blocking warnings and hints, and infrastructure errors. THE TARGET IS 100 IN EVERY CATEGORY AND ZERO FINDINGS: every category below 100, every scored Lighthouse audit below a perfect score, and every page-check error is a PROBLEM that blocks Publish live. A score like 90 is NOT acceptable on its own — keep fixing until 100 wherever achievable. Problems an editor accepted are marked ACCEPTED and do not block (a worse value than accepted blocks again). Warnings and hints never block. Use this after a Stage to see what to fix; use list_quality_acceptances to see what editors already accepted. To make a page faster, read its metrics here and fix the worst one in the module that renders the named element, then Stage again and re-read — repeat until Performance is 100. Use this instead of running Lighthouse yourself: the audit runs against the real staged build, which no other URL serves. Only what truly cannot be fixed goes to the operator, as one decision per group (accept_quality_findings with groups), with the reason why. A finding marked CAELO PLATFORM (back/forward cache, caching of Caelo's own files, server response, compression, redirects) is decided by Caelo's hosting/build setup, not by the site: report it with bug_report and never edit the site for it; it blocks until Caelo fixes it or the operator decides to accept it. Without an open operator chat (Power-MCP) a call on a queued or running audit waits for its result (up to ~50 s; call again if it is still running). Typical input: {} or {
|
|
4291
|
+
"description": "Read the quality check of the staged site: the page checks plugins add (such as SEO checks; when an active plugin adds any) that read EVERY page on every Stage, plus a Lighthouse audit (Performance, Accessibility, Best Practices, SEO; mobile) when the Stage changed how pages render. With no arguments: the newest audit of THIS chat's Stages. Shows per page the scores (the target is 100 in every category), the Performance metrics behind the Performance score (FCP, LCP, TBT, CLS, Speed Index: the best run's value with Lighthouse's rating, plus the LCP element, the LCP breakdown and the elements that shifted with their causes), every problem with its Lighthouse audit id (e.g. `image-alt`, `color-contrast`, `meta-description`, `render-blocking-insight`) and the elements it flagged (CSS selector, HTML snippet, why — e.g. the measured contrast and colours), every page-check problem with its rule id (`<check>/<rule>`, e.g. `seo-check/title-missing` — rule ids contain a slash) and a fix hint naming the tool to use, site-wide findings, non-blocking warnings and hints, and infrastructure errors. THE TARGET IS 100 IN EVERY CATEGORY AND ZERO FINDINGS: every category below 100, every scored Lighthouse audit below a perfect score, and every page-check error is a PROBLEM that blocks Publish live. A score like 90 is NOT acceptable on its own — keep fixing until 100 wherever achievable. Problems an editor accepted are marked ACCEPTED and do not block (a worse value than accepted blocks again). Warnings and hints never block. Use this after a Stage to see what to fix; use list_quality_acceptances to see what editors already accepted. To make a page faster, read its metrics here and fix the worst one in the module that renders the named element, then Stage again and re-read — repeat until Performance is 100. Use this instead of running Lighthouse yourself: the audit runs against the real staged build, which no other URL serves. Only what truly cannot be fixed goes to the operator, as one decision per group (accept_quality_findings with groups), with the reason why. A finding marked CAELO PLATFORM (back/forward cache, caching of Caelo's own files, server response, compression, redirects) is decided by Caelo's hosting/build setup, not by the site: report it with bug_report and never edit the site for it; it blocks until Caelo fixes it or the operator decides to accept it. Without an open operator chat (Power-MCP) a call on a queued or running audit waits for its result (up to ~50 s; call again if it is still running). Typical input: {}, { auditRunId }, or { pageId } to read only one page's section (from list_pages).",
|
|
4258
4292
|
"inputSchema": {
|
|
4259
4293
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4260
4294
|
"type": "object",
|
|
@@ -4270,6 +4304,12 @@
|
|
|
4270
4304
|
"type": "string",
|
|
4271
4305
|
"format": "uuid",
|
|
4272
4306
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
4307
|
+
},
|
|
4308
|
+
"pageId": {
|
|
4309
|
+
"description": "Only this page's section (scores, metrics, problems); the run summary and site-wide checks stay.",
|
|
4310
|
+
"type": "string",
|
|
4311
|
+
"format": "uuid",
|
|
4312
|
+
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
|
4273
4313
|
}
|
|
4274
4314
|
},
|
|
4275
4315
|
"additionalProperties": false
|
|
@@ -4637,7 +4677,7 @@
|
|
|
4637
4677
|
},
|
|
4638
4678
|
{
|
|
4639
4679
|
"name": "inspect_built_page",
|
|
4640
|
-
"description": "Read the BUILT HTML of a page from the staging or static bucket — what publishStaging or promoteToProduction actually uploaded. USE THIS AFTER STAGE to verify the build matches the live preview's expectations. Catches drift the live preview can't show: SEO meta absence, hashed-asset URLs, plugin render transforms, CSP-relevant <script src> tags, robots.txt overrides. Default target='staging' reads from the staged build; pass target='static' to read what's currently serving production.
|
|
4680
|
+
"description": "Read the BUILT HTML of a page from the staging or static bucket — what publishStaging or promoteToProduction actually uploaded. USE THIS AFTER STAGE to verify the build matches the live preview's expectations. Catches drift the live preview can't show: SEO meta absence, hashed-asset URLs, plugin render transforms, CSP-relevant <script src> tags, robots.txt overrides. Default target='staging' reads from the staged build; pass target='static' to read what's currently serving production.",
|
|
4641
4681
|
"inputSchema": {
|
|
4642
4682
|
"type": "object",
|
|
4643
4683
|
"additionalProperties": false,
|
|
@@ -5073,14 +5113,16 @@
|
|
|
5073
5113
|
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
|
|
5074
5114
|
},
|
|
5075
5115
|
"html": {
|
|
5076
|
-
"type": "string"
|
|
5116
|
+
"type": "string",
|
|
5117
|
+
"description": "Module HTML. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
5077
5118
|
},
|
|
5078
5119
|
"css": {
|
|
5079
|
-
"type": "string"
|
|
5120
|
+
"type": "string",
|
|
5121
|
+
"description": "Module CSS. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping)."
|
|
5080
5122
|
},
|
|
5081
5123
|
"js": {
|
|
5082
5124
|
"type": "string",
|
|
5083
|
-
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
5125
|
+
"description": "Vanilla JS. Emitted ONCE per page into a single shared <script defer> concatenated with every other module's JS — top-level scope is SHARED and there is NO implicit binding: `root`, `el`, `module`, and `this` are NOT defined. Wrap everything in an IIFE, select elements with document.querySelectorAll('.your-module-root-class …'), and iterate the matches so multiple placements of the module on one page all work. Omit entirely for purely presentational modules. Plain source text exactly as it is served: no `<![CDATA[ … ]]>` wrapper and no escaping of the whole value (the tool input is JSON — the string needs nothing beyond JSON escaping). **Vetted interactions - use these instead of writing JS for them.** Add `data-caelo-ui=\"<id>\"` to the markup and Caelo ships accessible, dependency-free behaviour once per page; style it freely in the module CSS. `accordion` (FAQ, collapsible sections): `<div data-caelo-ui=\"accordion\" data-single>` containing `<details id=\"...\"><summary>Question</summary><div>Answer</div></details>` items - `data-single` keeps one open; a link to `#<details id>` opens it. `tabs`: `<div data-caelo-ui=\"tabs\"><div role=\"tablist\" aria-label=\"...\"><button type=\"button\" role=\"tab\" id=\"t1\" aria-controls=\"p1\" aria-selected=\"true\">...</button><button type=\"button\" role=\"tab\" id=\"t2\" aria-controls=\"p2\" aria-selected=\"false\" tabindex=\"-1\">...</button></div><div role=\"tabpanel\" id=\"p1\" aria-labelledby=\"t1\">...</div><div role=\"tabpanel\" id=\"p2\" aria-labelledby=\"t2\" hidden>...</div></div>` - ids must be unique on the page. `carousel` (slider, testimonials): `<section data-caelo-ui=\"carousel\" aria-roledescription=\"carousel\" aria-label=\"...\"><div data-caelo-carousel-track tabindex=\"0\"><div role=\"group\" aria-roledescription=\"slide\" aria-label=\"1 of 3\">...</div>...</div><button type=\"button\" data-caelo-carousel-prev aria-label=\"Previous slide\"><span aria-hidden=\"true\">‹</span></button><button type=\"button\" data-caelo-carousel-next aria-label=\"Next slide\"><span aria-hidden=\"true\">›</span></button></section>` (an icon button keeps its visible content aria-hidden; a text button needs no aria-label) - one slide per view by default; set the slides' `flex-basis` in module CSS to show more. `mobile-nav` is applied by the built-in nav-menu renderer; never add it by hand. Write module JS only for behaviour the kit does not cover."
|
|
5084
5126
|
},
|
|
5085
5127
|
"fields": {
|
|
5086
5128
|
"type": "array",
|
|
@@ -5752,7 +5794,7 @@
|
|
|
5752
5794
|
},
|
|
5753
5795
|
{
|
|
5754
5796
|
"name": "propose_create_user",
|
|
5755
|
-
"description": "Propose inviting a new user to the CMS. Provide email + displayName + roleNames. DO NOT include a password
|
|
5797
|
+
"description": "Propose inviting a new user to the CMS. Provide email + displayName + roleNames. DO NOT include a password. On Google Cloud installs the approval also lets this email through Google IAP (browser and MCP), so use the person's Google account address (Gmail or Workspace): an address that is not a Google account is created but CANNOT sign in until it is one. Google offers no way to check an address before approval; the result's `operatorAccess` says how the sync went, and `operatorAccess.refusedMembers` names addresses Google has no account for — relay any `warning` to the operator and ask for the person's Google account address. An address outside the project's Google organisation is refused until the Owner has set up Google sign-in (Settings → Google sign-in, a console step only a human can do): relay that error's next step to the operator instead of retrying or proposing another address. CREDENTIALS: the password / token this creates is never shown to you — not in this result, not later, not on request. Never ask for it or offer to tell it. After approval, tell the operator where the approver collects it once in the Caelo admin (the result's `credential` field names the place). APPROVAL-GATED: calling this PAUSES for the operator's Approve/Reject right in the chat before anything changes. Do not claim the change is live until it is approved.\n\nOver MCP: call this tool yourself — it works here. The call queues an approval card in the operator's Caelo admin (their Live Edit chat and the pending-approvals page), waits for their Approve/Reject and returns the outcome (if the wait times out, continue with caelo_await_approval). Tell the operator you requested it and that they approve the card in Caelo; never ask them to paste a prompt into the Caelo chat to run it.",
|
|
5756
5798
|
"inputSchema": {
|
|
5757
5799
|
"type": "object",
|
|
5758
5800
|
"additionalProperties": false,
|
|
@@ -6078,7 +6120,7 @@
|
|
|
6078
6120
|
},
|
|
6079
6121
|
{
|
|
6080
6122
|
"name": "propose_create_mcp_token",
|
|
6081
|
-
"description": "Propose minting a new MCP bearer token. Pass displayName + optional scope + optional aiCostCapMicrocents. scope 'chat' (default) drives the caelo_chat surface; scope 'admin' additionally unlocks the Power-MCP tool surface for external agents. allowCaeloAi (admin only, default false) lets that agent spend the install's own AI (image generation, Caelo-side translation runs); leave it off unless the operator asks — the agent then does the AI work on its own subscription.
|
|
6123
|
+
"description": "Propose minting a new MCP bearer token. Pass displayName + optional scope + optional aiCostCapMicrocents. scope 'chat' (default) drives the caelo_chat surface; scope 'admin' additionally unlocks the Power-MCP tool surface for external agents. allowCaeloAi (admin only, default false) lets that agent spend the install's own AI (image generation, Caelo-side translation runs); leave it off unless the operator asks — the agent then does the AI work on its own subscription. CREDENTIALS: the password / token this creates is never shown to you — not in this result, not later, not on request. Never ask for it or offer to tell it. After approval, tell the operator where the approver collects it once in the Caelo admin (the result's `credential` field names the place). APPROVAL-GATED: calling this PAUSES for the operator's Approve/Reject right in the chat before anything changes. Do not claim the change is live until it is approved.\n\nOver MCP: call this tool yourself — it works here. The call queues an approval card in the operator's Caelo admin (their Live Edit chat and the pending-approvals page), waits for their Approve/Reject and returns the outcome (if the wait times out, continue with caelo_await_approval). Tell the operator you requested it and that they approve the card in Caelo; never ask them to paste a prompt into the Caelo chat to run it.",
|
|
6082
6124
|
"inputSchema": {
|
|
6083
6125
|
"type": "object",
|
|
6084
6126
|
"additionalProperties": false,
|
|
@@ -6685,22 +6727,17 @@
|
|
|
6685
6727
|
},
|
|
6686
6728
|
{
|
|
6687
6729
|
"name": "get_staging_access",
|
|
6688
|
-
"description": "Read the protected staging site: its URL (https://staging.<domain>), whether a staged build is served, and which access modes let people and crawlers in (admin 'Open staging' cookie, basic auth, secret header, IP allowlist). Use when the operator asks where to look at staging, how a crawler (e.g. a searchVIU dev crawl) gets in, or before propose_set_staging_access.
|
|
6730
|
+
"description": "Read the protected staging site: its URL (https://staging.<domain>), whether a staged build is served, and which access modes let people and crawlers in (admin 'Open staging' cookie, basic auth, secret header, IP allowlist). Use when the operator asks where to look at staging, how a crawler (e.g. a searchVIU dev crawl) gets in, or before propose_set_staging_access. You never see the basic-auth password or the secret header value: when the operator needs them, tell them to copy them from Settings → Staging access (/settings/staging).",
|
|
6689
6731
|
"inputSchema": {
|
|
6690
6732
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
6691
6733
|
"type": "object",
|
|
6692
|
-
"properties": {
|
|
6693
|
-
"includeCredentials": {
|
|
6694
|
-
"description": "Return the crawler credentials (Owner only; only when asked).",
|
|
6695
|
-
"type": "boolean"
|
|
6696
|
-
}
|
|
6697
|
-
},
|
|
6734
|
+
"properties": {},
|
|
6698
6735
|
"additionalProperties": false
|
|
6699
6736
|
}
|
|
6700
6737
|
},
|
|
6701
6738
|
{
|
|
6702
6739
|
"name": "propose_set_staging_access",
|
|
6703
|
-
"description": "Propose who may open the protected staging site (https://staging.<domain>): cookieEnabled (admin 'Open staging'), basicAuthEnabled + basicAuthUser (a crawler logs in; the password is generated on approval), headerEnabled + headerName (a crawler sends a generated secret header), ipAllowlist (CIDRs or addresses let in without credentials, e.g. a crawler's static egress IP). Send ONLY the settings that change. For a searchVIU dev crawl: basic auth (dev_auth_type PW) or the crawler's egress IP (dev_auth_type IP). At least one mode must stay on. Read the current settings with get_staging_access first. Never invent an IP address — take it from the operator. Applies on the gateway within seconds after approval; a newly enabled basic-auth password or header value is
|
|
6740
|
+
"description": "Propose who may open the protected staging site (https://staging.<domain>): cookieEnabled (admin 'Open staging'), basicAuthEnabled + basicAuthUser (a crawler logs in; the password is generated on approval), headerEnabled + headerName (a crawler sends a generated secret header), ipAllowlist (CIDRs or addresses let in without credentials, e.g. a crawler's static egress IP). Send ONLY the settings that change. For a searchVIU dev crawl: basic auth (dev_auth_type PW) or the crawler's egress IP (dev_auth_type IP). At least one mode must stay on. Read the current settings with get_staging_access first. Never invent an IP address — take it from the operator. Applies on the gateway within seconds after approval; a newly enabled basic-auth password or header value is never shown to you — the operator copies it from Settings → Staging access (/settings/staging). If get_staging_access says nothing is staged, the host serves nothing until stage_changes. APPROVAL-GATED: calling this PAUSES for the operator's Approve/Reject right in the chat before anything changes. Do not claim the change is live until it is approved.\n\nOver MCP: call this tool yourself — it works here. The call queues an approval card in the operator's Caelo admin (their Live Edit chat and the pending-approvals page), waits for their Approve/Reject and returns the outcome (if the wait times out, continue with caelo_await_approval). Tell the operator you requested it and that they approve the card in Caelo; never ask them to paste a prompt into the Caelo chat to run it.",
|
|
6704
6741
|
"inputSchema": {
|
|
6705
6742
|
"type": "object",
|
|
6706
6743
|
"properties": {
|
|
@@ -6741,7 +6778,7 @@
|
|
|
6741
6778
|
},
|
|
6742
6779
|
{
|
|
6743
6780
|
"name": "propose_rotate_staging_credentials",
|
|
6744
|
-
"description": "Propose replacing the staging crawler credentials (which: basic-auth, header or both). ONLY when the operator asks or a credential may have leaked: every crawler/tool using the old value is locked out until it gets the new one, and the old value cannot be restored. Give the reason in `reason`; it is shown on the approval. You never see the new value
|
|
6781
|
+
"description": "Propose replacing the staging crawler credentials (which: basic-auth, header or both). ONLY when the operator asks or a credential may have leaked: every crawler/tool using the old value is locked out until it gets the new one, and the old value cannot be restored. Give the reason in `reason`; it is shown on the approval. You never see the new value: the operator copies it from Settings → Staging access (/settings/staging). APPROVAL-GATED: calling this PAUSES for the operator's Approve/Reject right in the chat before anything changes. Do not claim the change is live until it is approved.\n\nOver MCP: call this tool yourself — it works here. The call queues an approval card in the operator's Caelo admin (their Live Edit chat and the pending-approvals page), waits for their Approve/Reject and returns the outcome (if the wait times out, continue with caelo_await_approval). Tell the operator you requested it and that they approve the card in Caelo; never ask them to paste a prompt into the Caelo chat to run it.",
|
|
6745
6782
|
"inputSchema": {
|
|
6746
6783
|
"type": "object",
|
|
6747
6784
|
"properties": {
|
|
@@ -7016,7 +7053,7 @@
|
|
|
7016
7053
|
},
|
|
7017
7054
|
{
|
|
7018
7055
|
"name": "set_theme_tokens",
|
|
7019
|
-
"description": "Update theme tokens for one theme. Accepts loose names (`primaryColor`, `fontHeading`, `spacingLg`) — server normalizes to canonical paths and returns what was written. Use fontBindings: {body: {id, sha256}, heading: {id, sha256}} from find_fonts/acquire_font to pin exact font files. Use fontAxes to change which part of a pinned VARIABLE font visitors download, e.g. `fontAxes: {heading: {opsz: \"full\"}}` or `{body: {wght: {min: 400, max: 700}}}`; `null` resets a role to the default. Per axis: \"full\" keeps the font's whole range, {min,max} clamps it, a number pins it. The policy belongs to the font FILE, so it applies to every role using that file. Default without an override (usually right, no action needed): wght clamped to the weights the site can render, every other axis pinned to the font's default — this roughly halves Inter's download. Trade-offs: pinning opsz makes large headings render with the text optical size (a visibly heavier, less refined display cut) — use {opsz: \"full\"} when the operator wants optical sizes kept; a wght range that excludes a weight the site uses makes that text render at the nearest kept weight. Read the current effective policy and byte sizes first with get_theme(as: \"font-delivery\"). Axis tags or values the font does not have are rejected with its real ranges. Pass `set` to add/replace tokens, `
|
|
7056
|
+
"description": "Update theme tokens for one theme. Accepts loose names (`primaryColor`, `fontHeading`, `spacingLg`) — server normalizes to canonical paths and returns what was written. Use fontBindings: {body: {id, sha256}, heading: {id, sha256}} from find_fonts/acquire_font to pin exact font files. Use fontAxes to change which part of a pinned VARIABLE font visitors download, e.g. `fontAxes: {heading: {opsz: \"full\"}}` or `{body: {wght: {min: 400, max: 700}}}`; `null` resets a role to the default. Per axis: \"full\" keeps the font's whole range, {min,max} clamps it, a number pins it. The policy belongs to the font FILE, so it applies to every role using that file. Default without an override (usually right, no action needed): wght clamped to the weights the site can render, every other axis pinned to the font's default — this roughly halves Inter's download. Trade-offs: pinning opsz makes large headings render with the text optical size (a visibly heavier, less refined display cut) — use {opsz: \"full\"} when the operator wants optical sizes kept; a wght range that excludes a weight the site uses makes that text render at the nearest kept weight. Read the current effective policy and byte sizes first with get_theme(as: \"font-delivery\"). Axis tags or values the font does not have are rejected with its real ranges. Pass `set` to add/replace tokens (loose names or canonical paths), or `tokens` with a (partial) DTCG document in the get_theme / propose_create_theme shape to write a whole composed palette at once; `remove` drops tokens. Works with the active theme by default; pass `themeSlug` to target a specific theme. For a complete theme replacement, use `set_theme_tokens` with all desired tokens (it's an upsert per token, not per-theme). When ambiguous (a bare name with no value-shape signal) the tool returns `UnknownTokenName` with did-you-mean suggestions; retry with the canonical path. Composite shapes — copy these exactly: typography leaves are `{$type: 'typography', $value: {fontFamily: 'Inter, sans-serif', fontSize: '1rem', fontWeight: 400, lineHeight: 1.6}}` (one composite per body/heading/mono — NOT per-property tokens, and fontFamily is a plain string). Shadow leaves are `{$type: 'shadow', $value: {color: '#0b132b33', offsetX: '0px', offsetY: '2px', blur: '8px', spread: '0px'}}` (structured object or array of them — NEVER a raw CSS string). Gradient leaves are a CSS `*-gradient(...)` string, NOT a color — set them at the `gradient.*` namespace and the `$type` is inferred: e.g. `set: {'gradient.hero': {$type: 'gradient', $value: 'linear-gradient(135deg, #4f46e5, #7c3aed)'}}` (the bare string `'linear-gradient(135deg, #4f46e5, #7c3aed)'` or a loose name like `heroGradient` also work — the server routes any `gradient.*`/`*Gradient` name to a gradient token). Only `linear-`/`radial-`/`conic-gradient(...)` values are valid; never alias ({group.token}) unless the target exists. Compose `spacing` so it covers what pages actually use — otherwise module CSS falls back to literals and the operator can never retune the site's density. A component-only scale (`xs`…`2xl`, 0.25–3rem) does NOT cover it: section bands need 3.5–5rem of vertical air, and controls need 0.6–0.9rem of padding. So ship the component steps PLUS named rhythm tokens — `section-sm` (tight band, ~2.5rem), `section` (standard band, ~4rem), `section-lg` (hero / feature band, ~5rem) — and reference them in module CSS as `padding: var(--spacing-section) var(--spacing-md)`. If you are about to write a padding literal, add the token instead. Record each token's ROLE via the DTCG envelope's `$description`, in either form: WITH the value when you already know it — `set: {'color.primary': {$type: 'color', $value: '#4f46e5', $description: 'CTAs, links and selected states — never large background fills'}}` — or as a value-LESS patch when the value is already settled and you are annotating it afterwards: `set: {'color.primary': {$description: '…'}}` (leaves `$value` untouched, so you never restate — and risk drifting — a value; needs the canonical path or the `--css-var` name, since a loose name infers its category from the value). Say where the token must NOT be used, not only where it should — the boundary is what keeps page nine on page one's visual line. Roles survive later value-only edits, and every module write replays the roles of the vars its CSS touches back to you, so this is the whole design-system record: there is no separate manifest to write. Design typography by role: body for sustained reading, heading for navigation and hierarchy, and an optional typography.display composite for expressive hero or campaign titles. Choose real, resolvable font families and supported numeric weights for the brief; use one or two complementary families unless the existing brand calls for more. A display title needs deliberate size, line breaks, weight, spacing and composition, not just a body font enlarged or rainbow-colored. Record intended use and exclusions in each token's $description; reference --font-display, --text-display, --font-weight-display and --tracking-display only after creating those tokens. Preserve established brand typography unless the operator asks for a redesign. Preview actual rendered fonts with the operator's text, including accents and non-Latin characters; a family name in a picker does not prove its face loaded. Check mobile line breaks and reading contrast. Use the existing self-hosted font resolver and surface unresolved families; do not add external font links. AI-generated lettering is a separate image asset, never a reusable font or editable text. Use it only when explicitly requested, verify exact spelling, and retain accessible semantic text without drawing a duplicate visible title. Do not claim that web-font availability establishes PDF embedding support.",
|
|
7020
7057
|
"inputSchema": {
|
|
7021
7058
|
"type": "object",
|
|
7022
7059
|
"properties": {
|
|
@@ -7107,6 +7144,21 @@
|
|
|
7107
7144
|
},
|
|
7108
7145
|
"additionalProperties": {}
|
|
7109
7146
|
},
|
|
7147
|
+
"tokens": {
|
|
7148
|
+
"description": "A (partial) DTCG token document, the same shape get_theme shows and propose_create_theme takes (`{color: {primary: {$value: '#4338ca', $description: '…'}}, typography: {…}}`). Every `{$value}` leaf is upserted at its path — use it to write a whole composed palette in one call. Loose names go in `set`.",
|
|
7149
|
+
"anyOf": [
|
|
7150
|
+
{
|
|
7151
|
+
"type": "object",
|
|
7152
|
+
"propertyNames": {
|
|
7153
|
+
"type": "string"
|
|
7154
|
+
},
|
|
7155
|
+
"additionalProperties": {}
|
|
7156
|
+
},
|
|
7157
|
+
{
|
|
7158
|
+
"type": "string"
|
|
7159
|
+
}
|
|
7160
|
+
]
|
|
7161
|
+
},
|
|
7110
7162
|
"remove": {
|
|
7111
7163
|
"type": "array",
|
|
7112
7164
|
"items": {
|
|
@@ -7898,7 +7950,7 @@
|
|
|
7898
7950
|
},
|
|
7899
7951
|
{
|
|
7900
7952
|
"name": "stage_changes",
|
|
7901
|
-
"description": "Stage: merge this chat's unstaged changes into the site and rebuild the STAGING site (noindex, never visitor-facing), then the quality check (Lighthouse) runs on it. Use it when the operator's requested change is done and should be reviewed on staging — 'stage it', 'put it on staging', or as the last step of finished work so the operator can review and Publish live. Changes another chat made to the SAME module/page ride along (the result names them — tell the operator). scope all_my_chats stages every open chat of the operator at once. Only pages with status 'published' are built — the result lists draft pages it left out; publish them with set_pages_status_many first when they should be on staging. It does NOT publish: Publish live is the operator's click after the quality check (get_publish_gate says whether it is open; get_quality_audit reads the findings). Never tell the operator the change is live. A failed build returns the generator's error — fix the cause and call again; the changes stay unstaged until a build succeeds.",
|
|
7953
|
+
"description": "Stage: merge this chat's unstaged changes into the site and rebuild the STAGING site (noindex, never visitor-facing), then the quality check (Lighthouse) runs on it. Use it when the operator's requested change is done and should be reviewed on staging — 'stage it', 'put it on staging', or as the last step of finished work so the operator can review and Publish live. Changes another chat made to the SAME module/page ride along (the result names them — tell the operator). scope all_my_chats stages every open chat of the operator at once. Only pages with status 'published' are built — the result lists draft pages it left out; publish them with set_pages_status_many first when they should be on staging. The build is REFUSED unless: (1) at least one page is published; (2) a published page serves the site root '/' (the homepage — set_home_page designates one, it keeps its slug); (3) every published page has a meta description (page.metaDescription in build_page, else ONE autofill_page_seo_many call). These cover pages you did not touch too (a homepage left as a draft, an older page without a description): the site-status note at the start of the turn and the results of build_page / set_pages_status_many / set_home_page / autofill_page_seo_many say 'A Stage would be refused right now' with the exact fixes — do those first, then stage once. It does NOT publish: Publish live is the operator's click after the quality check (get_publish_gate says whether it is open; get_quality_audit reads the findings). Never tell the operator the change is live. A failed build returns the generator's error — fix the cause and call again; the changes stay unstaged until a build succeeds.",
|
|
7902
7954
|
"inputSchema": {
|
|
7903
7955
|
"type": "object",
|
|
7904
7956
|
"properties": {
|
|
@@ -8022,7 +8074,7 @@
|
|
|
8022
8074
|
},
|
|
8023
8075
|
{
|
|
8024
8076
|
"name": "refresh_page_path",
|
|
8025
|
-
"description": "Recompute ONE page's
|
|
8077
|
+
"description": "Recompute ONE page's URL from its current data. A DRAFT page moves right away (it has no public URL yet). A PUBLISHED page whose URL is out of date does not move here: moving a live URL needs the Owner's approval, so the tool reports awaiting a URL migration and you call propose_url_migration (it previews the move and writes the 301s). Linking/unlinking language variants already refreshes the pages they touch — no follow-up call needed. Not for renaming a page (update_pages_many with a new slug does that, with redirects).",
|
|
8026
8078
|
"inputSchema": {
|
|
8027
8079
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
8028
8080
|
"type": "object",
|