@bettercms-ai/mcp 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -316,7 +316,7 @@ import { BetterCMS } from "@bettercms-ai/sdk";
316
316
  import { z } from "zod";
317
317
 
318
318
  // ../types/src/component.ts
319
- var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section component (list_components returns them with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; REFERENCE it as a `component` when the band recurs or needs variants. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
319
+ var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
320
320
 
321
321
  // ../types/src/layout-lucide-icons.ts
322
322
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -3176,6 +3176,13 @@ function buildToolDefs(deps) {
3176
3176
  return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
3177
3177
  }
3178
3178
  ),
3179
+ def(
3180
+ "set_binding_mode",
3181
+ "Set how the site's bindings are resolved",
3182
+ "Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which works once (at import, when the CMS values equal the built copy) and breaks the first time anyone edits a value. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order is push \u2192 release \u2192 get_binding_report shows mode 'text-match' with 0 unmatched \u2192 set_binding_mode \u2192 release again \u2192 get_binding_report shows mode 'declared'. Flipping back is the same call. REQUIRES a project-scoped connection carrying the artifact:write scope \u2014 the same authority that deploys the site \u2014 because this decides what every future release does to every page; a workspace-wide grant is refused with 403. See section 13 of the bettercms://playbook/schema resource.",
3183
+ z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
3184
+ async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
3185
+ ),
3179
3186
  def(
3180
3187
  "clone_project",
3181
3188
  "Clone a project",
@@ -3285,6 +3292,13 @@ function buildToolDefs(deps) {
3285
3292
  z.object({}).shape,
3286
3293
  async (c) => ok("Next steps.", await data(c, "GET", `/management/insights/next-steps`))
3287
3294
  ),
3295
+ def(
3296
+ "get_binding_report",
3297
+ "Check what on the live site is editable",
3298
+ "The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
3299
+ z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read; defaults to the one this project deploys to") }).shape,
3300
+ async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
3301
+ ),
3288
3302
  def(
3289
3303
  "get_analytics_overview",
3290
3304
  "Get traffic overview",
@@ -4192,8 +4206,9 @@ renders. Three consequences, each load-bearing:
4192
4206
  c. set_page_content values EXACTLY equal to the text the site renders \u2014
4193
4207
  binding matches by value, so a paraphrase binds nothing
4194
4208
  d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
4195
- 2. A value that renders in more than one place stays uneditable on the canvas (deliberate:
4196
- binding it would edit all of them at once). It remains editable in the side panel.
4209
+ 2. A value that renders in more than one place is still ONE field: bind every element that
4210
+ renders it, and the editor keeps them in sync \u2014 an edit patches every copy at once.
4211
+ Binding only one copy leaves the others showing the old text until the next rebuild.
4197
4212
  3. Make the chrome ITSELF editable by speaking the layout grammar: the nav/footer
4198
4213
  elements declare \`data-bcms-layout-section="navigation"\` / \`"footer"\`, and each
4199
4214
  CMS-backed text inside them a \`data-bcms-layout-field="layout:<sectionId>:<fieldId>"\`
@@ -4207,8 +4222,8 @@ renders. Three consequences, each load-bearing:
4207
4222
  STORED index then the slug (\`layout:navigation:navigation.links.0.label\`), nesting
4208
4223
  as deep as the schema goes (\`layout:footer:footer.link-groups.0.links.1.label\`).
4209
4224
  Three rules, each one a measured defect when broken:
4210
- a. a field rendered by TWO elements gets a marker on only one \u2014 the editor binds a
4211
- twice-carried field to neither;
4225
+ a. a field rendered by TWO elements gets a marker on BOTH \u2014 the editor keeps the copies
4226
+ in sync, and a marker on only one leaves the other stale;
4212
4227
  b. PROVENANCE \u2014 mark an element only when the LAYOUT supplied its value; a marker
4213
4228
  over a fallback/singleton-sourced string opens an editor for a row that does not
4214
4229
  exist;
@@ -4253,6 +4268,101 @@ green on the wrong document. Three rules, none optional:
4253
4268
  3. A tool param the schema does not declare is SILENTLY DROPPED, not rejected. If a
4254
4269
  call's behavior doesn't change when you change a param, treat the param as dead and
4255
4270
  verify through an independent channel before trusting any result built on it.
4271
+
4272
+ ## 13. Convert an imported repo into a CMS-backed, editable site
4273
+
4274
+ \xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
4275
+ receipt you can read: \`get_binding_report\` says \`mode: "declared"\` with zero unmatched paths.
4276
+
4277
+ **Scope, before you start.**
4278
+
4279
+ (a) This is the FIELD-driven conversion \u2014 page fields and collections. If the human answered
4280
+ \`components\` at the \xA710 gate, go to \xA710's recipe instead: a component's editing surface is its
4281
+ declared \`props\`, and those bindings are NOT what \`get_binding_report\` walks.
4282
+
4283
+ (b) It works for any framework that emits STATIC HTML, because the binding contract is plain
4284
+ HTML attributes \u2014 the annotator, the injector and the canvas stamper never see your source.
4285
+ There are SDK helpers for Astro and Next. A Node-runtime site (\`bcms-runtime.json\`) edits on
4286
+ the canvas but skips release annotation and publish-time injection, because there is no HTML on
4287
+ disk to annotate. Copy rendered on the client must carry the attributes in the HYDRATED DOM,
4288
+ and only the canvas sees it \u2014 a release scan cannot.
4289
+
4290
+ **If you know Sanity, this is the same shape under different names:**
4291
+
4292
+ defineType schema in code -> content models / page fields (create_content_model,
4293
+ add_page_field, or bcms-content.json "schema")
4294
+ TypeGen -> @bettercms-ai/codegen
4295
+ GROQ query in the page -> @bettercms-ai/sdk read client, or the bcms-content.json
4296
+ build snapshot
4297
+ <PortableText> body -> @bettercms-ai/richtext portableTextToHtml, field type
4298
+ 'document'
4299
+ data-sanity / stega -> data-bcms-field + data-bcms-kind (<BcmsField>), stega on
4300
+ draft reads
4301
+ Presentation tool overlays -> the visual editor canvas, framing your own build
4302
+
4303
+ **The steps.**
4304
+
4305
+ 1. \`pull_project_source\` (or clone the \`github\` remote it returns). Read the SOURCE. Never
4306
+ reconstruct content from the deployed HTML \u2014 that is how a site ends up bound to a copy of
4307
+ its own stale build.
4308
+ 2. Decide the architecture WITH the human (\xA710) and record it: \`set_authoring_preference\`.
4309
+ 3. Register the schema. Per route: \`create_page\` + \`add_page_field\` (text / longtext /
4310
+ richtext / image / array groups). Per repeated content type: \`create_content_model\`, with a
4311
+ \`document\` body for articles. Site chrome goes through \`update_layout\`; images through
4312
+ \`create_media_upload\` / \`upload_asset\`, then reference the CMS URL. The repo-owned
4313
+ alternative to calling these one by one is a committed \`bcms-content.json\` carrying a
4314
+ \`schema\` block, which seeds models, pages and entries at connect time \u2014 the \`defineType\`
4315
+ analogue.
4316
+ 4. Seed the entries with the copy EXACTLY as the source renders it (\`set_page_content\`,
4317
+ \`create_content_entry\`). Prose goes in as Portable Text arrays (\`_type: "block"\`); never
4318
+ markup inside a \`text\` field.
4319
+ **Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
4320
+ this platform stores \u2014 \`block\`, or \`bcmsBlock\` carrying a \`schemaKey\` such as
4321
+ \`builtin:table\` \u2014 and a foreign node (a Sanity-shaped \`{_type:"table", rows:[{cells}]}\` is
4322
+ the one that has actually happened) is not merely unrendered: it has no component here, so it
4323
+ vanishes from the HTML every delivery surface reads while the stored value still looks whole.
4324
+ Verify each \`_type\` against the schema before you write, not after.
4325
+ 5. Codemod the templates. Read each value from the CMS (\`@bettercms-ai/astro\` /
4326
+ \`@bettercms-ai/next\` client, or the \`bcms-content.json\` build snapshot), KEEP the in-code
4327
+ copy as the fallback, and declare the binding on the element that already renders it.
4328
+ Prefer schema-derived bindings \u2014 the TypeGen analogue:
4329
+ \`npx @bettercms-ai/codegen --bindings-out src/bettercms.bindings.generated.ts\`, then spread
4330
+ \`{...bcms.home.hero.title}\` / \`{...bcms.blog.features.$(i)}\`. The hand form is
4331
+ \`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`), \`data-bcms-props\`
4332
+ for an \`href\` / \`alt\` / \`src\`, the \xA711 layout markers for nav and footer, and
4333
+ \`<div data-bcms-field="body" data-bcms-kind="document">\` around a Portable Text render.
4334
+ Bind CONDITIONALLY (\`fromCms ? path : undefined\`) so a fallback row is never bound. A value
4335
+ rendered in N places carries the binding on ALL N \u2014 the editor keeps the copies in sync.
4336
+ **Read the LIVE SCHEMA before you bind \u2014 \`get_page\` / \`get_content_model\`, never the
4337
+ delivery snapshot.** A field nobody has authored yet is simply ABSENT from the payload, so a
4338
+ snapshot cannot tell "this field does not exist" from "this field is empty": bind against it
4339
+ and you declare a path for a \`cover\` field that was never created, which the report then
4340
+ reports as broken forever. The schema is the list of what exists; the snapshot is only what
4341
+ currently has a value.
4342
+ **An index or listing route binds NOTHING.** The editor loads ONE entry per route, and an
4343
+ index renders many, so a binding there addresses whichever entry the editor happened to
4344
+ load. Bind each item's fields on that item's OWN route (\`/blog/<slug>\`); on the index,
4345
+ render from the CMS and declare nothing.
4346
+ 5b. **Where the content comes from at build time.** Two lanes, and they differ:
4347
+ - GIT-CONNECTED (recommended for a converted site): the platform's provisioned workflow
4348
+ writes \`bcms-content.json\` into the repo root before \`build\`, using the repo's
4349
+ \`BCMS_API_KEY\` secret. Read that file, with in-code fallbacks so a local or CI build
4350
+ without it still renders.
4351
+ - ARCHIVE (\`deploy_project\` / \`deploy_from_upload\`): the sandbox build runs with no env
4352
+ and no network content step, by design. So the archive MUST SHIP its own
4353
+ \`bcms-content.json\` \u2014 generate it locally with a delivery key and commit it. Without
4354
+ one the site renders its fallbacks and the report says \`no-element\` for every path.
4355
+ 6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
4356
+ \`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
4357
+ values are byte-equal to what the build renders. Now \`set_binding_mode
4358
+ {declaredBindings: true}\` and release again (an empty commit is enough). Do not flip before
4359
+ the report is clean: in declared mode an undeclared field simply stops being editable.
4360
+ 7. **Receipts.** \`get_binding_report\` reads \`mode: "declared"\`, \`unmatched: []\`, and
4361
+ \`bound > 0\`. That certifies one thing only \u2014 that every non-empty field has SOME element
4362
+ carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
4363
+ text against its entry values yourself before you call the page done. Then publish, and
4364
+ fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
4365
+ \`get_next_steps\` keeps reporting the gap until every one of these holds.
4256
4366
  `;
4257
4367
 
4258
4368
  // src/prompts.ts