@caelo-cms/mcp-server 0.10.57 → 0.10.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/power-catalogue.json +177 -21
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.57",
4
+ "version": "0.10.59",
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.",
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "0.10.57",
2
+ "version": "0.10.59",
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). **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). 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`).",
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). **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). 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.",
7
7
  "inputSchema": {
8
8
  "type": "object",
9
9
  "additionalProperties": false,
@@ -213,7 +213,7 @@
213
213
  },
214
214
  {
215
215
  "name": "edit_content",
216
- "description": "Surgically edit a module's or template'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 ARE GATED: an edit_content on a template is a TWO-STEP §11.A flow — you propose, the Owner clicks Approve on the proposal card. Do not claim a template edit was applied.",
216
+ "description": "Surgically edit a module's or template'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 ARE GATED: an edit_content on a template is a TWO-STEP §11.A flow — you propose, the Owner clicks Approve on the proposal card. Do not claim a template 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, 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.",
217
217
  "inputSchema": {
218
218
  "$schema": "https://json-schema.org/draft/2020-12/schema",
219
219
  "type": "object",
@@ -319,7 +319,7 @@
319
319
  },
320
320
  {
321
321
  "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.",
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.",
323
323
  "inputSchema": {
324
324
  "type": "object",
325
325
  "additionalProperties": false,
@@ -350,9 +350,79 @@
350
350
  }
351
351
  }
352
352
  },
353
+ {
354
+ "name": "patch_page_module_fields",
355
+ "description": "Change individual values of ONE module placement — a heading, one bullet of a text-list, a link-list entry's text or target, a `url` field, an `image` field — and leave every other value of that placement untouched. Use this for targeted wording fixes ('the hero says Welcom, fix it', 'rename the second menu item to Pricing', or a reference chip naming `placement=(blockName, position), field=…[index]`): you send only the changed values instead of the placement's whole contentValues. `text` is the new value: plain text for text fields (it is HTML-escaped for you — never send markup), a path / https URL for `url` / `image` fields and link targets. `index` addresses a `text-list` item or a `link-list` entry; `part: \"href\"` sets that entry's target instead of its text. Several edits of one placement go in one call (all or none). Use `set_page_module_content` (NOT this tool) to replace a placement's values wholesale or to set nested modules; `set_page_module_content_many` for passes across several placements; `edit_module` for structure. SYNCED placements (content shared with other pages) need `scope`: `\"everywhere\"` changes it on every page that shows it, `\"here\"` detaches this page first and changes only it — ask the operator which they mean when it is not clear. Typical call: `{pageId: \"…\", blockName: \"content\", position: 0, edits: [{field: \"hero_title\", text: \"Welcome\"}, {field: \"nav_items\", index: 1, text: \"Pricing\"}]}`.",
356
+ "inputSchema": {
357
+ "type": "object",
358
+ "additionalProperties": false,
359
+ "required": [
360
+ "pageId",
361
+ "blockName",
362
+ "position",
363
+ "edits"
364
+ ],
365
+ "properties": {
366
+ "pageId": {
367
+ "type": "string",
368
+ "format": "uuid"
369
+ },
370
+ "blockName": {
371
+ "type": "string",
372
+ "minLength": 1
373
+ },
374
+ "position": {
375
+ "type": "integer",
376
+ "minimum": 0
377
+ },
378
+ "edits": {
379
+ "type": "array",
380
+ "minItems": 1,
381
+ "maxItems": 20,
382
+ "items": {
383
+ "type": "object",
384
+ "additionalProperties": false,
385
+ "required": [
386
+ "field",
387
+ "text"
388
+ ],
389
+ "properties": {
390
+ "field": {
391
+ "type": "string",
392
+ "minLength": 1,
393
+ "maxLength": 200
394
+ },
395
+ "index": {
396
+ "type": "integer",
397
+ "minimum": 0
398
+ },
399
+ "part": {
400
+ "type": "string",
401
+ "enum": [
402
+ "label",
403
+ "href"
404
+ ]
405
+ },
406
+ "text": {
407
+ "type": "string",
408
+ "maxLength": 10000
409
+ }
410
+ }
411
+ }
412
+ },
413
+ "scope": {
414
+ "type": "string",
415
+ "enum": [
416
+ "everywhere",
417
+ "here"
418
+ ]
419
+ }
420
+ }
421
+ }
422
+ },
353
423
  {
354
424
  "name": "build_page",
355
- "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}. Referenced entries come FIRST. 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).",
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}. Referenced entries come FIRST. 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, 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.",
356
426
  "inputSchema": {
357
427
  "type": "object",
358
428
  "additionalProperties": false,
@@ -704,7 +774,7 @@
704
774
  },
