@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 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: "files", sort: 1e3 },
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: "newspaper", sort: 0 },
2283
- { id: "fld_std_products", parentId: null, name: "Products", icon: "shopping-bag", sort: 1 }
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: "file-code" }
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** (lucide names): \`newspaper\` for posts, \`shopping-bag\` for products, \`book-open\`
2341
- for docs, \`files\` for Other pages, \`folder\` by default.
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 lucide
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
- "target project id (from list_projects); only for a workspace-wide grant."
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 supported Lucide icon").describe("Lucide icon name in canonical kebab-case; legacy CamelCase Layout icon names remain accepted");
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 icons. 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}`,
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("lucide icon name or null"),
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 lucide icon name, e.g. 'folder'")
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("lucide icon name, e.g. 'file-text'; null clears it")
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, the release lane already acts as `components`; only `fields` stops it. 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.",
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|categorizing|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `categorizing` means the bytes are live (`publishing` false) but the setup lanes (content, pages, bindings) are still running: not ready to edit yet. 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.",
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
- async (c) => ok("Deploy status.", await data(c, "GET", `/management/projects/deploy-status`))
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 at import, where the componentize lane in \xA710 reuses every field.
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 were derived at
5971
- import \u2014 flat field groups (\`hero-title\`, \`faq-question\`, the repeaters folded as \`group-*\`)
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 release lane behaves as \`components\`: the preference is null, and
6105
- each release still componentizes the derived pages in bind mode. \`fields\` is the only answer
6106
- that stops it. And a run over the WHOLE plan splits a section component an earlier run placed on
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 the import already derived.
6151
+ below reuses every field \`editor_setup\` already derived.
6116
6152
 
6117
- **Recommend \`components\`.** On a site whose pages were DERIVED at import \u2014 the site \xA713
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 site DERIVED at import, each release after this answer
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. npx @bettercms-ai/convert --componentize in the repo, so its templates render these
6141
- sections from \`pages[].blocks\`
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 release of a site derived at IMPORT
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 \`npx @bettercms-ai/convert
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 deployed, so its schema and values were
6483
- derived for you already \u2192 **Recipe A**. A 404 or an empty brief means the site is not in the CMS
6484
- yet \u2192 **Recipe B**. \`get_next_steps\` works on every plan and names the next unfinished step
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 made editable from
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\`. On a 404 \`no-approved-plan\`, call
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 \`componentize_sections\`
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: bring its content into the CMS so the visual editor can bind it, declare its presentation manifest, and publish everything that renders. A deploy makes a site LIVE, not EDITABLE \u2014 this flow closes that gap.",
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. AUTHORING GATE \u2014 if any deploy answered 409 AUTHORING_DECISION_REQUIRED, ask ME
7480
- components-or-fields (do not choose), then set_authoring_preference.
7481
- 2. CONTENT INTO THE CMS, per route: create_page (slug = route) -> add_field {pageId} (or
7482
- create_component + publish_component + component blocks) -> set_page_content with values
7483
- EXACTLY equal to the rendered text (binding matches by value; a paraphrase binds nothing)
7484
- -> update_page status 'published' (the canvas binds the PUBLISHED copy).
7485
- 3. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
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
- 4. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
7489
- content still only in the build, and placed-but-unpublished components), or tell me why
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.`