@bettercms-ai/mcp 0.65.2 → 0.66.1
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 +100 -55
- package/dist/index.js.map +1 -1
- package/dist/skills/bettercms/SKILL.md +4 -0
- package/dist/skills/bettercms/references/componentize.md +5 -3
- package/dist/skills/bettercms/references/components.md +1 -1
- package/dist/skills/bettercms/references/content-modeling.md +3 -1
- package/dist/skills/bettercms/references/finish-site.md +1 -0
- package/dist/skills/bettercms/references/forms-and-interactions.md +1 -0
- package/dist/skills/bettercms/references/getting-started.md +8 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2277,10 +2277,10 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
|
2277
2277
|
doc: {
|
|
2278
2278
|
schema: 1,
|
|
2279
2279
|
folders: [
|
|
2280
|
-
{ id: "home:pages", parentId: null, name: "Other pages", icon: "
|
|
2280
|
+
{ id: "home:pages", parentId: null, name: "Other pages", icon: "documents", sort: 1e3 },
|
|
2281
2281
|
{ id: "home:collections", parentId: null, name: "Shared", icon: null, sort: 1001 },
|
|
2282
|
-
{ id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "
|
|
2283
|
-
{ id: "fld_std_products", parentId: null, name: "Products", icon: "
|
|
2282
|
+
{ id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "document-text", sort: 0 },
|
|
2283
|
+
{ id: "fld_std_products", parentId: null, name: "Products", icon: "basket", sort: 1 }
|
|
2284
2284
|
],
|
|
2285
2285
|
items: [
|
|
2286
2286
|
{ kind: "page", id: "pg_home", folderId: "home:root", sort: 0, icon: null },
|
|
@@ -2293,7 +2293,7 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
|
2293
2293
|
{ kind: "collection", id: "cm_tags", folderId: "home:collections", sort: 0, icon: null },
|
|
2294
2294
|
{ kind: "page", id: "pg_about", folderId: "home:pages", sort: 0, icon: null },
|
|
2295
2295
|
{ kind: "page", id: "pg_contact", folderId: "home:pages", sort: 1, icon: null },
|
|
2296
|
-
{ kind: "page", id: "pg_404", folderId: "home:pages", sort: 2, icon: "
|
|
2296
|
+
{ kind: "page", id: "pg_404", folderId: "home:pages", sort: 2, icon: "code" }
|
|
2297
2297
|
]
|
|
2298
2298
|
}
|
|
2299
2299
|
};
|
|
@@ -2337,8 +2337,9 @@ ever changes.
|
|
|
2337
2337
|
7. **Settings closes the sidebar.** \`home:globals\` ("Settings") holds NO pages or collections \u2014
|
|
2338
2338
|
navigation, header, footer, site settings and SEO defaults open from it in the dashboard. The
|
|
2339
2339
|
three system folders always sort BELOW the folders you make: \u2026 Other pages, Shared, Settings.
|
|
2340
|
-
8. **Icons** (
|
|
2341
|
-
for docs, \`
|
|
2340
|
+
8. **Icons** (kit names, bettercms://icons/dock): \`document-text\` for posts, \`basket\` for products,
|
|
2341
|
+
\`book\` for docs, \`documents\` for Other pages, \`folder\` by default. Lucide names already stored
|
|
2342
|
+
still draw.
|
|
2342
2343
|
9. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
|
|
2343
2344
|
reason the user gave you.
|
|
2344
2345
|
10. **Organisation only.** Never rename a page, change a slug, touch a schema or move content to
|
|
@@ -2367,8 +2368,8 @@ What the structure writes refuse, and the code each refusal carries:
|
|
|
2367
2368
|
refuses a kind mismatch: it returns \`warnings\` and \`kindMismatches\` instead.
|
|
2368
2369
|
- **Page-bound models** are refused with 400 \`NAV_PAGE_BOUND_MODEL\` naming their page; organise
|
|
2369
2370
|
the page, by its page id.
|
|
2370
|
-
- **Shape:** folders nest at most 4 levels deep; names are 1-80 characters; icons are
|
|
2371
|
-
names (a-z, 0-9, dashes) and null clears one.
|
|
2371
|
+
- **Shape:** folders nest at most 4 levels deep; names are 1-80 characters; icons are kit
|
|
2372
|
+
names (bettercms://icons/dock; a-z, 0-9, dashes) and null clears one.
|
|
2372
2373
|
- **Access:** 403 \`SCHEMA_ACCESS_REQUIRED\` (rule 12).
|
|
2373
2374
|
- **Version:** 412 \`VERSION_CONFLICT\` (workflow step 4).
|
|
2374
2375
|
|
|
@@ -2676,9 +2677,17 @@ var NEW_TOOL_TEXT = {
|
|
|
2676
2677
|
search_docs: "Search the BetterCMS documentation. Returns the best pages (title, url, breadcrumbs, snippet). Use it before guessing how a feature works; `meta.unavailable` means docs search is down right now.",
|
|
2677
2678
|
generate_image: "Generate an image with AI and add it to the Media Library (5 AI credits; free with the workspace's own Together key). `width`/`height` are 256-1440 in multiples of 16 (default 1024x768). Returns the new asset (id, url, altText): use the url as an 'image' field value. Always set `altText`.",
|
|
2678
2679
|
refresh_search_index: "Tell search engines and site search about published changes; costs nothing. action 'indexnow' submits `urls` (this site's own, up to 1000) to IndexNow, or every change since the last submission when omitted. action 'reindex' rebuilds the project's site-search index. 503 means the index is unreachable: try again later.",
|
|
2680
|
+
editor_setup: "Make the live build editable. A build imports only SEO/meta, media, brand and the models bcms-content.json declares; this derives the rest from the live release: pages, collections, forms, bindings, button and section components (when the authoring preference is components), Component Groups and the Content sidebar. Run it after an import or a rebuild, before the conversion brief. Idempotent: `ready` when it already ran for the live sha, else it starts (`running`; poll with `check: true` or get_deploy_status `editorSetup`). `state`: never or stale (run it), running, ready, failed. 409 = nothing is live yet: deploy first. Then get_next_steps.",
|
|
2679
2681
|
transform_image: "Edit an existing Media Library image with an instruction (e.g. 'remove the background', 'make it dusk'); the result is saved as a NEW asset and the original is kept (5 AI credits). Returns the new asset (id, url, altText)."
|
|
2680
2682
|
};
|
|
2681
2683
|
var PARAM_TEXT = {
|
|
2684
|
+
editorSetupForce: "re-run a setup already ready for the live build",
|
|
2685
|
+
editorSetupCheck: "true = only report the state; starts nothing",
|
|
2686
|
+
pageDynamic: "a dynamic (collection template) page only",
|
|
2687
|
+
previewPath: "its route pattern, e.g. /blog/{slug}; null clears",
|
|
2688
|
+
pageModelId: "an existing collection this page lists (when it has no model yet)",
|
|
2689
|
+
formIgnored: "true = not one of the site's forms (a search box, a widget); false restores it",
|
|
2690
|
+
componentizeCodemod: "true = componentize nothing; run the section-order codemod (`--componentize`) in the connected repo on its own Actions, which commits `<Sections>` templates for the next build. After the components and pages are published. `skipped` = no repo to commit to: run the codemod yourself",
|
|
2682
2691
|
where: "field filters: {field: value} is equality, {field: {op: value}} with op eq|ne|gt|gte|lt|lte|contains|in, and an array means `in`. System fields slug, status, publishedAt, locale work anywhere; a data field needs modelId. Compares the served copy (live for published, working for draft)",
|
|
2683
2692
|
sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
|
|
2684
2693
|
page: "1-based page of `limit` rows; the response's meta says totalPages",
|
|
@@ -3227,7 +3236,7 @@ function authPrompt(err) {
|
|
|
3227
3236
|
var currentProject = new AsyncLocalStorage();
|
|
3228
3237
|
var currentBranch = new AsyncLocalStorage();
|
|
3229
3238
|
var projectIdArg = z.string().min(1).optional().describe(
|
|
3230
|
-
"
|
|
3239
|
+
"project id (list_projects); workspace-wide grants only"
|
|
3231
3240
|
);
|
|
3232
3241
|
var branchIdArg = z.string().min(1).optional().describe("content branch id (from list_branches); omit to work on main");
|
|
3233
3242
|
var withProjectId = (def) => ({
|
|
@@ -3510,7 +3519,8 @@ function buildToolDefs(deps) {
|
|
|
3510
3519
|
formId: z.string().min(1).describe("form id (from list_forms)"),
|
|
3511
3520
|
name: z.string().optional(),
|
|
3512
3521
|
fields: z.array(formFieldObject).optional().describe("REPLACES the field array \u2014 include all fields to keep"),
|
|
3513
|
-
...formSettingsShape
|
|
3522
|
+
...formSettingsShape,
|
|
3523
|
+
ignored: z.boolean().optional().describe(PARAM_TEXT.formIgnored)
|
|
3514
3524
|
});
|
|
3515
3525
|
const componentPropObject = z.object({
|
|
3516
3526
|
key: z.string().min(1),
|
|
@@ -3559,7 +3569,7 @@ function buildToolDefs(deps) {
|
|
|
3559
3569
|
projectId: z.string().min(1).optional().describe("required only for a workspace-scoped grant")
|
|
3560
3570
|
});
|
|
3561
3571
|
const layoutBinding = z.object({ inputId: z.string().min(1), fieldId: z.string().min(1) });
|
|
3562
|
-
const layoutIcon = z.string().min(1).max(80).regex(/^(?:[a-z0-9]+(?:-[a-z0-9]+)*|[A-Z][A-Za-z0-9]+)$/).refine((value) => LAYOUT_SECTION_ICON_SET.has(value), "Select a
|
|
3572
|
+
const layoutIcon = z.string().min(1).max(80).regex(/^(?:[a-z0-9]+(?:-[a-z0-9]+)*|[A-Z][A-Za-z0-9]+)$/).refine((value) => DOCK_ICON_NAME_SET.has(value) || LAYOUT_SECTION_ICON_SET.has(value), "Select a kit icon (bettercms://icons/dock) or a legacy Lucide icon").describe("kit icon name from bettercms://icons/dock (e.g. 'document-text'); legacy Lucide names (kebab-case or CamelCase) remain accepted");
|
|
3563
3573
|
const sectionTarget = { sectionId: z.string().min(1) };
|
|
3564
3574
|
const layoutCommand = z.discriminatedUnion("type", [
|
|
3565
3575
|
z.object({ type: z.literal("set-section-values"), ...sectionTarget, values: z.record(z.string(), z.unknown()) }),
|
|
@@ -3833,7 +3843,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3833
3843
|
def(
|
|
3834
3844
|
"set_content_structure",
|
|
3835
3845
|
"Replace the Content sidebar structure",
|
|
3836
|
-
`Replace the whole Content sidebar document in one write. Follow the structure standard (read ${STRUCTURE_PLAYBOOK_URI} first; suggest_content_structure returns a ready doc). Send the complete \`doc\` and the \`version\` get_content_structure returned (0 when the project has none yet). Validated exactly like the dashboard: unique folder ids, every parentId/folderId a folder in the doc, no cycles, depth \u22644, names 1-80 chars, lucide
|
|
3846
|
+
`Replace the whole Content sidebar document in one write. Follow the structure standard (read ${STRUCTURE_PLAYBOOK_URI} first; suggest_content_structure returns a ready doc). Send the complete \`doc\` and the \`version\` get_content_structure returned (0 when the project has none yet). Validated exactly like the dashboard: unique folder ids, every parentId/folderId a folder in the doc, no cycles, depth \u22644, names 1-80 chars, icons are kit names (bettercms://icons/dock; legacy lucide names still draw). A 412 VERSION_CONFLICT means someone changed it since you read it: read again and re-apply. Returns the new version and a diff. ${STRUCTURE_FENCE}`,
|
|
3837
3847
|
z.object({
|
|
3838
3848
|
doc: z.object({
|
|
3839
3849
|
schema: z.literal(1),
|
|
@@ -3841,7 +3851,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3841
3851
|
id: z.string().min(1).describe("your own stable id, up to 64 chars"),
|
|
3842
3852
|
parentId: z.string().nullable().describe("parent folder id; null = top level"),
|
|
3843
3853
|
name: z.string(),
|
|
3844
|
-
icon: z.string().nullable().describe("
|
|
3854
|
+
icon: z.string().nullable().describe("kit icon name (bettercms://icons/dock) or null"),
|
|
3845
3855
|
sort: z.number()
|
|
3846
3856
|
})),
|
|
3847
3857
|
items: z.array(z.object({
|
|
@@ -3863,7 +3873,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3863
3873
|
z.object({
|
|
3864
3874
|
name: z.string().min(1).describe("the folder's label, 1-80 chars"),
|
|
3865
3875
|
parentId: z.string().min(1).optional().describe("optional parent folder id (from get_content_structure); omit it, or send 'root', for the top level"),
|
|
3866
|
-
icon: z.string().min(1).optional().describe("optional
|
|
3876
|
+
icon: z.string().min(1).optional().describe("optional kit icon name (bettercms://icons/dock), e.g. 'folder'")
|
|
3867
3877
|
}).shape,
|
|
3868
3878
|
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/folders`, { name: a.name, parentId: a.parentId, icon: a.icon }))
|
|
3869
3879
|
),
|
|
@@ -3929,7 +3939,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3929
3939
|
"what the id IS on this branch \u2014 VERIFIED, not assumed: an id is either a page or a collection, never both, and a mismatch is refused with 400 NAV_KIND_MISMATCH naming the kind to use. Call it again with that kind, which also clears the wrong-kind entry."
|
|
3930
3940
|
),
|
|
3931
3941
|
id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
|
|
3932
|
-
icon: z.string().min(1).nullable().describe("
|
|
3942
|
+
icon: z.string().min(1).nullable().describe("kit icon name (bettercms://icons/dock), e.g. 'document-text'; null clears it")
|
|
3933
3943
|
}).shape,
|
|
3934
3944
|
async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/icon`, { kind: a.kind, id: a.id, icon: a.icon }))
|
|
3935
3945
|
),
|
|
@@ -4138,7 +4148,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4138
4148
|
def(
|
|
4139
4149
|
"set_authoring_preference",
|
|
4140
4150
|
"Set the site's authoring architecture",
|
|
4141
|
-
"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 and promote_project both refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, carrying this project's real page counts to show the user. Unset,
|
|
4151
|
+
"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 and promote_project both refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, carrying this project's real page counts to show the user. Unset, editor_setup componentizes nothing; only `components` lets it place sections. 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. Asked once per project; re-callable if the user changes their mind.",
|
|
4142
4152
|
// Optional in the schema for exactly the reason `framework` is above: a required arg is
|
|
4143
4153
|
// rejected by the SDK before the handler runs, which would kill the elicitation below
|
|
4144
4154
|
// and leave the model guessing. Optional here, answered by a human there. The backend
|
|
@@ -4292,14 +4302,14 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4292
4302
|
"update_page",
|
|
4293
4303
|
"Edit a page",
|
|
4294
4304
|
"Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing it REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`). A `blockJson` that swaps a component block to another component comes back with `warnings` (COMPONENT_SWAP_DROPS_OVERRIDES) naming the overrides the new component does not render; they stay on the block, so swapping back restores them. It does NOT change the field SCHEMA \u2014 use add_field (pageId) / set_page_content for that. Renaming the slug keeps content intact. Publishing copies the draft blocks live in the same call \u2014 and on a singleton it publishes the page's content entry too; if the entry's workflow stage refuses, the page still goes live and `warnings` carries ENTRY_STILL_DRAFT with the entry id and the reason. STRUCTURED DATA is native SEO, never a field: leave it out and the page gets Automatic JSON-LD from its content (WebSite on the home page, Blog/CollectionPage on a collection's list page, WebPage otherwise); `schemaType` picks an explicit schema.org type whose properties are filled from the content; `schema` is pasted JSON-LD and wins over both (null clears it). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
|
|
4295
|
-
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE) }).shape,
|
|
4305
|
+
z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1), title: z.string().optional(), slug: z.string().optional(), blockJson: z.array(blockObject).optional().describe("REPLACES the page's block composition"), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", "WebPage", "AboutPage", "ContactPage", "CollectionPage", "Blog", "BlogPosting", "Article", "Product", "FAQPage", "Event", "Organization", "LocalBusiness"]).optional().describe("structured data type; 'auto' (the default) derives it from the content"), schema: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).nullable().optional().describe("custom JSON-LD; wins over schemaType; null clears it"), status: z.enum(["draft", "published"]).optional(), scheduledAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageScheduledAt), unpublishAt: z.string().datetime().nullable().optional().describe(PARAM_TEXT.pageUnpublishAt), locale: z.string().nullable().optional().describe(PARAM_TEXT.locale), translationOf: z.string().nullable().optional().describe(PARAM_TEXT.translationOf), ifMatch: z.number().int().min(0).optional().describe(UPDATE_PAGE_IF_MATCH_NOTE), dynamic: z.object({ previewPath: z.string().nullable().optional().describe(PARAM_TEXT.previewPath), modelId: z.string().min(1).optional().describe(PARAM_TEXT.pageModelId) }).optional().describe(PARAM_TEXT.pageDynamic) }).shape,
|
|
4296
4306
|
async (c, a) => {
|
|
4297
4307
|
const res = await c.fetchJSON(
|
|
4298
4308
|
c.url(`/management/pages/${s(a.pageId)}/meta`),
|
|
4299
4309
|
{
|
|
4300
4310
|
method: "PATCH",
|
|
4301
4311
|
...a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {},
|
|
4302
|
-
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf })
|
|
4312
|
+
body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: a.blockJson, metaTitle: a.metaTitle, metaDescription: a.metaDescription, schemaType: a.schemaType, schema: a.schema, status: a.status, noindex: a.noindex, canonical: a.canonical, og: a.og, twitter: a.twitter, scheduledAt: a.scheduledAt, unpublishAt: a.unpublishAt, locale: a.locale, translationOf: a.translationOf, ...a.dynamic })
|
|
4303
4313
|
}
|
|
4304
4314
|
);
|
|
4305
4315
|
const note = res.redirectNote;
|
|
@@ -4721,7 +4731,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4721
4731
|
def(
|
|
4722
4732
|
"componentize_sections",
|
|
4723
4733
|
"Turn this site's derived sections into components",
|
|
4724
|
-
"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, so click-to-edit and the coverage meter do not change (`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: each id is an existing same-family component a NEW one would sit beside (reuse is by identity and slug, never family). 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 run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. Every component it CREATES is filed into a Component Group (see `group`). When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized, the plan reports them `NOT_A_SECTION` / `DECLINED` and stops proposing them, and a plan that proposes nothing undeclined is done.",
|
|
4734
|
+
"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, so click-to-edit and the coverage meter do not change (`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: each id is an existing same-family component a NEW one would sit beside (reuse is by identity and slug, never family). 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`. Every component it CREATES is filed into a Component Group (see `group`). When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized, the plan reports them `NOT_A_SECTION` / `DECLINED` and stops proposing them, and a plan that proposes nothing undeclined is done.",
|
|
4725
4735
|
z.object({
|
|
4726
4736
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
4727
4737
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
@@ -4731,9 +4741,10 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4731
4741
|
dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
|
|
4732
4742
|
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (a folder of the dashboard's Components tab; by NAME, created when missing), else its section family: nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A reused component keeps its Group; the receipt's `components.groups` lists the Groups filed into."),
|
|
4733
4743
|
decline: z.boolean().optional().describe("true = record `sections` as NOT sections (nothing is componentized; the plan stops proposing them). false = offer them again. Needs `sections`."),
|
|
4734
|
-
joinTwins: z.boolean().optional().describe("true = move each locale copy (`/fr`) still on its own component onto its twin's (`bindPaths` keeps its fields). A dry run lists each in `joins`. Whole plan only.")
|
|
4744
|
+
joinTwins: z.boolean().optional().describe("true = move each locale copy (`/fr`) still on its own component onto its twin's (`bindPaths` keeps its fields). A dry run lists each in `joins`. Whole plan only."),
|
|
4745
|
+
codemod: z.boolean().optional().describe(PARAM_TEXT.componentizeCodemod)
|
|
4735
4746
|
}).shape,
|
|
4736
|
-
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline, joinTwins: a.joinTwins }))
|
|
4747
|
+
async (c, a) => ok(a.codemod === true ? "Section-order codemod." : "Componentized the sections.", await data(c, "POST", a.codemod === true ? `/management/projects/current/section-order` : `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline, joinTwins: a.joinTwins, codemod: a.codemod }))
|
|
4737
4748
|
),
|
|
4738
4749
|
def(
|
|
4739
4750
|
"get_analytics_overview",
|
|
@@ -4949,9 +4960,21 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4949
4960
|
def(
|
|
4950
4961
|
"get_deploy_status",
|
|
4951
4962
|
"Get deploy/build status",
|
|
4952
|
-
"Get the connected project's deploy/build status: state (idle|queued|building|
|
|
4963
|
+
"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` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one; a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
|
|
4953
4964
|
z.object({}).shape,
|
|
4954
|
-
|
|
4965
|
+
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
4966
|
+
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
|
|
4967
|
+
),
|
|
4968
|
+
def(
|
|
4969
|
+
"editor_setup",
|
|
4970
|
+
"Set up editing for the live build",
|
|
4971
|
+
NEW_TOOL_TEXT.editor_setup,
|
|
4972
|
+
z.object({
|
|
4973
|
+
force: z.boolean().optional().describe(PARAM_TEXT.editorSetupForce),
|
|
4974
|
+
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck)
|
|
4975
|
+
}).shape,
|
|
4976
|
+
// The route answers `{ state, … }` with no `data` envelope, so the whole body is the result.
|
|
4977
|
+
async (c, a) => ok("Editor setup.", await c.fetchJSON(c.url(`/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
|
|
4955
4978
|
),
|
|
4956
4979
|
// ── Deploy to the user's own host (parity with remote /mcp) ──
|
|
4957
4980
|
// A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
|
|
@@ -5890,7 +5913,7 @@ A collection (\`create_content_model\`) plus entries.
|
|
|
5890
5913
|
|
|
5891
5914
|
Most real sites are both: components for the marketing pages, a collection for the blog.
|
|
5892
5915
|
Decide before your first call; converting later means rewriting content \u2014 unless the site was
|
|
5893
|
-
DERIVED
|
|
5916
|
+
DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every field.
|
|
5894
5917
|
|
|
5895
5918
|
## 2. The decision tree
|
|
5896
5919
|
|
|
@@ -5967,8 +5990,8 @@ sibling calls it \`headline\` silently drops that content on the swap.
|
|
|
5967
5990
|
Pair \`sectionType\` with a library \`category\` (hero, content, social-proof, conversion) or
|
|
5968
5991
|
the component never appears in the editor's "Add a section" picker.
|
|
5969
5992
|
|
|
5970
|
-
**An IMPORTED site gets there without you writing any of that.** Its pages
|
|
5971
|
-
|
|
5993
|
+
**An IMPORTED site gets there without you writing any of that.** Its pages are derived by
|
|
5994
|
+
\`editor_setup\` \u2014 flat field groups (\`hero-title\`, \`faq-question\`, the repeaters folded as \`group-*\`)
|
|
5972
5995
|
and an empty \`blockJson\` \u2014 so there is nothing in the picker and no section to swap. Read
|
|
5973
5996
|
\`get_componentize_plan\`: it groups those fields into sections, names each family, and proposes
|
|
5974
5997
|
one component per SHAPE (the same hero on two pages is one component with two placements),
|
|
@@ -6096,14 +6119,27 @@ written to the wrong project can only be demoted (\`sectionType: null\`) and lef
|
|
|
6096
6119
|
|
|
6097
6120
|
## 10. You imported a site, or you are about to deploy one
|
|
6098
6121
|
|
|
6122
|
+
**A build imports what the build itself ships, and nothing more:** SEO and meta, every media
|
|
6123
|
+
asset, the brand, and the models the repository declares in \`bcms-content.json\`. Everything
|
|
6124
|
+
that makes the site EDITABLE \u2014 pages, inferred collections, forms, bindings, button and section
|
|
6125
|
+
components \u2014 is \`editor_setup\`, run on demand against the live build. The import recipe:
|
|
6126
|
+
|
|
6127
|
+
import (import_github_repo / deploy_project) -> editor_setup (poll { check: true } until ready)
|
|
6128
|
+
-> the authoring answer -> componentize_sections, or get_conversion_brief (\xA713)
|
|
6129
|
+
-> the codemods (componentize_sections { codemod: true } runs the section-order one in the repo)
|
|
6130
|
+
-> submit_conversion_receipt / submit_componentize_receipt -> deploy
|
|
6131
|
+
|
|
6132
|
+
\`get_deploy_status\` \`editorSetup\` (and get_next_steps) say when it has not run for the live
|
|
6133
|
+
build: \`never\` or \`stale\` \u2192 run it before anything below.
|
|
6134
|
+
|
|
6099
6135
|
\`deploy_project\` and \`promote_project\` both answer **409
|
|
6100
6136
|
AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
|
|
6101
6137
|
asked ONCE per project, ever. It is not an error to retry or route around: read the message
|
|
6102
6138
|
out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
6103
6139
|
|
|
6104
|
-
Until someone answers, the
|
|
6105
|
-
|
|
6106
|
-
|
|
6140
|
+
Until someone answers, \`editor_setup\` derives the pages but componentizes nothing (the
|
|
6141
|
+
preference is null). Once \`components\` is recorded, each \`editor_setup\` componentizes the
|
|
6142
|
+
derived pages' NEW sections in bind mode; \`fields\` stops it for good. And a run over the WHOLE plan splits a section component an earlier run placed on
|
|
6107
6143
|
2 pages into one per page (the first page keeps it, the others get a clone), so two pages' heroes
|
|
6108
6144
|
stop editing each other; a run narrowed to one page or section never splits.
|
|
6109
6145
|
|
|
@@ -6112,9 +6148,9 @@ It exists because an imported site arrives **field-driven whether anyone chose t
|
|
|
6112
6148
|
and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
|
|
6113
6149
|
marketing site it is the wrong answer. So the platform stops once, at the last moment it is
|
|
6114
6150
|
still cheap \u2014 and on a derived site that moment costs nothing, because the componentize lane
|
|
6115
|
-
below reuses every field
|
|
6151
|
+
below reuses every field \`editor_setup\` already derived.
|
|
6116
6152
|
|
|
6117
|
-
**Recommend \`components\`.** On a site whose pages were DERIVED
|
|
6153
|
+
**Recommend \`components\`.** On a site whose pages were DERIVED by \`editor_setup\` \u2014 the site \xA713
|
|
6118
6154
|
describes \u2014 it redoes nothing: the componentize lane turns each top-level field GROUP into a
|
|
6119
6155
|
component placement, and the page keeps its fields, its values and its bindings. In this order:
|
|
6120
6156
|
|
|
@@ -6122,7 +6158,7 @@ component placement, and the page keeps its fields, its values and its bindings.
|
|
|
6122
6158
|
2. get_componentize_plan one component per section, computed live; creates nothing. It
|
|
6123
6159
|
reads this project's PUBLISHED pages, so no deploy is needed
|
|
6124
6160
|
to reach it \u2014 the deploy is the last step, not the first.
|
|
6125
|
-
On a
|
|
6161
|
+
On a DERIVED site, each editor_setup after this answer
|
|
6126
6162
|
runs this plan in bind mode for NEW sections (it never splits
|
|
6127
6163
|
or joins a placement, and runs nothing while the answer is
|
|
6128
6164
|
unset) and PUBLISHES what it created, so after one expect
|
|
@@ -6137,8 +6173,11 @@ component placement, and the page keeps its fields, its values and its bindings.
|
|
|
6137
6173
|
5. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
|
|
6138
6174
|
5b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
|
|
6139
6175
|
6. publish the pages
|
|
6140
|
-
7.
|
|
6141
|
-
|
|
6176
|
+
7. componentize_sections { digest, codemod: true } the section-order codemod
|
|
6177
|
+
(\`--componentize\`) on the repo's own Actions, so its templates
|
|
6178
|
+
render these sections from \`pages[].blocks\`; \`skipped\` = no
|
|
6179
|
+
repo it may commit to: run \`npx @bettercms-ai/convert
|
|
6180
|
+
--componentize\` in your checkout
|
|
6142
6181
|
8. deploy the ONLY deploy this sequence needs
|
|
6143
6182
|
|
|
6144
6183
|
Step 5 is not a formality: \`componentize_sections\` and \`create_component\` both land components
|
|
@@ -6371,10 +6410,10 @@ JSON. On an IMPORTED site, never materialize a builtin: read the repo's own comp
|
|
|
6371
6410
|
(\`src/components\`, its sections folder) and make ONE CMS component per source component, named
|
|
6372
6411
|
as the source names it \u2014 the site's Pricing section is "Pricing", not "Section 2" or "Simple".
|
|
6373
6412
|
On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
|
|
6374
|
-
of this in one call, and once \`components\` is recorded each
|
|
6413
|
+
of this in one call, and once \`components\` is recorded each \`editor_setup\` of a derived site
|
|
6375
6414
|
runs it in bind mode and publishes the components, so expect ALREADY_COMPONENTIZED \u2014 the placements it wrote are
|
|
6376
|
-
still DRAFT blocks on the pages, so publish the pages, then \`
|
|
6377
|
-
--componentize
|
|
6415
|
+
still DRAFT blocks on the pages, so publish the pages, then \`componentize_sections { codemod: true }\`
|
|
6416
|
+
(or \`npx @bettercms-ai/convert --componentize\` in your checkout).
|
|
6378
6417
|
|
|
6379
6418
|
**3. The hierarchy editors see is the GROUP.** \`create_component\` and \`update_component\` take
|
|
6380
6419
|
\`group\`: a NAME, resolved against the project's component groups and created when missing. That
|
|
@@ -6479,9 +6518,10 @@ anything. See \xA79.
|
|
|
6479
6518
|
|
|
6480
6519
|
**Which recipe.** There are TWO below and they are not alternatives you pick by taste \u2014 call
|
|
6481
6520
|
\`get_binding_report\` and \`get_conversion_brief\` first and let the answer choose. A brief that
|
|
6482
|
-
comes back WITH PAGES means this project was imported and
|
|
6483
|
-
|
|
6484
|
-
|
|
6521
|
+
comes back WITH PAGES means this project was imported and \`editor_setup\` derived its schema and
|
|
6522
|
+
values already \u2192 **Recipe A**. On an import whose \`get_deploy_status\` \`editorSetup\` is \`never\`
|
|
6523
|
+
or \`stale\`, run \`editor_setup\` FIRST and read the brief again. A 404 or an empty brief after
|
|
6524
|
+
that means the site is not in the CMS yet \u2192 **Recipe B**. \`get_next_steps\` works on every plan and names the next unfinished step
|
|
6485
6525
|
either way.
|
|
6486
6526
|
|
|
6487
6527
|
**If you know Sanity, this is the same shape under different names:**
|
|
@@ -6497,8 +6537,8 @@ either way.
|
|
|
6497
6537
|
draft reads
|
|
6498
6538
|
Presentation tool overlays -> the visual editor canvas, framing your own build
|
|
6499
6539
|
|
|
6500
|
-
**Imported site whose pages were DERIVED.** If this project was imported and
|
|
6501
|
-
its build, the schema and the values already exist \u2014 a page per route, a field per element, and
|
|
6540
|
+
**Imported site whose pages were DERIVED.** If this project was imported and \`editor_setup\` ran
|
|
6541
|
+
over its build, the schema and the values already exist \u2014 a page per route, a field per element, and
|
|
6502
6542
|
the original copy carried on each field as its \`defaultValue\`. Call \`get_conversion_brief\`
|
|
6503
6543
|
first: it lists those pages, their routes, every bindable path with its current and original
|
|
6504
6544
|
value, and the attributes to declare. SKIP Recipe B's steps 3 and 4 below and bind the keys it
|
|
@@ -6902,7 +6942,7 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
|
|
|
6902
6942
|
|
|
6903
6943
|
1. \`list_projects\`; pass \`projectId\`${projectId ? ` (${projectId})` : ""} on every project tool. Not listed = the grant is on
|
|
6904
6944
|
another workspace: have the user re-authenticate there. Never convert a different project.
|
|
6905
|
-
2. \`get_conversion_plan
|
|
6945
|
+
2. \`editor_setup\` until it is \`ready\` (poll with \`check: true\`), then \`get_conversion_plan\`; on a 404 \`no-approved-plan\`,
|
|
6906
6946
|
\`get_conversion_brief { complete: true }\` and follow \`cursor\` to the LAST page, keeping every
|
|
6907
6947
|
page \u2014 a codemod fed the capped brief converts a truncated site. Write them to \`brief.json\`.
|
|
6908
6948
|
3. \`pull_project_source\` (or clone the \`github\` remote it names) and work from the SOURCE, never
|
|
@@ -7056,8 +7096,8 @@ Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form
|
|
|
7056
7096
|
2. **One component per distinct section**, one-offs included. \`sectionType\` is the family;
|
|
7057
7097
|
components sharing one are VARIANTS and must share prop KEYS. A one-off gets its own family
|
|
7058
7098
|
plus \`allowedOn: ["slug:<page>"]\`. Start from the \`builtin:*\` blueprints in
|
|
7059
|
-
\`list_components\`. On a DERIVED site run \`get_componentize_plan\` \u2192
|
|
7060
|
-
first (dryRun first, and confirm the plan with the user).
|
|
7099
|
+
\`list_components\`. On a DERIVED site (\`editor_setup\` derives it) run \`get_componentize_plan\` \u2192
|
|
7100
|
+
\`componentize_sections\` first (dryRun first, and confirm the plan with the user).
|
|
7061
7101
|
3. **Hierarchy** = \`group\` on create_component/update_component (a NAME; created when missing).
|
|
7062
7102
|
Use Global, Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\`
|
|
7063
7103
|
is the separate picker TAB.
|
|
@@ -7461,7 +7501,7 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
7461
7501
|
"import-site",
|
|
7462
7502
|
{
|
|
7463
7503
|
title: "Make an imported site editable (guided)",
|
|
7464
|
-
description: "After deploying an existing site:
|
|
7504
|
+
description: "After importing or deploying an existing site: run the editor setup, componentize or convert it, declare its presentation manifest, and publish everything that renders. A build makes a site LIVE, not EDITABLE \u2014 this flow closes that gap.",
|
|
7465
7505
|
argsSchema: {
|
|
7466
7506
|
request: z2.string().optional().describe("scope, e.g. 'all routes' or 'just the home page'")
|
|
7467
7507
|
}
|
|
@@ -7476,17 +7516,22 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
7476
7516
|
|
|
7477
7517
|
Read bettercms://playbook/schema section 11 first, then work this order:
|
|
7478
7518
|
|
|
7479
|
-
1.
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7519
|
+
1. EDITOR SETUP \u2014 the build imported only SEO/meta, media, brand and the models the repo
|
|
7520
|
+
declares. Call editor_setup and poll editor_setup { check: true } until \`ready\`: it derives
|
|
7521
|
+
the pages, collections, forms and bindings from the live build.
|
|
7522
|
+
2. AUTHORING GATE \u2014 if get_next_steps or a deploy (409 AUTHORING_DECISION_REQUIRED) asks,
|
|
7523
|
+
ask ME components-or-fields (do not choose), then set_authoring_preference.
|
|
7524
|
+
3. COMPONENTS: get_componentize_plan -> componentize_sections (dryRun first) -> publish the
|
|
7525
|
+
components and pages -> componentize_sections { codemod: true } (the section-order codemod in
|
|
7526
|
+
the repo). FIELDS: the convert_site loop (get_conversion_brief, the codemod, the receipt).
|
|
7527
|
+
Only where the setup derived nothing: create_page (slug = route) -> add_field {pageId} ->
|
|
7528
|
+
set_page_content with values EXACTLY equal to the rendered text (binding matches by value; a
|
|
7529
|
+
paraphrase binds nothing) -> update_page status 'published'.
|
|
7530
|
+
4. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
|
|
7486
7531
|
scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
|
|
7487
7532
|
redeploy, or structural draft previews render in a generic theme, not this site's design.
|
|
7488
|
-
|
|
7489
|
-
|
|
7533
|
+
5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
|
|
7534
|
+
a setup that has not run, and placed-but-unpublished components), or tell me why
|
|
7490
7535
|
an item is being left.
|
|
7491
7536
|
|
|
7492
7537
|
On 401/403, the MCP key needs (re)authorizing.`
|