@bettercms-ai/mcp 0.65.1 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2676,9 +2676,17 @@ var NEW_TOOL_TEXT = {
2676
2676
  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
2677
  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
2678
  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.",
2679
+ 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
2680
  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
2681
  };
2681
2682
  var PARAM_TEXT = {
2683
+ editorSetupForce: "re-run a setup already ready for the live build",
2684
+ editorSetupCheck: "true = only report the state; starts nothing",
2685
+ pageDynamic: "a dynamic (collection template) page only",
2686
+ previewPath: "its route pattern, e.g. /blog/{slug}; null clears",
2687
+ pageModelId: "an existing collection this page lists (when it has no model yet)",
2688
+ formIgnored: "true = not one of the site's forms (a search box, a widget); false restores it",
2689
+ 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
2690
  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
2691
  sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
2684
2692
  page: "1-based page of `limit` rows; the response's meta says totalPages",
@@ -3227,7 +3235,7 @@ function authPrompt(err) {
3227
3235
  var currentProject = new AsyncLocalStorage();
3228
3236
  var currentBranch = new AsyncLocalStorage();
3229
3237
  var projectIdArg = z.string().min(1).optional().describe(
3230
- "target project id (from list_projects); only for a workspace-wide grant."
3238
+ "project id (list_projects); workspace-wide grants only"
3231
3239
  );
3232
3240
  var branchIdArg = z.string().min(1).optional().describe("content branch id (from list_branches); omit to work on main");
3233
3241
  var withProjectId = (def) => ({
@@ -3510,7 +3518,8 @@ function buildToolDefs(deps) {
3510
3518
  formId: z.string().min(1).describe("form id (from list_forms)"),
3511
3519
  name: z.string().optional(),
3512
3520
  fields: z.array(formFieldObject).optional().describe("REPLACES the field array \u2014 include all fields to keep"),
3513
- ...formSettingsShape
3521
+ ...formSettingsShape,
3522
+ ignored: z.boolean().optional().describe(PARAM_TEXT.formIgnored)
3514
3523
  });
3515
3524
  const componentPropObject = z.object({
3516
3525
  key: z.string().min(1),
@@ -4138,7 +4147,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4138
4147
  def(
4139
4148
  "set_authoring_preference",
4140
4149
  "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.",
4150
+ "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
4151
  // Optional in the schema for exactly the reason `framework` is above: a required arg is
4143
4152
  // rejected by the SDK before the handler runs, which would kill the elicitation below
4144
4153
  // and leave the model guessing. Optional here, answered by a human there. The backend
@@ -4292,14 +4301,14 @@ ${d.outline}` : "Proposed Content structure.", d);
4292
4301
  "update_page",
4293
4302
  "Edit a page",
4294
4303
  "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,
4304
+ 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
4305
  async (c, a) => {
4297
4306
  const res = await c.fetchJSON(
4298
4307
  c.url(`/management/pages/${s(a.pageId)}/meta`),
4299
4308
  {
4300
4309
  method: "PATCH",
4301
4310
  ...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 })
4311
+ 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
4312
  }
4304
4313
  );
4305
4314
  const note = res.redirectNote;
@@ -4721,7 +4730,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
4721
4730
  def(
4722
4731
  "componentize_sections",
4723
4732
  "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.",
4733
+ "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
4734
  z.object({
4726
4735
  digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
4727
4736
  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 +4740,10 @@ ${res.warnings.join("\n")}` : summary, res.data);
4731
4740
  dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
4732
4741
  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
4742
  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.")
4743
+ 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
+ codemod: z.boolean().optional().describe(PARAM_TEXT.componentizeCodemod)
4735
4745
  }).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 }))
4746
+ 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
4747
  ),
