@bettercms-ai/mcp 0.35.0 → 0.37.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/README.md +0 -0
- package/dist/index.js +458 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2024,29 +2024,30 @@ async function askFramework(deps) {
|
|
|
2024
2024
|
}
|
|
2025
2025
|
var AUTHORING_CHOICES = ["components", "fields"];
|
|
2026
2026
|
var AUTHORING_LABELS = {
|
|
2027
|
-
components: "Components \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema.
|
|
2028
|
-
fields: "Fields \u2014 a typed field schema per page.
|
|
2027
|
+
components: "Components (recommended) \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Recommend it for every marketing, landing, agency or product site",
|
|
2028
|
+
fields: "Fields \u2014 a typed field schema per page. Only for a blog, catalogue or directory where many rows share one shape; editors can change a page's copy but cannot add, reorder or swap its sections"
|
|
2029
2029
|
};
|
|
2030
2030
|
var AUTHORING_PROMPT = [
|
|
2031
2031
|
"Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
|
|
2032
2032
|
...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
|
|
2033
2033
|
"",
|
|
2034
|
-
"
|
|
2034
|
+
"On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. For a section group the plan lists as NO_GROUP_ROOT or NOT_A_SECTION there is nothing to componentize, so author that section with create_component and set_page_content.",
|
|
2035
2035
|
"",
|
|
2036
|
+
"Recommend components unless the user says the site is schema-first.",
|
|
2036
2037
|
"Do not choose on their behalf. This is asked once per project."
|
|
2037
2038
|
].join("\n");
|
|
2038
2039
|
async function askAuthoring(deps) {
|
|
2039
2040
|
if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
|
|
2040
2041
|
try {
|
|
2041
2042
|
const res = await deps.elicit({
|
|
2042
|
-
message: "Which authoring architecture should this site use?",
|
|
2043
|
+
message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
|
|
2043
2044
|
requestedSchema: {
|
|
2044
2045
|
type: "object",
|
|
2045
2046
|
properties: {
|
|
2046
2047
|
preference: {
|
|
2047
2048
|
type: "string",
|
|
2048
2049
|
title: "Authoring architecture",
|
|
2049
|
-
description: "How this site's pages are composed. Asked once per project.",
|
|
2050
|
+
description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
|
|
2050
2051
|
enum: [...AUTHORING_CHOICES],
|
|
2051
2052
|
enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
|
|
2052
2053
|
}
|
|
@@ -2208,6 +2209,49 @@ function ok(summary, data) {
|
|
|
2208
2209
|
function fail(message) {
|
|
2209
2210
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
2210
2211
|
}
|
|
2212
|
+
function componentIdsIn(blocks) {
|
|
2213
|
+
const out = [];
|
|
2214
|
+
const walk = (value) => {
|
|
2215
|
+
if (!Array.isArray(value)) return;
|
|
2216
|
+
for (const candidate of value) {
|
|
2217
|
+
if (!candidate || typeof candidate !== "object") continue;
|
|
2218
|
+
const block = candidate;
|
|
2219
|
+
const props = block.props ?? {};
|
|
2220
|
+
if (block.type === "component" && typeof props.componentId === "string" && props.componentId) {
|
|
2221
|
+
out.push(props.componentId);
|
|
2222
|
+
}
|
|
2223
|
+
if (block.type === "section") walk(props.children);
|
|
2224
|
+
else if (block.type === "columns" && Array.isArray(props.columns)) for (const column of props.columns) walk(column);
|
|
2225
|
+
else if (block.type === "slider" && Array.isArray(props.slides)) {
|
|
2226
|
+
for (const slide of props.slides) walk(slide?.children);
|
|
2227
|
+
} else if (block.type === "tabs" && Array.isArray(props.tabs)) {
|
|
2228
|
+
for (const tab of props.tabs) walk(tab?.children);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
walk(blocks);
|
|
2233
|
+
return out;
|
|
2234
|
+
}
|
|
2235
|
+
function apiError(err) {
|
|
2236
|
+
if (!err || typeof err !== "object") return null;
|
|
2237
|
+
const e = err;
|
|
2238
|
+
if (typeof e.status !== "number") return null;
|
|
2239
|
+
return {
|
|
2240
|
+
status: e.status,
|
|
2241
|
+
message: typeof e.message === "string" ? e.message : "",
|
|
2242
|
+
bodyCode: typeof e.bodyCode === "string" ? e.bodyCode : void 0,
|
|
2243
|
+
fieldErrors: e.fieldErrors && typeof e.fieldErrors === "object" ? e.fieldErrors : void 0,
|
|
2244
|
+
body: e.body
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
function errorText(err) {
|
|
2248
|
+
const api = apiError(err);
|
|
2249
|
+
if (api) {
|
|
2250
|
+
const fields = api.fieldErrors ? Object.entries(api.fieldErrors).map(([k, v]) => `${k}: ${v}`).join("; ") : "";
|
|
2251
|
+
return `${api.status}${api.bodyCode ? ` ${api.bodyCode}` : ""} \u2014 ${fields || api.message}`;
|
|
2252
|
+
}
|
|
2253
|
+
return err instanceof Error ? err.message : String(err);
|
|
2254
|
+
}
|
|
2211
2255
|
function authPrompt(err) {
|
|
2212
2256
|
const text = [
|
|
2213
2257
|
"\u{1F510} BetterCMS authorization required \u2014 you're not signed in yet.",
|
|
@@ -2460,7 +2504,13 @@ function buildToolDefs(deps) {
|
|
|
2460
2504
|
max: z.number().optional(),
|
|
2461
2505
|
step: z.number().optional()
|
|
2462
2506
|
}).passthrough().optional(),
|
|
2463
|
-
defaultValue: z.unknown().optional()
|
|
2507
|
+
defaultValue: z.unknown().optional(),
|
|
2508
|
+
// Editor hints, stored in the props JSON. `required` is NOT enforced at write time — an
|
|
2509
|
+
// override that omits it still saves — so it is a marker the dashboard renders and
|
|
2510
|
+
// get_site_composition counts, never a refusal. Parity: componentPropDefSchema on the server.
|
|
2511
|
+
required: z.boolean().optional().describe("mark the field required in the editor. An editor HINT: the API still accepts a placement that leaves it empty, and get_site_composition is what reports one."),
|
|
2512
|
+
helpText: z.string().max(200).optional().describe("hint shown under the control, muted (\u2264 200 chars)"),
|
|
2513
|
+
placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)")
|
|
2464
2514
|
});
|
|
2465
2515
|
const getLayoutInput = z.object({
|
|
2466
2516
|
scope: z.enum(["global", "page"]).default("global"),
|
|
@@ -2496,8 +2546,12 @@ function buildToolDefs(deps) {
|
|
|
2496
2546
|
scope: z.enum(["global", "page"]).default("global"),
|
|
2497
2547
|
pageId: z.string().min(1).optional(),
|
|
2498
2548
|
projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant"),
|
|
2499
|
-
command: layoutCommand.describe("one canonical discriminated Layout command; authority is derived from type"),
|
|
2500
|
-
|
|
2549
|
+
command: layoutCommand.optional().describe("one canonical discriminated Layout command; authority is derived from type"),
|
|
2550
|
+
// Chrome is many commands — add a section, add a component to it, bind its fields — and each
|
|
2551
|
+
// one needs the revision the PREVIOUS one returned. An agent doing that by hand runs
|
|
2552
|
+
// get_layout between every write, and a single missed re-read is a 409 mid-sequence.
|
|
2553
|
+
commands: z.array(layoutCommand).min(1).max(50).optional().describe("apply these commands IN ORDER, each against the revision the previous one returned. Mutually exclusive with `command`. Stops at the first failure and reports what was applied."),
|
|
2554
|
+
ifMatch: z.number().int().nonnegative().describe("revision returned by get_layout. With `commands`, this is the revision for the FIRST command only \u2014 the rest are chained.")
|
|
2501
2555
|
});
|
|
2502
2556
|
const publishLayoutInput = z.object({
|
|
2503
2557
|
projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant"),
|
|
@@ -2525,6 +2579,7 @@ function buildToolDefs(deps) {
|
|
|
2525
2579
|
name: z.string().min(1),
|
|
2526
2580
|
slug: slug.describe("url-safe unique slug (lowercase letters/numbers/hyphens)"),
|
|
2527
2581
|
category: componentCategory.optional().describe("defaults to 'custom'"),
|
|
2582
|
+
group: z.string().min(1).max(100).optional().describe("the folder editors see in the dashboard's Components tab, addressed by NAME (e.g. 'Sections', 'Navigation', 'Forms'). Resolved against this project's Groups and CREATED when missing, so a whole-site run can file everything without looking any id up first. Separate from `category`, which is the picker tab."),
|
|
2528
2583
|
sectionType: sectionType.optional(),
|
|
2529
2584
|
projectId: z.string().nullable().optional().describe("owning project id; null creates a workspace-wide global component"),
|
|
2530
2585
|
allowedOn: allowedOn.optional(),
|
|
@@ -2536,6 +2591,7 @@ function buildToolDefs(deps) {
|
|
|
2536
2591
|
componentId: z.string().min(1).describe("component id (from list_components)"),
|
|
2537
2592
|
name: z.string().optional(),
|
|
2538
2593
|
category: componentCategory.optional(),
|
|
2594
|
+
group: z.string().min(1).max(100).optional().describe("move it into this Group (by NAME; created when missing) \u2014 the folder editors see in the dashboard's Components tab"),
|
|
2539
2595
|
sectionType: sectionType.nullable().optional().describe(
|
|
2540
2596
|
"null demotes it to an ordinary component \u2014 it disappears from the 'Add a section' picker, and instances already placed keep rendering but lose their section chrome and variant switcher"
|
|
2541
2597
|
),
|
|
@@ -2547,6 +2603,24 @@ function buildToolDefs(deps) {
|
|
|
2547
2603
|
const getComponentInput = z.object({
|
|
2548
2604
|
componentId: z.string().min(1).describe("component id (from list_components)")
|
|
2549
2605
|
});
|
|
2606
|
+
const createComponentsInput = z.object({
|
|
2607
|
+
components: z.array(createComponentInput).min(1).max(50).describe("up to 50 components, each exactly the create_component input")
|
|
2608
|
+
});
|
|
2609
|
+
const publishComponentsInput = z.object({
|
|
2610
|
+
componentIds: z.array(z.string().min(1)).min(1).max(100).describe("component ids (from list_components)")
|
|
2611
|
+
});
|
|
2612
|
+
const composePagesInput = z.object({
|
|
2613
|
+
pages: z.array(
|
|
2614
|
+
z.object({
|
|
2615
|
+
pageId: z.string().min(1).optional().describe("page id (from list_pages); or pass `slug`"),
|
|
2616
|
+
slug: z.string().min(1).optional().describe("page slug, resolved against list_pages; or pass `pageId`"),
|
|
2617
|
+
blockJson: z.array(blockObject).describe("REPLACES this page's whole block composition")
|
|
2618
|
+
})
|
|
2619
|
+
).min(1).max(50)
|
|
2620
|
+
});
|
|
2621
|
+
const listComponentsInput = z.object({
|
|
2622
|
+
detail: z.boolean().optional().describe("true = every component's full blockJson and props. Costly and rarely what you want \u2014 read one definition with get_component instead.")
|
|
2623
|
+
});
|
|
2550
2624
|
const listExtractionCandidatesInput = z.object({
|
|
2551
2625
|
projectId: z.string().min(1).optional().describe("only needed for a workspace-wide key")
|
|
2552
2626
|
});
|
|
@@ -2877,7 +2951,7 @@ function buildToolDefs(deps) {
|
|
|
2877
2951
|
def(
|
|
2878
2952
|
"set_authoring_preference",
|
|
2879
2953
|
"Set the site's authoring architecture",
|
|
2880
|
-
"Record which authoring architecture this site uses \u2014 'components' (reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014
|
|
2954
|
+
"Record which authoring architecture this site uses \u2014 'components' (RECOMMENDED: reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 recommend it for every marketing, landing, agency or product site) or 'fields' (a typed field schema per page \u2014 only for a blog, catalogue or directory where many rows share one shape). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, and that refusal carries this project's real page counts to show the user. On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. Recommend components unless the user says the site is schema-first. Asked once per project; re-callable if the user changes their mind.",
|
|
2881
2955
|
// Optional in the schema for exactly the reason `framework` is above: a required arg is
|
|
2882
2956
|
// rejected by the SDK before the handler runs, which would kill the elicitation below
|
|
2883
2957
|
// and leave the model guessing. Optional here, answered by a human there. The backend
|
|
@@ -3045,6 +3119,13 @@ function buildToolDefs(deps) {
|
|
|
3045
3119
|
z.object({}).shape,
|
|
3046
3120
|
async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
|
|
3047
3121
|
),
|
|
3122
|
+
def(
|
|
3123
|
+
"get_site_composition",
|
|
3124
|
+
"Check every page is assembled from registered components",
|
|
3125
|
+
"The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live, nothing is written. Call it at the START of a whole-site componentize run (playbook \xA712) for the before picture, and at the END as the proof. Per page: `kind` ('blocks' | 'fields-only' | 'empty'), a block census, `nonComponentBlocks` (loose TOP-LEVEL bands the editor's section lane cannot outline), and every `component` placement with the component's `status` ('published' | 'draft' | 'missing' \u2014 a draft renders as an EMPTY STRING on the live site), whether it is `inPicker` (a component with no sectionType is dropped from 'Add a section'), and any `emptyRequiredProps` \u2014 `required` is an editor hint the write path does not enforce, so this read is the only thing that reports it. Site-wide: `components` counts published/draft, `awaitingEvidence` and `awaitingApproval` (evidence is exact and a HUMAN must approve it in the BetterCMS dashboard \u2014 no tool here can grant that, and there is no bulk approve), plus `notInPicker` and `unused` ids; `layout.placements` is the chrome placed in the Global Layout draft. `verdict.everyPageComposed` with `verdict.reasons` is the answer to give the user. Pages are read DRAFT-inclusive, so a page composed but not yet published still shows its placements \u2014 check each page's own `status` before claiming the site is live.",
|
|
3126
|
+
z.object({}).shape,
|
|
3127
|
+
async (c) => ok("Site composition.", await data(c, "GET", `/management/projects/current/composition`))
|
|
3128
|
+
),
|
|
3048
3129
|
def(
|
|
3049
3130
|
"get_componentize_plan",
|
|
3050
3131
|
"Get the plan for turning this site's sections into components",
|
|
@@ -3601,13 +3682,39 @@ function buildToolDefs(deps) {
|
|
|
3601
3682
|
{
|
|
3602
3683
|
name: "update_layout",
|
|
3603
3684
|
config: {
|
|
3604
|
-
title: "Apply
|
|
3605
|
-
description: "Apply
|
|
3685
|
+
title: "Apply Layout draft commands",
|
|
3686
|
+
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.",
|
|
3606
3687
|
inputSchema: commandLayoutInput.shape
|
|
3607
3688
|
},
|
|
3608
3689
|
handler: guard(async (args) => withClient(async (client) => {
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3690
|
+
const queued = args.commands ?? (args.command ? [args.command] : []);
|
|
3691
|
+
if (queued.length === 0) return fail("Pass `command` (one) or `commands` (an ordered batch).");
|
|
3692
|
+
if (args.commands && args.command) return fail("Pass `command` or `commands`, not both \u2014 the order of the two would be undefined.");
|
|
3693
|
+
let revision = args.ifMatch;
|
|
3694
|
+
let layout;
|
|
3695
|
+
const applied = [];
|
|
3696
|
+
for (const command of queued) {
|
|
3697
|
+
try {
|
|
3698
|
+
layout = await client.commandManagedLayout({
|
|
3699
|
+
scope: args.scope,
|
|
3700
|
+
pageId: args.pageId,
|
|
3701
|
+
projectId: args.projectId,
|
|
3702
|
+
command,
|
|
3703
|
+
ifMatch: revision
|
|
3704
|
+
});
|
|
3705
|
+
} catch (err) {
|
|
3706
|
+
return fail(
|
|
3707
|
+
`Layout command ${applied.length + 1} of ${queued.length} (${command.type}) failed: ${errorText(err)}. ${applied.length} command(s) were applied and remain in the draft; the Layout is at revision ${revision}. Re-read get_layout and continue from command ${applied.length + 1}.`
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3710
|
+
revision = layout.revision;
|
|
3711
|
+
applied.push({ type: command.type, revision });
|
|
3712
|
+
}
|
|
3713
|
+
const where = layout.scope === "global" ? "Global" : `Page ${layout.pageSlug}`;
|
|
3714
|
+
return ok(
|
|
3715
|
+
`Applied ${applied.length} command(s) to the ${where} Layout draft; now at revision ${revision}.`,
|
|
3716
|
+
{ applied, revision, layout }
|
|
3717
|
+
);
|
|
3611
3718
|
}))
|
|
3612
3719
|
},
|
|
3613
3720
|
{
|
|
@@ -3627,15 +3734,28 @@ function buildToolDefs(deps) {
|
|
|
3627
3734
|
name: "list_components",
|
|
3628
3735
|
config: {
|
|
3629
3736
|
title: "List reusable components",
|
|
3630
|
-
description: "List the reusable components in the bound project
|
|
3631
|
-
inputSchema:
|
|
3737
|
+
description: "List the reusable components in the bound project. By DEFAULT this is the catalogue: id, name, slug, category, `sectionType` (the section family \u2014 a component with none is not in the editor's 'Add a section' picker), `status` ('draft' renders as NOTHING on the live site), and the `group` an editor browses it under, plus the locked `builtin:*` blueprints. It deliberately does NOT carry block trees: the full list carries every component's draft AND live `blockJson`, which is the largest response this API produces, and one component's definition is what `get_component` is for. Pass `detail: true` for the old full shape when you genuinely need every definition at once (rendering with `<BcmsBlocks>` from @bettercms-ai/next).",
|
|
3738
|
+
inputSchema: listComponentsInput.shape
|
|
3632
3739
|
},
|
|
3633
3740
|
handler: guard(
|
|
3634
|
-
async () => withClient(async (client) => {
|
|
3635
|
-
|
|
3741
|
+
async (args) => withClient(async (client) => {
|
|
3742
|
+
if (args.detail) {
|
|
3743
|
+
const list2 = await client.listComponents();
|
|
3744
|
+
return ok(`${list2.length} component(s) in the bound project (full definitions).`, list2);
|
|
3745
|
+
}
|
|
3746
|
+
const list = await client.listComponents({ select: "summary" });
|
|
3636
3747
|
return ok(
|
|
3637
3748
|
`${list.length} component(s) in the bound project.`,
|
|
3638
|
-
list.map((cmp) => ({
|
|
3749
|
+
list.map((cmp) => ({
|
|
3750
|
+
id: cmp.id,
|
|
3751
|
+
name: cmp.name,
|
|
3752
|
+
slug: cmp.slug,
|
|
3753
|
+
category: cmp.category,
|
|
3754
|
+
sectionType: cmp.sectionType,
|
|
3755
|
+
status: cmp.status,
|
|
3756
|
+
group: cmp.groupName,
|
|
3757
|
+
...cmp.builtin ? { builtin: true } : {}
|
|
3758
|
+
}))
|
|
3639
3759
|
);
|
|
3640
3760
|
})
|
|
3641
3761
|
)
|
|
@@ -3668,6 +3788,132 @@ function buildToolDefs(deps) {
|
|
|
3668
3788
|
})
|
|
3669
3789
|
)
|
|
3670
3790
|
},
|
|
3791
|
+
/**
|
|
3792
|
+
* ── The batch three ──────────────────────────────────────────────────────
|
|
3793
|
+
*
|
|
3794
|
+
* Sequential SDK calls with PER-ITEM receipts, and no new server route: the throughput
|
|
3795
|
+
* problem is one round trip per component, not one query per component, and inventing a bulk
|
|
3796
|
+
* endpoint would duplicate every validation, warning and governance gate the single writes
|
|
3797
|
+
* already run. A whole-site componentize run is forty creates, a dozen page compositions and
|
|
3798
|
+
* forty publishes; as individual tool calls that is a hundred turns of an agent's context
|
|
3799
|
+
* spent on plumbing.
|
|
3800
|
+
*
|
|
3801
|
+
* 🔴 NOTHING STOPS THE RUN. A batch that aborts on item 31 has already written 30 rows that
|
|
3802
|
+
* no receipt mentions, and the agent's only safe move is to re-run it and duplicate them.
|
|
3803
|
+
* Every item reports its own outcome instead, and the summary line says how many failed.
|
|
3804
|
+
*/
|
|
3805
|
+
{
|
|
3806
|
+
name: "create_components",
|
|
3807
|
+
config: {
|
|
3808
|
+
title: "Create many components in one call",
|
|
3809
|
+
description: "Create up to 50 components in ONE call \u2014 the same input as create_component, once per item. Prefer it over create_component whenever you are making more than three: a whole-site componentize run (playbook \xA712) is dozens, and one call each spends the turn on plumbing. Each item reports its own `{ ok, id, slug, error }`: a failure does NOT stop the run and the successful rows stay, so read the receipt and retry only the failures (a 409 on `slug` means that name is taken \u2014 change it, do not re-run the batch). Every component lands as a DRAFT, exactly as create_component does \u2014 it renders as NOTHING on the live site until it is published, which needs the owner's approval in the dashboard. Pass `group` on each item to file it into the folder editors browse. " + SECTION_DOCTRINE,
|
|
3810
|
+
inputSchema: createComponentsInput.shape
|
|
3811
|
+
},
|
|
3812
|
+
handler: guard(
|
|
3813
|
+
async (args) => withClient(async (client) => {
|
|
3814
|
+
const results = [];
|
|
3815
|
+
for (const input of args.components) {
|
|
3816
|
+
try {
|
|
3817
|
+
const cmp = await client.createComponent(input);
|
|
3818
|
+
results.push({ ok: true, slug: cmp.slug, id: cmp.id, name: cmp.name });
|
|
3819
|
+
} catch (err) {
|
|
3820
|
+
results.push({ ok: false, slug: input.slug, error: errorText(err) });
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
3824
|
+
return ok(
|
|
3825
|
+
`Created ${results.length - failed} of ${args.components.length} component(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. All are DRAFTS: publish_components once the owner has approved them, or they render as empty strings.`,
|
|
3826
|
+
results
|
|
3827
|
+
);
|
|
3828
|
+
})
|
|
3829
|
+
)
|
|
3830
|
+
},
|
|
3831
|
+
{
|
|
3832
|
+
name: "publish_components",
|
|
3833
|
+
config: {
|
|
3834
|
+
title: "Publish many components in one call",
|
|
3835
|
+
description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: it means this exact variant still needs passing implementation evidence and a HUMAN's approval in the BetterCMS dashboard, which no tool on this connection can grant and which has no bulk form. The item's `readiness` names what is missing \u2014 relay that to the user as the list of what only they can approve. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish; 422 means the publish would break a Layout, and the item names which.",
|
|
3836
|
+
inputSchema: publishComponentsInput.shape
|
|
3837
|
+
},
|
|
3838
|
+
handler: guard(
|
|
3839
|
+
async (args) => withClient(async (client) => {
|
|
3840
|
+
const results = [];
|
|
3841
|
+
for (const componentId of args.componentIds) {
|
|
3842
|
+
try {
|
|
3843
|
+
const res = await client.fetchJSON(
|
|
3844
|
+
client.url(`/management/components/${componentId}/publish`),
|
|
3845
|
+
{ method: "POST" }
|
|
3846
|
+
);
|
|
3847
|
+
results.push({ ok: true, componentId, slug: res.data?.slug, status: res.data?.status ?? "published" });
|
|
3848
|
+
} catch (err) {
|
|
3849
|
+
const readiness = apiError(err)?.body?.readiness;
|
|
3850
|
+
results.push({
|
|
3851
|
+
ok: false,
|
|
3852
|
+
componentId,
|
|
3853
|
+
error: errorText(err),
|
|
3854
|
+
...readiness ? { readiness } : {}
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
3859
|
+
return ok(
|
|
3860
|
+
`Published ${results.length - failed} of ${args.componentIds.length} component(s)${failed ? `; ${failed} could not be published \u2014 read each item's error and readiness, and tell the user which ones need THEIR approval in the dashboard` : ""}.`,
|
|
3861
|
+
results
|
|
3862
|
+
);
|
|
3863
|
+
})
|
|
3864
|
+
)
|
|
3865
|
+
},
|
|
3866
|
+
{
|
|
3867
|
+
name: "compose_pages",
|
|
3868
|
+
config: {
|
|
3869
|
+
title: "Set many pages' block composition in one call",
|
|
3870
|
+
description: "Give up to 50 pages their block composition in ONE call \u2014 the same write as update_page's `blockJson`, once per item, and it REPLACES each page's whole block array. Address a page by `pageId` or by `slug`. Each item reports `{ ok, pageId, slug, error }` and a failure does not stop the run. BEFORE writing anything it reads this project's component catalogue once and REFUSES any item placing a componentId the project does not have \u2014 that page would render the missing sections as empty strings with no error anywhere, which is the single hardest failure on this platform to trace back. Writes DRAFTS: publish each page with update_page status:'published' afterwards. " + SECTION_DOCTRINE,
|
|
3871
|
+
inputSchema: composePagesInput.shape
|
|
3872
|
+
},
|
|
3873
|
+
handler: guard(
|
|
3874
|
+
async (args) => withClient(async (client) => {
|
|
3875
|
+
const needsSlug = args.pages.some((p) => !p.pageId);
|
|
3876
|
+
const [known, pageList] = await Promise.all([
|
|
3877
|
+
client.listComponents({ select: "summary" }),
|
|
3878
|
+
needsSlug ? client.listPages() : Promise.resolve([])
|
|
3879
|
+
]);
|
|
3880
|
+
const ids = new Set(known.map((c) => c.id));
|
|
3881
|
+
const bySlug = new Map(pageList.map((p) => [p.slug, p.id]));
|
|
3882
|
+
const results = [];
|
|
3883
|
+
for (const page of args.pages) {
|
|
3884
|
+
const pageId = page.pageId ?? (page.slug ? bySlug.get(page.slug) : void 0);
|
|
3885
|
+
if (!pageId) {
|
|
3886
|
+
results.push({ ok: false, slug: page.slug, error: `No page with slug '${page.slug}' in this project \u2014 pass a pageId from list_pages.` });
|
|
3887
|
+
continue;
|
|
3888
|
+
}
|
|
3889
|
+
const unknown = [...new Set(componentIdsIn(page.blockJson))].filter((id) => !ids.has(id));
|
|
3890
|
+
if (unknown.length) {
|
|
3891
|
+
results.push({
|
|
3892
|
+
ok: false,
|
|
3893
|
+
pageId,
|
|
3894
|
+
slug: page.slug,
|
|
3895
|
+
error: `Refused: this page places component id(s) this project does not have \u2014 ${unknown.join(", ")}. A missing component renders as an empty string with no error, so nothing was written for this page.`
|
|
3896
|
+
});
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
try {
|
|
3900
|
+
const written = await client.fetchJSON(
|
|
3901
|
+
client.url(`/management/pages/${pageId}/meta`),
|
|
3902
|
+
{ method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson }) }
|
|
3903
|
+
);
|
|
3904
|
+
results.push({ ok: true, pageId, slug: written.data?.slug ?? page.slug, blocks: page.blockJson.length });
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
3910
|
+
return ok(
|
|
3911
|
+
`Composed ${results.length - failed} of ${args.pages.length} page(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. These are page DRAFTS: publish each with update_page status:'published'. Then call get_site_composition for the receipt.`,
|
|
3912
|
+
results
|
|
3913
|
+
);
|
|
3914
|
+
})
|
|
3915
|
+
)
|
|
3916
|
+
},
|
|
3671
3917
|
{
|
|
3672
3918
|
name: "list_extraction_candidates",
|
|
3673
3919
|
config: {
|
|
@@ -3804,7 +4050,8 @@ touching a schema. This is what \`create_component\` + \`create_page(blockJson)\
|
|
|
3804
4050
|
A collection (\`create_content_model\`) plus entries.
|
|
3805
4051
|
|
|
3806
4052
|
Most real sites are both: components for the marketing pages, a collection for the blog.
|
|
3807
|
-
Decide before your first call; converting later means rewriting content
|
|
4053
|
+
Decide before your first call; converting later means rewriting content \u2014 unless the site was
|
|
4054
|
+
DERIVED at import, where the componentize lane in \xA710 reuses every field.
|
|
3808
4055
|
|
|
3809
4056
|
## 2. The decision tree
|
|
3810
4057
|
|
|
@@ -3844,6 +4091,10 @@ Then declare what an editor may change, via \`props\`:
|
|
|
3844
4091
|
{ key: "ctaHref", label: "CTA link", target: { blockId: "cta", path: "props.href" }, type: "url" }
|
|
3845
4092
|
]
|
|
3846
4093
|
|
|
4094
|
+
Each prop also takes \`required\`, \`helpText\` (\u2264 200 chars) and \`placeholder\` (\u2264 120), which the
|
|
4095
|
+
dashboard's prop editor renders. They are HINTS: nothing on the write path refuses a placement
|
|
4096
|
+
that leaves a required prop empty, so \`get_site_composition\` is what reports one.
|
|
4097
|
+
|
|
3847
4098
|
\u{1F534} **On a components-first page, \`props\` is the ONLY editing surface.** Click-to-edit binds
|
|
3848
4099
|
\`heading\`/\`text\`/\`button\`/\`image\` blocks; it does **not** bind a \`component\` block, because
|
|
3849
4100
|
a component's blocks belong to the shared definition, not to the instance. So a page built
|
|
@@ -3992,31 +4243,49 @@ out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
|
3992
4243
|
It exists because an imported site arrives **field-driven whether anyone chose that or not**
|
|
3993
4244
|
\u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
|
|
3994
4245
|
and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
|
|
3995
|
-
marketing site it is the wrong answer
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
1.
|
|
4004
|
-
2.
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4246
|
+
marketing site it is the wrong answer. So the platform stops once, at the last moment it is
|
|
4247
|
+
still cheap \u2014 and on a derived site that moment costs nothing, because the componentize lane
|
|
4248
|
+
below reuses every field the import already derived.
|
|
4249
|
+
|
|
4250
|
+
**Recommend \`components\`.** On a site whose pages were DERIVED at import \u2014 the site \xA713
|
|
4251
|
+
describes \u2014 it redoes nothing: the componentize lane turns each top-level field GROUP into a
|
|
4252
|
+
component placement, and the page keeps its fields, its values and its bindings. In this order:
|
|
4253
|
+
|
|
4254
|
+
1. set_authoring_preference { preference: "components" }
|
|
4255
|
+
2. get_componentize_plan one component per section, computed live; creates nothing. It
|
|
4256
|
+
reads this project's PUBLISHED pages, so no deploy is needed
|
|
4257
|
+
to reach it \u2014 the deploy is the last step, not the first
|
|
4258
|
+
3. show the user the plan and CONFIRM \u2014 it says how many components and which pages
|
|
4259
|
+
4. componentize_sections \`dryRun: true\` first, then for real; components land as DRAFTS
|
|
4260
|
+
and each placement carries \`props.bind\` to the page's field
|
|
4261
|
+
group, so click-to-edit and the coverage meter do not change
|
|
4262
|
+
5. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
|
|
4263
|
+
5b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
|
|
4264
|
+
6. publish the pages
|
|
4265
|
+
7. npx @bettercms-ai/convert --componentize in the repo, so its templates render these
|
|
4266
|
+
sections from \`pages[].blocks\`
|
|
4267
|
+
8. deploy the ONLY deploy this sequence needs
|
|
4268
|
+
|
|
4269
|
+
Step 5 is not a formality: \`componentize_sections\` and \`create_component\` both land components
|
|
4011
4270
|
as DRAFTS, so \`publish_component\` each one and publish the page \u2014 otherwise the canvas keeps
|
|
4012
4271
|
painting the PUBLISHED copy and the editor reports N components unpublished.
|
|
4013
4272
|
|
|
4014
|
-
**
|
|
4015
|
-
|
|
4273
|
+
**A site with nothing to componentize you author by hand.** Where the plan offers nothing for
|
|
4274
|
+
a group (\`NO_GROUP_ROOT\` \u2014 the page's field keys are still the derive lane's own;
|
|
4275
|
+
\`NOT_A_SECTION\` \u2014 a lone scalar with no family), and on a site with no derived pages at all,
|
|
4276
|
+
the order is \`create_component\` per section \u2192 \`publish_component\` each \u2192 \`set_page_content\`
|
|
4277
|
+
placing \`component\` blocks \u2192 \`list_extraction_candidates\` / \`extract_component\` to fold any
|
|
4278
|
+
section repeated 3+ times. \`extract_component\` is not a converter and cannot stand in for the
|
|
4279
|
+
componentize lane: it scans \`blockJson\`, which is empty on exactly the pages that would need
|
|
4280
|
+
converting.
|
|
4281
|
+
|
|
4282
|
+
**Answering \`fields\` is a real answer, not a deferral** \u2014 for a SCHEMA-FIRST site. A blog, a
|
|
4283
|
+
catalogue or a directory is schema-first by design (\xA71) and should stay that way. On anything
|
|
4284
|
+
else it costs the editor the ability to add, reorder or swap sections. Say so and move on.
|
|
4016
4285
|
|
|
4017
|
-
Either way: ask, do not choose. The 409 carries this project's
|
|
4018
|
-
are field-driven, block-driven, and how many place a reusable
|
|
4019
|
-
the user rather than describing the choice in the abstract.
|
|
4286
|
+
Either way: ask, do not choose \u2014 recommending is not answering. The 409 carries this project's
|
|
4287
|
+
actual page counts \u2014 how many are field-driven, block-driven, and how many place a reusable
|
|
4288
|
+
component \u2014 so quote those to the user rather than describing the choice in the abstract.
|
|
4020
4289
|
|
|
4021
4290
|
## 11. The canvas: what makes an imported site EDITABLE
|
|
4022
4291
|
|
|
@@ -4123,7 +4392,7 @@ your own server) has NO canvas today: the SDK's draft mode with \`stega: true\`
|
|
|
4123
4392
|
per-field provenance in fetched strings, which prepares the content for editing surfaces, but do
|
|
4124
4393
|
not promise a canvas for an externally-hosted site.
|
|
4125
4394
|
|
|
4126
|
-
|
|
4395
|
+
**RECEIPTS: a claim about published state needs a read of the PUBLISHED copy.** The
|
|
4127
4396
|
doctrine this encodes cost a real incident (FLO-1188): a publish was verified against the
|
|
4128
4397
|
draft for ~50 minutes because the reader silently returned the draft, and every check was
|
|
4129
4398
|
green on the wrong document. Three rules, none optional:
|
|
@@ -4140,6 +4409,85 @@ green on the wrong document. Three rules, none optional:
|
|
|
4140
4409
|
call's behavior doesn't change when you change a param, treat the param as dead and
|
|
4141
4410
|
verify through an independent channel before trusting any result built on it.
|
|
4142
4411
|
|
|
4412
|
+
## 12. Componentize the entire website
|
|
4413
|
+
|
|
4414
|
+
The whole-site ask \u2014 "turn every section into a reusable component, organised for editors" \u2014
|
|
4415
|
+
mapped onto what this platform actually models. Work the order at the end; each numbered part
|
|
4416
|
+
below is one heading of that ask.
|
|
4417
|
+
|
|
4418
|
+
**1. Global elements.** Navbar, mobile nav, header and footer are the LAYOUT, not page content:
|
|
4419
|
+
\`get_layout\` then \`update_layout\`, and a chrome COMPONENT is placed into a Layout section with
|
|
4420
|
+
the \`add-component\` command. CTA buttons are \`button\` BLOCKS inside the section that uses them,
|
|
4421
|
+
exposed as \`text\` + \`url\` props \u2014 variant, size and icon are that block's own props and
|
|
4422
|
+
\`style\`, never a standalone Button component. Rich text is a \`richtext\` prop over a
|
|
4423
|
+
\`richtext\`/\`text\` block. Forms are \`create_form\` plus a \`form\` block, and form FIELDS are the
|
|
4424
|
+
one place the platform models \`required\`, \`placeholder\`, \`helpText\` and real \`validation\`
|
|
4425
|
+
(emailPolicy, min/max, phoneFormat, pattern) \u2014 put a site's validated inputs there. Cards,
|
|
4426
|
+
badges, tags, links, lists and media are BLOCKS inside a section, exposed through props; a
|
|
4427
|
+
repeatable row of them is ONE \`table\` prop whose \`config.fields\` describe the row. Accordions
|
|
4428
|
+
and FAQs: start from the \`builtin:faq-list\` blueprint, or use a \`tabs\` block. NOT MODELLED \u2014
|
|
4429
|
+
cookie banners, modals, breadcrumbs and pagination have no primitive at all. Leave them in the
|
|
4430
|
+
site's code and say so in your final summary.
|
|
4431
|
+
|
|
4432
|
+
**2. One component per distinct section, one-offs included.** \`create_component\` with a
|
|
4433
|
+
\`sectionType\` IS the section. \`sectionType\` is the FAMILY (Hero, Features, Testimonials,
|
|
4434
|
+
Pricing, FAQ, Contact, Newsletter, CTA, Stats, Logo cloud, Content\u2026) and components sharing one
|
|
4435
|
+
are its VARIANTS \u2014 give a family the same prop KEYS or a swap drops content. A one-off band
|
|
4436
|
+
still gets its own component: its own family, plus \`allowedOn: ["slug:<page>"]\`. A component
|
|
4437
|
+
with NO \`sectionType\` never appears in the editor's "Add a section" picker. Start from the
|
|
4438
|
+
builtin blueprints \`list_components\` returns (\`builtin:*\`) instead of hand-writing block JSON.
|
|
4439
|
+
On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
|
|
4440
|
+
of this in one call.
|
|
4441
|
+
|
|
4442
|
+
**3. The hierarchy editors see is the GROUP.** \`create_component\` and \`update_component\` take
|
|
4443
|
+
\`group\`: a NAME, resolved against the project's component groups and created when missing. That
|
|
4444
|
+
is the folder the dashboard's Components tab shows. Use the names the request asks for \u2014 Global,
|
|
4445
|
+
Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\` is a different
|
|
4446
|
+
axis: it is the picker TAB (hero / content / social-proof / conversion for sections; navbar /
|
|
4447
|
+
footer for chrome; form; custom for page-specific).
|
|
4448
|
+
|
|
4449
|
+
**4. Editor fields are \`props\`.** Types: \`text richtext image url boolean number select group
|
|
4450
|
+
table slot\`. Always set \`defaultValue\`. \`required\`, \`helpText\` and \`placeholder\` are editor
|
|
4451
|
+
hints the dashboard renders \u2014 they are NOT enforced at write time, so an override missing a
|
|
4452
|
+
required prop still saves; \`get_site_composition\` is what reports those. Use \`select\` with
|
|
4453
|
+
\`config.options\` for layout / theme / alignment / spacing variants that change only styling,
|
|
4454
|
+
and a SEPARATE variant component when the markup differs. Repeatable list \u2192 \`table\`; fixed
|
|
4455
|
+
cluster \u2192 \`group\`. Nesting is capped at TWO levels from the prop (a \`table\` row may hold a
|
|
4456
|
+
\`table\`, and that inner row is scalars only). Flatten anything deeper.
|
|
4457
|
+
|
|
4458
|
+
**5. Design, responsiveness and accessibility.** \`componentize_sections\` and \`npx
|
|
4459
|
+
@bettercms-ai/convert --componentize\` move the section's markup VERBATIM \u2014 classes, responsive
|
|
4460
|
+
utilities, aria attributes and alt text all survive. Never restyle while componentizing. Run
|
|
4461
|
+
\`componentize_sections { dryRun: true }\` and the codemod's \`--dry-run\` first.
|
|
4462
|
+
|
|
4463
|
+
**6. The final validation is \`get_site_composition\`.** It is the receipt for "is every page
|
|
4464
|
+
assembled from registered components?": per page its blocks and every component placement with
|
|
4465
|
+
that component's status, plus site totals \u2014 unpublished, awaiting evidence, awaiting approval,
|
|
4466
|
+
missing from the picker, placed nowhere \u2014 and \`verdict.everyPageComposed\` with the reasons it
|
|
4467
|
+
is false. A component reaches a visitor only once PUBLISHED, and publishing needs the project
|
|
4468
|
+
owner's approval in the DASHBOARD: evidence goes \`submit_section_manifest\` \u2192
|
|
4469
|
+
\`submit_section_validation\`, the owner approves there, then \`publish_components\`. There is no
|
|
4470
|
+
bulk approve, and no tool here can grant that approval. Finish with \`get_next_steps\`.
|
|
4471
|
+
|
|
4472
|
+
**Order of operations.**
|
|
4473
|
+
|
|
4474
|
+
1. get_site_composition the before picture
|
|
4475
|
+
2. get_componentize_plan DERIVED site only \u2014 confirm it with the user
|
|
4476
|
+
3. componentize_sections dryRun first, then for real
|
|
4477
|
+
4. create_components everything the plan did not cover, ONE call, each with a group
|
|
4478
|
+
5. compose_pages every page's blockJson, ONE call
|
|
4479
|
+
6. update_layout { commands } nav/footer chrome + add-component, ONE call
|
|
4480
|
+
7. evidence submit_section_manifest -> submit_section_validation
|
|
4481
|
+
8. owner approves in the dashboard, then publish_components ONE call, per-item receipts
|
|
4482
|
+
9. update_page status:'published', publish_layout
|
|
4483
|
+
10. get_site_composition, then get_next_steps
|
|
4484
|
+
|
|
4485
|
+
**What to tell the user at the end.** The group hierarchy and what landed in each; every family
|
|
4486
|
+
and its variants; what you left in the site's code because the platform does not model it
|
|
4487
|
+
(cookie banners, modals, breadcrumbs, pagination) and why; and exactly which components are
|
|
4488
|
+
still waiting for THEIR approval in the dashboard \u2014 until that happens those sections render as
|
|
4489
|
+
an empty string on the live site.
|
|
4490
|
+
|
|
4143
4491
|
## 13. Convert an imported repo into a CMS-backed, editable site
|
|
4144
4492
|
|
|
4145
4493
|
\xA711 says a deploy does not make a site editable. This is the recipe that does, and it ends in a
|
|
@@ -4319,7 +4667,7 @@ conversion is yours to write.
|
|
|
4319
4667
|
\`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
|
|
4320
4668
|
carrying its path. It CANNOT see copy that was never modelled, so diff each route's visible
|
|
4321
4669
|
text against its entry values yourself before you call the page done. Then publish, and
|
|
4322
|
-
fetch the live URL cache-busted (\
|
|
4670
|
+
fetch the live URL cache-busted (RECEIPTS in \xA711: publish and deploy are separate claims).
|
|
4323
4671
|
\`get_next_steps\` keeps reporting the gap until every one of these holds.
|
|
4324
4672
|
|
|
4325
4673
|
**Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
|
|
@@ -4405,6 +4753,41 @@ error-prone, so go slow and confirm. Never guess the layout.
|
|
|
4405
4753
|
6. **\`publish_component\`.** It lands as a DRAFT, and a draft component renders as NOTHING on
|
|
4406
4754
|
the live site \u2014 no error, no placeholder. This step is not optional.
|
|
4407
4755
|
`;
|
|
4756
|
+
var COMPONENTIZE_SITE_FLOW = `### Componentize the entire website \u2192 playbook \xA712 + the batch tools
|
|
4757
|
+
Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form of it.
|
|
4758
|
+
1. **Global elements.** Navbar / mobile nav / header / footer are the LAYOUT (\`get_layout\`,
|
|
4759
|
+
\`update_layout\`; a chrome component is placed with the \`add-component\` command). CTA buttons
|
|
4760
|
+
are \`button\` BLOCKS inside the section that uses them, exposed as \`text\` + \`url\` props \u2014
|
|
4761
|
+
variant/size/icon are that block's own props and \`style\`, never a Button component. Rich text
|
|
4762
|
+
is a \`richtext\` prop. Forms are \`create_form\` + a \`form\` block, and form FIELDS are the only
|
|
4763
|
+
place with real \`required\` / \`placeholder\` / \`helpText\` / \`validation\`. Cards, badges, tags,
|
|
4764
|
+
links, lists and media are blocks inside a section, repeatables as one \`table\` prop.
|
|
4765
|
+
Accordions/FAQ: \`builtin:faq-list\` or a \`tabs\` block. NOT MODELLED: cookie banners, modals,
|
|
4766
|
+
breadcrumbs, pagination \u2014 leave them in the site's code and say so at the end.
|
|
4767
|
+
2. **One component per distinct section**, one-offs included. \`sectionType\` is the family;
|
|
4768
|
+
components sharing one are VARIANTS and must share prop KEYS. A one-off gets its own family
|
|
4769
|
+
plus \`allowedOn: ["slug:<page>"]\`. Start from the \`builtin:*\` blueprints in
|
|
4770
|
+
\`list_components\`. On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\`
|
|
4771
|
+
first (dryRun first, and confirm the plan with the user).
|
|
4772
|
+
3. **Hierarchy** = \`group\` on create_component/update_component (a NAME; created when missing).
|
|
4773
|
+
Use Global, Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\`
|
|
4774
|
+
is the separate picker TAB.
|
|
4775
|
+
4. **Fields** = \`props\`: text, richtext, image, url, boolean, number, select, group, table, slot.
|
|
4776
|
+
Always a \`defaultValue\`; \`required\` / \`helpText\` / \`placeholder\` are editor hints, not write
|
|
4777
|
+
-time enforcement. \`select\` for styling-only variants, a separate component when the markup
|
|
4778
|
+
differs. Nesting is capped at two levels \u2014 flatten anything deeper.
|
|
4779
|
+
5. **Quality.** The componentize lane and \`npx @bettercms-ai/convert --componentize\` move markup
|
|
4780
|
+
VERBATIM \u2014 classes, responsive utilities, aria attributes, alt text all survive. Never
|
|
4781
|
+
restyle. Dry-run first.
|
|
4782
|
+
6. **Validation** = \`get_site_composition\`: every page's placements, each component's status,
|
|
4783
|
+
and \`verdict.everyPageComposed\` with its reasons. Publishing needs the OWNER's approval in
|
|
4784
|
+
the dashboard (evidence via \`submit_section_manifest\` \u2192 \`submit_section_validation\`), then
|
|
4785
|
+
\`publish_components\`. No tool here can grant that approval, and there is no bulk approve.
|
|
4786
|
+
|
|
4787
|
+
**Order:** get_site_composition \u2192 (get_componentize_plan \u2192 componentize_sections) \u2192
|
|
4788
|
+
create_components \u2192 compose_pages \u2192 update_layout { commands } \u2192 evidence + owner approval \u2192
|
|
4789
|
+
publish_components \u2192 publish the pages / publish_layout \u2192 get_site_composition \u2192 get_next_steps.
|
|
4790
|
+
Prefer the batch tools whenever there is more than three of anything.`;
|
|
4408
4791
|
var LAYOUT_FLOW = `### Project/page Layout authoring \u2192 \`get_layout\` / \`update_layout\` / \`publish_layout\`
|
|
4409
4792
|
Edit the project's draft Global Layout or one page's draft override. \`update_layout\` never
|
|
4410
4793
|
changes the live site; \`publish_layout\` takes the Global Layout draft live (page overrides go
|
|
@@ -4491,6 +4874,8 @@ ${FORM_FLOW}
|
|
|
4491
4874
|
|
|
4492
4875
|
${COMPONENT_FLOW}
|
|
4493
4876
|
|
|
4877
|
+
${COMPONENTIZE_SITE_FLOW}
|
|
4878
|
+
|
|
4494
4879
|
${LAYOUT_FLOW}
|
|
4495
4880
|
|
|
4496
4881
|
${BUILD_SITE_FLOW}
|
|
@@ -4642,6 +5027,36 @@ On 409 slug_taken, offer an alternative slug; on 401/403, the MCP key needs (re)
|
|
|
4642
5027
|
]
|
|
4643
5028
|
})
|
|
4644
5029
|
);
|
|
5030
|
+
server.registerPrompt(
|
|
5031
|
+
"componentize_site",
|
|
5032
|
+
{
|
|
5033
|
+
title: "Componentize the entire website (guided)",
|
|
5034
|
+
description: "Turn every section of a site into a reusable component, organised into groups editors can browse: audit with get_site_composition, create the components in one call, compose every page, then publish what the owner has approved.",
|
|
5035
|
+
argsSchema: {
|
|
5036
|
+
request: z2.string().optional().describe("scope or emphasis, e.g. 'marketing pages only'")
|
|
5037
|
+
}
|
|
5038
|
+
},
|
|
5039
|
+
({ request }) => ({
|
|
5040
|
+
messages: [
|
|
5041
|
+
{
|
|
5042
|
+
role: "user",
|
|
5043
|
+
content: {
|
|
5044
|
+
type: "text",
|
|
5045
|
+
text: `Componentize this entire website in BetterCMS.${request ? ` Scope: "${request}".` : ""}
|
|
5046
|
+
|
|
5047
|
+
${COMPONENTIZE_SITE_FLOW}
|
|
5048
|
+
|
|
5049
|
+
${STRUCTURE_RULE}
|
|
5050
|
+
|
|
5051
|
+
Confirm the component library with the user before creating it. End with the summary \xA712 asks
|
|
5052
|
+
for: the group hierarchy, each family and its variants, what you left in the site's code because
|
|
5053
|
+
the platform does not model it, and which components are still waiting for the owner's approval
|
|
5054
|
+
in the dashboard \u2014 those render as an empty string until they are published.`
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
]
|
|
5058
|
+
})
|
|
5059
|
+
);
|
|
4645
5060
|
server.registerPrompt(
|
|
4646
5061
|
"build_site",
|
|
4647
5062
|
{
|
|
@@ -4784,7 +5199,7 @@ function buildServer(deps) {
|
|
|
4784
5199
|
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
4785
5200
|
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
4786
5201
|
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
4787
|
-
instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to
|
|
5202
|
+
instructions: "When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication."
|
|
4788
5203
|
}
|
|
4789
5204
|
);
|
|
4790
5205
|
server.registerResource(
|