@bettercms-ai/mcp 0.26.0 → 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 +64 -10
- 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,14 +3312,17 @@ 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.",
|
|
3306
|
-
z.object({
|
|
3307
|
-
|
|
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 })}`))
|
|
3308
3321
|
),
|
|
3309
3322
|
def(
|
|
3310
3323
|
"get_conversion_plan",
|
|
3311
3324
|
"Get the approved conversion to apply",
|
|
3312
|
-
"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. 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.",
|
|
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.",
|
|
3313
3326
|
z.object({}).shape,
|
|
3314
3327
|
async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
|
|
3315
3328
|
),
|
|
@@ -4321,6 +4334,34 @@ first: it lists those pages, their routes, every bindable path with its current
|
|
|
4321
4334
|
value, and the attributes to declare. SKIP steps 3 and 4 below and bind the keys it names \u2014
|
|
4322
4335
|
registering the schema again builds a second one over the first.
|
|
4323
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
|
+
|
|
4324
4365
|
**Or let BetterCMS propose the edit.** \`get_conversion_plan\` returns an APPROVED conversion \u2014 the
|
|
4325
4366
|
exact new contents of each template file, reviewed by a human in the dashboard and already checked
|
|
4326
4367
|
against this project's real field paths. When there is one, apply it instead of doing step 5 by
|
|
@@ -4356,9 +4397,20 @@ conversion is yours to write.
|
|
|
4356
4397
|
Prefer schema-derived bindings \u2014 the TypeGen analogue:
|
|
4357
4398
|
\`npx @bettercms-ai/codegen --bindings-out src/bettercms.bindings.generated.ts\`, then spread
|
|
4358
4399
|
\`{...bcms.home.hero.title}\` / \`{...bcms.blog.features.$(i)}\`. The hand form is
|
|
4359
|
-
\`data-bcms-field="<path>"\` (plus \`data-bcms-kind="richtext"|"image"\`),
|
|
4360
|
-
for
|
|
4361
|
-
|
|
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.
|
|
4362
4414
|
Bind CONDITIONALLY (\`fromCms ? path : undefined\`) so a fallback row is never bound. A value
|
|
4363
4415
|
rendered in N places carries the binding on ALL N \u2014 the editor keeps the copies in sync.
|
|
4364
4416
|
**Read the LIVE SCHEMA before you bind \u2014 \`get_page\` / \`get_content_model\`, never the
|
|
@@ -4382,11 +4434,13 @@ conversion is yours to write.
|
|
|
4382
4434
|
one the site renders its fallbacks and the report says \`no-element\` for every path.
|
|
4383
4435
|
6. Push, or \`deploy_project\`; poll \`get_deploy_status\` until it is live. Then
|
|
4384
4436
|
\`get_binding_report\` \u2014 still \`text-match\`, and \`unmatched\` should be EMPTY because the
|
|
4385
|
-
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
|
|
4386
4440
|
{declaredBindings: true}\` and release again (an empty commit is enough). Do not flip before
|
|
4387
4441
|
the report is clean: in declared mode an undeclared field simply stops being editable.
|
|
4388
|
-
7. **Receipts.** \`get_binding_report\` reads \`mode: "declared"\`, \`unmatched: []\`,
|
|
4389
|
-
\`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
|
|
4390
4444
|
carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
|
|
4391
4445
|
text against its entry values yourself before you call the page done. Then publish, and
|
|
4392
4446
|
fetch the live URL cache-busted (\xA712: publish and deploy are separate claims).
|