@bettercms-ai/mcp 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
File without changes
package/SKILL.md CHANGED
@@ -25,7 +25,7 @@ Decide the architecture before the first call. A marketing site is **components-
25
25
  a library of `create_component` sections, each with a `sectionType` and a library
26
26
  category, placed on pages via `create_page`'s `blockJson` as `component` blocks. A blog or
27
27
  catalogue is **schema-first**: a `create_content_model` collection whose body field is
28
- `document` (the article canvas — collections only, one per model, top level).
28
+ `document` (the article canvas — pages or collections, one per schema, top level; cannot coexist with a Section zone).
29
29
 
30
30
  Within a page's own schema, destructure into a tree: plain fields, **group** (one nested
31
31
  object), **repeater** (a repeatable array). Never a 1-item repeater, never a repeater of a
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { BetterCMS } from "@bettercms-ai/sdk";
14
14
  import { z } from "zod";
15
15
 
16
16
  // ../types/src/component.ts
17
- var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: a component's children render WITHOUT field bindings, so their text is NOT click-to-edit on the canvas \u2014 it is edited through the component's declared `props` in the section dock. A `section` block's children stay click-to-edit. So when you choose a component, DECLARE A PROP for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
17
+ var SECTION_DOCTRINE = "STRUCTURE (separate from schema): a page is composed of SECTIONS. NEVER build a page out of loose top-level heading/text/image/button/spacer blocks \u2014 they cannot be moved, duplicated or swapped as a unit, the visual editor cannot outline or name them, and every one of them becomes its own section in the editor. A hero of a headline, a lede and two CTAs is ONE section, not four. TWO SHAPES, and the choice is about REUSE. (1) A band that appears on more than one page, or that needs layout variants, is a COMPONENT with a `sectionType` \u2014 see create_component. Components sharing a `sectionType` are that section's VARIANTS (one Hero: 'Centered' for the home page and 'Two-column' for about, same prop keys so a swap keeps the content). This is also the only shape the editor's 'Add a section' picker can insert, and the only one that gets a family name and a variant switcher. (2) A genuinely one-off band on a single page is a `section` BLOCK whose `props.children` hold its blocks. THE TRADEOFF, stated in the present tense because it is real today: inside a component, ONLY the leaves a declared prop TARGETS are click-to-edit on the canvas \u2014 each declared prop's `target` is re-keyed to that PLACEMENT's own override address, so editing it changes this page and not the shared definition. A prop with no `target` still SHOWS as a dock control, but its override reaches no rendered slot, so editing it changes nothing on the page. Copy that no prop points at is worse still: no binding and no control, unreachable from click-to-edit AND from the dock, changeable only by editing the component definition, which rewrites every page that places it. A `section` block's children stay click-to-edit unconditionally. So when you choose a component, DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch; a component with un-propped editable copy is the defect, not the component. In the dock an unset prop shows EMPTY and inherits the definition's default, so set props explicitly when you want the current copy visible there. Do not hand-write a band's JSON: start from a built-in section blueprint (list_components returns locked `builtin:*` blueprints with no projectId \u2014 hero-centered, hero-split, feature-grid-three, cta-banner and nine more), each already rooted in a `section` block with its editable leaves declared as props. INLINE its blockJson as a `section` block for a one-off band; for a recurring band, materialize the blueprint with create_component so it becomes project-scoped before implementation validation, Output or publication. Direct `builtin:*` component references exist only for legacy delivery compatibility. Two consecutive call-to-action buttons are two sibling `button` blocks inside the same section \u2014 never a `columns` block, which is a `repeat(N,1fr)` grid and would stretch each CTA to half the container. Buttons are inline-level and flow side by side on their own.";
18
18
 
19
19
  // ../types/src/layout-lucide-icons.ts