4738
4748
  def(
4739
4749
  "get_analytics_overview",
@@ -4949,9 +4959,21 @@ ${res.warnings.join("\n")}` : summary, res.data);
4949
4959
  def(
4950
4960
  "get_deploy_status",
4951
4961
  "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.",
4962
+ "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
4963
  z.object({}).shape,
4954
- async (c) => ok("Deploy status.", await data(c, "GET", `/management/projects/deploy-status`))
4964
+ // Flat body, no `data` envelope: `data()` read it as undefined.
4965
+ async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status`), { method: "GET" }))
4966
+ ),
4967
+ def(
4968
+ "editor_setup",
4969
+ "Set up editing for the live build",
4970
+ NEW_TOOL_TEXT.editor_setup,
4971
+ z.object({
4972
+ force: z.boolean().optional().describe(PARAM_TEXT.editorSetupForce),
4973
+ check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck)
4974
+ }).shape,
4975
+ // The route answers `{ state, … }` with no `data` envelope, so the whole body is the result.
4976
+ 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
4977
  ),
4956
4978
  // ── Deploy to the user's own host (parity with remote /mcp) ──
4957
4979
  // A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
@@ -5890,7 +5912,7 @@ A collection (\`create_content_model\`) plus entries.
5890
5912
 
5891
5913
  Most real sites are both: components for the marketing pages, a collection for the blog.
5892
5914
  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.
5915
+ DERIVED by \`editor_setup\`, where the componentize lane in \xA710 reuses every field.
5894
5916
 
5895
5917
  ## 2. The decision tree
5896
5918
 
@@ -5967,8 +5989,8 @@ sibling calls it \`headline\` silently drops that content on the swap.
5967
5989
  Pair \`sectionType\` with a library \`category\` (hero, content, social-proof, conversion) or
5968
5990
  the component never appears in the editor's "Add a section" picker.
5969
5991
 
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-*\`)
5992
+ **An IMPORTED site gets there without you writing any of that.** Its pages are derived by
5993
+ \`editor_setup\` \u2014 flat field groups (\`hero-title\`, \`faq-question\`, the repeaters folded as \`group-*\`)
5972
5994
  and an empty \`blockJson\` \u2014 so there is nothing in the picker and no section to swap. Read
5973
5995
  \`get_componentize_plan\`: it groups those fields into sections, names each family, and proposes
5974
5996
  one component per SHAPE (the same hero on two pages is one component with two placements),
@@ -6096,14 +6118,27 @@ written to the wrong project can only be demoted (\`sectionType: null\`) and lef
6096
6118
 
6097
6119
  ## 10. You imported a site, or you are about to deploy one
6098
6120
 
6121
+ **A build imports what the build itself ships, and nothing more:** SEO and meta, every media
6122
+ asset, the brand, and the models the repository declares in \`bcms-content.json\`. Everything
6123
+ that makes the site EDITABLE \u2014 pages, inferred collections, forms, bindings, button and section
6124
+ components \u2014 is \`editor_setup\`, run on demand against the live build. The import recipe:
6125
+
6126
+ import (import_github_repo / deploy_project) -> editor_setup (poll { check: true } until ready)
6127
+ -> the authoring answer -> componentize_sections, or get_conversion_brief (\xA713)
6128
+ -> the codemods (componentize_sections { codemod: true } runs the section-order one in the repo)
6129
+ -> submit_conversion_receipt / submit_componentize_receipt -> deploy
6130
+
6131
+ \`get_deploy_status\` \`editorSetup\` (and get_next_steps) say when it has not run for the live
6132
+ build: \`never\` or \`stale\` \u2192 run it before anything below.
6133
+
6099
6134
  \`deploy_project\` and \`promote_project\` both answer **409
6100
6135
  AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
6101
6136
  asked ONCE per project, ever. It is not an error to retry or route around: read the message
6102
6137
  out, let the user pick, call \`set_authoring_preference\`, then deploy again.
6103
6138
 
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
6139
+ Until someone answers, \`editor_setup\` derives the pages but componentizes nothing (the
6140
+ preference is null). Once \`components\` is recorded, each \`editor_setup\` componentizes the
6141
+ 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
6142
  2 pages into one per page (the first page keeps it, the others get a clone), so two pages' heroes
6108
6143
  stop editing each other; a run narrowed to one page or section never splits.
6109
6144
 
@@ -6112,9 +6147,9 @@ It exists because an imported site arrives **field-driven whether anyone chose t
6112
6147
  and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
6113
6148
  marketing site it is the wrong answer. So the platform stops once, at the last moment it is
6114
6149
  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.
6150
+ below reuses every field \`editor_setup\` already derived.
6116
6151
 
6117
- **Recommend \`components\`.** On a site whose pages were DERIVED at import \u2014 the site \xA713
6152
+ **Recommend \`components\`.** On a site whose pages were DERIVED by \`editor_setup\` \u2014 the site \xA713
6118
6153
  describes \u2014 it redoes nothing: the componentize lane turns each top-level field GROUP into a
6119
6154
  component placement, and the page keeps its fields, its values and its bindings. In this order:
6120
6155
 
@@ -6122,7 +6157,7 @@ component placement, and the page keeps its fields, its values and its bindings.
6122
6157
  2. get_componentize_plan one component per section, computed live; creates nothing. It
6123
6158
  reads this project's PUBLISHED pages, so no deploy is needed
6124
6159
  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
6160
+ On a DERIVED site, each editor_setup after this answer
6126
6161
  runs this plan in bind mode for NEW sections (it never splits
6127
6162
  or joins a placement, and runs nothing while the answer is
6128
6163
  unset) and PUBLISHES what it created, so after one expect
@@ -6137,8 +6172,11 @@ component placement, and the page keeps its fields, its values and its bindings.
6137
6172
  5. publish_component each one \u2014 unpublished renders as NOTHING, on a page that 200s
6138
6173
  5b. publish_layout if you authored site chrome \u2014 the layout is a separate publish
6139
6174
  6. publish the pages
6140
- 7. npx @bettercms-ai/convert --componentize in the repo, so its templates render these
6141
- sections from \`pages[].blocks\`
6175
+ 7. componentize_sections { digest, codemod: true } the section-order codemod
6176
+ (\`--componentize\`) on the repo's own Actions, so its templates
6177
+ render these sections from \`pages[].blocks\`; \`skipped\` = no
6178
+ repo it may commit to: run \`npx @bettercms-ai/convert
6179
+ --componentize\` in your checkout
6142
6180
  8. deploy the ONLY deploy this sequence needs
6143
6181
 
6144
6182
  Step 5 is not a formality: \`componentize_sections\` and \`create_component\` both land components
@@ -6298,10 +6336,12 @@ but a code-built site ships only what its own templates draw:
6298
6336
  itself, in order, each section root stamped \`data-bcms-block="<blockId>"\`. \`npx @bettercms-ai/convert
6299
6337
  --componentize\` writes that loop (\`src/lib/bcms-sections.ts\`) and the stamps. Until it ships, the canvas
6300
6338
  only ARRANGES the static build to match the draft (a section the build never rendered is stood in for
6301
- by a server render), the editor locks Add section and reordering on routes whose code sets the
6302
- sections, and each such route leads \`submit_conversion_receipt\`'s \`pending\` with a
6303
- \`SECTIONS_UNSTAMPED\` row, so \`complete\` stays false. Never hard-code a page's bands in a template
6304
- whose order the CMS owns.
6339
+ by a server render); the editor still saves added, moved and swapped sections but says the live site
6340
+ keeps its own order, and offers "Let BetterCMS control section order" (that same codemod, committed
6341
+ to the connected repository); and each such route leads \`submit_conversion_receipt\`'s \`pending\`
6342
+ with a \`SECTIONS_UNSTAMPED\` row, so \`complete\` stays false. Never hard-code a page's bands, or
6343
+ literal \`data-bcms-block\` ids, in a template whose order the CMS owns: a release measures whether
6344
+ the stamped roots come out in the CMS order (\`ordered\` on the binding report's structure row).
6305
6345
  - A placement the site never renders (a leftover nav, footer or modal its layout draws) moves
6306
6346
  nothing: the editor counts section moves in RENDERED sections only and locks that row. Remove
6307
6347
  such placements; do not reorder around them.
@@ -6369,10 +6409,10 @@ JSON. On an IMPORTED site, never materialize a builtin: read the repo's own comp
6369
6409
  (\`src/components\`, its sections folder) and make ONE CMS component per source component, named
6370
6410
  as the source names it \u2014 the site's Pricing section is "Pricing", not "Section 2" or "Simple".
6371
6411
  On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
6372
- of this in one call, and once \`components\` is recorded each release of a site derived at IMPORT
6412
+ of this in one call, and once \`components\` is recorded each \`editor_setup\` of a derived site
6373
6413
  runs it in bind mode and publishes the components, so expect ALREADY_COMPONENTIZED \u2014 the placements it wrote are
6374
- still DRAFT blocks on the pages, so publish the pages, then \`npx @bettercms-ai/convert
6375
- --componentize\`.
6414
+ still DRAFT blocks on the pages, so publish the pages, then \`componentize_sections { codemod: true }\`
6415
+ (or \`npx @bettercms-ai/convert --componentize\` in your checkout).
6376
6416
 
6377
6417
  **3. The hierarchy editors see is the GROUP.** \`create_component\` and \`update_component\` take
6378
6418
  \`group\`: a NAME, resolved against the project's component groups and created when missing. That
@@ -6477,9 +6517,10 @@ anything. See \xA79.
6477
6517
 
6478
6518
  **Which recipe.** There are TWO below and they are not alternatives you pick by taste \u2014 call
6479
6519
  \`get_binding_report\` and \`get_conversion_brief\` first and let the answer choose. A brief that
6480
- comes back WITH PAGES means this project was imported and deployed, so its schema and values were
6481
- derived for you already \u2192 **Recipe A**. A 404 or an empty brief means the site is not in the CMS
6482
- yet \u2192 **Recipe B**. \`get_next_steps\` works on every plan and names the next unfinished step
6520
+ comes back WITH PAGES means this project was imported and \`editor_setup\` derived its schema and
6521
+ values already \u2192 **Recipe A**. On an import whose \`get_deploy_status\` \`editorSetup\` is \`never\`
6522
+ or \`stale\`, run \`editor_setup\` FIRST and read the brief again. A 404 or an empty brief after
6523
+ 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
6483
6524
  either way.
6484
6525
 
6485
6526
  **If you know Sanity, this is the same shape under different names:**
@@ -6495,8 +6536,8 @@ either way.
6495
6536
  draft reads
6496
6537
  Presentation tool overlays -> the visual editor canvas, framing your own build
6497
6538
 
6498
- **Imported site whose pages were DERIVED.** If this project was imported and made editable from
6499
- its build, the schema and the values already exist \u2014 a page per route, a field per element, and
6539
+ **Imported site whose pages were DERIVED.** If this project was imported and \`editor_setup\` ran
6540
+ over its build, the schema and the values already exist \u2014 a page per route, a field per element, and
6500
6541
  the original copy carried on each field as its \`defaultValue\`. Call \`get_conversion_brief\`
6501
6542
  first: it lists those pages, their routes, every bindable path with its current and original
6502
6543
  value, and the attributes to declare. SKIP Recipe B's steps 3 and 4 below and bind the keys it
@@ -6900,7 +6941,7 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
6900
6941
 
6901
6942
  1. \`list_projects\`; pass \`projectId\`${projectId ? ` (${projectId})` : ""} on every project tool. Not listed = the grant is on
6902
6943
  another workspace: have the user re-authenticate there. Never convert a different project.
6903
- 2. \`get_conversion_plan\`. On a 404 \`no-approved-plan\`, call
6944
+ 2. \`editor_setup\` until it is \`ready\` (poll with \`check: true\`), then \`get_conversion_plan\`; on a 404 \`no-approved-plan\`,
6904
6945
  \`get_conversion_brief { complete: true }\` and follow \`cursor\` to the LAST page, keeping every
6905
6946
  page \u2014 a codemod fed the capped brief converts a truncated site. Write them to \`brief.json\`.
6906
6947
  3. \`pull_project_source\` (or clone the \`github\` remote it names) and work from the SOURCE, never
@@ -7054,8 +7095,8 @@ Read \`bettercms://playbook/schema\` \xA712 first \u2014 this is the short form
7054
7095
  2. **One component per distinct section**, one-offs included. \`sectionType\` is the family;
7055
7096
  components sharing one are VARIANTS and must share prop KEYS. A one-off gets its own family
7056
7097
  plus \`allowedOn: ["slug:<page>"]\`. Start from the \`builtin:*\` blueprints in
7057
- \`list_components\`. On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\`
7058
- first (dryRun first, and confirm the plan with the user).
7098
+ \`list_components\`. On a DERIVED site (\`editor_setup\` derives it) run \`get_componentize_plan\` \u2192
7099
+ \`componentize_sections\` first (dryRun first, and confirm the plan with the user).
7059
7100
  3. **Hierarchy** = \`group\` on create_component/update_component (a NAME; created when missing).
7060
7101
  Use Global, Navigation, Actions, Content, Forms, Media, Sections, Page-specific. \`category\`
7061
7102
  is the separate picker TAB.
@@ -7459,7 +7500,7 @@ On 401/403, the MCP key needs (re)authorizing.`
7459
7500
  "import-site",
7460
7501
  {
7461
7502
  title: "Make an imported site editable (guided)",
7462
- 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.",
7503
+ 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.",
7463
7504
  argsSchema: {
7464
7505
  request: z2.string().optional().describe("scope, e.g. 'all routes' or 'just the home page'")
7465
7506
  }
@@ -7474,17 +7515,22 @@ On 401/403, the MCP key needs (re)authorizing.`
7474
7515
 
7475
7516
  Read bettercms://playbook/schema section 11 first, then work this order:
7476
7517
 
7477
- 1. AUTHORING GATE \u2014 if any deploy answered 409 AUTHORING_DECISION_REQUIRED, ask ME
7478
- components-or-fields (do not choose), then set_authoring_preference.
7479
- 2. CONTENT INTO THE CMS, per route: create_page (slug = route) -> add_field {pageId} (or
7480
- create_component + publish_component + component blocks) -> set_page_content with values
7481
- EXACTLY equal to the rendered text (binding matches by value; a paraphrase binds nothing)
7482
- -> update_page status 'published' (the canvas binds the PUBLISHED copy).
7483
- 3. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
7518
+ 1. EDITOR SETUP \u2014 the build imported only SEO/meta, media, brand and the models the repo
7519
+ declares. Call editor_setup and poll editor_setup { check: true } until \`ready\`: it derives
7520
+ the pages, collections, forms and bindings from the live build.
7521
+ 2. AUTHORING GATE \u2014 if get_next_steps or a deploy (409 AUTHORING_DECISION_REQUIRED) asks,
7522
+ ask ME components-or-fields (do not choose), then set_authoring_preference.
7523
+ 3. COMPONENTS: get_componentize_plan -> componentize_sections (dryRun first) -> publish the
7524
+ components and pages -> componentize_sections { codemod: true } (the section-order codemod in
7525
+ the repo). FIELDS: the convert_site loop (get_conversion_brief, the codemod, the receipt).
7526
+ Only where the setup derived nothing: create_page (slug = route) -> add_field {pageId} ->
7527
+ set_page_content with values EXACTLY equal to the rendered text (binding matches by value; a
7528
+ paraphrase binds nothing) -> update_page status 'published'.
7529
+ 4. PRESENTATION MANIFEST \u2014 add bcms-presentation.json to public/ (container width, type
7484
7530
  scale, nav position/background, footer surface as DTCG {"$type","$value"} entries) and
7485
7531
  redeploy, or structural draft previews render in a generic theme, not this site's design.
7486
- 4. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
7487
- content still only in the build, and placed-but-unpublished components), or tell me why
7532
+ 5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
7533
+ a setup that has not run, and placed-but-unpublished components), or tell me why
7488
7534
  an item is being left.
7489
7535
 
7490
7536
  On 401/403, the MCP key needs (re)authorizing.`