705
775
  {
706
776
  "name": "set_page_module_content_many",
707
- "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: \"…\"}}]}`.",
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.",
708
778
  "inputSchema": {
709
779
  "type": "object",
710
780
  "additionalProperties": false,
@@ -751,7 +821,7 @@
751
821
  },
752
822
  {
753
823
  "name": "create_content_instances",
754
- "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: {…}}]}`.",
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.",
755
825
  "inputSchema": {
756
826
  "type": "object",
757
827
  "additionalProperties": false,
@@ -865,7 +935,7 @@
865
935
  },
866
936
  {
867
937
  "name": "create_content_instance",
868
- "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.",
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.",
869
939
  "inputSchema": {
870
940
  "type": "object",
871
941
  "additionalProperties": false,
@@ -899,7 +969,7 @@
899
969
  },
900
970
  {
901
971
  "name": "set_content_instance_values",
902
- "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.",
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.",
903
973
  "inputSchema": {
904
974
  "type": "object",
905
975
  "properties": {
@@ -981,7 +1051,7 @@
981
1051
  },
982
1052
  {
983
1053
  "name": "set_content_instance_values_many",
984
- "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?}`.",
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.",
985
1055
  "inputSchema": {
986
1056
  "type": "object",
987
1057
  "additionalProperties": false,
@@ -1865,6 +1935,34 @@
1865
1935
  }
1866
1936
  }
1867
1937
  },
1938
+ {
1939
+ "name": "get_link_migration_report",
1940
+ "description": "Internal links are stored as page references (they follow a page when it is renamed or moved). Content written before that was converted once by the link migration; this tool reports it: links converted per place, and every link it could NOT convert (it matched no page or media asset — never guessed), with the module / content / menu it sits in. Use when the operator asks about broken or old links after an update, or before fixing links site-wide. Fix each listed link with the normal edit tools (edit_module, set_content_instance_values, set_structured_set) by pointing it at an existing page's path (list_pages) or removing it; the edit stores the reference. A link in a layout's own HTML (the page frame) is the Owner's to fix under Settings → Layouts: name the layout and the path to use. Filter with `search` (matches the link, the label and the field). Running or reverting the migration is the Owner's (Settings → Internal links); you only read.",
1941
+ "inputSchema": {
1942
+ "type": "object",
1943
+ "additionalProperties": false,
1944
+ "properties": {
1945
+ "runId": {
1946
+ "type": "string",
1947
+ "format": "uuid",
1948
+ "description": "Omit for the latest site-wide run."
1949
+ },
1950
+ "search": {
1951
+ "type": "string",
1952
+ "maxLength": 200
1953
+ },
1954
+ "limit": {
1955
+ "type": "integer",
1956
+ "minimum": 1,
1957
+ "maximum": 500
1958
+ },
1959
+ "offset": {
1960
+ "type": "integer",
1961
+ "minimum": 0
1962
+ }
1963
+ }
1964
+ }
1965
+ },
1868
1966
  {
1869
1967
  "name": "set_migration_budget",
1870
1968
  "description": "Propose the money ceiling for a migration run, in the currency the operator named. Approving an import proposal already arms a ceiling from the estimate's high end, so call this only when the operator names a DIFFERENT amount — or to RAISE the budget after the gate paused the run ('Cost ceiling reached'), which re-arms it so the run can continue. `ceiling` is the whole-run budget in major units (10 for €10), not per-page. This is a TWO-STEP flow: you propose the amount, the operator clicks Approve on the card in the chat, and only then is the ceiling recorded. Do not claim the budget was applied, and do not carry on as if the higher ceiling were already in force. Never propose an amount the operator has not named — the click confirms their number, it does not authorise yours. Use `check_run_budget` to track spend against the ceiling.\n\nOver MCP: when this call needs the operator's approval, it queues an approval card in their Caelo admin (Live Edit chat and the pending-approvals page) and returns the outcome once they decide — tell them to approve the card; never ask them to paste a prompt into the Caelo chat.",
@@ -2062,7 +2160,7 @@
2062
2160
  },
2063
2161
  {
2064
2162
  "name": "bug_report",
2065
- "description": "File a bug report against Caelo ITSELF when a tool or surface behaves contrary to its documented contract — e.g. a tool result contradicts persisted state, a screenshot/render ignores a parameter, an error fires on input the description says is valid. This is for defects in the SYSTEM, not for your own mistakes (wrong arguments, misread state) and not for content/design issues on the site being built. Report ONCE per distinct defect (do not re-file the same bug in one session), then CONTINUE the task via a workaround when one exists. Only when the bug genuinely blocks the task: file with blockedTask=true, tell the operator in one sentence what is blocked, and stop that line of work instead of retrying into the same wall. `whatHappened` = what you observed (include the exact tool + arguments); `expected` = what the tool's contract promised; `evidence` = the result excerpt that proves it.",
2163
+ "description": "File a bug report against Caelo ITSELF when a tool or surface behaves contrary to its documented contract — e.g. a tool result contradicts persisted state, a screenshot/render ignores a parameter, an error fires on input the description says is valid. This is for defects in the SYSTEM, not for your own mistakes (wrong arguments, misread state) and not for content/design issues on the site being built. Report ONCE per distinct defect (do not re-file the same bug in one session), then CONTINUE the task via a workaround when one exists. Only when the bug genuinely blocks the task: file with blockedTask=true, tell the operator in one sentence what is blocked, and stop that line of work instead of retrying into the same wall. `whatHappened` = what you observed (include the exact tool + arguments); `expected` = what the tool's contract promised; `evidence` = the result excerpt that proves it. The result says where the report went (stored on this site only, or sent to the Caelo team) — tell the operator exactly that and never claim the Caelo team was notified unless the result says it was sent.",
2066
2164
  "inputSchema": {
2067
2165
  "type": "object",
2068
2166
  "additionalProperties": false,
@@ -2390,7 +2488,7 @@
2390
2488
  },
2391
2489
  {
2392
2490
  "name": "add_module",
2393
- "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\"`).\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).",
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).",
2394
2492
  "inputSchema": {
2395
2493
  "type": "object",
2396
2494
  "additionalProperties": false,
@@ -2821,7 +2919,7 @@
2821
2919
  },
2822
2920
  {
2823
2921
  "name": "set_structured_set",
2824
- "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.",
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.",
2825
2923
  "inputSchema": {
2826
2924
  "type": "object",
2827
2925
  "additionalProperties": false,
@@ -4120,6 +4218,40 @@
4120
4218
  "additionalProperties": false
4121
4219
  }
4122
4220
  },
4221
+ {
4222
+ "name": "check_links",
4223
+ "description": "Check the site's internal links (links between its own pages and media) in the draft you see — every place a link is stored: page content, module HTML, field defaults, layouts, templates, navigation menus and plugin data. Reports links to deleted pages or media and links without a page in the linking page's language (both BLOCK Stage and Publish live), links to unpublished pages (BLOCK Publish live, a warning at Stage), and old plain-path links (warnings). Each problem names the page, where the link is stored and the tool that fixes it. Use it before stage_changes when you deleted, unpublished or translated pages, when a Stage was refused for links, or when the operator asks about broken links. Not for external links or what the built site serves (get_quality_audit has the staged-build link check).",
4224
+ "inputSchema": {
4225
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4226
+ "type": "object",
4227
+ "properties": {
4228
+ "for": {
4229
+ "description": "Which gate to check for. 'publish' (default): Publish live / promote — a link to an unpublished page blocks. 'stage': such a link is only a warning.",
4230
+ "type": "string",
4231
+ "enum": [
4232
+ "stage",
4233
+ "publish"
4234
+ ]
4235
+ },
4236
+ "pagePath": {
4237
+ "description": "Only problems on this page (its path, e.g. '/about'); site-wide places (menus, plugin data) are always included.",
4238
+ "type": "string",
4239
+ "minLength": 1,
4240
+ "maxLength": 2000
4241
+ },
4242
+ "blockingOnly": {
4243
+ "description": "True = only problems that block.",
4244
+ "type": "boolean"
4245
+ },
4246
+ "limit": {
4247
+ "type": "integer",
4248
+ "minimum": 1,
4249
+ "maximum": 500
4250
+ }
4251
+ },
4252
+ "additionalProperties": false
4253
+ }
4254
+ },
4123
4255
  {
4124
4256
  "name": "get_quality_audit",
4125
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 { auditRunId }.",
@@ -4535,7 +4667,7 @@
4535
4667
  },
4536
4668
  {
4537
4669
  "name": "screenshot_page",
4538
- "description": "Capture a screenshot of the rendered page (the image comes back WITHIN this same turn — keep working and analyse it on your next step, do NOT end your turn to wait for it). Use for the GROSS visual impression — 'does the hero feel crowded?', 'is the color/imagery right?', 'what's the overall layout impression?'. Works on EVERY surface: in an interactive chat the operator's browser captures (html2canvas on their exact viewport); without an attached browser (MCP, background sends, subagent turns) the server renders the branch preview in real headless Chromium instead — the result text names which backend ran. FIDELITY LIMIT (browser-captured results only) — html2canvas is an APPROXIMATION, not a real browser render. It reliably mis-renders TWO things: (1) fine text spacing — words can show large irregular gaps that look like broken text-align:justify, and (2) flex-wrap — wrapping rows can stack one-item-per-line. These are html2canvas artifacts, NOT real CSS bugs. If a browser-captured screenshot shows justified-looking word gaps or single-column flex stacking, DO NOT edit CSS or file a bug from the screenshot alone — confirm against `inspect_page_render` first. Server-rendered results are real Chromium output and carry no such artifacts. ALWAYS call this after composing a page or making structural/styling changes — desktop AND mobile viewports — and fix real issues the screenshot reveals BEFORE telling the operator you're done (max two review rounds; skip for content-only edits). For ANY spacing/layout/CSS question (word gaps, flex stacking, white halo around the header, wrong colors, broken layout) prefer `inspect_page_render` — its summary then `target`/`search` give the composed HTML and each CSS layer separately, the accurate source of truth for spacing + layout. Pass `selector` (CSS selector) to capture a SINGLE element instead of the whole page — right choice when checking one module (a footer, a hero) instead of the page. By default the capture shows THIS chat's branch preview (your pending edits included); pass `chatBranchId` only to capture a different branch. The image is available to you in this turn only, not persisted across the chat.",
4670
+ "description": "Capture a screenshot of the rendered page (the image comes back WITHIN this same turn — keep working and analyse it on your next step, do NOT end your turn to wait for it). Use for the GROSS visual impression — 'does the hero feel crowded?', 'is the color/imagery right?', 'what's the overall layout impression?'. Works on EVERY surface: in an interactive chat the operator's browser captures (html2canvas on their exact viewport); without an attached browser (MCP, background sends, subagent turns) the server renders the branch preview in real headless Chromium instead — the result text names which backend ran. FIDELITY LIMIT (browser-captured results only) — html2canvas is an APPROXIMATION, not a real browser render. It reliably mis-renders TWO things: (1) fine text spacing — words can show large irregular gaps that look like broken text-align:justify, and (2) flex-wrap — wrapping rows can stack one-item-per-line. These are html2canvas artifacts, NOT real CSS bugs. If a browser-captured screenshot shows justified-looking word gaps or single-column flex stacking, DO NOT edit CSS or file a bug from the screenshot alone — confirm against `inspect_page_render` first. Server-rendered results are real Chromium output and carry no such artifacts. DESKTOP IS NOT ONE WIDTH — type sized with vw/clamp() wraps differently per width, so a headline that sits on 3 lines at 1280px can take 4 at 1740px. In an interactive chat, desktop captures the operator's live preview size (the result states the size and whose view it is); omit `width` for that capture. For typography/layout reviews ALSO capture a second desktop width via `width` (1280 when the operator's view is wider, 1920 when it is narrower) and make the design hold at both. When the operator reports a wrap or break you don't see, re-capture without `width` before concluding it is fine. ALWAYS call this after composing a page or making structural/styling changes — desktop AND mobile viewports — and fix real issues the screenshot reveals BEFORE telling the operator you're done (max two review rounds; skip for content-only edits). For ANY spacing/layout/CSS question (word gaps, flex stacking, white halo around the header, wrong colors, broken layout) prefer `inspect_page_render` — its summary then `target`/`search` give the composed HTML and each CSS layer separately, the accurate source of truth for spacing + layout. Pass `selector` (CSS selector) to capture a SINGLE element instead of the whole page — right choice when checking one module (a footer, a hero) instead of the page. By default the capture shows THIS chat's branch preview (your pending edits included); pass `chatBranchId` only to capture a different branch. The image is available to you in this turn only, not persisted across the chat.",
4539
4671
  "inputSchema": {
4540
4672
  "type": "object",
4541
4673
  "additionalProperties": false,
@@ -4559,7 +4691,13 @@
4559
4691
  "tablet",
4560
4692
  "mobile"
4561
4693
  ],
4562
- "description": "Optional viewport hint. ChatPanel applies the corresponding iframe dimensions before capture (1280x800 / 768x1024 / 375x812). Defaults to desktop."
4694
+ "description": "Optional viewport. desktop (default) captures at the operator's live preview size in an interactive chat (falls back to the 1280x800 preset when no preview is open or on the server-side renderer); tablet = 768x1024, mobile = 375x812."
4695
+ },
4696
+ "width": {
4697
+ "type": "integer",
4698
+ "minimum": 320,
4699
+ "maximum": 3840,
4700
+ "description": "Optional layout width in CSS px — overrides the viewport's width (height stays the viewport's). Use it to check a SECOND desktop width on purpose (e.g. 1280 and 1920), not for the first capture: omitting it shows you the operator's own view."
4563
4701
  },
4564
4702
  "selector": {
4565
4703
  "type": "string",
@@ -4761,7 +4899,7 @@
4761
4899
  },
4762
4900
  {
4763
4901
  "name": "update_pages_many",
4764
- "description": "Edit page metadata — the ONE tool for it, for 1 page or 200 (pass a single-item `updates` array for one page; there is no separate rename/retitle/reslug tool). Use for 'rename this page', 'change the browser-tab title', 'move this to /pricing', 'archive all draft posts', 'move these pages to the {tpl} template'. **A page has THREE independently-editable identifiers — never substitute one for another:** `name` = the internal editor label (page picker / breadcrumbs; not public), `title` = the HTML <title> tag (browser tab + search results), `slug` = the URL path. If the operator just says 'rename' without saying which, ASK before guessing — 'rename' usually means `name`. **Changing `slug` moves the page's public URL — and the URL of every page nested below it**: a 301 from each old URL is created automatically and every nav-menu / link-list / module / content link pointing at them is rewritten, in one transaction. A rename that moves 10+ URLs (the page plus its subtree) needs one Owner click in chat before it runs. To put a page under another page (or back at the top level) use move_pages, not this tool. Pass `redirectFromOld: 'skip'` ONLY when the operator explicitly says the old URL should 404 — the default keeps inbound links alive. Per-item failures (not-found, version conflict) are reported; the rest of the batch still applies. DO NOT use for SEO sidecar (`pages_seo.set_many`) or modules (`update_modules_many`) — those have their own bulk tools.\n\nOver MCP: when this call needs the operator's approval, it queues an approval card in their Caelo admin (Live Edit chat and the pending-approvals page) and returns the outcome once they decide — tell them to approve the card; never ask them to paste a prompt into the Caelo chat.",
4902
+ "description": "Edit page metadata — the ONE tool for it, for 1 page or 200 (pass a single-item `updates` array for one page; there is no separate rename/retitle/reslug tool). Use for 'rename this page', 'change the browser-tab title', 'move this to /pricing', 'archive all draft posts', 'move these pages to the {tpl} template'. **A page has THREE independently-editable identifiers — never substitute one for another:** `name` = the internal editor label (page picker / breadcrumbs; not public), `title` = the HTML <title> tag (browser tab + search results), `slug` = the URL path. If the operator just says 'rename' without saying which, ASK before guessing — 'rename' usually means `name`. **Changing `slug` moves the page's public URL — and the URL of every page nested below it**: a 301 from each old URL is created automatically in the same transaction, for links from outside the site. Links on this site (nav menus, link lists, modules, content) follow the page on their own — do not update them after a rename. A rename that moves 10+ URLs (the page plus its subtree) needs one Owner click in chat before it runs. To put a page under another page (or back at the top level) use move_pages, not this tool. Pass `redirectFromOld: 'skip'` ONLY when the operator explicitly says the old URL should 404 — the default keeps inbound links alive. Per-item failures (not-found, version conflict) are reported; the rest of the batch still applies. DO NOT use for SEO sidecar (`pages_seo.set_many`) or modules (`update_modules_many`) — those have their own bulk tools.\n\nOver MCP: when this call needs the operator's approval, it queues an approval card in their Caelo admin (Live Edit chat and the pending-approvals page) and returns the outcome once they decide — tell them to approve the card; never ask them to paste a prompt into the Caelo chat.",
4765
4903
  "inputSchema": {
4766
4904
  "type": "object",
4767
4905
  "additionalProperties": false,
@@ -4804,7 +4942,7 @@
4804
4942
  "type": "string",
4805
4943
  "minLength": 1,
4806
4944
  "maxLength": 120,
4807
- "description": "URL path. Changing it auto-creates a 301 from the old URL and rewrites links pointing at it."
4945
+ "description": "URL path. Changing it auto-creates a 301 from the old URL; links on the site follow the page automatically."
4808
4946
  },
4809
4947
  "templateId": {
4810
4948
  "type": "string",
@@ -4834,7 +4972,7 @@
4834
4972
  },
4835
4973
  {
4836
4974
  "name": "move_pages",
4837
- "description": "Put pages under other pages (nested URLs), or back at the top level — for 1 page or 200 in one call. Use for 'put Alerts under SEO Monitoring', 'move the pricing page into Products', 'make this a top-level page again', 'group these 5 guides under /guides'. A page's URL is its parent's URL + '/' + its slug, so moving `/alerts` under `/seo-monitoring` makes it `/seo-monitoring/alerts`; every page below a moved page moves with it. Each move: `page` = the page's id or current URL path, `parent` = the new parent's id or path, or null for the top level, optional `newSlug` to rename in the same step. Everything happens in one transaction: 301s from every old URL, and nav menus, link lists, module and content links are rewritten to the new URLs. If the target URL is already taken, the call changes nothing and names the clash — retry with `newSlug`. The homepage can neither move under a page nor have child pages; nesting is limited to 6 levels. Moving 10+ URLs in one call (pages + their subtrees) needs one Owner click in chat before it runs. To change only a page's name/title/slug use update_pages_many; to create a page under a parent use build_page with page.parentPageId.\n\nOver MCP: when this call needs the operator's approval, it queues an approval card in their Caelo admin (Live Edit chat and the pending-approvals page) and returns the outcome once they decide — tell them to approve the card; never ask them to paste a prompt into the Caelo chat.",
4975
+ "description": "Put pages under other pages (nested URLs), or back at the top level — for 1 page or 200 in one call. Use for 'put Alerts under SEO Monitoring', 'move the pricing page into Products', 'make this a top-level page again', 'group these 5 guides under /guides'. A page's URL is its parent's URL + '/' + its slug, so moving `/alerts` under `/seo-monitoring` makes it `/seo-monitoring/alerts`; every page below a moved page moves with it. Each move: `page` = the page's id or current URL path, `parent` = the new parent's id or path, or null for the top level, optional `newSlug` to rename in the same step. Everything happens in one transaction, with a 301 from every old URL for links from outside the site. Links on this site (nav menus, link lists, module and content links) follow the pages automatically — there is no link-update step after a move. If the target URL is already taken, the call changes nothing and names the clash — retry with `newSlug`. The homepage can neither move under a page nor have child pages; nesting is limited to 6 levels. Moving 10+ URLs in one call (pages + their subtrees) needs one Owner click in chat before it runs. To change only a page's name/title/slug use update_pages_many; to create a page under a parent use build_page with page.parentPageId.\n\nOver MCP: when this call needs the operator's approval, it queues an approval card in their Caelo admin (Live Edit chat and the pending-approvals page) and returns the outcome once they decide — tell them to approve the card; never ask them to paste a prompt into the Caelo chat.",
4838
4976
  "inputSchema": {
4839
4977
  "type": "object",
4840
4978
  "additionalProperties": false,
@@ -5527,7 +5665,8 @@
5527
5665
  "private_files",
5528
5666
  "image_generation",
5529
5667
  "site_media_read",
5530
- "content_variants"
5668
+ "content_variants",
5669
+ "link_resolve"
5531
5670
  ]
5532
5671
  },
5533
5672
  "target": {
@@ -5613,7 +5752,7 @@
5613
5752
  },
5614
5753
  {
5615
5754
  "name": "propose_create_user",
5616
- "description": "Propose inviting a new user to the CMS. Provide email + displayName + roleNames. DO NOT include a password — the Owner approves and a one-time temporary password is generated server-side. 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); the result's `operatorAccess` says whether that worked — relay any `warning` to the operator. 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.",
5755
+ "description": "Propose inviting a new user to the CMS. Provide email + displayName + roleNames. DO NOT include a password — the Owner approves and a one-time temporary password is generated server-side. 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); the result's `operatorAccess` says whether that worked — relay any `warning` to the operator. 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. 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.",
5617
5756
  "inputSchema": {
5618
5757
  "type": "object",
5619
5758
  "additionalProperties": false,
@@ -7418,6 +7557,23 @@
7418
7557
  "additionalProperties": false
7419
7558
  }
7420
7559
  },
7560
+ {
7561
+ "name": "resolve_import_links",
7562
+ "description": "While you rebuild an import run, link to the migrated site's pages by their real paths even before you have built them: a link to a page of THIS run (a crawled page, a page in the run's URL list, or a page the crawled pages link to) is kept as written and recorded against the run instead of refused. A link to any other unknown path is still refused. Call this ONCE when every page of the run is built (before get_import_run_report): it rewrites the recorded content so each link whose page now exists is stored as that page, and lists the links that still match no page, with where they are — fix each (build the page, or point the link at an existing page's path) and call it again. Safe to repeat; the run's cleanup does it too. Do not use it for links outside an import (those are checked when you write them).",
7563
+ "inputSchema": {
7564
+ "type": "object",
7565
+ "additionalProperties": false,
7566
+ "required": [
7567
+ "runId"
7568
+ ],
7569
+ "properties": {
7570
+ "runId": {
7571
+ "type": "string",
7572
+ "format": "uuid"
7573
+ }
7574
+ }
7575
+ }
7576
+ },
7421
7577
  {
7422
7578
  "name": "send_test_email",
7423
7579
  "description": "Send the fixed 'Caelo email transport test' message through the site's configured email transport and report whether the provider accepted it. Use after an email config change was approved (`propose_set_email_config`), or when password-reset / form emails seem not to arrive. The recipient must be on the sender's own domain (e.g. sender noreply@example.com → you may test to team@example.com); for any other address, tell the operator to send the test from Settings → Email. Subject and body are fixed — this cannot send custom mail. A failure names the provider's reason (bad API key, unverified sender domain, transport 'none'); relay it rather than retrying unchanged.",
@@ -7582,7 +7738,7 @@
7582
7738
  },
7583
7739
  {
7584
7740
  "name": "list_bug_reports",
7585
- "description": "List the defect reports already filed against Caelo (by you via bug_report, or auto-captured by the chat), newest first. Call BEFORE bug_report to see whether the problem is already reported; filter by status (new / triaged / fixed / invalid).",
7741
+ "description": "List the defect reports already filed against Caelo (by you via bug_report, or auto-captured by the chat), newest first. Call BEFORE bug_report to see whether the problem is already reported; filter by status (new / triaged / fixed / invalid). The forwarding column says where each report went: local_only = stored on this site only, the Caelo team has NOT seen it; queued = will be sent to the Caelo team; sent = the Caelo team received it; failed = sending failed, stored here only. Never tell the operator the Caelo team knows about a report unless its forwarding is sent.",
7586
7742
  "inputSchema": {
7587
7743
  "$schema": "https://json-schema.org/draft/2020-12/schema",
7588
7744
  "type": "object",