20
20
  var LAYOUT_SECTION_ICONS = Object.freeze([
@@ -2089,7 +2089,7 @@ var fieldType = z.enum([
2089
2089
  "modular",
2090
2090
  "location",
2091
2091
  // All LEAF types, so the pass-through branch above covers them. `document` is THE article
2092
- // body — the rich document canvas, collections only, at most one per model, top level
2092
+ // body — the rich document canvas, pages or collections, at most one per schema, top level
2093
2093
  // only. It was the conspicuous omission: the dashboard shipped an editor for it while no
2094
2094
  // agent could create the field. `sections` is a page-section zone composed in the Visual
2095
2095
  // Editor, so it has no inline control.
@@ -3028,20 +3028,23 @@ function buildToolDefs(deps) {
3028
3028
  def(
3029
3029
  "get_componentize_plan",
3030
3030
  "Get the plan for turning this site's sections into components",
3031
- "What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it. Groups with the SAME shape across pages collapse into ONE component with several placements. `pending` says why a group is not offered: `NO_GROUP_ROOT` (the page's field keys are still the derive lane's own \u2014 `h1-welcome`, `p-we-build-things` \u2014 so there is no family to group by; rename them into families first), `NOT_A_SECTION` (a lone scalar with no family, or the page's own metadata \u2014 a section is a group field, a repeater, or a family two or more leaves share, so a legal page of `title`/`metaDescription`/`intro` proposes nothing), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (a repeater inside a repeater \u2014 one prop cannot describe two levels of rows), `PAGE_NOT_EMPTY` (the page holds blocks this lane does not own and will not overwrite). Chrome is NEVER a section: `nav-`/`footer-` keys and everything promoted into the project Layout are edited through the Layout. Keep the `digest` \u2014 componentize_sections refuses any other.",
3031
+ "What this site's SECTIONS would become as components \u2014 a proposal that creates nothing, changes nothing and is computed live on every call. For a site whose pages were DERIVED at import (the site get_conversion_brief describes), each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import already folded (`group-*`). Per page it returns each section's `groupKey`, its `sectionType` family (Hero, FAQ, CTA, Features, Social proof\u2026), its leaf `fields` (key, path, type, the value the CMS holds), a `shapeHash`, and either the component that already renders it (`reuse.componentId`) or the one this plan proposes (`reuse.proposedSlug`) \u2014 and the components themselves under `components`: a NEW one carries the exact `props` and `blockJson` create_component would take, while a row for a component that ALREADY EXISTS carries its `componentId`, `slug` and `name` and no definition, because nothing will be written for it. Groups with the SAME shape across pages collapse into ONE component with several placements. `pending` says why a group is not offered: `NO_GROUP_ROOT` (the page's field keys are still the derive lane's own \u2014 `h1-welcome`, `p-we-build-things` \u2014 so there is no family to group by; rename them into families first), `NOT_A_SECTION` (a lone scalar with no family, or the page's own metadata \u2014 a section is a group field, a repeater, or a family two or more leaves share, so a legal page of `title`/`metaDescription`/`intro` proposes nothing), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (a repeater THREE deep; TWO levels are expressed exactly \u2014 the group's `table` prop gains a nested `table` sub-field, and each row's nested column is an array of row objects), `PAGE_NOT_EMPTY` (the page holds blocks this lane does not own and will not overwrite). Chrome is NEVER a section: `nav-`/`footer-` keys and everything promoted into the project Layout are edited through the Layout. Keep the `digest` \u2014 componentize_sections refuses any other.",
3032
3032
  z.object({}).shape,
3033
3033
  async (c) => ok("Componentize plan.", await data(c, "GET", `/management/projects/current/componentize-plan`))
3034
3034
  ),
3035
3035
  def(
3036
3036
  "componentize_sections",
3037
3037
  "Turn this site's derived sections into components",
3038
- "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`.",
3038
+ "Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections and say how many components it will create and which pages it will rewrite. It creates each proposed component as a DRAFT (its `sectionType` family, category 'section', placeable on any page) and replaces each page's DRAFT blocks with an ordered list of `component` instances \u2014 one per group, each carrying `props.bind: \"<groupKey>\"`, which points at the page field group that already holds the copy. So nothing is copied and nothing moves: the page keeps its `fields`, its values and its bindings, click-to-edit keeps working and the coverage meter does not change. Pass the plan's `digest`; a 409 `stale-plan` means the site changed since you read that plan, so read it again, show the user what changed and confirm again. Call it with `dryRun: true` first \u2014 same receipt, nothing written. Running it twice is safe: a group that already has a placement comes back in `sections.pending` as ALREADY_COMPONENTIZED and no second component is created. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages before this reaches a visitor. Then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. `copy` says WHO OWNS THE COPY. The default `bind` is the above: the words stay in the page's field group. `copy: \"instance\"` is the page-builder model \u2014 each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words. Run it again and it converts nothing and duplicates nothing. `sections` narrows a run to named groupKeys (`hero`, `group-fast`); a key the plan does not list on the pages you named is refused as `unknown-section` rather than quietly doing nothing.",
3039
3039
  z.object({
3040
3040
  digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
3041
3041
  pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
3042
+ pages: z.array(z.string().min(1)).optional().describe("The same page filter under the name the dashboard uses. Unioned with pageIds; an EMPTY list is refused rather than treated as every page."),
3043
+ sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan). Every other section on the page keeps the placement it already has."),
3044
+ copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy: 'bind' (default) leaves it in the page fields; 'instance' moves it onto the placement and marks those fields origin: componentized."),
3042
3045
  dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first.")
3043
3046
  }).shape,
3044
- async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, dryRun: a.dryRun }))
3047
+ 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 }))
3045
3048
  ),
