@bettercms-ai/mcp 0.53.0 → 0.54.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 +36 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1999,7 +1999,7 @@ import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
|
|
|
1999
1999
|
// src/structure-playbook.ts
|
|
2000
2000
|
var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
|
|
2001
2001
|
var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
|
|
2002
|
-
var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, read the installed `bettercms` skill
|
|
2002
|
+
var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, explore the repo, then read the installed `bettercms` skill (`npx @bettercms-ai/install --project -y` adds it if missing): it turns what you found into the one or two bettercms-* skills the task needs \u2014 read only those.";
|
|
2003
2003
|
var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
2004
2004
|
version: 0,
|
|
2005
2005
|
doc: {
|
|
@@ -2424,7 +2424,7 @@ function toField(f) {
|
|
|
2424
2424
|
...f.config ? { config: f.config } : {}
|
|
2425
2425
|
};
|
|
2426
2426
|
}
|
|
2427
|
-
var BRAND_KIT_NOTE = "`brandKit` is the project's brand kit
|
|
2427
|
+
var BRAND_KIT_NOTE = "`brandKit` is the project's brand kit: colors, typography, radius and any typeScale/spacing/elevation/motion/graphics. It is edited in the BetterCMS dashboard AND by set_brand_assets and set_brand_graphics below; every write is a version a person can restore. ASSETS: point `mark`/`favicon` at a media asset OF THIS PROJECT by id (another project's id is refused); null clears one; a slot a person chose needs `replace: true`. GRAPHICS: gradients are STRUCTURED \u2014 `kind`, optional `angle`, 2-8 stops naming kit colour keys (`colors.*` or an `extras` key) \u2014 never a CSS string and never a hex; a colour the kit lacks is added to `extras` first. CONSUME, DO NOT COPY: on a hosted page the kit is `--brand-color-<key>`, `--brand-shadow-<key>` and `--brand-gradient-<key>`; style sections with their `style` tokens (`nav.backgroundGradient` and `footer.backgroundGradient` take a gradient key) rather than copying its values into props. Never invent brand facts: read them here.";
|
|
2428
2428
|
var MODEL_IF_MATCH_NOTE = "optimisticVersion from get_content_model. When sent, the update applies only if the model is still at that version; a 409 means someone changed it since \u2014 read it again and re-apply, never retry blindly. Omitted, the last write wins.";
|
|
2429
2429
|
var ENTRY_META_NOTE = "SEO is native: set this entry's metaTitle, metaDescription, noindex, canonical, og, twitter, schemaType or schema in `meta` (never as model fields). `meta` MERGES into what is stored: a key you leave out is kept, null or an empty string clears it.";
|
|
2430
2430
|
var PAGE_HEAD_NOTE = "noindex, canonical, og and twitter set the page's head extras; each MERGES into what is stored (a key you leave out is kept, null or an empty string clears it).";
|
|
@@ -3237,6 +3237,36 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3237
3237
|
}).shape,
|
|
3238
3238
|
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
|
|
3239
3239
|
),
|
|
3240
|
+
def(
|
|
3241
|
+
"set_brand_assets",
|
|
3242
|
+
"Point the brand's logo or favicon at a media asset",
|
|
3243
|
+
`Point the brand's logo (mark) or favicon at a media asset of this project. The asset must already be in this project's media library \u2014 upload it first if it is not. null clears a slot. A slot a PERSON chose is refused unless you pass replace: true, and the refusal says so. Every write is a brand version the dashboard can restore. ${BRAND_KIT_NOTE}`,
|
|
3244
|
+
z.object({
|
|
3245
|
+
mark: z.string().min(1).nullable().optional().describe("media asset id for the logo; null clears it"),
|
|
3246
|
+
favicon: z.string().min(1).nullable().optional().describe("media asset id for the favicon; null clears it"),
|
|
3247
|
+
replace: z.boolean().optional().describe("override a slot a person chose (default false)")
|
|
3248
|
+
}).shape,
|
|
3249
|
+
async (c, a) => ok("Brand assets.", await data(c, "POST", `/management/projects/current/brand-kit/assets`, { mark: a.mark, favicon: a.favicon, replace: a.replace }))
|
|
3250
|
+
),
|
|
3251
|
+
def(
|
|
3252
|
+
"set_brand_graphics",
|
|
3253
|
+
"Add, replace or remove the brand's gradients",
|
|
3254
|
+
`Add, replace or remove the brand's gradients \u2014 the blobs and washes a site uses as backgrounds. They are TOKENS, not media: a gradient is a value, so it is stored as a kind, an angle and stops that name the kit's own colour keys, never as CSS and never as a hex. A stop naming a colour the kit does not have is refused; add it to extras first. Upsert matches BY KEY. ${BRAND_KIT_NOTE}`,
|
|
3255
|
+
z.object({
|
|
3256
|
+
upsert: z.array(z.object({
|
|
3257
|
+
key: z.string().min(1).describe("token key: lowercase letters, digits and hyphens. Emitted as --brand-gradient-<key>"),
|
|
3258
|
+
label: z.string().optional(),
|
|
3259
|
+
kind: z.enum(["linear", "radial", "conic"]),
|
|
3260
|
+
angle: z.number().optional().describe("degrees 0-360; ignored for radial"),
|
|
3261
|
+
stops: z.array(z.object({
|
|
3262
|
+
color: z.string().min(1).describe("a kit colour KEY \u2014 a colors.* role name or an extras key. Never a hex."),
|
|
3263
|
+
at: z.number().describe("position 0-100")
|
|
3264
|
+
})).min(2).max(8)
|
|
3265
|
+
})).max(12).optional().describe("gradients to add or replace, matched BY KEY"),
|
|
3266
|
+
remove: z.array(z.string().min(1)).max(12).optional().describe("gradient keys to remove")
|
|
3267
|
+
}).shape,
|
|
3268
|
+
async (c, a) => ok("Brand graphics.", await data(c, "POST", `/management/projects/current/brand-kit/graphics`, { upsert: a.upsert, remove: a.remove }))
|
|
3269
|
+
),
|
|
3240
3270
|
def(
|
|
3241
3271
|
"set_icon",
|
|
3242
3272
|
"Set a Content sidebar icon",
|
|
@@ -3453,7 +3483,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3453
3483
|
def(
|
|
3454
3484
|
"submit_conversion_receipt",
|
|
3455
3485
|
"Record what the conversion codemod could and could not do",
|
|
3456
|
-
"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, fix }] }`. `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`. \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE. The response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows WITH their `fix` \u2014 `{ action, file, line, col?, snippet, why? }`, where `action` is one of `wrap-span` (wrap the literal in a `<span data-bcms-field=\u2026>`), `declare-attr` (add `data-bcms-field=\u2026` to the element at file:line), `bind-expression` (replace the expression with the framework's bcmsField helper), `declare-richtext` (bind the container with the richtext helper), `bind-data` (the literal comes from the data file at file:line \u2014 bind that field) or `manual` (with a one-sentence `why`). Apply every fix in the source, rerun the codemod, resubmit. `complete: true` is the only receipt that ends a conversion \u2014 do not report a site converted on anything less. 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. \u{1F534} SUBMIT THE `--forms` RECEIPT TOO, AS A SECOND CALL. A run of `npx @bettercms-ai/convert --forms` writes a receipt whose `paths` are all zero and whose account is in a `forms` block (`{ wired, alreadyWired, pending[{ id, name, reason }], notes, wiredForms[{ id, file }] }`). Write it to its own file (`--receipt forms-receipt.json`) and submit it here under the same `briefDigest`: it is stored BESIDE the binding receipt and never touches coverage, and the next release reads `wiredForms` to say which component renders each form. Then read `forms.pending` and publish every form `forms.notes` names in the Forms tab. Requires artifact:write, the same authority as set_binding_mode; on a workspace-wide connection pass `projectId`.",
|
|
3486
|
+
"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, fix }] }`. `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`. \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE. The response answers `complete` and `pendingTotal`, and echoes the first 40 pending rows WITH their `fix` \u2014 `{ action, file, line, col?, snippet, why?, kind? }`, where `action` is one of `wrap-span` (wrap the literal in a `<span data-bcms-field=\u2026>`), `declare-attr` (add `data-bcms-field=\u2026` to the element at file:line), `bind-expression` (replace the expression with the framework's bcmsField helper), `declare-richtext` (bind the container with the richtext helper \u2014 and when it carries `kind: document` the container is the ONE element wrapping every block of a Body, never the paragraph holding its first block), `bind-data` (the literal comes from the data file at file:line \u2014 bind that field) or `manual` (with a one-sentence `why`). Apply every fix in the source, rerun the codemod, resubmit. `complete: true` is the only receipt that ends a conversion \u2014 do not report a site converted on anything less. 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. \u{1F534} SUBMIT THE `--forms` RECEIPT TOO, AS A SECOND CALL. A run of `npx @bettercms-ai/convert --forms` writes a receipt whose `paths` are all zero and whose account is in a `forms` block (`{ wired, alreadyWired, pending[{ id, name, reason }], notes, wiredForms[{ id, file }] }`). Write it to its own file (`--receipt forms-receipt.json`) and submit it here under the same `briefDigest`: it is stored BESIDE the binding receipt and never touches coverage, and the next release reads `wiredForms` to say which component renders each form. Then read `forms.pending` and publish every form `forms.notes` names in the Forms tab. Requires artifact:write, the same authority as set_binding_mode; on a workspace-wide connection pass `projectId`.",
|
|
3457
3487
|
z.object({
|
|
3458
3488
|
briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
|
|
3459
3489
|
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
|
|
@@ -5675,8 +5705,8 @@ ${SECTION_DOCTRINE}`;
|
|
|
5675
5705
|
var CONVERT_SITE_TEXT = (projectId) => `Convert my site so every visible element is editable in BetterCMS${projectId ? `, project ${projectId}` : ""}.
|
|
5676
5706
|
Do not stop at the plan or at a partial receipt \u2014 finish the whole site.
|
|
5677
5707
|
|
|
5678
|
-
1. \`list_projects\`; pass \`projectId\`${projectId ? ` (${projectId})` : ""} on every project tool
|
|
5679
|
-
|
|
5708
|
+
1. \`list_projects\`; pass \`projectId\`${projectId ? ` (${projectId})` : ""} on every project tool. Not listed = the grant is on
|
|
5709
|
+
another workspace: have the user re-authenticate there. Never convert a different project.
|
|
5680
5710
|
2. \`get_conversion_plan\`. On a 404 \`no-approved-plan\`, call
|
|
5681
5711
|
\`get_conversion_brief { complete: true }\` and follow \`cursor\` to the LAST page, keeping every
|
|
5682
5712
|
page \u2014 a codemod fed the capped brief converts a truncated site. Write them to \`brief.json\`.
|
|
@@ -6093,7 +6123,7 @@ in the dashboard \u2014 those render as an empty string until they are published
|
|
|
6093
6123
|
"convert_site",
|
|
6094
6124
|
{
|
|
6095
6125
|
title: "Convert a site so nothing is left pending (guided)",
|
|
6096
|
-
description: "Run the conversion end to end: pull the source, run @bettercms-ai/convert, apply every fix it lists as pending, rerun until nothing is pending, and finish only when submit_conversion_receipt returns complete and get_binding_report reads unmatched 0.",
|
|
6126
|
+
description: "Run the conversion end to end: pull the source, run @bettercms-ai/convert, apply every fix it lists as pending, rerun until nothing is pending, and finish only when submit_conversion_receipt returns complete and get_binding_report reads unmatched 0, coverage.pending empty and every route's unaddressable under 2%.",
|
|
6097
6127
|
argsSchema: {
|
|
6098
6128
|
projectId: z2.string().optional().describe("the project to convert; omit if this connection is scoped to one")
|
|
6099
6129
|
}
|