@bettercms-ai/mcp 0.25.2 → 0.27.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
@@ -3183,6 +3183,16 @@ function buildToolDefs(deps) {
3183
3183
  z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
3184
3184
  async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
3185
3185
  ),
3186
+ def(
3187
+ "submit_conversion_receipt",
3188
+ "Record what the conversion codemod could and could not do",
3189
+ "Hand BetterCMS the codemod's own account of a conversion run, so the coverage meter can say WHY a path is not declared instead of only that it is not. Submit the receipt `npx @bettercms-ai/convert` wrote (`--receipt out.json`) for the SAME `briefDigest` get_conversion_brief { complete: true } returned: `{ briefDigest, receipt }`, where the receipt carries `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message }] }`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length, and each pending `reason` is one of the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026) \u2014 a path with no receipt row simply reads `not-declared`. It is a RECORD, not a release: it changes nothing about the site, and the meter picks it up on the next get_binding_report after the next deploy. A 404 `unknown-brief` means that digest was never issued here, so convert against a brief this project actually returned. Requires a project-scoped connection carrying artifact:write, the same authority as set_binding_mode.",
3190
+ z.object({
3191
+ briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
3192
+ receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
3193
+ }).shape,
3194
+ async (c, a) => ok("Recorded the conversion receipt.", await data(c, "POST", `/management/projects/current/conversion-receipt`, { briefDigest: a.briefDigest, receipt: a.receipt }))
3195
+ ),
3186
3196
  def(
3187
3197
  "clone_project",
3188
3198
  "Clone a project",
@@ -3302,9 +3312,19 @@ function buildToolDefs(deps) {
3302
3312
  def(
3303
3313
  "get_conversion_brief",
3304
3314
  "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.",
3315
+ "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. Pass `complete: true` when you are about to run the CODEMOD (`npx @bettercms-ai/convert`): that returns the brief UNCAPPED and paged \u2014 nothing truncated, no page omitted \u2014 and pins the full path list under a `briefDigest`, which is the list the coverage meter in get_binding_report measures the build against. Follow `cursor` until it stops coming back; a 409 `BRIEF_CHANGED` means the brief was re-derived while you paged, so start again.",
3316
+ z.object({
3317
+ complete: z.boolean().optional().describe("true = the COMPLETE brief for a codemod: nothing truncated, no page omitted, paged 50 pages at a time. Page 1 pins the path list the coverage meter measures against."),
3318
+ cursor: z.string().optional().describe("The `cursor` from the previous page. Implies complete. A 409 BRIEF_CHANGED means the brief was re-derived mid-pagination \u2014 start again with no cursor.")
3319
+ }).shape,
3320
+ async (c, a) => ok("Conversion brief.", await data(c, "GET", `/management/projects/current/conversion-brief${q({ complete: a.complete, cursor: a.cursor })}`))
3321
+ ),
3322
+ def(
3323
+ "get_conversion_plan",
3324
+ "Get the approved conversion to apply",
3325
+ "The APPROVED conversion a human reviewed in the BetterCMS dashboard: the exact new contents of each template file, already checked against this project's real field paths. Apply it instead of writing the bindings by hand. Check out `baseHeadOid` (the exact commit it was written against \u2014 a plan applied to a different base is a different change), branch from there, write each file's `content` verbatim (whole file, no merge, no reformatting), then push or deploy however this project ships. `stale.head` / `stale.brief` say the repository or the CMS moved since it was approved: stop and ask for a fresh proposal rather than applying it anyway. `receipt.paths.pending` lists every field the codemod could NOT bind, each with a reason (`DIALECT_UNSUPPORTED`, `PROP_TARGET_NOT_FOUND`, `REPEATER_FIXED_LENGTH`, \u2026) \u2014 apply the plan first, then bind those by hand. Finish the loop the same way as a hand conversion \u2014 get_binding_report until `unmatched` is empty, then set_binding_mode { declaredBindings: true } and release once more. 404 with `code: \"no-approved-plan\"` means nobody has approved one: use get_conversion_brief and do the conversion yourself. Requires a project-scoped connection carrying artifact:write.",
3306
3326
  z.object({}).shape,
3307
- async (c) => ok("Conversion brief.", await data(c, "GET", `/management/projects/current/conversion-brief`))
3327
+ async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
3308
3328
  ),
3309
3329
  def(
3310
3330
  "get_analytics_overview",
@@ -4314,6 +4334,41 @@ first: it lists those pages, their routes, every bindable path with its current
4314
4334
  value, and the attributes to declare. SKIP steps 3 and 4 below and bind the keys it names \u2014
4315
4335
  registering the schema again builds a second one over the first.
4316
4336
 
4337
+ **Run the CODEMOD rather than editing by hand.** For a derived site the whole of step 5 is
4338
+ mechanical, and there is a tool that does it. THE ORDER, and every step of it matters:
4339
+
4340
+ 1. \`get_conversion_brief { complete: true }\` \u2014 the COMPLETE brief, not the capped one. It comes
4341
+ back uncapped and paged (50 pages at a time): follow \`cursor\` until it stops coming back and
4342
+ keep every page. The capped read truncates values and omits pages, and a codemod fed a
4343
+ truncated \`original\` searches the source for a string that is not there. Page 1 PINS the
4344
+ path list under a \`briefDigest\`; that pinned list is what the coverage meter measures the
4345
+ build against, so nothing later can move the denominator. A 409 \`BRIEF_CHANGED\` mid-pagination
4346
+ means the brief was re-derived under you \u2014 start again with no cursor.
4347
+ 2. Write the pages you collected to \`brief.json\` and run
4348
+ \`npx @bettercms-ai/convert --brief brief.json --root . --receipt receipt.json\`, then read
4349
+ \`git diff\`. It rewrites the templates to read from BetterCMS, keeps the in-code copy as the
4350
+ fallback, and declares each binding.
4351
+ 3. REVIEW THE PENDING LIST. Every path the codemod could not do is in \`paths.pending\` with a
4352
+ reason (\`IN_EXPRESSION\`, \`AMBIGUOUS_LITERAL\`, \`REPEATER_FIXED_LENGTH\`, \`PARSE_ERROR\`, \u2026).
4353
+ Do those by hand, or decide they are genuinely not convertible \u2014 do not skip past them.
4354
+ 4. \`submit_conversion_receipt { briefDigest, receipt }\` with the receipt file, verbatim. This is
4355
+ what lets the coverage meter say WHY a path is undeclared instead of only that it is; without
4356
+ it every one of them reads \`not-declared\`, which looks like a broken site rather than work
4357
+ with a reason. It records and releases nothing.
4358
+ 5. Push, or \`deploy_project\`, and wait for the release to be live (step 6 below).
4359
+ 6. \`get_binding_report\`. Alongside \`unmatched\` it now carries \`coverage\` \u2014 \`declared\` (what the
4360
+ brief listed), \`bound\` (what the build declares) and \`pending\` with the reasons. Fix what it
4361
+ names and release again until \`coverage.pending\` is empty.
4362
+ 7. \`set_binding_mode { declaredBindings: true }\` and release ONE MORE TIME \u2014 the mode applies to
4363
+ the next release, not to the one already out.
4364
+
4365
+ **Or let BetterCMS propose the edit.** \`get_conversion_plan\` returns an APPROVED conversion \u2014 the
4366
+ exact new contents of each template file, reviewed by a human in the dashboard and already checked
4367
+ against this project's real field paths. When there is one, apply it instead of doing step 5 by
4368
+ hand: check out its \`baseHeadOid\`, branch from there, write each file's \`content\` verbatim, and
4369
+ carry on from step 6. A 404 with \`code: "no-approved-plan"\` means nobody approved one, so the
4370
+ conversion is yours to write.
4371
+
4317
4372
  **The steps.**
4318
4373
 
4319
4374
  1. \`pull_project_source\` (or clone the \`github\` remote it returns). Read the SOURCE. Never
@@ -4342,9 +4397,20 @@ registering the schema again builds a second one over the first.
4342
4397
  Prefer schema-derived bindings \u2014 the TypeGen analogue:
4343
4398
  \`npx @bettercms-ai/codegen --bindings-out src/bettercms.bindings.generated.ts\`, then spread
4344
4399
  \`{...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.
4400
+ \`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`), the \xA711 layout markers
4401
+ for nav and footer, and \`<div data-bcms-field="body" data-bcms-kind="document">\` around a
4402
+ Portable Text render.
4403
+ **A value that lives in an ATTRIBUTE \u2014 an \`href\`, an \`alt\`, an \`src\` \u2014 rides
4404
+ \`data-bcms-props\`, and its grammar is PIPES, NOT JSON:**
4405
+ \`data-bcms-props="<path>|<kind>|<domAttribute>"\`, semicolon-separated for several on one
4406
+ element, where \`kind\` is \`text | richtext | image | url | number | array\`. A link is
4407
+ \`data-bcms-props="cta.url|url|href"\`; an image's alt text is
4408
+ \`data-bcms-props="hero.image.alt|text|alt"\`; an \`<a>\` wrapping an \`<img>\` carries both as
4409
+ \`data-bcms-props="cta.url|url|href;hero.image.alt|text|alt"\`. A JSON object there parses to
4410
+ NOTHING \u2014 the reader splits on \`;\` and \`|\` and drops what does not match \u2014 so the value stays
4411
+ uneditable and the binding report never mentions it. The SDK helpers
4412
+ (\`@bettercms-ai/astro\`, \`@bettercms-ai/next\`) and \`@bettercms-ai/codegen\` emit this form
4413
+ for you; write it by hand only when you are not using them.
4348
4414
  Bind CONDITIONALLY (\`fromCms ? path : undefined\`) so a fallback row is never bound. A value
4349
4415
  rendered in N places carries the binding on ALL N \u2014 the editor keeps the copies in sync.
4350
4416
  **Read the LIVE SCHEMA before you bind \u2014 \`get_page\` / \`get_content_model\`, never the
@@ -4368,11 +4434,13 @@ registering the schema again builds a second one over the first.
4368
4434
  one the site renders its fallbacks and the report says \`no-element\` for every path.
4369
4435
  6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
4370
4436
  \`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
4437
+ values are byte-equal to what the build renders. On a converted site read \`coverage\` too: it
4438
+ counts the PINNED brief's paths, which \`unmatched\` cannot, because \`unmatched\` only ever
4439
+ speaks about fields that already hold a value. Now \`set_binding_mode
4372
4440
  {declaredBindings: true}\` and release again (an empty commit is enough). Do not flip before
4373
4441
  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
4442
+ 7. **Receipts.** \`get_binding_report\` reads \`mode: "declared"\`, \`unmatched: []\`,
4443
+ \`bound > 0\`, and \u2014 for a site converted against a pinned brief \u2014 \`coverage.pending: []\`. That certifies one thing only \u2014 that every non-empty field has SOME element
4376
4444
  carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
4377
4445
  text against its entry values yourself before you call the page done. Then publish, and
4378
4446
  fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).