3046
3049
  def(
3047
3050
  "get_analytics_overview",
@@ -3836,7 +3839,11 @@ with every string, link and image already declared as a prop. Show the user the
3836
3839
  **confirm before writing**, then \`componentize_sections { digest, dryRun: true }\` and, once the
3837
3840
  receipt reads right, without \`dryRun\`. Each page's blocks become ordered \`component\`
3838
3841
  instances carrying \`props.bind\` \u2014 the field group keeps the copy, so nothing moves and
3839
- click-to-edit keeps working. Then run \`npx @bettercms-ai/convert --componentize\` in the repo
3842
+ click-to-edit keeps working. Pass \`copy: "instance"\` instead when the INSTANCE should own its
3843
+ words (the page-builder model): each placement takes that group's current values into
3844
+ \`props.overrides\`, records where each came from in \`props.source\`, and the page's fields are
3845
+ kept and marked \`origin: "componentized"\` so the editor stops offering a second place to type
3846
+ them. Either way, running it again converts nothing and duplicates nothing. Then run \`npx @bettercms-ai/convert --componentize\` in the repo
3840
3847
  so its templates render those sections from \`pages[].blocks\`, and finish with the publishes:
3841
3848
  \`publish_component\` every new component and publish the pages, or the site renders the
3842
3849
  sections as empty strings.
@@ -3848,15 +3855,39 @@ model's \`modular\` field. Create the blocks FIRST, then name their slugs (not i
3848
3855
  \`config.blockSlugs\`. This is the page-builder shape: one \`sections\` field holding an
3849
3856
  ordered list of typed blocks.
3850
3857
 
3858
+ ## 5b. NEVER MODEL SEO. It is native.
3859
+
3860
+ Do not create \`seoTitle\`, \`metaTitle\`, \`metaDescription\`, \`ogImage\`, \`canonicalUrl\`,
3861
+ \`noindex\` or an \`seo\` group on ANY model, page or component. Every one of those already
3862
+ exists on the platform:
3863
+
3864
+ per page / entry metaTitle, metaDescription \u2014 set with update_page / update_content_entry
3865
+ site-wide defaults get_seo / update_seo \u2014 { metaTitle, metaDescription, ogImage, twitterHandle }
3866
+ generated for you generate_seo_meta
3867
+ audited for you list_seo_issues
3868
+
3869
+ A modelled copy does not add a capability, it adds a SECOND place the same fact lives \u2014 and the
3870
+ two immediately disagree. The native fields are what the renderer emits into \`<head>\`, what
3871
+ sitemap and llms.txt read, and what \`list_seo_issues\` scans; a field called \`seoTitle\` on a
3872
+ model is a string an author edits that changes nothing on the page. It is worse than useless:
3873
+ it looks like the control and is not.
3874
+
3875
+ If a page needs its own meta, set it on the page. If the whole site needs a default, set it with
3876
+ update_seo. If you want it written for you, call generate_seo_meta and apply the result.
3877
+
3878
+ The one legitimate exception is a field that is genuinely CONTENT and happens to be used in meta
3879
+ too \u2014 a blog post's \`excerpt\`, an article's \`coverImage\`. Model those as content, name them
3880
+ for what they are, and let \`generate_seo_meta\` read them.
3881
+
3851
3882
  ## 6. Blogs
3852
3883
 
3853
3884
  author collection: name, avatar, bio
3854
3885
  blog-post collection: title, slug, excerpt, cover (image),
3855
3886
  body (document), author (reference -> author)
3856
3887
 
3857
- \`document\` is THE article body: a rich document canvas, **collections only**, at most one
3858
- per model, top level only (never inside a group or repeater). A page's body is its block
3859
- content, so \`document\` on a page is rejected. Use \`richtext\` for a short formatted field,
3888
+ \`document\` is THE article body: a rich document canvas on pages or collections, at most one
3889
+ per schema, top level only (never inside a group or repeater). It cannot coexist with a
3890
+ Section zone, which also owns the document canvas. Use \`richtext\` for a short formatted field,
3860
3891
  \`longtext\` for multi-line plain text.
3861
3892
 
3862
3893
  Then the pages: