@bettercms-ai/mcp 0.23.0 → 0.25.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 +129 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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,20 @@ 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
|
+
),
|
|
3302
|
+
def(
|
|
3303
|
+
"get_conversion_brief",
|
|
3304
|
+
"Get the brief for making this site's bindings durable",
|
|
3305
|
+
"The per-project brief for making this site's bindings DURABLE \u2014 read it before you touch the templates. Returns what already exists in the CMS: every live page with its route, and every bindable field path with its `label`, `kind`, the value the CMS holds now (`current`) and the copy the repo renders today (`original`, the field's defaultValue) \u2014 plus the exact attributes to declare, and the ordered steps. Call it for any site whose pages were DERIVED at import, and whenever get_next_steps reports `bindings-not-declared`. It REPLACES re-registering a schema: these pages, fields and values exist already, so create_page / add_page_field / create_content_model would build a second schema over the first \u2014 edit values with set_page_content instead. `lane` says how to get the source ('git-connected' = pull_project_source returns a repo; 'archive' = a tarball). The full recipe is section 13 of the bettercms://playbook/schema resource; get_binding_report is the receipt that says you finished.",
|
|
3306
|
+
z.object({}).shape,
|
|
3307
|
+
async (c) => ok("Conversion brief.", await data(c, "GET", `/management/projects/current/conversion-brief`))
|
|
3308
|
+
),
|
|
3288
3309
|
def(
|
|
3289
3310
|
"get_analytics_overview",
|
|
3290
3311
|
"Get traffic overview",
|
|
@@ -4192,8 +4213,9 @@ renders. Three consequences, each load-bearing:
|
|
|
4192
4213
|
c. set_page_content values EXACTLY equal to the text the site renders \u2014
|
|
4193
4214
|
binding matches by value, so a paraphrase binds nothing
|
|
4194
4215
|
d. update_page status 'published' \u2014 the canvas binds the PUBLISHED copy
|
|
4195
|
-
2. A value that renders in more than one place
|
|
4196
|
-
|
|
4216
|
+
2. A value that renders in more than one place is still ONE field: bind every element that
|
|
4217
|
+
renders it, and the editor keeps them in sync \u2014 an edit patches every copy at once.
|
|
4218
|
+
Binding only one copy leaves the others showing the old text until the next rebuild.
|
|
4197
4219
|
3. Make the chrome ITSELF editable by speaking the layout grammar: the nav/footer
|
|
4198
4220
|
elements declare \`data-bcms-layout-section="navigation"\` / \`"footer"\`, and each
|
|
4199
4221
|
CMS-backed text inside them a \`data-bcms-layout-field="layout:<sectionId>:<fieldId>"\`
|
|
@@ -4207,8 +4229,8 @@ renders. Three consequences, each load-bearing:
|
|
|
4207
4229
|
STORED index then the slug (\`layout:navigation:navigation.links.0.label\`), nesting
|
|
4208
4230
|
as deep as the schema goes (\`layout:footer:footer.link-groups.0.links.1.label\`).
|
|
4209
4231
|
Three rules, each one a measured defect when broken:
|
|
4210
|
-
a. a field rendered by TWO elements gets a marker on
|
|
4211
|
-
|
|
4232
|
+
a. a field rendered by TWO elements gets a marker on BOTH \u2014 the editor keeps the copies
|
|
4233
|
+
in sync, and a marker on only one leaves the other stale;
|
|
4212
4234
|
b. PROVENANCE \u2014 mark an element only when the LAYOUT supplied its value; a marker
|
|
4213
4235
|
over a fallback/singleton-sourced string opens an editor for a row that does not
|
|
4214
4236
|
exist;
|
|
@@ -4253,6 +4275,108 @@ green on the wrong document. Three rules, none optional:
|
|
|
4253
4275
|
3. A tool param the schema does not declare is SILENTLY DROPPED, not rejected. If a
|
|
4254
4276
|
call's behavior doesn't change when you change a param, treat the param as dead and
|
|
4255
4277
|
verify through an independent channel before trusting any result built on it.
|
|
4278
|
+
|
|
4279
|
+
## 13. Convert an imported repo into a CMS-backed, editable site
|
|
4280
|
+
|
|
4281
|
+
\xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
|
|
4282
|
+
receipt you can read: \`get_binding_report\` says \`mode: "declared"\` with zero unmatched paths.
|
|
4283
|
+
|
|
4284
|
+
**Scope, before you start.**
|
|
4285
|
+
|
|
4286
|
+
(a) This is the FIELD-driven conversion \u2014 page fields and collections. If the human answered
|
|
4287
|
+
\`components\` at the \xA710 gate, go to \xA710's recipe instead: a component's editing surface is its
|
|
4288
|
+
declared \`props\`, and those bindings are NOT what \`get_binding_report\` walks.
|
|
4289
|
+
|
|
4290
|
+
(b) It works for any framework that emits STATIC HTML, because the binding contract is plain
|
|
4291
|
+
HTML attributes \u2014 the annotator, the injector and the canvas stamper never see your source.
|
|
4292
|
+
There are SDK helpers for Astro and Next. A Node-runtime site (\`bcms-runtime.json\`) edits on
|
|
4293
|
+
the canvas but skips release annotation and publish-time injection, because there is no HTML on
|
|
4294
|
+
disk to annotate. Copy rendered on the client must carry the attributes in the HYDRATED DOM,
|
|
4295
|
+
and only the canvas sees it \u2014 a release scan cannot.
|
|
4296
|
+
|
|
4297
|
+
**If you know Sanity, this is the same shape under different names:**
|
|
4298
|
+
|
|
4299
|
+
defineType schema in code -> content models / page fields (create_content_model,
|
|
4300
|
+
add_page_field, or bcms-content.json "schema")
|
|
4301
|
+
TypeGen -> @bettercms-ai/codegen
|
|
4302
|
+
GROQ query in the page -> @bettercms-ai/sdk read client, or the bcms-content.json
|
|
4303
|
+
build snapshot
|
|
4304
|
+
<PortableText> body -> @bettercms-ai/richtext portableTextToHtml, field type
|
|
4305
|
+
'document'
|
|
4306
|
+
data-sanity / stega -> data-bcms-field + data-bcms-kind (<BcmsField>), stega on
|
|
4307
|
+
draft reads
|
|
4308
|
+
Presentation tool overlays -> the visual editor canvas, framing your own build
|
|
4309
|
+
|
|
4310
|
+
**Imported site whose pages were DERIVED.** If this project was imported and made editable from
|
|
4311
|
+
its build, the schema and the values already exist \u2014 a page per route, a field per element, and
|
|
4312
|
+
the original copy carried on each field as its \`defaultValue\`. Call \`get_conversion_brief\`
|
|
4313
|
+
first: it lists those pages, their routes, every bindable path with its current and original
|
|
4314
|
+
value, and the attributes to declare. SKIP steps 3 and 4 below and bind the keys it names \u2014
|
|
4315
|
+
registering the schema again builds a second one over the first.
|
|
4316
|
+
|
|
4317
|
+
**The steps.**
|
|
4318
|
+
|
|
4319
|
+
1. \`pull_project_source\` (or clone the \`github\` remote it returns). Read the SOURCE. Never
|
|
4320
|
+
reconstruct content from the deployed HTML \u2014 that is how a site ends up bound to a copy of
|
|
4321
|
+
its own stale build.
|
|
4322
|
+
2. Decide the architecture WITH the human (\xA710) and record it: \`set_authoring_preference\`.
|
|
4323
|
+
3. Register the schema. Per route: \`create_page\` + \`add_page_field\` (text / longtext /
|
|
4324
|
+
richtext / image / array groups). Per repeated content type: \`create_content_model\`, with a
|
|
4325
|
+
\`document\` body for articles. Site chrome goes through \`update_layout\`; images through
|
|
4326
|
+
\`create_media_upload\` / \`upload_asset\`, then reference the CMS URL. The repo-owned
|
|
4327
|
+
alternative to calling these one by one is a committed \`bcms-content.json\` carrying a
|
|
4328
|
+
\`schema\` block, which seeds models, pages and entries at connect time \u2014 the \`defineType\`
|
|
4329
|
+
analogue.
|
|
4330
|
+
4. Seed the entries with the copy EXACTLY as the source renders it (\`set_page_content\`,
|
|
4331
|
+
\`create_content_entry\`). Prose goes in as Portable Text arrays (\`_type: "block"\`); never
|
|
4332
|
+
markup inside a \`text\` field.
|
|
4333
|
+
**Prose MIGRATED from another CMS needs one extra check.** Every block's \`_type\` must be one
|
|
4334
|
+
this platform stores \u2014 \`block\`, or \`bcmsBlock\` carrying a \`schemaKey\` such as
|
|
4335
|
+
\`builtin:table\` \u2014 and a foreign node (a Sanity-shaped \`{_type:"table", rows:[{cells}]}\` is
|
|
4336
|
+
the one that has actually happened) is not merely unrendered: it has no component here, so it
|
|
4337
|
+
vanishes from the HTML every delivery surface reads while the stored value still looks whole.
|
|
4338
|
+
Verify each \`_type\` against the schema before you write, not after.
|
|
4339
|
+
5. Codemod the templates. Read each value from the CMS (\`@bettercms-ai/astro\` /
|
|
4340
|
+
\`@bettercms-ai/next\` client, or the \`bcms-content.json\` build snapshot), KEEP the in-code
|
|
4341
|
+
copy as the fallback, and declare the binding on the element that already renders it.
|
|
4342
|
+
Prefer schema-derived bindings \u2014 the TypeGen analogue:
|
|
4343
|
+
\`npx @bettercms-ai/codegen --bindings-out src/bettercms.bindings.generated.ts\`, then spread
|
|
4344
|
+
\`{...bcms.home.hero.title}\` / \`{...bcms.blog.features.$(i)}\`. The hand form is
|
|
4345
|
+
\`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`), \`data-bcms-props\`
|
|
4346
|
+
for an \`href\` / \`alt\` / \`src\`, the \xA711 layout markers for nav and footer, and
|
|
4347
|
+
\`<div data-bcms-field="body" data-bcms-kind="document">\` around a Portable Text render.
|
|
4348
|
+
Bind CONDITIONALLY (\`fromCms ? path : undefined\`) so a fallback row is never bound. A value
|
|
4349
|
+
rendered in N places carries the binding on ALL N \u2014 the editor keeps the copies in sync.
|
|
4350
|
+
**Read the LIVE SCHEMA before you bind \u2014 \`get_page\` / \`get_content_model\`, never the
|
|
4351
|
+
delivery snapshot.** A field nobody has authored yet is simply ABSENT from the payload, so a
|
|
4352
|
+
snapshot cannot tell "this field does not exist" from "this field is empty": bind against it
|
|
4353
|
+
and you declare a path for a \`cover\` field that was never created, which the report then
|
|
4354
|
+
reports as broken forever. The schema is the list of what exists; the snapshot is only what
|
|
4355
|
+
currently has a value.
|
|
4356
|
+
**An index or listing route binds NOTHING.** The editor loads ONE entry per route, and an
|
|
4357
|
+
index renders many, so a binding there addresses whichever entry the editor happened to
|
|
4358
|
+
load. Bind each item's fields on that item's OWN route (\`/blog/<slug>\`); on the index,
|
|
4359
|
+
render from the CMS and declare nothing.
|
|
4360
|
+
5b. **Where the content comes from at build time.** Two lanes, and they differ:
|
|
4361
|
+
- GIT-CONNECTED (recommended for a converted site): the platform's provisioned workflow
|
|
4362
|
+
writes \`bcms-content.json\` into the repo root before \`build\`, using the repo's
|
|
4363
|
+
\`BCMS_API_KEY\` secret. Read that file, with in-code fallbacks so a local or CI build
|
|
4364
|
+
without it still renders.
|
|
4365
|
+
- ARCHIVE (\`deploy_project\` / \`deploy_from_upload\`): the sandbox build runs with no env
|
|
4366
|
+
and no network content step, by design. So the archive MUST SHIP its own
|
|
4367
|
+
\`bcms-content.json\` \u2014 generate it locally with a delivery key and commit it. Without
|
|
4368
|
+
one the site renders its fallbacks and the report says \`no-element\` for every path.
|
|
4369
|
+
6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
|
|
4370
|
+
\`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
|
|
4371
|
+
values are byte-equal to what the build renders. Now \`set_binding_mode
|
|
4372
|
+
{declaredBindings: true}\` and release again (an empty commit is enough). Do not flip before
|
|
4373
|
+
the report is clean: in declared mode an undeclared field simply stops being editable.
|
|
4374
|
+
7. **Receipts.** \`get_binding_report\` reads \`mode: "declared"\`, \`unmatched: []\`, and
|
|
4375
|
+
\`bound > 0\`. That certifies one thing only \u2014 that every non-empty field has SOME element
|
|
4376
|
+
carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
|
|
4377
|
+
text against its entry values yourself before you call the page done. Then publish, and
|
|
4378
|
+
fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
|
|
4379
|
+
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
4256
4380
|
`;
|
|
4257
4381
|
|
|
4258
4382
|
// src/prompts.ts
|