@bettercms-ai/mcp 0.71.0 → 0.72.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
@@ -15,7 +15,7 @@ import { AsyncLocalStorage } from "async_hooks";
15
15
  import { z } from "zod";
16
16
 
17
17
  // ../types/src/component.ts
18
- 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. An IMPORTED site's components are its own (read the repo); its nav, footer and modals are the Layout. On a blank site, 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
+ 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. An IMPORTED site's components are its own (read the repo); its nav, footer and modals are the Layout. On a blank site, 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 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 it with create_component `from` 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.";
19
19
  var COMPONENT_PROP_TYPES = [
20
20
  "text",
21
21
  "richtext",
@@ -56,7 +56,12 @@ var COMPONENT_PROP_TYPES = [
56
56
  // CMS items, as the same-named content-model FIELDS store them: an entry id (or a polymorphic
57
57
  // `{ entryId, modelSlug }`), and a list of them. Delivery hydrates them like an entry's.
58
58
  "reference",
59
- "multi-reference"
59
+ "multi-reference",
60
+ // V2 chrome types, each storing what the same-named content-model FIELD stores: a video value,
61
+ // a primitive list (`config.itemType`), and multi-line text (the field lane's V2 `text`).
62
+ "video",
63
+ "array",
64
+ "longtext"
60
65
  ];
61
66
 
62
67
  // ../types/src/layout-lucide-icons.ts
@@ -2023,6 +2028,7 @@ var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
2023
2028
 
2024
2029
  // ../types/src/dock-icon-names.ts
2025
2030
  var DOCK_ICON_NAMES = [
2031
+ "abc",
2026
2032
  "access-denied",
2027
2033
  "activity",
2028
2034
  "add",
@@ -2030,14 +2036,40 @@ var DOCK_ICON_NAMES = [
2030
2036
  "add-comment",
2031
2037
  "add-document",
2032
2038
  "add-user",
2039
+ "address-book",
2040
+ "adjustments",
2041
+ "adjustments-horizontal",
2042
+ "alarm",
2043
+ "alert-triangle",
2044
+ "align-left",
2033
2045
  "api",
2046
+ "app-window",
2047
+ "apple",
2048
+ "apps",
2034
2049
  "archive",
2050
+ "arrow-back-up",
2035
2051
  "arrow-down",
2052
+ "arrow-guide",
2036
2053
  "arrow-left",
2037
2054
  "arrow-right",
2038
2055
  "arrow-top-right",
2039
2056
  "arrow-up",
2057
+ "arrows-diagonal",
2058
+ "arrows-down",
2059
+ "arrows-exchange",
2060
+ "arrows-horizontal",
2061
+ "arrows-left-right",
2062
+ "arrows-shuffle",
2063
+ "arrows-sort",
2064
+ "article",
2065
+ "aspect-ratio",
2040
2066
  "asterisk",
2067
+ "award",
2068
+ "axis-x",
2069
+ "backpack",
2070
+ "badge",
2071
+ "badge-cc",
2072
+ "badges",
2041
2073
  "bar-chart",
2042
2074
  "basket",
2043
2075
  "bell",
@@ -2049,25 +2081,76 @@ var DOCK_ICON_NAMES = [
2049
2081
  "bold",
2050
2082
  "bolt",
2051
2083
  "book",
2084
+ "book-2",
2052
2085
  "bookmark",
2053
2086
  "bookmark-filled",
2087
+ "books",
2054
2088
  "bottle",
2089
+ "box",
2090
+ "brand-dribbble",
2091
+ "brand-facebook",
2092
+ "brand-instagram",
2093
+ "brand-linkedin",
2094
+ "brand-pinterest",
2095
+ "brand-tiktok",
2096
+ "brand-x",
2097
+ "brand-youtube",
2098
+ "briefcase",
2099
+ "broadcast",
2100
+ "browser",
2055
2101
  "bug",
2102
+ "building",
2103
+ "building-skyscraper",
2104
+ "building-store",
2056
2105
  "bulb-filled",
2057
2106
  "bulb-outline",
2107
+ "calculator",
2058
2108
  "calendar",
2109
+ "calendar-dollar",
2110
+ "calendar-event",
2111
+ "calendar-exclamation",
2112
+ "calendar-off",
2113
+ "calendar-plus",
2114
+ "calendar-star",
2115
+ "calendar-time",
2116
+ "camera",
2117
+ "campfire",
2118
+ "carousel-horizontal",
2059
2119
  "case",
2120
+ "cash",
2121
+ "category",
2122
+ "category-2",
2123
+ "chart-area",
2124
+ "chart-arrows",
2125
+ "chart-bar",
2126
+ "chart-bar-popular",
2127
+ "chart-donut",
2128
+ "chart-dots",
2129
+ "chart-line",
2060
2130
  "chart-upward",
2131
+ "check",
2132
+ "checkbox",
2133
+ "checklist",
2061
2134
  "checkmark",
2062
2135
  "checkmark-circle",
2136
+ "chef-hat",
2063
2137
  "chevron-down",
2064
2138
  "chevron-left",
2065
2139
  "chevron-right",
2066
2140
  "chevron-up",
2141
+ "chevrons-right",
2067
2142
  "circle",
2143
+ "circle-check",
2144
+ "circle-dot",
2145
+ "circle-number-1",
2146
+ "circle-plus",
2147
+ "click",
2068
2148
  "clipboard",
2069
2149
  "clipboard-image",
2150
+ "clipboard-text",
2070
2151
  "clock",
2152
+ "clock-check",
2153
+ "clock-hour-4",
2071
2154
  "close",
2072
2155
  "close-circle",
2073
2156
  "code",
@@ -2075,20 +2158,31 @@ var DOCK_ICON_NAMES = [
2075
2158
  "cog",
2076
2159
  "collapse",
2077
2160
  "color-wheel",
2161
+ "columns",
2162
+ "columns-2",
2163
+ "columns-3",
2078
2164
  "comment",
2079
2165
  "component",
2080
2166
  "compose",
2081
2167
  "compose-sparkles",
2082
2168
  "confetti",
2083
2169
  "controls",
2170
+ "cookie",
2084
2171
  "copy",
2085
2172
  "credit-card",
2086
2173
  "crop",
2087
2174
  "cube",
2175
+ "currency-dollar",
2088
2176
  "dashboard",
2089
2177
  "database",
2178
+ "database-search",
2090
2179
  "desktop",
2180
+ "device-desktop",
2181
+ "device-mobile",
2182
+ "device-tablet",
2183
+ "devices",
2091
2184
  "diamond",
2185
+ "discount",
2092
2186
  "document",
2093
2187
  "document-pdf",
2094
2188
  "document-remove",
@@ -2098,6 +2192,7 @@ var DOCK_ICON_NAMES = [
2098
2192
  "document-word",
2099
2193
  "document-zip",
2100
2194
  "documents",
2195
+ "door-exit",
2101
2196
  "dot",
2102
2197
  "double-chevron-down",
2103
2198
  "double-chevron-left",
@@ -2107,9 +2202,12 @@ var DOCK_ICON_NAMES = [
2107
2202
  "download",
2108
2203
  "drag-handle",
2109
2204
  "drop",
2205
+ "droplet",
2206
+ "droplet-half",
2110
2207
  "earth-americas",
2111
2208
  "earth-globe",
2112
2209
  "edit",
2210
+ "egg",
2113
2211
  "ellipsis-horizontal",
2114
2212
  "ellipsis-vertical",
2115
2213
  "empty",
@@ -2122,77 +2220,169 @@ var DOCK_ICON_NAMES = [
2122
2220
  "error-screen",
2123
2221
  "expand",
2124
2222
  "eye-closed",
2223
+ "eye-off",
2125
2224
  "eye-open",
2126
2225
  "face-happy",
2127
2226
  "face-indifferent",
2128
2227
  "face-sad",
2228
+ "feather",
2129
2229
  "feedback",
2230
+ "file",
2231
+ "file-certificate",
2232
+ "file-code",
2233
+ "file-download",
2234
+ "file-text",
2130
2235
  "filter",
2236
+ "flag",
2237
+ "flame",
2238
+ "flask",
2239
+ "focus-2",
2131
2240
  "folder",
2241
+ "folders",
2242
+ "forms",
2243
+ "frame",
2132
2244
  "generate",
2245
+ "gift",
2246
+ "git-branch",
2133
2247
  "github",
2248
+ "grid-dots",
2134
2249
  "groq",
2250
+ "hammer",
2251
+ "hand-finger",
2252
+ "hanger",
2135
2253
  "hash",
2254
+ "headphones",
2255
+ "headset",
2136
2256
  "heart",
2137
2257
  "heart-filled",
2258
+ "heart-handshake",
2138
2259
  "help-circle",
2139
2260
  "highlight",
2261
+ "history",
2140
2262
  "home",
2263
+ "hourglass",
2141
2264
  "ice-cream",
2265
+ "id-badge",
2266
+ "id-badge-2",
2142
2267
  "image",
2143
2268
  "image-remove",
2144
2269
  "images",
2145
2270
  "inbox",
2271
+ "infinity",
2272
+ "info-circle",
2146
2273
  "info-filled",
2147
2274
  "info-outline",
2275
+ "info-square-rounded",
2148
2276
  "inline",
2149
2277
  "inline-element",
2278
+ "input-search",
2150
2279
  "insert-above",
2151
2280
  "insert-below",
2152
2281
  "italic",
2153
2282
  "joystick",
2154
2283
  "json",
2284
+ "language",
2155
2285
  "launch",
2286
+ "layout",
2287
+ "layout-align-center",
2288
+ "layout-board",
2289
+ "layout-board-split",
2290
+ "layout-bottombar",
2291
+ "layout-bottombar-expand",
2292
+ "layout-cards",
2293
+ "layout-columns",
2294
+ "layout-dashboard",
2295
+ "layout-grid",
2296
+ "layout-list",
2297
+ "layout-navbar",
2298
+ "layout-rows",
2299
+ "layout-sidebar",
2300
+ "layout-sidebar-right",
2301
+ "leaf",
2156
2302
  "leave",
2157
2303
  "lemon",
2304
+ "library-photo",
2305
+ "lifebuoy",
2158
2306
  "link",
2159
2307
  "link-removed",
2160
2308
  "linkedin",
2161
2309
  "list",
2310
+ "list-check",
2311
+ "list-details",
2312
+ "list-numbers",
2313
+ "list-tree",
2162
2314
  "lock",
2163
2315
  "logo-js",
2164
2316
  "logo-ts",
2317
+ "mail",
2318
+ "map-2",
2319
+ "map-pin",
2320
+ "map-pins",
2165
2321
  "marker",
2166
2322
  "marker-removed",
2167
2323
  "master-detail",
2324
+ "math-function",
2325
+ "meat",
2168
2326
  "menu",
2327
+ "message",
2328
+ "message-2",
2329
+ "message-check",
2330
+ "message-circle",
2331
+ "message-circle-2",
2332
+ "message-plus",
2333
+ "message-star",
2169
2334
  "microphone",
2170
2335
  "microphone-slash",
2171
2336
  "mobile-device",
2337
+ "mood-empty",
2172
2338
  "moon",
2339
+ "movie",
2340
+ "news",
2341
+ "notes",
2173
2342
  "number",
2174
2343
  "ok-hand",
2175
2344
  "olist",
2176
2345
  "overage",
2177
2346
  "package",
2347
+ "packages",
2348
+ "palette",
2178
2349
  "panel-left",
2179
2350
  "panel-right",
2351
+ "paperclip",
2180
2352
  "pause",
2353
+ "pencil",
2354
+ "phone",
2355
+ "photo",
2356
+ "photo-scan",
2357
+ "photo-search",
2358
+ "pig-money",
2181
2359
  "pin",
2182
2360
  "pin-filled",
2183
2361
  "pin-removed",
2184
2362
  "play",
2363
+ "player-play",
2364
+ "player-track-next",
2365
+ "playlist-add",
2185
2366
  "plug",
2367
+ "plug-connected",
2368
+ "plus-minus",
2369
+ "point",
2186
2370
  "presentation",
2371
+ "printer",
2187
2372
  "progress-50",
2188
2373
  "progress-75",
2189
2374
  "projects",
2190
2375
  "publish",
2376
+ "qrcode",
2377
+ "quote",
2191
2378
  "read-only",
2379
+ "receipt",
2380
+ "recycle",
2192
2381
  "redo",
2193
2382
  "refresh",
2194
2383
  "remove",
2195
2384
  "remove-circle",
2385
+ "repeat",
2196
2386
  "reset",
2197
2387
  "restore",
2198
2388
  "retrieve",
@@ -2200,49 +2390,90 @@ var DOCK_ICON_NAMES = [
2200
2390
  "revert",
2201
2391
  "robot",
2202
2392
  "rocket",
2393
+ "rosette",
2394
+ "route",
2395
+ "ruler",
2396
+ "ruler-measure",
2397
+ "salad",
2398
+ "scale",
2203
2399
  "schema",
2400
+ "school",
2204
2401
  "search",
2402
+ "section",
2205
2403
  "select",
2404
+ "send",
2405
+ "server",
2406
+ "settings",
2206
2407
  "share",
2408
+ "shield-check",
2409
+ "shield-lock",
2410
+ "shopping-bag",
2411
+ "shopping-cart",
2412
+ "shopping-cart-plus",
2413
+ "signature",
2414
+ "sitemap",
2415
+ "slideshow",
2207
2416
  "sort",
2417
+ "sort-descending",
2418
+ "spacing-vertical",
2208
2419
  "sparkle",
2209
2420
  "sparkles",
2421
+ "speakerphone",
2210
2422
  "spinner",
2211
2423
  "split-horizontal",
2212
2424
  "split-vertical",
2213
2425
  "square",
2214
2426
  "stack",
2427
+ "stack-2",
2215
2428
  "stack-compact",
2429
+ "stairs",
2430
+ "stairs-up",
2216
2431
  "star",
2217
2432
  "star-filled",
2433
+ "stars",
2218
2434
  "stop",
2219
2435
  "strikethrough",
2220
2436
  "string",
2437
+ "subtask",
2221
2438
  "sun",
2439
+ "superscript",
2222
2440
  "sync",
2441
+ "table",
2442
+ "table-options",
2223
2443
  "tablet-device",
2224
2444
  "tag",
2225
2445
  "tags",
2226
2446
  "target",
2227
2447
  "task",
2448
+ "template",
2449
+ "tent",
2228
2450
  "terminal",
2451
+ "terminal-2",
2229
2452
  "text",
2230
2453
  "th-large",
2231
2454
  "th-list",
2455
+ "thumb-up",
2232
2456
  "thumbs-down",
2233
2457
  "thumbs-up",
2458
+ "ticket",
2234
2459
  "tiers",
2235
2460
  "timeline",
2236
2461
  "toggle-arrow-right",
2462
+ "toggle-right",
2237
2463
  "token",
2464
+ "tool",
2465
+ "tools-kitchen-2",
2238
2466
  "transfer",
2239
2467
  "translate",
2240
2468
  "trash",
2241
2469
  "trend-upward",
2242
2470
  "triangle-outline",
2243
2471
  "trolley",
2472
+ "truck",
2473
+ "truck-delivery",
2244
2474
  "truncate",
2245
2475
  "twitter",
2476
+ "typography",
2246
2477
  "ulist",
2247
2478
  "unarchive",
2248
2479
  "underline",
@@ -2253,12 +2484,22 @@ var DOCK_ICON_NAMES = [
2253
2484
  "unpublish",
2254
2485
  "upload",
2255
2486
  "user",
2487
+ "user-circle",
2488
+ "user-plus",
2489
+ "user-search",
2490
+ "user-star",
2256
2491
  "users",
2492
+ "users-group",
2257
2493
  "versions",
2258
2494
  "video",
2495
+ "vocabulary",
2496
+ "vs",
2259
2497
  "warning-filled",
2260
2498
  "warning-outline",
2261
- "wrench"
2499
+ "weight",
2500
+ "world",
2501
+ "wrench",
2502
+ "zoom-question"
2262
2503
  ];
2263
2504
  var DOCK_ICON_NAME_SET = new Set(DOCK_ICON_NAMES);
2264
2505
 
@@ -2284,6 +2525,8 @@ never carries an icon.
2284
2525
  Two vocabularies, on purpose: Layout section icons (update_layout \`add-section\`) stay Lucide names.
2285
2526
  A name from one list is not valid in the other.
2286
2527
 
2528
+ The list includes the V2 kit's line-icon names (\`building\`, \`settings\`, \`layout-grid\`, \u2026) as themselves.
2529
+
2287
2530
  ${DOCK_ICON_NAMES.length} names:
2288
2531
 
2289
2532
  ${DOCK_ICON_NAMES.join(", ")}
@@ -2297,8 +2540,8 @@ var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
2297
2540
  var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, organise them per the structure playbook: read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure (read-only), show the user its outline, then apply it with set_content_structure and the version it returned (the If-Match). A project that already has a structure keeps it: file only what you created, with move_to_folder, unless the user asks for a full re-organisation.`;
2298
2541
  var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, explore the repo, then read the installed `bettercms` skill (`npx @bettercms-ai/install --project -y` adds it if missing): its table turns what you found into the one reference the task needs \u2014 read only that. Without the skill installed, read the same files as resources: bettercms://skill/bettercms, then bettercms://skill/<reference>.";
2299
2542
  var DOCK_UI_ROUTING_INSTRUCTION = "How fields and props look in the editor (groups, tabs, rows, icons, item names) is playbook \xA714 or the `field-ui` prompt: write it with update_content_model field* or update_component prop* plus ifMatch, read it back, and finish when get_next_steps reports no dock-* step.";
2300
- var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished. On an imported site you are finished only when `receipt.complete` is true: work `receipt.gate.blocking` in order, and record a gap you cannot close with editor_setup `exempt` (an allowed reason and a note), never in prose.";
2301
- var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items: missing meta descriptions, drafts, empty collections, unnotified forms, writes awaiting approval, unstamped bindings or sections, \`authoring-undecided\` and the Layout lane (\`layout-write-blocked\`, \`layout-field-unread\`, \`layout-locator-stale\`, \`layout-rebuild-required\`, \`chrome-duplicated\`). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. \`receipt.complete\` is the setup gate's verdict and \`receipt.gate\` its numbers: per built route the page, text, image and link coverage, \`blocking\` first, each with its \`fix\`. Call it again after each batch; you are finished only when \`complete\` is true. \`organise-content\`: pages and collections are in no folder \u2014 read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and, once the user agrees, set_content_structure.`;
2543
+ var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is already done (`done`), what is still missing (`missing`, each item citing the count it reacted to) and the one `nextAction`. Work from that receipt instead of re-reading the site, call get_next_steps again after each batch of edits, and before you tell the user you are finished. On an imported site the gate is the definition of done: run `receipt.gate.nextAction`, read again, and repeat until `receipt.complete` is true. Record a gap you cannot close with editor_setup `exempt` only for an allowed reason that is true, with a note, never in prose.";
2544
+ var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items by \`ruleId\` (skill finish-site lists them). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. \`receipt.complete\` is the setup gate's verdict and \`receipt.gate\` its numbers per built route. Run \`receipt.gate.nextAction\`, call it again after each batch; you are finished only when \`complete\` is true. \`organise-content\`: pages and collections are in no folder \u2014 read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and, once the user agrees, set_content_structure.`;
2302
2545
  var LAYOUT_CHROME_INSTRUCTION = 'When the user asks to make the navigation, footer or other site chrome editable, or get_next_steps lists a `layout-*` or `chrome-duplicated` item: this is playbook \xA711 item 3. Chrome is the project Layout, never page fields: in the source, wrap each Section in `data-bcms-layout-section` (the slot, its base slug) and mark each value `data-bcms-layout-field="layout:<sections[slug].id>:<fieldId>"`, never `data-bcms-field` inside it, then push; the strip removes the page copies, never delete them by hand. On a site BetterCMS serves, publish_layout writes Layout VALUES into the live pages; a structure change waits for a Rebuild. Never place nav, footer or modal blocks or fields on a page: the Layout (or the site\'s own layout) draws chrome, so a page copy either duplicates it or renders nowhere (get_componentize_plan reports one as LAYOUT_CHROME).';
2303
2546
  var CTA_BUTTON_INSTRUCTION = "A call to action is ONE `button` field (value {label, href, variant?, size?, arrow?, icon?, newTab?}), rendered with BcmsButton from @bettercms-ai/astro or @bettercms-ai/next: never a text field beside a link field. A page that already holds such a pair (primaryCtaText + primaryCtaHref) becomes one button with update_field `mergeHref`. Inside a component a CTA is a `button` block whose text and href two props target.";
2304
2547
  var STRUCTURE_EXAMPLE_PAYLOAD = {
@@ -2539,7 +2782,7 @@ function allKeys(fields) {
2539
2782
  })
2540
2783
  ]);
2541
2784
  }
2542
- var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode"];
2785
+ var CLEARABLE = ["helpText", "showIf", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode", "localized"];
2543
2786
  var SETTABLE = ["label", "required", ...CLEARABLE];
2544
2787
  function mergeConfig(current, patch) {
2545
2788
  const out = { ...current ?? {} };
@@ -2577,7 +2820,7 @@ function planFieldEdit(model, edit, callerIfMatch) {
2577
2820
  }
2578
2821
  const changes = SETTABLE.filter((k) => edit[k] !== void 0);
2579
2822
  if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
2580
- return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, type or key." };
2823
+ return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, localized, type or key." };
2581
2824
  }
2582
2825
  const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
2583
2826
  if (echo) return { ok: false, error: echo };
@@ -2748,11 +2991,12 @@ var PARAM_TEXT = {
2748
2991
  editorSetupForce: "re-run a setup already ready for the live build",
2749
2992
  editorSetupCheck: "true = only report the state; starts nothing",
2750
2993
  editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
2994
+ pageBlockJson: "an ARRAY REPLACES the page's block composition (a `component` block per section). An OBJECT edits ONE section of the stored page instead: `insertSection` adds a component (its id, or a builtin:* id: copied into the project once) at index `at`, or `before` / `after` a section's block id (none = last), with its `style` and `overrides`; `sectionStyle` sets a section's block style (a key replaces the stored one, null unsets it). `section` in the answer names the block (skill publishing)",
2751
2995
  pageDynamic: "a dynamic (collection template) page only",
2752
2996
  previewPath: "its route pattern, e.g. /blog/{slug}; null clears",
2753
2997
  pageModelId: "an existing collection this page lists (when it has no model yet)",
2754
2998
  formIgnored: "true = not one of the site's forms (a search box, a widget); false restores it",
2755
- 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",
2999
+ componentizeCodemod: "true = componentize nothing; run the section-order codemod in the connected repo on its own Actions, which commits for the next build. After the components and pages are published. `skipped` = no repo to commit to: run `convert --sections` in a checkout (skill componentize)",
2756
3000
  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)",
2757
3001
  sort: "comma list, a leading - sorts descending, e.g. '-publishedAt,title'",
2758
3002
  page: "1-based page of `limit` rows; the response's meta says totalPages",
@@ -2784,7 +3028,7 @@ var SEO_KEY_TEXT = {
2784
3028
  var VARIANT_GROUP_NOTE = "When the component belongs to a family of looks, `variantGroup` names the family and lists its looks.";
2785
3029
  var UNPROPPED_COPY_NOTE = "A `warnings` entry UNPROPPED_COPY names copy no prop targets, which nobody can edit: add a prop with a `target` for each leaf it lists.";
2786
3030
  var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write applies only at that version, and a 409 means the page changed since, so read it again. Send it whenever you pass blockJson, which REPLACES the whole composition.";
2787
- var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section.";
3031
+ var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section. V2 slots, 'table' mode 'types': playbook \xA75.";
2788
3032
  var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty.";
2789
3033
  var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
2790
3034
 
@@ -2956,20 +3200,25 @@ var fieldType = z.enum([
2956
3200
  // V2 leaves: a taxonomy string[], a rating number, a code value. The pass-through branch carries their config.
2957
3201
  "tags",
2958
3202
  "rating",
2959
- "code"
3203
+ "code",
3204
+ "video",
3205
+ "table",
3206
+ "slot"
2960
3207
  ]);
2961
3208
  var OPEN_MODE_INVALID = "openMode_invalid: openMode must be one of 'inline', 'drill', 'popover', 'dialog'";
2962
3209
  var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
2963
3210
  var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
3211
+ var dockIconArg = z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`);
2964
3212
  var uiObject = z.strictObject({
2965
3213
  collapsed: z.boolean().optional().describe("start this panel collapsed. Use it for long optional sections."),
2966
3214
  preview: z.strictObject({
2967
3215
  title: z.string().min(1).optional().describe("CHILD KEY whose value titles a collapsed row"),
2968
3216
  subtitle: z.string().min(1).optional().describe("CHILD KEY whose value is the row's second line (e.g. 'level' \u2192 'Heading \xB7 h2')"),
2969
3217
  media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)"),
2970
- fallback: z.string().min(1).max(60).optional().describe("a literal row label while the title child is empty ('Untitled question'), not a key")
3218
+ fallback: z.string().min(1).max(60).optional().describe("a literal row label while the title child is empty ('Untitled question'), not a key"),
3219
+ icon: dockIconArg.optional().describe("the row's icon while it has no media, a dock icon name")
2971
3220
  }).optional().describe(
2972
- "how ONE ITEM summarises itself when collapsed. Every slot names a CHILD KEY of THIS field/prop \u2014 not a value, not a dotted path. Without it a repeater of testimonials reads 'Item 1, Item 2, Item 3'; with {title:'author', media:'avatar'} it reads the names with their faces."
3221
+ "how ONE ITEM summarises itself when collapsed. Every slot names a CHILD KEY of THIS field/prop (a dotted path reads into it: 'author.name'). Without it a repeater of testimonials reads 'Item 1, Item 2, Item 3'; with {title:'author', media:'avatar'} it reads the names with their faces."
2973
3222
  ),
2974
3223
  layout: z.enum(["list", "grid", "table", "rows", "chips", "gallery"]).optional().describe(
2975
3224
  "how the ITEMS are arranged. Omit (\u2261 'list') for rows of text; 'grid' for items whose thumbnail is the thing you scan \u2014 a gallery, a logo wall, a team; 'table' for up to 3 scalar columns. Same placement rule as `preview`. V2 'rows' | 'chips' | 'gallery' are also valid on a primitive array."
@@ -2977,13 +3226,21 @@ var uiObject = z.strictObject({
2977
3226
  reorderable: z.boolean().optional().describe(
2978
3227
  "omit (\u2261 true) unless the order is PART OF THE MEANING. `false` LOCKS the list \u2014 a 'three steps' band that must stay three steps in that order, a nav whose order is semantic, a timeline. Valid only where there is a list to lock."
2979
3228
  ),
2980
- control: z.enum(["segmented", "slider", "rating"]).optional().describe(
2981
- "editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns); 'rating' on a number with config.min, max and step. Anything else is refused with the rule named."
3229
+ control: z.enum(["segmented", "slider", "rating", "dropdown", "radio", "cards", "grid", "switch", "checkbox"]).optional().describe(
3230
+ "editor control for a LEAF. Omit for the default. 'segmented' on a select with a few short options (alignment, theme, size); 'slider' on a number that has config.min < config.max (opacity, columns); 'rating' on a number with config.min, max and step; 'dropdown'/'radio'/'grid'/'cards' on a select ('cards' also a multiselect); 'switch'/'checkbox' on a boolean ('checkbox' also a multiselect). Anything else is refused with the rule named."
2982
3231
  ),
2983
- // A refine, not an enum: 236 names inlined into every schema that carries `ui` would be paid on
2984
- // every tools/list. The layoutIcon precedent; the server's z.enum is the guard.
2985
- icon: z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`).optional().describe(`the branch row's icon, a name from ${DOCK_ICONS_URI}. Nesting fields only.`),
2986
- itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields only. V2 itemNoun goes here."),
3232
+ icon: dockIconArg.optional().describe(`the branch row's icon, a name from ${DOCK_ICONS_URI}. Nesting fields only.`),
3233
+ itemLabel: z.string().min(1).max(24).optional().describe("what ONE item is called ('Slide'): the add button, crumbs and empty state. Nesting fields and lists. V2 itemNoun goes here."),
3234
+ // V2 chrome keys (the server's fieldUiSchema): editor-only, each refused with a named code where it means nothing.
3235
+ itemName: z.string().max(64).optional().describe("a list's item type name (an identifier). Lists only."),
3236
+ itemTitle: z.string().min(1).max(60).optional().describe("a list's item type title. Lists only."),
3237
+ rows: z.number().int().min(1).max(40).optional().describe("the lines a text box opens with. text / longtext only."),
3238
+ placeholder: z.string().min(1).max(120).optional().describe("ghost text, a field of any type. A prop uses its own `placeholder`."),
3239
+ synced: z.strictObject({ from: z.string().min(1).max(64) }).optional().describe("the value comes from this global; edits apply everywhere."),
3240
+ valueShape: z.enum(["string", "object"]).optional().describe("a link / image / button writes a plain string or the object."),
3241
+ access: z.strictObject({ view: z.literal("schema.view").optional(), edit: z.literal("schema.manage").optional() }).optional().describe("who sees (view: developer-only) and who changes (edit) it, gated in the editor. The enforced lock is readOnly:'developer'."),
3242
+ hidden: z.boolean().optional().describe("true: never shown in the form (a system value). A conditional hide is showIf."),
3243
+ blocks: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), preview: z.record(z.string(), z.string()).optional() })).optional().describe("modular only: per block slug, {icon?, preview?} for its rows."),
2987
3244
  input: z.object({
2988
3245
  name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
2989
3246
  writes: z.array(z.string().min(1).max(64)).max(16).optional()
@@ -2997,8 +3254,13 @@ var propUiObject = uiObject.extend({
2997
3254
  label: z.string().min(1).max(32).optional(),
2998
3255
  kind: z.enum(["group", "row"]).optional(),
2999
3256
  icon: uiObject.shape.icon,
3000
- collapsed: z.boolean().optional()
3001
- }).optional().describe("the group this prop sits in. The FIRST prop of a group defines {id, label, kind?, icon?, collapsed?}; later props send only {id}.")
3257
+ collapsed: z.boolean().optional(),
3258
+ description: z.string().min(1).max(200).optional(),
3259
+ columns: z.union([z.literal(1), z.literal(2)]).optional(),
3260
+ collapsible: z.boolean().optional(),
3261
+ layout: z.enum(["stack", "breakpoints"]).optional(),
3262
+ order: z.number().int().min(0).max(99).optional()
3263
+ }).optional().describe("the group this prop (or sub-field) sits in. The FIRST member defines {id, label, kind?, icon?, collapsed?, description?, columns?, collapsible?, layout?, order?}; later ones send only {id}.")
3002
3264
  });
3003
3265
  var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
3004
3266
  var fieldShape = {
@@ -3017,7 +3279,7 @@ var fieldShape = {
3017
3279
  "choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
3018
3280
  ),
3019
3281
  config: z.record(z.string(), z.unknown()).optional().describe(
3020
- "per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: 'text'|'number'|'date'}, date {includeTime, timezone}, modular {blockSlugs: ['quote','gallery'], minItems?, maxItems?} \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
3282
+ "per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
3021
3283
  ),
3022
3284
  fields: z.array(z.lazy(() => fieldObject)).optional().describe(
3023
3285
  "NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
@@ -3061,7 +3323,8 @@ var fieldShape = {
3061
3323
  helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
3062
3324
  advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
3063
3325
  recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
3064
- openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode)
3326
+ openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode),
3327
+ localized: z.boolean().optional()
3065
3328
  };
3066
3329
  var fieldObject = z.object(fieldShape);
3067
3330
  var urlPatternArg = z.string().nullable().optional().describe(
@@ -3098,8 +3361,9 @@ var seoPatchShape = {
3098
3361
  metaDescription: z.string().max(500).optional(),
3099
3362
  ogImage: z.string().url().max(2e3).nullable().optional(),
3100
3363
  twitterHandle: z.string().max(255).nullable().optional(),
3101
- og: z.object({ image: z.string().max(2048).optional(), type: z.string().max(50).optional() }).optional(),
3364
+ og: z.object({ image: z.string().max(2048).optional(), type: z.string().max(50).optional(), siteName: z.string().max(255).optional(), locale: z.string().max(20).optional() }).optional(),
3102
3365
  twitter: z.object({ card: z.string().max(50).optional() }).optional(),
3366
+ feeds: z.array(z.object({ type: z.string().max(100), href: z.string().max(2048), title: z.string().max(255).optional() })).max(10).optional(),
3103
3367
  collectionSchemaTypes: z.record(z.string().min(1).max(64), z.enum(SCHEMA_ORG_TYPES)).optional()
3104
3368
  }).optional().describe(SEO_KEY_TEXT.seoDefaults),
3105
3369
  siteSchema: z.union([jsonObject, z.array(jsonObject)]).optional().describe(SEO_KEY_TEXT.siteSchema),
@@ -3172,6 +3436,7 @@ function toField(f) {
3172
3436
  // The developer lock: stripped here, an agent writing a model back would unlock every field.
3173
3437
  ...f.readOnly !== void 0 ? { readOnly: f.readOnly } : {},
3174
3438
  ...f.lockReason !== void 0 ? { lockReason: f.lockReason } : {},
3439
+ ...f.localized !== void 0 ? { localized: f.localized } : {},
3175
3440
  // V2 editor keys: on `base` so the group/repeater rebuild cannot drop them.
3176
3441
  ...f.helpMore !== void 0 ? { helpMore: f.helpMore } : {},
3177
3442
  ...f.advanced !== void 0 ? { advanced: f.advanced } : {},
@@ -3266,6 +3531,26 @@ var HOSTING_TEXT = {
3266
3531
  deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build (skill publishing). The first call creates the site on the host and makes it the project's host; a later call for the same provider redeploys that site (`redeployed: true`). `connectionId` (from list_hosting_connections) defaults to the newest working one, `siteName` to the project's handle. A refusal carries a `code` and a `fixUrl` (not_connected, reconnect, pick_existing_site, grant_required, ADMIN_REQUIRED): each is fixed by the user at `fixUrl`, so relay it and stop. `deploymentQueued: false` means the project has no live build yet: deploy_project first, and that release reaches the host on its own. Never run the Vercel or Netlify CLI while a connection exists. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
3267
3532
  get_host_deploy_status: "The connected project's recent deployments to Vercel, Netlify or Cloudflare, newest first: state (queued|deploying|live|failed), providerUrl, error, sha and targetName. Poll it after deploy_to_host until the newest one is `live`, whose `providerUrl` is the live URL to hand the user, or `failed`, whose `error` you read back to them."
3268
3533
  };
3534
+ async function builtinCopyId(client, builtinId) {
3535
+ const listed = await client.fetchJSON(client.url(`/management/components?select=summary`));
3536
+ const copy = (listed.data ?? []).find((row) => row.sourceKey === builtinId && !row.builtin);
3537
+ if (copy) return copy.id;
3538
+ const made = await client.fetchJSON(client.url(`/management/components`), { method: "POST", body: JSON.stringify({ from: builtinId }) });
3539
+ return made.data.id;
3540
+ }
3541
+ function publishedLines(outcome, summary) {
3542
+ if (!outcome) return [summary];
3543
+ if (outcome.notLive.length > 0) {
3544
+ const values = outcome.notLive.map((v) => `${v.route} ${v.address}: ${v.reason ?? "not live"} (${v.action?.label ?? "no action"})`).join("; ");
3545
+ if (outcome.next && outcome.notLive.every((v) => v.reason === "NOT_CHECKED")) {
3546
+ return [`PENDING: ${values}. Read ${outcome.next} \`publishes\` until each reads live or held; do not report it as live before.`, summary];
3547
+ }
3548
+ return [`NOT LIVE: ${outcome.summary ?? outcome.status}. ${values}. Take each action, then read get_deploy_status \`publishes\`.`, summary];
3549
+ }
3550
+ if (outcome.next) return [`${summary} Published, and not judged yet: nothing has looked at the served page. Read ${outcome.next} \`publishes\` before saying it is live.`];
3551
+ if (outcome.status !== "live") return [`${summary} Publish status: ${outcome.status}.`];
3552
+ return [`${summary} Published: ${outcome.live > 0 ? `${outcome.live} ${outcome.live === 1 ? "value" : "values"} live on the served page` : "live"}.`];
3553
+ }
3269
3554
  function ok(summary, data) {
3270
3555
  return {
3271
3556
  content: [
@@ -3690,6 +3975,7 @@ function buildToolDefs(deps) {
3690
3975
  // Stored markers on a prop: nothing refuses a prop value for them. Declared so an echo keeps them.
3691
3976
  readOnly: z.union([z.literal(true), z.literal("developer")]).optional(),
3692
3977
  lockReason: z.string().max(200).optional(),
3978
+ localized: z.boolean().optional(),
3693
3979
  showIf: z.object({
3694
3980
  match: z.enum(["all", "any"]),
3695
3981
  rules: z.array(z.object({
@@ -4466,22 +4752,27 @@ ${d.outline}` : "Proposed Content structure.", d);
4466
4752
  def(
4467
4753
  "update_page",
4468
4754
  "Edit a page",
4469
- "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: skill components). A section change the live page cannot show comes back in `holds`: saved as a draft, not live until each `action` is taken (skill publishing). 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 that is refused, the page still goes live and `warnings` carries ENTRY_STILL_DRAFT (skill publishing). STRUCTURED DATA is native SEO, never a field: Automatic JSON-LD from the content, unless `schemaType` picks a schema.org type or pasted `schema` (wins over both, null clears it) is set (skill seo). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
4470
- z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), 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", ...SCHEMA_ORG_TYPES]).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,
4755
+ "Edit a page: title, slug, SEO metaTitle/metaDescription, structured data (`schemaType`, `schema`), publish status (draft|published), and `blockJson` (its block composition \u2014 passing an array REPLACES the whole array, so read get_page first and send its `optimisticVersion` as `ifMatch`; an object adds or styles ONE section). An answer that starts with `held` is a draft the live page will NOT show until each of `holds` has its `action` taken, and a publish answers with `outcome`: `partial` lists the values not live. Never report either as done (skill publishing). `warnings`: COMPONENT_SWAP_DROPS_OVERRIDES (skill components), ENTRY_STILL_DRAFT and COMPONENT_DRAFT (skill publishing). 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, and a singleton's content entry with them. STRUCTURED DATA is native SEO, never a field: Automatic JSON-LD from the content, unless `schemaType` picks a schema.org type or pasted `schema` (wins over both, null clears it) is set (skill seo). " + PAGE_HEAD_NOTE + " " + SECTION_DOCTRINE,
4756
+ z.object({ ...headPatchShape, noindex: z.boolean().optional().describe("hide this page from search engines and site search"), pageId: z.string().min(1).describe(PARAM_TEXT.pageRef), title: z.string().optional(), slug: z.string().optional(), blockJson: z.union([z.array(blockObject), z.object({ insertSection: z.object({ componentId: z.string().min(1), at: z.number().int().min(0).optional(), before: z.string().optional(), after: z.string().optional(), style: z.record(z.string(), z.unknown()).optional(), overrides: z.record(z.string(), z.unknown()).optional() }).strict().optional(), sectionStyle: z.object({ blockId: z.string().min(1), style: z.record(z.string(), z.unknown()) }).strict().optional() }).strict()]).optional().describe(PARAM_TEXT.pageBlockJson), metaTitle: z.string().optional(), metaDescription: z.string().optional(), schemaType: z.enum(["auto", ...SCHEMA_ORG_TYPES]).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,
4471
4757
  async (c, a) => {
4758
+ const edit = Array.isArray(a.blockJson) ? void 0 : a.blockJson;
4759
+ const insert = edit?.insertSection;
4760
+ const insertSection = insert?.componentId.startsWith("builtin:") ? { ...insert, componentId: await builtinCopyId(c, insert.componentId) } : insert;
4472
4761
  const res = await c.fetchJSON(
4473
4762
  c.url(`/management/pages/${s(a.pageId)}/meta`),
4474
4763
  {
4475
4764
  method: "PATCH",
4476
4765
  ...a.ifMatch !== void 0 ? { headers: { "If-Match": `W/"${a.ifMatch}"` } } : {},
4477
- 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 })
4766
+ body: JSON.stringify({ title: a.title, slug: a.slug, blockJson: Array.isArray(a.blockJson) ? a.blockJson : void 0, insertSection, sectionStyle: edit?.sectionStyle, 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 })
4478
4767
  }
4479
4768
  );
4480
4769
  const note = res.redirectNote;
4481
- const summary = note ? `Updated page. An existing redirect from ${note.source} already points to ${note.existingDestination}; it wasn't changed, so the old address does not redirect to the new one.` : "Updated page.";
4482
- const held = (res.holds ?? []).map((h) => `HELD ${h.reason}: ${h.message} Action: ${h.action.label}.`);
4483
- const lines = [summary, ...res.warnings ?? [], ...held].join("\n");
4484
- return ok(lines, res.holds?.length ? { ...res.data, holds: res.holds } : res.data);
4770
+ const edited = res.section ? insertSection ? `Added section ${res.section.id} at position ${res.section.index + 1} of the draft.` : `Set the style of section ${res.section.id} in the draft.` : null;
4771
+ const saved = res.held ? `${edited ? `${edited} ` : ""}The draft is saved; the live page is unchanged.` : edited ?? "Updated page.";
4772
+ const summary = note ? `${saved} An existing redirect from ${note.source} already points to ${note.existingDestination}; it wasn't changed, so the old address does not redirect to the new one.` : saved;
4773
+ const lines = [...res.held ? [res.held] : [], ...publishedLines(res.outcome, summary), ...res.warnings ?? []].join("\n");
4774
+ const extra = { ...res.holds?.length ? { holds: res.holds } : {}, ...res.outcome ? { outcome: res.outcome } : {}, ...res.section ? { section: res.section } : {} };
4775
+ return ok(lines, Object.keys(extra).length ? { ...res.data, ...extra } : res.data);
4485
4776
  }
4486
4777
  ),
4487
4778
  // ── Code + deploy (parity with remote /mcp; needs artifact:write) ──
@@ -4632,6 +4923,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4632
4923
  advanced: z.boolean().nullable().optional().describe(PARAM_TEXT.advanced),
4633
4924
  recommendedMax: z.number().int().positive().nullable().optional().describe(PARAM_TEXT.recommendedMax),
4634
4925
  openMode: z.enum(["inline", "drill", "popover", "dialog"], { error: OPEN_MODE_INVALID }).nullable().optional().describe(PARAM_TEXT.openMode),
4926
+ localized: z.boolean().nullable().optional(),
4635
4927
  type: fieldType.optional().describe("a new type; needs allowRetype"),
4636
4928
  key: fieldKey.optional().describe("a new key (flat namespace, unique in the schema)"),
4637
4929
  allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
@@ -4734,7 +5026,11 @@ ${d.outline}` : "Proposed Content structure.", d);
4734
5026
  entryIds: z.array(z.string().min(1)).max(100).optional().describe("entry ids (from list_content_entries)"),
4735
5027
  pageIds: z.array(z.string().min(1)).max(100).optional().describe("page ids (from list_pages); publish only")
4736
5028
  }).shape,
4737
- async (c, a) => ok(a.action === "publish" ? "Publish receipt." : "Unpublish receipt.", await data(c, "POST", `/management/publish`, { action: a.action, entryIds: a.entryIds, pageIds: a.pageIds }))
5029
+ async (c, a) => {
5030
+ const receipt = await data(c, "POST", `/management/publish`, { action: a.action, entryIds: a.entryIds, pageIds: a.pageIds });
5031
+ const outcome = receipt?.outcome;
5032
+ return ok(a.action === "publish" ? publishedLines(outcome, "Publish receipt.").join("\n") : "Unpublish receipt.", receipt);
5033
+ }
4738
5034
  ),
4739
5035
  def(
4740
5036
  "create_content_entries",
@@ -4898,7 +5194,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4898
5194
  "Get the plan for turning this site's sections into components",
4899
5195
  "What this site's SECTIONS would become as components \u2014 a proposal that creates nothing and is computed live. For a site whose pages were DERIVED at import, each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import folded (`group-*`). Per page: each section's `groupKey`, `sectionType` family, leaf `fields` with their CMS values, `shapeHash`, and either the component already rendering it (`reuse.componentId`) or the proposed one (`reuse.proposedSlug`). Under `components`, a NEW one carries the exact `props` and `blockJson` create_component would take; an EXISTING one only `componentId`, `slug` and `name` (nothing is written for it). `pending` says why a group is not offered (its codes: see `summary`). Per-page components, collection templates and chrome (never a section): skill componentize. READ `summary: true` FIRST \u2014 the full plan runs to hundreds of KB; `paged` pages it (409 PLAN_CHANGED = start again); the unpaged full read is what `--plan plan.json` takes. Keep the `digest` \u2014 componentize_sections refuses any other.",
4900
5196
  z.object({
4901
- summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first. `pending` codes: `NO_GROUP_ROOT` (keys are still derive-lane names like `h1-welcome` \u2014 rename them into families first), `NOT_A_SECTION` (a lone scalar or the page's own metadata; `detail: ENTRY_METADATA` = collection-template frontmatter, `detail: DECLINED` = declined with componentize_sections `decline`), `IMAGE_AS_TEXT` (a text field holding an image path \u2014 make it an image field), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (three deep; two levels become a nested `table` sub-field), `PAGE_NOT_EMPTY` (blocks this lane does not own and will not overwrite)."),
5197
+ summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first. `pending` codes (each explained in skill componentize): `NO_GROUP_ROOT`, `NOT_A_SECTION` (`detail`: `ENTRY_METADATA`, `DECLINED`), `IMAGE_AS_TEXT`, `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER`, `PAGE_NOT_EMPTY`."),
4902
5198
  paged: z.boolean().optional().describe("true = 10 pages at a time; the response carries `page`, `pageCount` and a `cursor` for the next."),
4903
5199
  cursor: z.string().optional().describe("The `cursor` from the previous page. Implies paged. A 409 `PLAN_CHANGED` means start again with no cursor.")
4904
5200
  }).shape,
@@ -4907,15 +5203,15 @@ ${d.outline}` : "Proposed Content structure.", d);
4907
5203
  def(
4908
5204
  "componentize_sections",
4909
5205
  "Turn this site's derived sections into components",
4910
- "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 (`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 (skill components). 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`. When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized and the plan stops proposing them (skill componentize).",
5206
+ "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. The page keeps its `fields`, values and bindings (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 (skill components). 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` for the repository half (skill componentize). A group the user says is NOT a section: `decline: true` with those `sections` (skill componentize).",
4911
5207
  z.object({
4912
5208
  digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
4913
5209
  pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
4914
5210
  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."),
4915
5211
  sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan); every other section keeps its placement."),
4916
- copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy. 'bind' (default) leaves it in the page fields. 'instance' is the page-builder model: 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\"`."),
5212
+ 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 each placement; the page's fields are KEPT and marked `origin: \"componentized\"` (skill componentize)."),
4917
5213
  dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
4918
- 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."),
5214
+ group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (by NAME, created when missing), else its section family (skill componentize). A reused component keeps its Group; the receipt's `components.groups` lists the Groups filed into."),
4919
5215
  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`."),
4920
5216
  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."),
4921
5217
  codemod: z.boolean().optional().describe(PARAM_TEXT.componentizeCodemod)
@@ -5836,7 +6132,7 @@ ${notes.join("\n")}` : summary, created);
5836
6132
  client.url(`/management/pages/${pageId}/meta`),
5837
6133
  { method: "PATCH", body: JSON.stringify({ blockJson: page.blockJson, ...args.dryRun ? { dryRun: true } : {} }) }
5838
6134
  );
5839
- results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length, ...written.holds?.length ? { holds: written.holds } : {} });
6135
+ results.push({ ok: true, ...args.dryRun ? { status: "would-update" } : {}, pageId, slug: written.data?.slug ?? page.slug, path: written.data?.path, blocks: page.blockJson.length, ...written.held ? { held: written.held } : {}, ...written.holds?.length ? { holds: written.holds } : {} });
5840
6136
  } catch (err) {
5841
6137
  results.push({ ok: false, pageId, slug: page.slug, error: errorText(err) });
5842
6138
  }
@@ -5845,8 +6141,11 @@ ${notes.join("\n")}` : summary, created);
5845
6141
  if (args.dryRun) {
5846
6142
  return ok(DRY_RUN_SUMMARY, { dryRun: true, results, wouldCompose: results.length - failed, failed });
5847
6143
  }
6144
+ const held = results.filter((r) => typeof r.held === "string");
6145
+ const lead = held.length ? `HELD on ${held.length} of ${args.pages.length} page(s). ${held.map((r) => `${String(r.slug ?? r.pageId)}: ${String(r.held)}`).join(" ")}
6146
+ ` : "";
5848
6147
  return ok(
5849
- `Composed ${results.length - failed} of ${args.pages.length} page(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. These are page DRAFTS: publish each with update_page status:'published'. Then call get_site_composition for the receipt.`,
6148
+ `${lead}Composed ${results.length - failed} of ${args.pages.length} page(s)${failed ? `; ${failed} failed \u2014 see the per-item errors` : ""}. These are page DRAFTS: publish each with update_page status:'published'. Then call get_site_composition for the receipt.`,
5850
6149
  results
5851
6150
  );
5852
6151
  })
@@ -6241,6 +6540,15 @@ model's \`modular\` field. Create the blocks FIRST, then name their slugs (not i
6241
6540
  \`config.blockSlugs\`. This is the page-builder shape: one \`sections\` field holding an
6242
6541
  ordered list of typed blocks.
6243
6542
 
6543
+ A \`slot\` field (V2 blocks inside a block) takes the same \`config.blockSlugs\` (plus
6544
+ \`minItems\`, \`maxItems\` and \`slotSettings\`, leaf fields each child's \`_settings\` holds),
6545
+ but stores each child INLINE: \`[{ _key, _type: '<block slug>', _variant?, _settings?, ...values }]\`,
6546
+ its values judged by its block's fields. A component's \`slot\` prop with \`config.multiple\`
6547
+ takes the same child shape with \`_type\` the COMPONENT id (no \`_variant\`: a variant is its own
6548
+ component). A list of several item shapes on a component is a \`table\` prop with
6549
+ \`config.mode: 'types'\` and \`config.itemTypes: [{ name, label?, fields }]\`, rows
6550
+ \`[{ _key?, _type: '<name>', ...values }]\`.
6551
+
6244
6552
  ## 5b. NEVER MODEL SEO. It is native.
6245
6553
 
6246
6554
  Do not create \`seoTitle\`, \`metaTitle\`, \`metaDescription\`, \`ogImage\`, \`canonicalUrl\`,
@@ -6568,7 +6876,8 @@ not carry falls back always (a static build has no file to frame).
6568
6876
  but a code-built site ships only what its own templates draw:
6569
6877
  - An added or moved section reaches the live site only when the site renders the page's block list
6570
6878
  itself, in order, each section root stamped \`data-bcms-block="<blockId>"\`. \`npx @bettercms-ai/convert
6571
- --componentize\` writes that loop (\`src/lib/bcms-sections.ts\`) and the stamps. Until it ships, the canvas
6879
+ --sections\` wraps a page's sections in \`<BcmsSections>\`, which is that loop and the stamps
6880
+ (\`--componentize\` lifts them into components first, where a plan exists). Until it ships, the canvas
6572
6881
  only ARRANGES the static build to match the draft (a section the build never rendered is stood in for
6573
6882
  by a server render); the editor still saves added, moved and swapped sections but says the live site
6574
6883
  keeps its own order, and offers "Let BetterCMS control section order" (that same codemod, committed
@@ -6576,6 +6885,15 @@ but a code-built site ships only what its own templates draw:
6576
6885
  with a \`SECTIONS_UNSTAMPED\` row, so \`complete\` stays false. Never hard-code a page's bands, or
6577
6886
  literal \`data-bcms-block\` ids, in a template whose order the CMS owns: a release measures whether
6578
6887
  the stamped roots come out in the CMS order (\`ordered\` on the binding report's structure row).
6888
+ - To add ONE section or set ONE section's spacing, send \`update_page\` an OBJECT \`blockJson\`:
6889
+ \`{ insertSection: { componentId, at | before | after, style?, overrides? } }\` or
6890
+ \`{ sectionStyle: { blockId, style } }\`. The server edits the stored page, so nothing else is
6891
+ resent. An answer that begins with \`held\` is a saved draft this site's code will not show:
6892
+ take the hold's action first. A publish answers with \`outcome\`; \`partial\` names each value
6893
+ that is not live and its action. Neither is ever "done". \`get_next_steps\` keeps them listed
6894
+ (\`publish-held\`), and its gate fails a route on \`sections\` or \`style\` until the build draws
6895
+ the page's sections from the CMS and marks every root for style, or the release writes them in
6896
+ place. Each failing row's \`fixCode\` says which: the project's switch, the wrap, or a rebuild.
6579
6897
  - A placement the site never renders (a leftover nav, footer or modal its layout draws) moves
6580
6898
  nothing: the editor counts section moves in RENDERED sections only and locks that row. Remove
6581
6899
  such placements; do not reorder around them.
@@ -6955,8 +7273,9 @@ conversion is yours to write.
6955
7273
  **The finish line is \`receipt.complete\`.** \`get_next_steps\` answers it from the setup gate
6956
7274
  (\`receipt.gate\`): every route of the live build has a published page (or entry), one page only,
6957
7275
  at most 2% of its visible text unowned, every image a field's and 95% of its links. \`blocking\`
6958
- lists what fails, worst route first, each with the \`fix\` call to run; a null \`fix\` is the
6959
- user's to decide. A gap left on purpose is RECORDED, \`editor_setup { exempt: [{ route, check,
7276
+ lists what fails, worst route first, each with the \`fix\` call to run, and \`nextAction\` is the
7277
+ one to run now (the fix that closes the most rows): run it, read again after each batch, repeat
7278
+ until \`complete\`. A null \`fix\` is the user's to decide. A gap left on purpose is RECORDED, \`editor_setup { exempt: [{ route, check,
6960
7279
  reason, note }] }\`, with a reason \`allowedExceptions\` lists for that check. Running out of time,
6961
7280
  context or patience is in \`neverExceptions\`: it is refused, and telling the user why instead
6962
7281
  leaves the gate incomplete. \`bettercms init\` reads the same gate when you exit and prints it.
@@ -7244,7 +7563,7 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
7244
7563
  \`data-bcms-style=""\` plus \`style={...}\` from \`block.style\`. For \`upgrade-element-styles\` (field width, spacing, text handles)
7245
7564
  do the same with \`npx @bettercms-ai/convert@latest --upgrade-element-styles --root . --dry-run\`. Run \`--verify-styles\` again
7246
7565
  and restore what it lists (the marker, and the inline style from \`block.style\` or \`fieldAttrs\`, are allowed; changing a class
7247
- or an existing style is not). Deploy, poll, call \`get_next_steps\` again, tell me what it still lists. Not listed: say so; invent no work.
7566
+ or an existing style is not). Deploy, poll, call \`get_next_steps\` again and run \`receipt.gate.nextAction\` until \`receipt.complete\` is true; what else it lists, tell me by name. Not listed: say so; invent no work. Its \`receipt.gate\` fails a route on \`sections\` or \`style\` while I could not add a section to that page, reorder it or space it: run each such row's \`fix\` too. A write that answers \`held\`, or a publish whose \`outcome\` is \`partial\`, is not done until its action is taken.
7248
7567
  11. Ask me whether to commit and push to my repository, commit only, or leave it uncommitted (unpushed, the
7249
7568
  next rebuild from my repo drops it). Touch git only as I answer.`;
7250
7569
  var FIELD_UI_PROMPT = {
@@ -7357,7 +7676,7 @@ error-prone, so go slow and confirm. Never guess the layout.
7357
7676
  the same prop keys across a family or a swap drops content.
7358
7677
  4. **Props** \u2014 declare only what should really be editable:
7359
7678
  { key, label, target: { blockId, path },
7360
- type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect|object|datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference }.
7679
+ type: text|richtext|image|url|boolean|number|select|group|table|slot|form|link|button|date|color|file|icon|multiselect|object|datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference|video|array|longtext }.
7361
7680
  'select'/'multiselect' need config.options; 'group'/'table'/'object' need config.fields; a 'button'
7362
7681
  is ONE prop on a button block's 'props', never a label prop plus a url prop; 'slot' holds ONE nested component (a list with config.multiple);
7363
7682
  'form' holds ONE Form document \u2014 point target.path at 'props' on an empty form block and set
@@ -7818,9 +8137,9 @@ Read bettercms://playbook/schema section 11 first, then work this order:
7818
8137
  redeploy, or structural draft previews render in a generic theme, not this site's design.
7819
8138
  5. VERIFY \u2014 call get_next_steps and fix what it lists (it knows about missing manifests,
7820
8139
  a setup that has not run, and placed-but-unpublished components). You are finished only
7821
- when its receipt.complete is true: work receipt.gate.blocking top down, and record an item
7822
- you cannot close with editor_setup { exempt } under an allowed reason. Telling me why is
7823
- not an exception.
8140
+ when its receipt.complete is true: run receipt.gate.nextAction, read it again after each
8141
+ batch, and record an item you cannot close with editor_setup { exempt } only under an allowed
8142
+ reason that is true. Telling me why is not an exception.
7824
8143
 
7825
8144
  On 401/403, the MCP key needs (re)authorizing.`
7826
8145
  }