@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 +76 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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("
|
|
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"\`),
|
|
4346
|
-
for
|
|
4347
|
-
|
|
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.
|
|
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: []\`,
|
|
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).
|