@bettercms-ai/mcp 0.69.0 → 0.70.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 +38 -27
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/references/astro.md +5 -1
- package/dist/skills/bettercms/references/bindings.md +47 -0
- package/dist/skills/bettercms/references/componentize.md +6 -4
- package/dist/skills/bettercms/references/components.md +38 -3
- package/dist/skills/bettercms/references/content-modeling.md +10 -7
- package/dist/skills/bettercms/references/detection.md +2 -2
- package/dist/skills/bettercms/references/editor-chrome.md +4 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +16 -4
- package/dist/skills/bettercms/references/frameworks.md +2 -2
- package/dist/skills/bettercms/references/next.md +2 -2
- package/dist/skills/bettercms/references/publishing.md +15 -0
- package/dist/skills/bettercms/references/site-blueprints.md +18 -4
- package/dist/skills/bettercms/references/worked-examples-advanced.md +17 -3
- package/dist/skills/bettercms/references/worked-examples.md +22 -9
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2673,8 +2673,9 @@ function listsTool(name, sets) {
|
|
|
2673
2673
|
}
|
|
2674
2674
|
|
|
2675
2675
|
// src/tool-text.ts
|
|
2676
|
-
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. To change a field that exists, use update_field.";
|
|
2676
|
+
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options` (`allowCustom` for tags); `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field.";
|
|
2677
2677
|
var MOVE_STAGE_DESCRIPTION = "Move a content entry (`entryId`) or a page (`pageId`) to another editorial stage \u2014 this is how you take it OUT of review so it can be published. `workflowStage` is a stage key from get_workflow_board; pass `fromStage` to 409 if someone moved it first. Moving into the publish-gate stage is an approval and needs a publish-enabled connection. Publish afterwards with update_content_entry or update_page status:'published' \u2014 approving and publishing are deliberately two separate calls.";
|
|
2678
|
+
var WINDOW_NOTE = "{from?, until?}, ISO 8601 with offset: shown only between the two instants; null clears (skill publishing).";
|
|
2678
2679
|
var WEBHOOK_EVENTS = [
|
|
2679
2680
|
"content.publish",
|
|
2680
2681
|
"content.unpublish",
|
|
@@ -2715,6 +2716,7 @@ var PARAM_TEXT = {
|
|
|
2715
2716
|
lockReason: "which system owns the value (\u2264 200 chars)",
|
|
2716
2717
|
lockClears: "as on add_field; null clears it",
|
|
2717
2718
|
helpText: "shown under the field: its purpose, where it shows, any constraint; never its label or value. Omitted: written for you.",
|
|
2719
|
+
fromBuiltin: "a builtin:* id from list_components: copies it into the project; fields you send win",
|
|
2718
2720
|
describes: "for editors: what it is and holds; omitted: written for you",
|
|
2719
2721
|
runtimeBundle: "instead of pageId: the server-app bundle cleanup",
|
|
2720
2722
|
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
@@ -2755,6 +2757,8 @@ var SEO_KEY_TEXT = {
|
|
|
2755
2757
|
var VARIANT_GROUP_NOTE = "When the component belongs to a family of looks, `variantGroup` names the family and lists its looks.";
|
|
2756
2758
|
var UNPROPPED_COPY_NOTE = "A `warnings` entry UNPROPPED_COPY names copy no prop targets, which nobody can edit: add a prop with a `target` for each leaf it lists.";
|
|
2757
2759
|
var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write applies only at that version, and a 409 means the page changed since, so read it again. Send it whenever you pass blockJson, which REPLACES the whole composition.";
|
|
2760
|
+
var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section.";
|
|
2761
|
+
var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
|
|
2758
2762
|
|
|
2759
2763
|
// src/entry-query.ts
|
|
2760
2764
|
var text = (v) => Array.isArray(v) ? v.map(String).join(",") : String(v);
|
|
@@ -2916,7 +2920,9 @@ var fieldType = z.enum([
|
|
|
2916
2920
|
"color",
|
|
2917
2921
|
"json",
|
|
2918
2922
|
"file",
|
|
2919
|
-
"sections"
|
|
2923
|
+
"sections",
|
|
2924
|
+
"multiselect",
|
|
2925
|
+
"icon"
|
|
2920
2926
|
]);
|
|
2921
2927
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2922
2928
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
@@ -2935,8 +2941,8 @@ var uiObject = z.strictObject({
|
|
|
2935
2941
|
reorderable: z.boolean().optional().describe(
|
|
2936
2942
|
"omit (\u2261 true) unless the order is PART OF THE MEANING. `false` LOCKS the list \u2014 a 'three steps' band that must stay three steps in that order, a nav whose order is semantic, a timeline. Valid only where there is a list to lock."
|
|
2937
2943
|
),
|
|
2938
|
-
control: z.enum(["segmented", "slider"]).optional().describe(
|
|
2939
|
-
"editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns). Anything else is refused with the rule named."
|
|
2944
|
+
control: z.enum(["segmented", "slider", "rating"]).optional().describe(
|
|
2945
|
+
"editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns); 'rating' on a number with config.min, max and step. Anything else is refused with the rule named."
|
|
2940
2946
|
),
|
|
2941
2947
|
// A refine, not an enum: 236 names inlined into every schema that carries `ui` would be paid on
|
|
2942
2948
|
// every tools/list. The layoutIcon precedent; the server's z.enum is the guard.
|
|
@@ -3344,6 +3350,7 @@ function buildToolDefs(deps) {
|
|
|
3344
3350
|
}
|
|
3345
3351
|
};
|
|
3346
3352
|
}
|
|
3353
|
+
const windowArg = z.object({ from: z.string().optional(), until: z.string().optional() });
|
|
3347
3354
|
const blockObject = z.object({
|
|
3348
3355
|
type: z.enum([
|
|
3349
3356
|
"heading",
|
|
@@ -3368,8 +3375,9 @@ function buildToolDefs(deps) {
|
|
|
3368
3375
|
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant? primary|secondary|tertiary|brand|dark|light|outline|link, size? sm|md|lg, fullWidth?, arrow? boolean, icon? ui.icon name, newTab? boolean, rel? nofollow|sponsored|ugc}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may hold ANY block; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, detailComponentId?, titleField?, excerptField?, limit?, order?, emptyText?, source? {modelId}, filters? [{field, op, value}], sort?, pinnedIds?, excludeCurrent?} \u2014 lists this page's published entries as cards, and renders ONE entry on /<page>/<entrySlug>; with `source`, that model's entries as cards on any page"
|
|
3369
3376
|
),
|
|
3370
3377
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
3371
|
-
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour). A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
3372
|
-
)
|
|
3378
|
+
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour), hideOn [mobile|tablet|desktop], never all three. A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
3379
|
+
),
|
|
3380
|
+
window: windowArg.nullable().optional().describe(WINDOW_NOTE)
|
|
3373
3381
|
});
|
|
3374
3382
|
const createPageInput = z.object({
|
|
3375
3383
|
title: z.string().min(1).describe("display title of the page, e.g. 'Home'"),
|
|
@@ -3541,7 +3549,9 @@ function buildToolDefs(deps) {
|
|
|
3541
3549
|
"date",
|
|
3542
3550
|
"url",
|
|
3543
3551
|
"consent",
|
|
3544
|
-
"hidden"
|
|
3552
|
+
"hidden",
|
|
3553
|
+
"step"
|
|
3554
|
+
// `step` is a divider: the fields after it are one page
|
|
3545
3555
|
]),
|
|
3546
3556
|
placeholder: z.string().optional(),
|
|
3547
3557
|
helpText: z.string().optional().describe("hint shown under the control, muted"),
|
|
@@ -3601,8 +3611,8 @@ function buildToolDefs(deps) {
|
|
|
3601
3611
|
// enum omitted could not be echoed back, and an agent reading a component and writing it
|
|
3602
3612
|
// straight back DESTROYED every prop of that type on a 200.
|
|
3603
3613
|
type: z.enum(COMPONENT_PROP_TYPES),
|
|
3604
|
-
// 'slot' holds ONE nested component instance
|
|
3605
|
-
// fill it. Absent here until now, so a slot allowlist was unreachable from stdio even
|
|
3614
|
+
// 'slot' holds ONE nested component instance, or a list with config.multiple (the keys
|
|
3615
|
+
// pass through to the server); config.componentIds restricts what may fill it. Absent here until now, so a slot allowlist was unreachable from stdio even
|
|
3606
3616
|
// once the enum allowed the type.
|
|
3607
3617
|
config: z.object({
|
|
3608
3618
|
componentIds: z.array(z.string()).optional(),
|
|
@@ -3661,9 +3671,9 @@ function buildToolDefs(deps) {
|
|
|
3661
3671
|
z.object({ type: z.literal("swap-component-variant"), ...sectionTarget, itemId: z.string().min(1), componentId: z.string().min(1) }),
|
|
3662
3672
|
z.object({ type: z.literal("set-page-state"), ...sectionTarget, state: z.enum(["inherit", "override-content", "customize-structure", "disable", "reset"]) }),
|
|
3663
3673
|
z.object({ type: z.literal("set-section-state"), ...sectionTarget, state: z.enum(["inherit", "override-content", "customize-structure", "disable", "reset"]) }),
|
|
3664
|
-
z.object({ type: z.literal("add-section"), name: z.string().min(1), icon: layoutIcon, zone: z.enum(["before_page", "after_page"]) }),
|
|
3674
|
+
z.object({ type: z.literal("add-section"), name: z.string().min(1), icon: layoutIcon, zone: z.enum(["before_page", "after_page"]), window: windowArg.optional() }),
|
|
3665
3675
|
z.object({ type: z.literal("restore-section"), section: z.object({ id: z.string().min(1) }).passthrough(), data: z.object({ fields: z.record(z.string(), z.unknown()) }), placement: z.object({ zone: z.enum(["above-page-content", "below-page-content"]), localOrderKey: z.string().min(1), afterSectionId: z.string().optional(), beforeSectionId: z.string().optional() }).optional() }),
|
|
3666
|
-
z.object({ type: z.literal("update-section"), ...sectionTarget, name: z.string().min(1).optional(), icon: layoutIcon.optional(), slug: z.string().min(1).optional(), zone: z.enum(["above-page-content", "below-page-content"]).optional(), orderKey: z.string().min(1).optional(), description: z.string().optional() }),
|
|
3676
|
+
z.object({ type: z.literal("update-section"), ...sectionTarget, name: z.string().min(1).optional(), icon: layoutIcon.optional(), slug: z.string().min(1).optional(), zone: z.enum(["above-page-content", "below-page-content"]).optional(), orderKey: z.string().min(1).optional(), description: z.string().optional(), window: windowArg.nullable().optional().describe("as a block's `window`") }),
|
|
3667
3677
|
z.object({ type: z.literal("remove-section"), ...sectionTarget }),
|
|
3668
3678
|
z.object({ type: z.literal("move-section"), ...sectionTarget, afterId: z.string().nullable().optional(), beforeId: z.string().nullable().optional() }),
|
|
3669
3679
|
z.object({ type: z.literal("add-field"), ...sectionTarget, parentFieldId: z.string().optional(), fieldType: z.enum(["text", "longtext", "richtext", "number", "toggle", "date", "image", "file", "link", "email", "phone", "select", "color", "json", "reference", "multi-reference", "group", "repeater"]).optional(), field: z.object({ id: z.string(), slug: z.string(), label: z.string(), type: z.string() }).passthrough().optional() }),
|
|
@@ -3712,12 +3722,13 @@ function buildToolDefs(deps) {
|
|
|
3712
3722
|
group: z.string().min(1).max(100).optional().describe("the folder editors see in the dashboard's Components tab, by NAME (e.g. 'Sections', 'Forms'); CREATED when missing. Separate from `category`, the picker tab."),
|
|
3713
3723
|
sectionType: sectionType.optional(),
|
|
3714
3724
|
variantOf: z.string().min(1).max(255).optional().describe("id/slug of the component this is a VARIANT of: joins or starts its family (422 if its shared inputs are missing)"),
|
|
3725
|
+
from: z.string().min(1).max(255).optional().describe(PARAM_TEXT.fromBuiltin),
|
|
3715
3726
|
projectId: z.string().nullable().optional().describe("owning project id; null creates a workspace-wide global component"),
|
|
3716
3727
|
allowedOn: allowedOn.optional(),
|
|
3717
3728
|
description: z.string().optional().describe(PARAM_TEXT.describes),
|
|
3718
3729
|
blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
|
|
3719
3730
|
props: z.array(componentPropObject).default([]).describe(
|
|
3720
|
-
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required)."
|
|
3731
|
+
"overridable fields. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a prop targets are click-to-edit. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all. 'button' is ONE unit {label, href, variant?, size?, arrow?, icon?, newTab?} on a button block's 'props', never a text prop plus a url prop. 'link' an href, 'date' YYYY-MM-DD, 'color' a hex, 'file' {url, name?}, 'icon' an icon name or an image, 'multiselect' a string[] from config.options (required). " + SLOT_MULTIPLE_NOTE
|
|
3721
3732
|
)
|
|
3722
3733
|
});
|
|
3723
3734
|
const updateComponentInput = z.object({
|
|
@@ -4287,7 +4298,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4287
4298
|
def(
|
|
4288
4299
|
"submit_conversion_receipt",
|
|
4289
4300
|
"Record what the conversion codemod could and could not do",
|
|
4290
|
-
"Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert --receipt out.json` wrote as `{ briefDigest, receipt }`, for the SAME `briefDigest` get_conversion_brief { complete: true } returned (its shape: see `receipt`). \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE: the response answers `complete`
|
|
4301
|
+
"Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert --receipt out.json` wrote as `{ briefDigest, receipt }`, for the SAME `briefDigest` get_conversion_brief { complete: true } returned (its shape: see `receipt`). \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` (`action` one of `wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; the row shape: skill bindings; a 404 `unknown-brief` means that digest was never issued here). Apply every fix, rerun, resubmit: only `complete: true` ends a conversion \u2014 never report a site converted on less. It is a RECORD, not a release: nothing on the site changes; the meter reads it on the get_binding_report after the next deploy. \u{1F534} The same run owes two more things (playbook \xA713 steps 2b and 3b): the `--forms` receipt (a SECOND call, same `briefDigest`) and the `styleEdits` of `convert --verify-styles` inside this receipt; any changed class or style is refused with 409 `style-edits`. Requires artifact:write.",
|
|
4291
4302
|
z.object({
|
|
4292
4303
|
briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
|
|
4293
4304
|
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim: `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message, fix }] }` plus `styleEdits`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length; each pending `reason` is the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026); a path with no row reads `not-declared`.")
|
|
@@ -4427,7 +4438,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4427
4438
|
def(
|
|
4428
4439
|
"deploy_project",
|
|
4429
4440
|
"Deploy new source/build",
|
|
4430
|
-
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`, or the `uploadKey` of an archive you PUT through create_deploy_upload (no size ceiling: use it for anything large, or after a 413/502). SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git and build output/caches (dist, build, .next, .astro, .cache) from the archive: the source is reinstalled and built server-side, and the ~100 MB upload ceiling is enforced in transit (a 413/502 with no server-side detail) \u2014 server-side stripping runs AFTER the upload and cannot rescue an over-limit body. Returns `canvas.lane` (
|
|
4441
|
+
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`, or the `uploadKey` of an archive you PUT through create_deploy_upload (no size ceiling: use it for anything large, or after a 413/502). SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git and build output/caches (dist, build, .next, .astro, .cache) from the archive: the source is reinstalled and built server-side, and the ~100 MB upload ceiling is enforced in transit (a 413/502 with no server-side detail) \u2014 server-side stripping runs AFTER the upload and cannot rescue an over-limit body. Returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user. On a workspace-wide connection pass `projectId` (from list_projects) to say which site this ships to.",
|
|
4431
4442
|
z.object({
|
|
4432
4443
|
data: z.string().min(1).optional().describe("base64 .tgz/.zip of the project; or send uploadKey"),
|
|
4433
4444
|
uploadKey: z.string().min(1).optional().describe("instead of data: the uploadKey from create_deploy_upload, after PUTting the archive to its uploadUrl"),
|
|
@@ -4791,14 +4802,14 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4791
4802
|
def(
|
|
4792
4803
|
"get_binding_report",
|
|
4793
4804
|
"Check what on the live site is editable",
|
|
4794
|
-
"The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason
|
|
4795
|
-
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves.
|
|
4805
|
+
"The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason (reasons: skill bindings). DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: a HEADLESS site never gets one (`never-generated`); `layoutRoutes` and `coverage.error`: skill bindings. `unaddressable` is the visible live text no field owns, by route (null = unmeasured: diff visible text against entry values); over `count / visible > 0.02`, fix it in the SOURCE and re-read (playbook \xA711). `canvas.lane`: `none` \u2192 get_next_steps.",
|
|
4806
|
+
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves. Other per-slot fields: skill bindings.") }).shape,
|
|
4796
4807
|
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
4797
4808
|
),
|
|
4798
4809
|
def(
|
|
4799
4810
|
"get_conversion_brief",
|
|
4800
4811
|
"Get the brief for making this site's bindings durable",
|
|
4801
|
-
"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, every bindable field path with its `label`, `kind`, the CMS value (`current`) and the copy the repo renders today (`original`), 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_field / create_content_model would build a second schema over the first \u2014 edit values with set_page_content instead. `lane`: 'git-connected' = pull_project_source returns a repo; 'archive' = a tarball.
|
|
4812
|
+
"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, every bindable field path with its `label`, `kind`, the CMS value (`current`) and the copy the repo renders today (`original`), 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_field / create_content_model would build a second schema over the first \u2014 edit values with set_page_content instead. `lane`: 'git-connected' = pull_project_source returns a repo; 'archive' = a tarball. Recipe: playbook \xA713; get_binding_report is the finish line. Pass `complete: true` before running the CODEMOD (`npx @bettercms-ai/convert`): the brief UNCAPPED and paged, its full path list pinned under a `briefDigest` \u2014 what the coverage meter measures the build against. The complete brief also carries `forms`: wiring them and publishing the forms it names, skill bindings.",
|
|
4802
4813
|
z.object({
|
|
4803
4814
|
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."),
|
|
4804
4815
|
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.")
|
|
@@ -4815,14 +4826,14 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4815
4826
|
def(
|
|
4816
4827
|
"get_site_composition",
|
|
4817
4828
|
"Check every page is assembled from registered components",
|
|
4818
|
-
"The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live. Call it before a whole-site componentize run (playbook \xA712) and after it, as the proof. Per page: `kind
|
|
4829
|
+
"The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live. Call it before a whole-site componentize run (playbook \xA712) and after it, as the proof. Per page: `kind`, a block census, `nonComponentBlocks` and every `component` placement with its `status` (a draft renders as an EMPTY STRING on the live site), `inPicker` and `emptyRequiredProps`. Site-wide: counts, `awaitingApproval` (a HUMAN approves in the dashboard; no tool here can), `notInPicker`, `unused` and `layout.placements`. Each field: skill components. `verdict.everyPageComposed` with `verdict.reasons` is the answer to give the user. Pages are read DRAFT-inclusive: check each page's own `status` before claiming the site is live.",
|
|
4819
4830
|
z.object({}).shape,
|
|
4820
4831
|
async (c) => ok("Site composition.", await data(c, "GET", `/management/projects/current/composition`))
|
|
4821
4832
|
),
|
|
4822
4833
|
def(
|
|
4823
4834
|
"get_componentize_plan",
|
|
4824
4835
|
"Get the plan for turning this site's sections into components",
|
|
4825
|
-
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing and is computed live. For a site whose pages were DERIVED at import, each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import folded (`group-*`). Per page: each section's `groupKey`, `sectionType` family, leaf `fields` with their CMS values, `shapeHash`, and either the component already rendering it (`reuse.componentId`) or the proposed one (`reuse.proposedSlug`). Under `components`, a NEW one carries the exact `props` and `blockJson` create_component would take; an EXISTING one only `componentId`, `slug` and `name` (nothing is written for it).
|
|
4836
|
+
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing and is computed live. For a site whose pages were DERIVED at import, each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import folded (`group-*`). Per page: each section's `groupKey`, `sectionType` family, leaf `fields` with their CMS values, `shapeHash`, and either the component already rendering it (`reuse.componentId`) or the proposed one (`reuse.proposedSlug`). Under `components`, a NEW one carries the exact `props` and `blockJson` create_component would take; an EXISTING one only `componentId`, `slug` and `name` (nothing is written for it). `pending` says why a group is not offered (its codes: see `summary`). Per-page components, collection templates and chrome (never a section): skill componentize. READ `summary: true` FIRST \u2014 the full plan runs to hundreds of KB; `paged` pages it (409 PLAN_CHANGED = start again); the unpaged full read is what `--plan plan.json` takes. Keep the `digest` \u2014 componentize_sections refuses any other.",
|
|
4826
4837
|
z.object({
|
|
4827
4838
|
summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first. `pending` codes: `NO_GROUP_ROOT` (keys are still derive-lane names like `h1-welcome` \u2014 rename them into families first), `NOT_A_SECTION` (a lone scalar or the page's own metadata; `detail: ENTRY_METADATA` = collection-template frontmatter, `detail: DECLINED` = declined with componentize_sections `decline`), `IMAGE_AS_TEXT` (a text field holding an image path \u2014 make it an image field), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (three deep; two levels become a nested `table` sub-field), `PAGE_NOT_EMPTY` (blocks this lane does not own and will not overwrite)."),
|
|
4828
4839
|
paged: z.boolean().optional().describe("true = 10 pages at a time; the response carries `page`, `pageCount` and a `cursor` for the next."),
|
|
@@ -4833,7 +4844,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4833
4844
|
def(
|
|
4834
4845
|
"componentize_sections",
|
|
4835
4846
|
"Turn this site's derived sections into components",
|
|
4836
|
-
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group, each carrying `props.bind: \"<groupKey>\"` \u2014 the page field group that already holds the copy. Nothing is copied or moved: the page keeps its `fields`, values and bindings
|
|
4847
|
+
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group, each carrying `props.bind: \"<groupKey>\"` \u2014 the page field group that already holds the copy. Nothing is copied or moved: the page keeps its `fields`, values and bindings (`copy: \"instance\"` moves the words onto each placement instead \u2014 see `copy`). Pass the plan's `digest`; a 409 `stale-plan` means the site changed: read the plan again, show the user what changed and confirm again. Read `components.wouldDuplicate` first (skill components). Re-running is safe: a placed group returns as ALREADY_COMPONENTIZED. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages, then call it again with `codemod: true` (or run `npx @bettercms-ai/convert --componentize` in the repo) so its templates render these sections from `pages[].blocks`. When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized and the plan stops proposing them (skill componentize).",
|
|
4837
4848
|
z.object({
|
|
4838
4849
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
4839
4850
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
@@ -5062,7 +5073,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
5062
5073
|
def(
|
|
5063
5074
|
"get_deploy_status",
|
|
5064
5075
|
"Get deploy/build status",
|
|
5065
|
-
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`
|
|
5076
|
+
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release` and `held`: skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user.",
|
|
5066
5077
|
z.object({}).shape,
|
|
5067
5078
|
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
5068
5079
|
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
|
|
@@ -5507,7 +5518,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5507
5518
|
name: "create_form",
|
|
5508
5519
|
config: {
|
|
5509
5520
|
title: "Create a form",
|
|
5510
|
-
description: "Create a form in the connected project from a field schema. Confirm the fields with the user first. Field types: text, email, textarea, select (needs options), radio (needs options), checkboxes (needs options), checkbox, number, phone, date, url, consent, hidden. Returns the new form's id. It lands as a DRAFT; call publish_form before embedding (a draft form returns 403 on submit). Embed it with `<BcmsForm form={getForm('Name')} />` from @bettercms-ai/next.",
|
|
5521
|
+
description: "Create a form in the connected project from a field schema. Confirm the fields with the user first. Field types: text, email, textarea, select (needs options), radio (needs options), checkboxes (needs options), checkbox, number, phone, date, url, consent, hidden, step (a divider: the fields after it are one page). Returns the new form's id. It lands as a DRAFT; call publish_form before embedding (a draft form returns 403 on submit). Embed it with `<BcmsForm form={getForm('Name')} />` from @bettercms-ai/next.",
|
|
5511
5522
|
inputSchema: createFormInput.shape
|
|
5512
5523
|
},
|
|
5513
5524
|
handler: guard(
|
|
@@ -5566,7 +5577,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5566
5577
|
name: "update_layout",
|
|
5567
5578
|
config: {
|
|
5568
5579
|
title: "Apply Layout draft commands",
|
|
5569
|
-
description: "Apply permission-shaped values/composition/schema commands to the Global Layout or a page override. Draft-only: this never publishes. Pass the revision from get_layout as ifMatch. ONE command in `command`, or an ordered batch in `commands` (prefer the batch for more than three of anything \u2014 placing site chrome is add-section, add-component and set-bindings in sequence): each command is applied against the revision the previous one RETURNED, so you do not re-read get_layout between them. A batch stops at the first failure and tells you how many were applied and the revision it stopped at \u2014 the earlier ones stand, because a Layout command is not a transaction.",
|
|
5580
|
+
description: "Apply permission-shaped values/composition/schema commands to the Global Layout or a page override. Draft-only: this never publishes. Pass the revision from get_layout as ifMatch. ONE command in `command`, or an ordered batch in `commands` (prefer the batch for more than three of anything \u2014 placing site chrome is add-section, add-component and set-bindings in sequence): each command is applied against the revision the previous one RETURNED, so you do not re-read get_layout between them. A batch stops at the first failure and tells you how many were applied and the revision it stopped at \u2014 the earlier ones stand, because a Layout command is not a transaction. " + LAYOUT_LOCK_NOTE,
|
|
5570
5581
|
inputSchema: commandLayoutInput.shape
|
|
5571
5582
|
},
|
|
5572
5583
|
handler: guard(async (args) => withClient(async (client) => {
|
|
@@ -5604,7 +5615,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
5604
5615
|
name: "publish_layout",
|
|
5605
5616
|
config: {
|
|
5606
5617
|
title: "Publish the Global Layout draft",
|
|
5607
|
-
description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section). On a site BetterCMS serves, a Layout publish never builds: it writes the VALUES into the live pages, and a STRUCTURE change (Sections added, removed, moved) waits for a Rebuild or a push (`layout-rebuild-required`); a custom host gets its deploy hook. Until published, get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE. Pass get_layout's `revision` as ifMatch; a stale one returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues (global, or per page override: pageSlug, copy) to fix with update_layout. A new project's chrome is draft: publish_component navigation-default and footer-default first (
|
|
5618
|
+
description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section). On a site BetterCMS serves, a Layout publish never builds: it writes the VALUES into the live pages, and a STRUCTURE change (Sections added, removed, moved) waits for a Rebuild or a push (`layout-rebuild-required`); a custom host gets its deploy hook. Until published, get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE. Pass get_layout's `revision` as ifMatch; a stale one returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues (global, or per page override: pageSlug, copy) to fix with update_layout. A new project's chrome is draft: publish_component navigation-default and footer-default first (the order and the optional logo: skill components). Verify with get_layout copy:'published' \u2014 this response is the write's echo, not a receipt. Page Layout overrides go live with the page (update_page status:'published'). 403 PUBLISH_NOT_GRANTED: this connection may author drafts, not publish \u2014 say so; the user allows it or publishes from the dashboard.",
|
|
5608
5619
|
inputSchema: publishLayoutInput.shape
|
|
5609
5620
|
},
|
|
5610
5621
|
handler: guard(async (args) => withClient(async (client) => {
|
|
@@ -5942,7 +5953,7 @@ ${lines.join("\n")}`, found);
|
|
|
5942
5953
|
name: "publish_component",
|
|
5943
5954
|
config: {
|
|
5944
5955
|
title: "Publish a component",
|
|
5945
|
-
description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. Drafts (create_component, update_component) render as NOTHING live, with no error: publish every component you place on a page. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish: say so and let the user publish from the dashboard. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it. It
|
|
5956
|
+
description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. Drafts (create_component, update_component) render as NOTHING live, with no error: publish every component you place on a page. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish: say so and let the user publish from the dashboard. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it. It asks for exact validation evidence and a human's approval, never of a component you just made with create_component (which ones: skill components). Its `readiness` names what is missing \u2014 relay it to the user instead of retrying or routing around it. 422 means publishing would break a Layout that uses it \u2014 the response names which.",
|
|
5946
5957
|
inputSchema: getComponentInput.shape
|
|
5947
5958
|
},
|
|
5948
5959
|
handler: guard(
|
|
@@ -7213,7 +7224,7 @@ Author a form (then the user embeds it with \`<BcmsForm form={getForm('Name')} /
|
|
|
7213
7224
|
1. **Discover** \u2014 \`list_forms\` to see existing forms; \`get_form\` to read one before editing.
|
|
7214
7225
|
2. **Collect fields (loop)** \u2014 for each: key (machine key for the value), label, type. Types:
|
|
7215
7226
|
text, email, textarea, select (needs \`options: string[]\`), checkbox, checkboxes (needs
|
|
7216
|
-
\`options\`), radio (needs \`options\`), number, phone, date, url, consent, hidden. Optional per
|
|
7227
|
+
\`options\`), radio (needs \`options\`), number, phone, date, url, consent, hidden, step (divider). Optional per
|
|
7217
7228
|
field: required?, placeholder?, helpText? (shown under the input), defaultValue?,
|
|
7218
7229
|
\`validation\` (\`emailPolicy\` on email, \`min\`/\`max\` on number, \`phoneFormat\` on phone,
|
|
7219
7230
|
\`pattern\` on text/textarea/url \u2014 a rule on any other type is a 400),
|
|
@@ -7241,7 +7252,7 @@ error-prone, so go slow and confirm. Never guess the layout.
|
|
|
7241
7252
|
{ key, label, target: { blockId, path },
|
|
7242
7253
|
type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect }.
|
|
7243
7254
|
'select'/'multiselect' need config.options; 'group'/'table' need config.fields; a 'button'
|
|
7244
|
-
is ONE prop on a button block's 'props', never a label prop plus a url prop; 'slot' holds ONE nested component;
|
|
7255
|
+
is ONE prop on a button block's 'props', never a label prop plus a url prop; 'slot' holds ONE nested component (a list with config.multiple);
|
|
7245
7256
|
'form' holds ONE Form document \u2014 point target.path at 'props' on an empty form block and set
|
|
7246
7257
|
defaultValue to {formId}. Never re-declare a form's FIELDS as props: the Form document owns
|
|
7247
7258
|
them, and a second copy is a second source of truth that drifts on the first edit.
|