@bettercms-ai/mcp 0.61.0 → 0.62.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/SKILL.md +1 -1
- package/dist/index.js +556 -147
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1992,16 +1992,285 @@ var LAYOUT_SECTION_ICONS = Object.freeze([
|
|
|
1992
1992
|
]);
|
|
1993
1993
|
var LAYOUT_SECTION_ICON_SET = new Set(LAYOUT_SECTION_ICONS);
|
|
1994
1994
|
|
|
1995
|
+
// ../types/src/dock-icon-names.ts
|
|
1996
|
+
var DOCK_ICON_NAMES = [
|
|
1997
|
+
"access-denied",
|
|
1998
|
+
"activity",
|
|
1999
|
+
"add",
|
|
2000
|
+
"add-circle",
|
|
2001
|
+
"add-comment",
|
|
2002
|
+
"add-document",
|
|
2003
|
+
"add-user",
|
|
2004
|
+
"api",
|
|
2005
|
+
"archive",
|
|
2006
|
+
"arrow-down",
|
|
2007
|
+
"arrow-left",
|
|
2008
|
+
"arrow-right",
|
|
2009
|
+
"arrow-top-right",
|
|
2010
|
+
"arrow-up",
|
|
2011
|
+
"asterisk",
|
|
2012
|
+
"bar-chart",
|
|
2013
|
+
"basket",
|
|
2014
|
+
"bell",
|
|
2015
|
+
"bill",
|
|
2016
|
+
"binary-document",
|
|
2017
|
+
"block-content",
|
|
2018
|
+
"block-element",
|
|
2019
|
+
"blockquote",
|
|
2020
|
+
"bold",
|
|
2021
|
+
"bolt",
|
|
2022
|
+
"book",
|
|
2023
|
+
"bookmark",
|
|
2024
|
+
"bookmark-filled",
|
|
2025
|
+
"bottle",
|
|
2026
|
+
"bug",
|
|
2027
|
+
"bulb-filled",
|
|
2028
|
+
"bulb-outline",
|
|
2029
|
+
"calendar",
|
|
2030
|
+
"case",
|
|
2031
|
+
"chart-upward",
|
|
2032
|
+
"checkmark",
|
|
2033
|
+
"checkmark-circle",
|
|
2034
|
+
"chevron-down",
|
|
2035
|
+
"chevron-left",
|
|
2036
|
+
"chevron-right",
|
|
2037
|
+
"chevron-up",
|
|
2038
|
+
"circle",
|
|
2039
|
+
"clipboard",
|
|
2040
|
+
"clipboard-image",
|
|
2041
|
+
"clock",
|
|
2042
|
+
"close",
|
|
2043
|
+
"close-circle",
|
|
2044
|
+
"code",
|
|
2045
|
+
"code-block",
|
|
2046
|
+
"cog",
|
|
2047
|
+
"collapse",
|
|
2048
|
+
"color-wheel",
|
|
2049
|
+
"comment",
|
|
2050
|
+
"component",
|
|
2051
|
+
"compose",
|
|
2052
|
+
"compose-sparkles",
|
|
2053
|
+
"confetti",
|
|
2054
|
+
"controls",
|
|
2055
|
+
"copy",
|
|
2056
|
+
"credit-card",
|
|
2057
|
+
"crop",
|
|
2058
|
+
"cube",
|
|
2059
|
+
"dashboard",
|
|
2060
|
+
"database",
|
|
2061
|
+
"desktop",
|
|
2062
|
+
"diamond",
|
|
2063
|
+
"document",
|
|
2064
|
+
"document-pdf",
|
|
2065
|
+
"document-remove",
|
|
2066
|
+
"document-sheet",
|
|
2067
|
+
"document-text",
|
|
2068
|
+
"document-video",
|
|
2069
|
+
"document-word",
|
|
2070
|
+
"document-zip",
|
|
2071
|
+
"documents",
|
|
2072
|
+
"dot",
|
|
2073
|
+
"double-chevron-down",
|
|
2074
|
+
"double-chevron-left",
|
|
2075
|
+
"double-chevron-right",
|
|
2076
|
+
"double-chevron-up",
|
|
2077
|
+
"double-quote",
|
|
2078
|
+
"download",
|
|
2079
|
+
"drag-handle",
|
|
2080
|
+
"drop",
|
|
2081
|
+
"earth-americas",
|
|
2082
|
+
"earth-globe",
|
|
2083
|
+
"edit",
|
|
2084
|
+
"ellipsis-horizontal",
|
|
2085
|
+
"ellipsis-vertical",
|
|
2086
|
+
"empty",
|
|
2087
|
+
"enter",
|
|
2088
|
+
"enter-right",
|
|
2089
|
+
"envelope",
|
|
2090
|
+
"equal",
|
|
2091
|
+
"error-filled",
|
|
2092
|
+
"error-outline",
|
|
2093
|
+
"error-screen",
|
|
2094
|
+
"expand",
|
|
2095
|
+
"eye-closed",
|
|
2096
|
+
"eye-open",
|
|
2097
|
+
"face-happy",
|
|
2098
|
+
"face-indifferent",
|
|
2099
|
+
"face-sad",
|
|
2100
|
+
"feedback",
|
|
2101
|
+
"filter",
|
|
2102
|
+
"folder",
|
|
2103
|
+
"generate",
|
|
2104
|
+
"github",
|
|
2105
|
+
"groq",
|
|
2106
|
+
"hash",
|
|
2107
|
+
"heart",
|
|
2108
|
+
"heart-filled",
|
|
2109
|
+
"help-circle",
|
|
2110
|
+
"highlight",
|
|
2111
|
+
"home",
|
|
2112
|
+
"ice-cream",
|
|
2113
|
+
"image",
|
|
2114
|
+
"image-remove",
|
|
2115
|
+
"images",
|
|
2116
|
+
"inbox",
|
|
2117
|
+
"info-filled",
|
|
2118
|
+
"info-outline",
|
|
2119
|
+
"inline",
|
|
2120
|
+
"inline-element",
|
|
2121
|
+
"insert-above",
|
|
2122
|
+
"insert-below",
|
|
2123
|
+
"italic",
|
|
2124
|
+
"joystick",
|
|
2125
|
+
"json",
|
|
2126
|
+
"launch",
|
|
2127
|
+
"leave",
|
|
2128
|
+
"lemon",
|
|
2129
|
+
"link",
|
|
2130
|
+
"link-removed",
|
|
2131
|
+
"linkedin",
|
|
2132
|
+
"list",
|
|
2133
|
+
"lock",
|
|
2134
|
+
"logo-js",
|
|
2135
|
+
"logo-ts",
|
|
2136
|
+
"marker",
|
|
2137
|
+
"marker-removed",
|
|
2138
|
+
"master-detail",
|
|
2139
|
+
"menu",
|
|
2140
|
+
"microphone",
|
|
2141
|
+
"microphone-slash",
|
|
2142
|
+
"mobile-device",
|
|
2143
|
+
"moon",
|
|
2144
|
+
"number",
|
|
2145
|
+
"ok-hand",
|
|
2146
|
+
"olist",
|
|
2147
|
+
"overage",
|
|
2148
|
+
"package",
|
|
2149
|
+
"panel-left",
|
|
2150
|
+
"panel-right",
|
|
2151
|
+
"pause",
|
|
2152
|
+
"pin",
|
|
2153
|
+
"pin-filled",
|
|
2154
|
+
"pin-removed",
|
|
2155
|
+
"play",
|
|
2156
|
+
"plug",
|
|
2157
|
+
"presentation",
|
|
2158
|
+
"progress-50",
|
|
2159
|
+
"progress-75",
|
|
2160
|
+
"projects",
|
|
2161
|
+
"publish",
|
|
2162
|
+
"read-only",
|
|
2163
|
+
"redo",
|
|
2164
|
+
"refresh",
|
|
2165
|
+
"remove",
|
|
2166
|
+
"remove-circle",
|
|
2167
|
+
"reset",
|
|
2168
|
+
"restore",
|
|
2169
|
+
"retrieve",
|
|
2170
|
+
"retry",
|
|
2171
|
+
"revert",
|
|
2172
|
+
"robot",
|
|
2173
|
+
"rocket",
|
|
2174
|
+
"schema",
|
|
2175
|
+
"search",
|
|
2176
|
+
"select",
|
|
2177
|
+
"share",
|
|
2178
|
+
"sort",
|
|
2179
|
+
"sparkle",
|
|
2180
|
+
"sparkles",
|
|
2181
|
+
"spinner",
|
|
2182
|
+
"split-horizontal",
|
|
2183
|
+
"split-vertical",
|
|
2184
|
+
"square",
|
|
2185
|
+
"stack",
|
|
2186
|
+
"stack-compact",
|
|
2187
|
+
"star",
|
|
2188
|
+
"star-filled",
|
|
2189
|
+
"stop",
|
|
2190
|
+
"strikethrough",
|
|
2191
|
+
"string",
|
|
2192
|
+
"sun",
|
|
2193
|
+
"sync",
|
|
2194
|
+
"tablet-device",
|
|
2195
|
+
"tag",
|
|
2196
|
+
"tags",
|
|
2197
|
+
"target",
|
|
2198
|
+
"task",
|
|
2199
|
+
"terminal",
|
|
2200
|
+
"text",
|
|
2201
|
+
"th-large",
|
|
2202
|
+
"th-list",
|
|
2203
|
+
"thumbs-down",
|
|
2204
|
+
"thumbs-up",
|
|
2205
|
+
"tiers",
|
|
2206
|
+
"timeline",
|
|
2207
|
+
"toggle-arrow-right",
|
|
2208
|
+
"token",
|
|
2209
|
+
"transfer",
|
|
2210
|
+
"translate",
|
|
2211
|
+
"trash",
|
|
2212
|
+
"trend-upward",
|
|
2213
|
+
"triangle-outline",
|
|
2214
|
+
"trolley",
|
|
2215
|
+
"truncate",
|
|
2216
|
+
"twitter",
|
|
2217
|
+
"ulist",
|
|
2218
|
+
"unarchive",
|
|
2219
|
+
"underline",
|
|
2220
|
+
"undo",
|
|
2221
|
+
"unknown",
|
|
2222
|
+
"unlink",
|
|
2223
|
+
"unlock",
|
|
2224
|
+
"unpublish",
|
|
2225
|
+
"upload",
|
|
2226
|
+
"user",
|
|
2227
|
+
"users",
|
|
2228
|
+
"versions",
|
|
2229
|
+
"video",
|
|
2230
|
+
"warning-filled",
|
|
2231
|
+
"warning-outline",
|
|
2232
|
+
"wrench"
|
|
2233
|
+
];
|
|
2234
|
+
var DOCK_ICON_NAME_SET = new Set(DOCK_ICON_NAMES);
|
|
2235
|
+
|
|
1995
2236
|
// src/tools.ts
|
|
1996
2237
|
import { BetterCMSError } from "@bettercms-ai/sdk";
|
|
2238
|
+
|
|
2239
|
+
// src/dock-icons.ts
|
|
2240
|
+
var DOCK_ICONS_URI = "bettercms://icons/dock";
|
|
2241
|
+
var DOCK_ICON_SET = DOCK_ICON_NAME_SET;
|
|
2242
|
+
var DOCK_ICONS_RESOURCE = {
|
|
2243
|
+
uri: DOCK_ICONS_URI,
|
|
2244
|
+
name: "dock-icons",
|
|
2245
|
+
title: "BetterCMS dock icon names",
|
|
2246
|
+
description: "Every name `ui.icon`, a fieldset's `icon` and a prop's `ui.group.icon` accept. Layout section icons are a different list (Lucide names).",
|
|
2247
|
+
mimeType: "text/markdown"
|
|
2248
|
+
};
|
|
2249
|
+
var DOCK_ICONS_TEXT = `# Dock icon names
|
|
2250
|
+
|
|
2251
|
+
\`ui.icon\` on a nesting field, \`icon\` on a fieldset and \`ui.group.icon\` on a component prop take ONE
|
|
2252
|
+
name from this list. Any other string is refused with a 400 that names this resource. A leaf field
|
|
2253
|
+
never carries an icon.
|
|
2254
|
+
|
|
2255
|
+
Two vocabularies, on purpose: Layout section icons (update_layout \`add-section\`) stay Lucide names.
|
|
2256
|
+
A name from one list is not valid in the other.
|
|
2257
|
+
|
|
2258
|
+
${DOCK_ICON_NAMES.length} names:
|
|
2259
|
+
|
|
2260
|
+
${DOCK_ICON_NAMES.join(", ")}
|
|
2261
|
+
`;
|
|
2262
|
+
|
|
2263
|
+
// src/tools.ts
|
|
1997
2264
|
import { DeviceAuthPendingError } from "@bettercms-ai/device-auth";
|
|
1998
2265
|
|
|
1999
2266
|
// src/structure-playbook.ts
|
|
2000
2267
|
var STRUCTURE_PLAYBOOK_URI = "bettercms://playbook/structure";
|
|
2001
2268
|
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.`;
|
|
2002
2269
|
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.";
|
|
2270
|
+
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.";
|
|
2003
2271
|
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. Fix what it lists, or say why you are leaving it.";
|
|
2004
|
-
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (
|
|
2272
|
+
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. Call it again after a batch of edits and before telling the user you are done. \`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.`;
|
|
2273
|
+
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.';
|
|
2005
2274
|
var STRUCTURE_EXAMPLE_PAYLOAD = {
|
|
2006
2275
|
version: 0,
|
|
2007
2276
|
doc: {
|
|
@@ -2264,24 +2533,37 @@ var fieldType = z.enum([
|
|
|
2264
2533
|
]);
|
|
2265
2534
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2266
2535
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_]+$/, "letters, numbers, and underscores only");
|
|
2267
|
-
var uiObject = z.
|
|
2536
|
+
var uiObject = z.strictObject({
|
|
2268
2537
|
collapsed: z.boolean().optional().describe("start this panel collapsed. Use it for long optional sections."),
|
|
2269
|
-
preview: z.
|
|
2538
|
+
preview: z.strictObject({
|
|
2270
2539
|
title: z.string().min(1).optional().describe("CHILD KEY whose value titles a collapsed row"),
|
|
2271
2540
|
subtitle: z.string().min(1).optional().describe("CHILD KEY whose value is the row's second line (e.g. 'level' \u2192 'Heading \xB7 h2')"),
|
|
2272
2541
|
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)")
|
|
2273
2542
|
}).optional().describe(
|
|
2274
2543
|
"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."
|
|
2275
2544
|
),
|
|
2276
|
-
layout: z.enum(["list", "grid"]).optional().describe(
|
|
2277
|
-
"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. Same placement rule as `preview`."
|
|
2545
|
+
layout: z.enum(["list", "grid", "table"]).optional().describe(
|
|
2546
|
+
"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`."
|
|
2278
2547
|
),
|
|
2279
2548
|
reorderable: z.boolean().optional().describe(
|
|
2280
2549
|
"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."
|
|
2281
2550
|
),
|
|
2282
2551
|
control: z.enum(["segmented", "slider"]).optional().describe(
|
|
2283
2552
|
"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). Anything else is refused with the rule named."
|
|
2284
|
-
)
|
|
2553
|
+
),
|
|
2554
|
+
// A refine, not an enum: 236 names inlined into every schema that carries `ui` would be paid on
|
|
2555
|
+
// every tools/list. The layoutIcon precedent; the server's z.enum is the guard.
|
|
2556
|
+
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.`),
|
|
2557
|
+
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.")
|
|
2558
|
+
});
|
|
2559
|
+
var propUiObject = uiObject.extend({
|
|
2560
|
+
group: z.strictObject({
|
|
2561
|
+
id: z.string().min(1).max(40),
|
|
2562
|
+
label: z.string().min(1).max(32).optional(),
|
|
2563
|
+
kind: z.enum(["group", "row"]).optional(),
|
|
2564
|
+
icon: uiObject.shape.icon,
|
|
2565
|
+
collapsed: z.boolean().optional()
|
|
2566
|
+
}).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}.")
|
|
2285
2567
|
});
|
|
2286
2568
|
var fieldShape = {
|
|
2287
2569
|
key: fieldKey.describe(
|
|
@@ -2345,14 +2627,23 @@ var urlPatternArg = z.string().nullable().optional().describe(
|
|
|
2345
2627
|
"the public URL of one entry, e.g. '/blog/:slug'. Must start with '/' and contain ':slug' exactly once ('{slug}' is refused). Drives RSS item links and the automatic 301 when an entry's slug changes. null clears it."
|
|
2346
2628
|
);
|
|
2347
2629
|
var fieldsetsArg = z.array(
|
|
2348
|
-
z.
|
|
2630
|
+
z.strictObject({
|
|
2349
2631
|
id: z.string().min(1),
|
|
2350
2632
|
name: z.string().min(1).max(60),
|
|
2351
|
-
description: z.string().max(500).optional()
|
|
2633
|
+
description: z.string().max(500).optional(),
|
|
2634
|
+
kind: z.enum(["group", "row", "tab"]).optional(),
|
|
2635
|
+
tab: z.string().min(1).optional(),
|
|
2636
|
+
icon: uiObject.shape.icon,
|
|
2637
|
+
collapsed: z.boolean().optional(),
|
|
2638
|
+
// Server-owned provenance, as get_content_model returns it: an echo parses and is dropped
|
|
2639
|
+
// before the write (dropProvenance), never refused.
|
|
2640
|
+
uiOrigin: z.unknown().optional(),
|
|
2641
|
+
uiBefore: z.unknown().optional()
|
|
2352
2642
|
})
|
|
2353
2643
|
).max(30).optional().describe(
|
|
2354
|
-
"the model's editor cards: [{id, name, description?}], at most 30, ids unique, order = array index.
|
|
2644
|
+
"the model's editor cards: [{id, name, description?, kind?, tab?, icon?, collapsed?}], at most 30, ids unique, order = array index. kind 'group' is a disclosure, 'row' puts up to 3 short scalars on one line, 'tab' is a tab (at most 3, top-level fields only; untabbed fields stay pinned above the tabs; a group's `tab` names its tab). A field joins one with `fieldsetId`. Editor-only: never changes what is stored or delivered."
|
|
2355
2645
|
);
|
|
2646
|
+
var dropProvenance = (sets) => sets?.map(({ uiOrigin: _origin, uiBefore: _before, ...set }) => set) ?? sets;
|
|
2356
2647
|
var RICH_TEXT_NOTE = "\u{1F534} A field of type 'text' is created as RICH TEXT unless you send `richText: false` on it. Send `richText: false` for every value that is not prose: a name used as a title, a URL, slug, id, SKU, email, phone, CSS class or icon name.";
|
|
2357
2648
|
var PAGES_NOT_COLLECTIONS_NOTE = "A collection is only for items one template route repeats (/blog/:slug: set urlPattern). A single page's copy (home, about, contact, pricing, legal) is page fields in sections (add_page_field / set_page_content), never a one-entry collection.";
|
|
2358
2649
|
function flatFieldKeys(fields) {
|
|
@@ -2437,7 +2728,25 @@ function toField(f) {
|
|
|
2437
2728
|
};
|
|
2438
2729
|
}
|
|
2439
2730
|
var BRAND_KIT_NOTE = "`brandKit` (colors, typography, radius and any typeScale/spacing/elevation/motion/graphics) is edited in the BetterCMS dashboard AND by set_brand_assets and set_brand_graphics; every write is a version a person can restore. ASSETS: a media asset OF THIS PROJECT by id; null clears one; a slot a person chose needs `replace: true`. GRAPHICS: `kind`, optional `angle`, 2-8 stops naming `colors.*` or `extras` keys \u2014 never a CSS string and never a hex. CONSUME, DO NOT COPY: a hosted page exposes `--brand-color-<key>`, `--brand-shadow-<key>` and `--brand-gradient-<key>`; style sections with their `style` tokens (`nav.backgroundGradient` and `footer.backgroundGradient` take a gradient key), never copied values. Never invent brand facts: read them from get_project.";
|
|
2440
|
-
var
|
|
2731
|
+
var UPDATE_MODEL_DESCRIPTION = "Edit a content model: rename it, change description/slug/urlPattern, set its `fieldsets`, or reshape how its EXISTING fields look in the editor. Never adds, removes or retypes a field (use add_field). Send modelId, or pageId for the model a page's schema lives in. `fieldsets` REPLACES the list (null drops all); a field joins one with fieldFieldset or add_field's `fieldsetId`. The field* args address fields by dotted key path ('hero.title'). A taken slug is 409 `slug_taken`. Section 14 of bettercms://playbook/schema.";
|
|
2732
|
+
var MODEL_IF_MATCH_NOTE = "optimisticVersion from get_content_model (with pageId: get_page backingModel). The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply, never retry blindly. REQUIRED with field*, override or a fieldset kind/tab/icon/collapsed (428 without); otherwise, omitted, the last write wins.";
|
|
2733
|
+
var COMPONENT_IF_MATCH_NOTE = "optimisticVersion from get_component. The write applies only at that version; a 409 version-conflict means it changed since: read it again and re-apply. REQUIRED with prop*, override or a prop ui icon/itemLabel/group (428 without).";
|
|
2734
|
+
var PROP_UI_ARG_NOTE = "top-level prop key \u2192 ui patch, the same vocabulary as a prop's `ui`: each key merges, a null key clears it, null clears the whole ui";
|
|
2735
|
+
var OVERRIDE_NOTE = "replace chrome a person set (otherwise 409 ui_human_owned names those paths). Send it only on the user's word.";
|
|
2736
|
+
var FIELD_UI_ARG_NOTE = "path \u2192 ui patch, a field's `ui` vocabulary: each key merges, a null key clears it, null clears the whole ui";
|
|
2737
|
+
var MODEL_UPDATE_KEYS = [
|
|
2738
|
+
"name",
|
|
2739
|
+
"slug",
|
|
2740
|
+
"description",
|
|
2741
|
+
"urlPattern",
|
|
2742
|
+
"fieldsets",
|
|
2743
|
+
"fieldUi",
|
|
2744
|
+
"fieldFieldset",
|
|
2745
|
+
"fieldOrder",
|
|
2746
|
+
"fieldLabel",
|
|
2747
|
+
"fieldHelpText",
|
|
2748
|
+
"override"
|
|
2749
|
+
];
|
|
2441
2750
|
var ENTRY_META_NOTE = "SEO is native: set this entry's metaTitle, metaDescription, noindex, canonical, og, twitter, schemaType or schema in `meta` (never as model fields). `meta` MERGES into what is stored: a key you leave out is kept, null or an empty string clears it.";
|
|
2442
2751
|
var PAGE_HEAD_NOTE = "noindex, canonical, og and twitter set the page's head extras; each MERGES into what is stored (a key you leave out is kept, null or an empty string clears it).";
|
|
2443
2752
|
var STRUCTURE_NOTE = 'ORGANISATION ONLY: Content sidebar folders never change a URL, slug, content or publish state; URL folders (page folders) are set in the dashboard. Folders nest at most 4 levels deep. Icons are lucide names (a-z, 0-9, dashes); null clears one. SYSTEM FOLDERS "home:pages" (Other pages), "home:collections" (Shared) and "home:globals" (Settings) always exist and can never be moved or deleted (400 NAV_SYSTEM_FOLDER); rename_folder and set_icon work on them. TYPE PURITY (400 NAV_TYPE_MISMATCH): Other pages holds only pages at any depth, Shared only collections, Settings neither \u2014 globals are not sidebar items. ACCESS: changing the structure needs Admin or Developer, reading only content access; a 403 SCHEMA_ACCESS_REQUIRED means neither: stop and tell the user, do not retry. PINS sit above the folders in their own order, pages or collections; in a document a pin is folderId "home:root", a virtual id with no folder record (a folder record with that id is 400 NAV_SYSTEM_FOLDER). set_content_structure never refuses a kind mismatch: it returns `warnings` and `kindMismatches` (the single writes do \u2014 see `kind`). PAGE-BOUND MODELS: a page with fields has a bound model that is NOT a collection: never listed or filed, and the writes refuse it with 400 NAV_PAGE_BOUND_MODEL naming its page. Organise the PAGE instead, by its page id. ';
|
|
@@ -2594,6 +2903,7 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
|
|
|
2594
2903
|
"set_authoring_preference",
|
|
2595
2904
|
"set_media_delivery",
|
|
2596
2905
|
"delete_form_submission",
|
|
2906
|
+
"remove_page_fields",
|
|
2597
2907
|
// The ABM campaign lane: a marketer's dashboard surface, plus the worker tools a task runner
|
|
2598
2908
|
// claims with. Ten more tools than the tail was written for, and a connector that truncates
|
|
2599
2909
|
// would otherwise lose `get_deploy_status` and the GitHub lane to make room for them.
|
|
@@ -2663,7 +2973,7 @@ function buildToolDefs(deps) {
|
|
|
2663
2973
|
]).describe("block type; section/slider/tabs/columns nest child blocks"),
|
|
2664
2974
|
id: z.string().min(1).describe("stable unique block id"),
|
|
2665
2975
|
props: z.record(z.string(), z.unknown()).describe(
|
|
2666
|
-
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may NOT hold columns/section/slider/tabs; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, detailComponentId?, titleField?, excerptField?, limit?, order?, emptyText?} \u2014 lists this page's published entries as cards, and renders ONE entry on /<page>/<entrySlug>"
|
|
2976
|
+
"per-type props: heading {text, level}; text/richtext {html} (NOT {text}), and both also take level 1-6, rendering the html AS that <hN> with inline marks kept (one line of inline copy only) \u2014 use it for a title that carries a styled span; image {src, alt}; button {text, href, variant? primary|secondary, size? sm|md|lg (default md), fullWidth? boolean}; spacer {height}; video {url}; form {formId}; component {componentId, overrides?}; navbar {links:[{label,href}], logo?, cta?}; footer {columns, copyright?}; section {children: block[]}; columns {columns: block[][], gap} \u2014 a column may NOT hold columns/section/slider/tabs; slider {slides:[{id,children}]}; tabs {tabs:[{id,label,children}]}; collection {cardComponentId?, detailComponentId?, titleField?, excerptField?, limit?, order?, emptyText?} \u2014 lists this page's published entries as cards, and renders ONE entry on /<page>/<entrySlug>"
|
|
2667
2977
|
),
|
|
2668
2978
|
style: z.record(z.string(), z.unknown()).optional().describe(
|
|
2669
2979
|
"design tokens: theme, bg (none|surface|muted|accent|dark|custom), bgCustom hex, paddingTop/paddingBottom/paddingSides px, contentWidth (narrow|default|wide|full), align, corner, shadow, borderTop/borderBottom, border (all four sides, in the text colour). A real marketing band is a `section` block carrying bg + padding + contentWidth."
|
|
@@ -2907,7 +3217,7 @@ function buildToolDefs(deps) {
|
|
|
2907
3217
|
// ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
|
|
2908
3218
|
// sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
|
|
2909
3219
|
// the declaration gone. `ui` rode through the backend's looseObject `config` and died here.
|
|
2910
|
-
ui:
|
|
3220
|
+
ui: propUiObject.optional().describe(
|
|
2911
3221
|
"AUTHORING CHROME for this prop's control in the inspector \u2014 never a delivery effect, and the same object a FIELD's `ui` takes. `preview` and `layout` are valid on 'group', 'table' and 'slot' only; `reorderable` on 'table' ALONE, the one prop type whose value is a LIST (a 'group' is one object, a 'slot' one component instance). preview keys name a SUB-FIELD of this prop's config.fields and are checked against them (on a 'slot' the shape is checked but the keys cannot be \u2014 they belong to whichever component fills it). An unknown key inside `ui` is a 400, not a silent strip. Read it back with get_component: a prop whose `ui` is absent there was not stored."
|
|
2912
3222
|
)
|
|
2913
3223
|
});
|
|
@@ -3003,7 +3313,12 @@ function buildToolDefs(deps) {
|
|
|
3003
3313
|
blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
|
|
3004
3314
|
props: z.array(componentPropObject).optional().describe(
|
|
3005
3315
|
"REPLACES the prop array \u2014 include every prop you want to keep, WITH its stored `ui`, or that prop's authoring chrome is dropped along with the prop. Read get_component first."
|
|
3006
|
-
)
|
|
3316
|
+
),
|
|
3317
|
+
propUi: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe(PROP_UI_ARG_NOTE),
|
|
3318
|
+
propGroup: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe("prop key \u2192 its ui.group ({id, label?, kind?, icon?, collapsed?}), null takes it out"),
|
|
3319
|
+
propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label"),
|
|
3320
|
+
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
3321
|
+
ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE)
|
|
3007
3322
|
});
|
|
3008
3323
|
const setComponentSourceInput = z.object({
|
|
3009
3324
|
componentId: z.string().min(1).describe("component id (from list_components)"),
|
|
@@ -3530,7 +3845,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3530
3845
|
def(
|
|
3531
3846
|
"set_authoring_preference",
|
|
3532
3847
|
"Set the site's authoring architecture",
|
|
3533
|
-
"Record which authoring architecture this site uses \u2014 'components' (RECOMMENDED: reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 recommend it for every marketing, landing, agency or product site) or 'fields' (a typed field schema per page \u2014 only for a blog, catalogue or directory where many rows share one shape). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set,
|
|
3848
|
+
"Record which authoring architecture this site uses \u2014 'components' (RECOMMENDED: reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema \u2014 recommend it for every marketing, landing, agency or product site) or 'fields' (a typed field schema per page \u2014 only for a blog, catalogue or directory where many rows share one shape). ASK THE USER; do not pick for them. Called without `preference`, this tool asks them directly (or hands you the question to ask). deploy_project, deploy_from_upload and promote_project all refuse with 409 AUTHORING_DECISION_REQUIRED until it is set, carrying this project's real page counts to show the user. Unset, the release lane already acts as `components`; only `fields` stops it. On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. Asked once per project; re-callable if the user changes their mind.",
|
|
3534
3849
|
// Optional in the schema for exactly the reason `framework` is above: a required arg is
|
|
3535
3850
|
// rejected by the SDK before the handler runs, which would kill the elicitation below
|
|
3536
3851
|
// and leave the model guessing. Optional here, answered by a human there. The backend
|
|
@@ -3620,17 +3935,39 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3620
3935
|
def(
|
|
3621
3936
|
"update_content_model",
|
|
3622
3937
|
"Update a content model's metadata",
|
|
3623
|
-
|
|
3624
|
-
z.object({
|
|
3938
|
+
UPDATE_MODEL_DESCRIPTION,
|
|
3939
|
+
z.object({
|
|
3940
|
+
modelId: z.string().min(1).optional().describe("content model id (from list_content_models); or send pageId"),
|
|
3941
|
+
pageId: z.string().min(1).optional().describe("instead of modelId: the page whose schema model to edit; ifMatch is get_page backingModel.optimisticVersion"),
|
|
3942
|
+
name: z.string().optional(),
|
|
3943
|
+
slug: z.string().optional(),
|
|
3944
|
+
description: z.string().optional(),
|
|
3945
|
+
urlPattern: urlPatternArg,
|
|
3946
|
+
fieldsets: fieldsetsArg.nullable(),
|
|
3947
|
+
fieldUi: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe(FIELD_UI_ARG_NOTE),
|
|
3948
|
+
fieldFieldset: z.record(z.string(), z.string().min(1).nullable()).optional().describe("path \u2192 fieldset id, null takes it out"),
|
|
3949
|
+
fieldOrder: z.strictObject({ parentPath: z.string().min(1).optional(), order: z.array(z.string().min(1)).min(1).max(200) }).optional().describe("the full new order of one parent's child keys (top level without parentPath)"),
|
|
3950
|
+
fieldLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("path \u2192 label"),
|
|
3951
|
+
fieldHelpText: z.record(z.string(), z.string().max(500).nullable()).optional().describe("path \u2192 help text, null clears it"),
|
|
3952
|
+
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
3953
|
+
ifMatch: z.number().int().min(0).optional().describe(MODEL_IF_MATCH_NOTE)
|
|
3954
|
+
}).shape,
|
|
3625
3955
|
// `fields` is deliberately not sent: the PATCH replaces `fields` wholesale when present.
|
|
3626
|
-
async (c, a) =>
|
|
3627
|
-
"
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3956
|
+
async (c, a) => {
|
|
3957
|
+
if (Boolean(a.modelId) === Boolean(a.pageId)) return fail("Send exactly one of modelId or pageId.");
|
|
3958
|
+
const path = a.pageId ? `/management/pages/${s(a.pageId)}/model` : `/management/content/models/${s(a.modelId)}`;
|
|
3959
|
+
const body = Object.fromEntries(
|
|
3960
|
+
MODEL_UPDATE_KEYS.filter((k) => a[k] !== void 0).map((k) => [k, k === "fieldsets" ? dropProvenance(a[k]) : a[k]])
|
|
3961
|
+
);
|
|
3962
|
+
return ok(
|
|
3963
|
+
"Updated content model.",
|
|
3964
|
+
(await c.fetchJSON(c.url(path), {
|
|
3965
|
+
method: "PATCH",
|
|
3966
|
+
body: JSON.stringify(body),
|
|
3967
|
+
...a.ifMatch !== void 0 ? { headers: { "if-match": `W/"${a.ifMatch}"` } } : {}
|
|
3968
|
+
})).data
|
|
3969
|
+
);
|
|
3970
|
+
}
|
|
3634
3971
|
),
|
|
3635
3972
|
def(
|
|
3636
3973
|
"get_content_types",
|
|
@@ -3640,6 +3977,25 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3640
3977
|
async (c) => ok("Content types.", await data(c, "GET", `/management/content/types`))
|
|
3641
3978
|
),
|
|
3642
3979
|
// ── Pages (metadata edit; parity with remote /mcp) ──
|
|
3980
|
+
def(
|
|
3981
|
+
"remove_page_fields",
|
|
3982
|
+
"Remove fields from a page",
|
|
3983
|
+
"Delete page fields and their stored values (draft, published, every repeater row) in one transaction. Dry run by default: returns removedKeys, entriesWithContent, keysWithContent, liveCopyChanges and writes nothing. A field holding content needs discardContent:true (409 FIELDS_HOLD_CONTENT otherwise). keys: exact get_page keys, dotted for a group/repeater child (`features.icon`). Changing the live copy needs publish permission. Never use it to rename a field.",
|
|
3984
|
+
z.object({
|
|
3985
|
+
pageId: z.string().min(1).describe("page id (from list_pages)"),
|
|
3986
|
+
keys: z.array(z.string().min(1)).min(1).describe("field keys to remove"),
|
|
3987
|
+
dryRun: z.boolean().optional().describe("default true"),
|
|
3988
|
+
discardContent: z.boolean().optional().describe("delete fields that hold content, and the content")
|
|
3989
|
+
}).shape,
|
|
3990
|
+
// The whole body: a dry run answers with the report and no `data`.
|
|
3991
|
+
async (c, a) => {
|
|
3992
|
+
const res = await c.fetchJSON(c.url(`/management/pages/${s(a.pageId)}/fields/remove`), {
|
|
3993
|
+
method: "POST",
|
|
3994
|
+
body: JSON.stringify({ keys: a.keys, dryRun: a.dryRun, discardContent: a.discardContent })
|
|
3995
|
+
});
|
|
3996
|
+
return ok(res.dryRun ? "Dry run \u2014 nothing removed." : "Removed fields.", res);
|
|
3997
|
+
}
|
|
3998
|
+
),
|
|
3643
3999
|
def(
|
|
3644
4000
|
"update_page",
|
|
3645
4001
|
"Edit a page",
|
|
@@ -3811,7 +4167,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3811
4167
|
def(
|
|
3812
4168
|
"get_binding_report",
|
|
3813
4169
|
"Check what on the live site is editable",
|
|
3814
|
-
"The receipt for 'is this site actually EDITABLE?'.
|
|
4170
|
+
"The receipt for 'is this site actually EDITABLE?'. Each release scans the built HTML for the element rendering each CMS field value; per slot this returns `mode` ('text-match' = guessed from rendered text, 'declared' = template-declared), what was bound, and `unmatched`: each path with its reason \u2014 not-declared, ambiguous-text, no-element, declared-only, needs-wrap, refused, layout-owned, page-owned, layout-locator-stale, locale-unvarianted, svg-unavailable, report-outdated. DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not; `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path. `unaddressable` is the copy it cannot see: visible live text no field owns, by route and bucket, biggest first (null = unmeasured: diff visible text against entry values). Over `count / visible > 0.02`, fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook \xA711. `builtRoutes`: routes the live build has HTML for (null = cannot say). `layoutIncomplete`: Layout paths the last content apply could not write (their publish waits); `layoutUnread`: Layout fields no route reads (`layoutUnreadJudged` false = not judged). `canvas.lane`: the editor's live-preview lane (`bridge`, `draft-route`; `none` \u2192 get_next_steps). `coverage.error` `nothing-bound`: no element of this build is bound, so its percentage describes another site.",
|
|
3815
4171
|
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves.") }).shape,
|
|
3816
4172
|
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
3817
4173
|
),
|
|
@@ -3853,18 +4209,19 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3853
4209
|
def(
|
|
3854
4210
|
"componentize_sections",
|
|
3855
4211
|
"Turn this site's derived sections into components",
|
|
3856
|
-
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group, each carrying `props.bind: \"<groupKey>\"` \u2014 the page field group that already holds the copy. Nothing is copied or moved: the page keeps its `fields`, values and bindings, so click-to-edit and the coverage meter do not change (`copy: \"instance\"` moves the words onto each placement instead \u2014 see `copy`). Pass the plan's `digest`; a 409 `stale-plan` means the site changed: read the plan again, show the user what changed and confirm again. Read `components.wouldDuplicate`
|
|
4212
|
+
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group, each carrying `props.bind: \"<groupKey>\"` \u2014 the page field group that already holds the copy. Nothing is copied or moved: the page keeps its `fields`, values and bindings, so click-to-edit and the coverage meter do not change (`copy: \"instance\"` moves the words onto each placement instead \u2014 see `copy`). Pass the plan's `digest`; a 409 `stale-plan` means the site changed: read the plan again, show the user what changed and confirm again. Read `components.wouldDuplicate` first: each id is an existing same-family component a NEW one would sit beside (reuse is by identity and slug, never family). Re-running is safe: a placed group returns as ALREADY_COMPONENTIZED. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages, then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. Every component it CREATES is filed into a Component Group (see `group`). When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized, the plan reports them `NOT_A_SECTION` / `DECLINED` and stops proposing them, and a plan that proposes nothing undeclined is done.",
|
|
3857
4213
|
z.object({
|
|
3858
4214
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
3859
4215
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
3860
4216
|
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."),
|
|
3861
|
-
sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan)
|
|
3862
|
-
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\"
|
|
4217
|
+
sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan); every other section keeps its placement."),
|
|
4218
|
+
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\"`."),
|
|
3863
4219
|
dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
|
|
3864
4220
|
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."),
|
|
3865
|
-
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`.")
|
|
4221
|
+
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`."),
|
|
4222
|
+
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.")
|
|
3866
4223
|
}).shape,
|
|
3867
|
-
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline }))
|
|
4224
|
+
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline, joinTwins: a.joinTwins }))
|
|
3868
4225
|
),
|
|
3869
4226
|
def(
|
|
3870
4227
|
"get_analytics_overview",
|
|
@@ -4310,7 +4667,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4310
4667
|
...args.description !== void 0 ? { description: args.description } : {},
|
|
4311
4668
|
...args.urlPattern !== void 0 ? { urlPattern: args.urlPattern } : {},
|
|
4312
4669
|
...args.kind !== void 0 ? { kind: args.kind } : {},
|
|
4313
|
-
...args.fieldsets !== void 0 ? { fieldsets: args.fieldsets } : {},
|
|
4670
|
+
...args.fieldsets !== void 0 ? { fieldsets: dropProvenance(args.fieldsets) } : {},
|
|
4314
4671
|
fields: toFields(args.fields)
|
|
4315
4672
|
});
|
|
4316
4673
|
return ok(
|
|
@@ -4667,7 +5024,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4667
5024
|
name: "publish_layout",
|
|
4668
5025
|
config: {
|
|
4669
5026
|
title: "Publish the Global Layout draft",
|
|
4670
|
-
description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section)
|
|
5027
|
+
description: "Publish the connected project's GLOBAL Layout draft (navigation, footer, every reserved section). On a site BetterCMS serves, a Layout publish never builds: it writes the VALUES into the live pages, and a STRUCTURE change (Sections added, removed, moved) waits for a Rebuild or a push (`layout-rebuild-required`); a custom host gets its deploy hook. Until published, get_layout copy:'published' answers PUBLISHED_LAYOUT_UNAVAILABLE. Pass get_layout's `revision` as ifMatch; a stale one returns 409 \u2014 re-read, never retry blindly. A 422 lists validation issues (global, or per page override: pageSlug, copy) to fix with update_layout. A new project's chrome is draft: publish_component navigation-default and footer-default first (each independently). navigation.logo is optional: empty, the nav shows the brand mark (set_brand_assets) or the site name. Never add a logo the site does not have. Verify with get_layout copy:'published' \u2014 this response is the write's echo, not a receipt. Page Layout overrides go live with the page (update_page status:'published'). 403 PUBLISH_NOT_GRANTED: this connection may author drafts, not publish \u2014 say so; the user allows it or publishes from the dashboard.",
|
|
4671
5028
|
inputSchema: publishLayoutInput.shape
|
|
4672
5029
|
},
|
|
4673
5030
|
handler: guard(async (args) => withClient(async (client) => {
|
|
@@ -4907,8 +5264,8 @@ ${lines.join("\n")}`, found);
|
|
|
4907
5264
|
},
|
|
4908
5265
|
handler: guard(
|
|
4909
5266
|
async (args) => withClient(async (client) => {
|
|
4910
|
-
const { componentId, ...input } = args;
|
|
4911
|
-
const cmp = await client.updateComponent(componentId, input);
|
|
5267
|
+
const { componentId, ifMatch, ...input } = args;
|
|
5268
|
+
const cmp = await client.updateComponent(componentId, input, { ifMatch });
|
|
4912
5269
|
return ok(`Updated component '${cmp.name}' (id ${cmp.id}).`, cmp);
|
|
4913
5270
|
})
|
|
4914
5271
|
)
|
|
@@ -5370,6 +5727,12 @@ AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architectu
|
|
|
5370
5727
|
asked ONCE per project, ever. It is not an error to retry or route around: read the message
|
|
5371
5728
|
out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
5372
5729
|
|
|
5730
|
+
Until someone answers, the release lane behaves as \`components\`: the preference is null, and
|
|
5731
|
+
each release still componentizes the derived pages in bind mode. \`fields\` is the only answer
|
|
5732
|
+
that stops it. And a run over the WHOLE plan splits a section component an earlier run placed on
|
|
5733
|
+
2 pages into one per page (the first page keeps it, the others get a clone), so two pages' heroes
|
|
5734
|
+
stop editing each other; a run narrowed to one page or section never splits.
|
|
5735
|
+
|
|
5373
5736
|
It exists because an imported site arrives **field-driven whether anyone chose that or not**
|
|
5374
5737
|
\u2014 a crawl-based import (Webflow, a starter, a template) emits pages with a typed field schema
|
|
5375
5738
|
and an empty block tree, because that is all a crawl can infer. Nobody decided it. On a
|
|
@@ -5385,11 +5748,14 @@ component placement, and the page keeps its fields, its values and its bindings.
|
|
|
5385
5748
|
2. get_componentize_plan one component per section, computed live; creates nothing. It
|
|
5386
5749
|
reads this project's PUBLISHED pages, so no deploy is needed
|
|
5387
5750
|
to reach it \u2014 the deploy is the last step, not the first.
|
|
5388
|
-
On a site DERIVED at import
|
|
5389
|
-
this plan in bind mode
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5751
|
+
On a site DERIVED at import, each release after this answer
|
|
5752
|
+
runs this plan in bind mode for NEW sections (it never splits
|
|
5753
|
+
or joins a placement, and runs nothing while the answer is
|
|
5754
|
+
unset) and PUBLISHES what it created, so after one expect
|
|
5755
|
+
ALREADY_COMPONENTIZED and skip to step 6 (publish the pages) \u2014
|
|
5756
|
+
the lane publishes the COMPONENTS, not the placements, which
|
|
5757
|
+
are still draft blocks on the pages. A /fr still on its own
|
|
5758
|
+
components joins its twin's with \`joinTwins: true\` (step 4)
|
|
5393
5759
|
3. show the user the plan and CONFIRM \u2014 it says how many components and which pages
|
|
5394
5760
|
4. componentize_sections \`dryRun: true\` first, then for real; components land as DRAFTS
|
|
5395
5761
|
and each placement carries \`props.bind\` to the page's field
|
|
@@ -5502,7 +5868,7 @@ renders. Three consequences, each load-bearing:
|
|
|
5502
5868
|
Binding only one copy leaves the others showing the old text until the next rebuild.
|
|
5503
5869
|
3. Make the chrome ITSELF editable by speaking the layout grammar: the nav/footer
|
|
5504
5870
|
elements declare \`data-bcms-layout-section="navigation"\` / \`"footer"\`, and each
|
|
5505
|
-
CMS-backed text inside them a \`data-bcms-layout-field="layout:<
|
|
5871
|
+
CMS-backed text inside them a \`data-bcms-layout-field="layout:<sections[slug].id>:<fieldId>"\`
|
|
5506
5872
|
marker (fieldId is the field's REAL id, verbatim \u2014 production layout ids are
|
|
5507
5873
|
section-prefixed and dotted, e.g. \`footer.tagline\`, so the marker reads
|
|
5508
5874
|
\`layout:footer:footer.tagline\`) \u2014 the canvas then gives them hover chrome, the
|
|
@@ -5512,18 +5878,31 @@ renders. Three consequences, each load-bearing:
|
|
|
5512
5878
|
sub appends its slug (\`layout:navigation:navigation.cta.label\`), a repeater row its
|
|
5513
5879
|
STORED index then the slug (\`layout:navigation:navigation.links.0.label\`), nesting
|
|
5514
5880
|
as deep as the schema goes (\`layout:footer:footer.link-groups.0.links.1.label\`).
|
|
5515
|
-
|
|
5881
|
+
Five rules, each one a measured defect when broken:
|
|
5516
5882
|
a. a field rendered by TWO elements gets a marker on BOTH \u2014 the editor keeps the copies
|
|
5517
5883
|
in sync, and a marker on only one leaves the other stale;
|
|
5518
5884
|
b. PROVENANCE \u2014 mark an element only when the LAYOUT supplied its value; a marker
|
|
5519
5885
|
over a fallback/singleton-sourced string opens an editor for a row that does not
|
|
5520
5886
|
exist;
|
|
5521
5887
|
c. row markers use the value's STORED index (a row you filtered out of the render
|
|
5522
|
-
still occupies its slot), or the edit lands on the wrong row
|
|
5888
|
+
still occupies its slot), or the edit lands on the wrong row;
|
|
5889
|
+
d. the address names the DELIVERED entry's id, \`sections[slug].id\`, never the slug \u2014 a
|
|
5890
|
+
Section created in the dashboard has a UUID id. On a translated route address the
|
|
5891
|
+
entry that fills the slot, by ITS own id: a page's own \`layout\` already delivers it
|
|
5892
|
+
at \`sections['navigation']\`, the Global Layout carries it beside the base at
|
|
5893
|
+
\`sections['navigation--fr']\`; the \`data-bcms-layout-section\` wrapper still names
|
|
5894
|
+
the SLOT, the base slug (\`navigation\`);
|
|
5895
|
+
e. ONE OWNER PER ELEMENT: never a \`data-bcms-field\` inside a Layout-declared region.
|
|
5896
|
+
The report will not flag one you write: it binds, and the element gets two writers.
|
|
5897
|
+
\`layout-owned\` names only the page copies left with no element, which the strip removes.
|
|
5523
5898
|
Text/longtext leaves edit inline; link/select/image leaves are side-panel-only by
|
|
5524
5899
|
design (their formats need a real control). THE DOCTRINE: every string a marketer
|
|
5525
5900
|
can see must be addressable \u2014 no marker means read-only on the canvas, so an
|
|
5526
5901
|
unmarked CMS-backed string is a defect, not a style choice.
|
|
5902
|
+
On a site BetterCMS serves, a Layout publish never builds: it writes the new VALUES into
|
|
5903
|
+
the live pages, and a STRUCTURE change (a Section added, removed or moved) waits for a
|
|
5904
|
+
Rebuild or a push \u2014 get_next_steps says \`layout-rebuild-required\` until a build ships
|
|
5905
|
+
it. A custom host gets its deploy hook instead.
|
|
5527
5906
|
4. Keep chrome semantic \u2014 \`<nav>\`, \`<footer>\`, page content inside \`<main>\`, mastheads
|
|
5528
5907
|
as a top-level \`<header>\`. Chrome is edited through the project Layout, not the page,
|
|
5529
5908
|
and semantic landmarks are how the editor keeps a nav edit from being written into page
|
|
@@ -5571,7 +5950,7 @@ below is one heading of that ask.
|
|
|
5571
5950
|
**1. Global elements.** Navbar, mobile nav, header and footer are the LAYOUT, not page content:
|
|
5572
5951
|
\`get_layout\` then \`update_layout\`, and a chrome COMPONENT is placed into a Layout section with
|
|
5573
5952
|
the \`add-component\` command. CTA buttons are \`button\` BLOCKS inside the section that uses them,
|
|
5574
|
-
exposed as \`text\` + \`url\` props \u2014 variant, size and
|
|
5953
|
+
exposed as \`text\` + \`url\` props \u2014 variant, size and fullWidth are that block's own props and
|
|
5575
5954
|
\`style\`, never a standalone Button component. Rich text is a \`richtext\` prop over a
|
|
5576
5955
|
\`richtext\`/\`text\` block. Forms are \`create_form\` plus a \`form\` block, and form FIELDS are the
|
|
5577
5956
|
one place the platform models \`required\`, \`placeholder\`, \`helpText\` and real \`validation\`
|
|
@@ -5590,8 +5969,8 @@ still gets its own component: its own family, plus \`allowedOn: ["slug:<page>"]\
|
|
|
5590
5969
|
with NO \`sectionType\` never appears in the editor's "Add a section" picker. Start from the
|
|
5591
5970
|
builtin blueprints \`list_components\` returns (\`builtin:*\`) instead of hand-writing block JSON.
|
|
5592
5971
|
On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
|
|
5593
|
-
of this in one call, and
|
|
5594
|
-
mode and
|
|
5972
|
+
of this in one call, and once \`components\` is recorded each release of a site derived at IMPORT
|
|
5973
|
+
runs it in bind mode and publishes the components, so expect ALREADY_COMPONENTIZED \u2014 the placements it wrote are
|
|
5595
5974
|
still DRAFT blocks on the pages, so publish the pages, then \`npx @bettercms-ai/convert
|
|
5596
5975
|
--componentize\`.
|
|
5597
5976
|
|
|
@@ -5889,7 +6268,7 @@ delivered value \u2014 they change editor chrome only.
|
|
|
5889
6268
|
\`\`\`jsonc
|
|
5890
6269
|
{ "key": "testimonials", "label": "Testimonials", "type": "repeater",
|
|
5891
6270
|
"ui": { "collapsed": true, "preview": { "title": "author", "media": "avatar" },
|
|
5892
|
-
"layout": "grid", "
|
|
6271
|
+
"layout": "grid", "icon": "double-quote", "itemLabel": "Testimonial" },
|
|
5893
6272
|
"helpText": "Three to five. Shortest quotes read best.",
|
|
5894
6273
|
"fields": [ { "key": "author", ... }, { "key": "avatar", "type": "image", ... }, ... ] }
|
|
5895
6274
|
\`\`\`
|
|
@@ -5919,6 +6298,11 @@ delivered value \u2014 they change editor chrome only.
|
|
|
5919
6298
|
\`select\` with a few short options (alignment, theme, size: one click instead of a dropdown);
|
|
5920
6299
|
\`'slider'\` on a \`number\` with \`config.min\` < \`config.max\` (opacity, columns). Anywhere else it
|
|
5921
6300
|
is refused with the rule named.
|
|
6301
|
+
- **\`ui.icon\`** \u2014 the branch row's icon: ONE name from \`bettercms://icons/dock\` (read that resource;
|
|
6302
|
+
any other string is a 400). Nesting fields, fieldsets and a prop's \`ui.group\` only. Layout
|
|
6303
|
+
section icons are a different vocabulary (Lucide names), so a name valid there may be refused here.
|
|
6304
|
+
- **\`ui.itemLabel\`** \u2014 what ONE item of a list is called ("Slide", "Person"), at most 24 characters:
|
|
6305
|
+
the add button, the crumbs and the empty state say it. Nesting fields only.
|
|
5922
6306
|
|
|
5923
6307
|
**Six refusals, all deliberate, all 400 with the fix in the message.**
|
|
5924
6308
|
1. An unknown key inside \`ui\` is REJECTED, not stripped. \`ui\` is a strict object precisely so
|
|
@@ -5942,7 +6326,10 @@ only prop whose value is a list (a \`group\` is one object, a \`slot\` one compo
|
|
|
5942
6326
|
prop's \`preview\` keys name a SUB-FIELD from its own \`config.fields\` and are checked against
|
|
5943
6327
|
them \u2014 except on a \`slot\`, whose children belong to whichever component fills it and therefore
|
|
5944
6328
|
cannot be resolved at save time. \`update_component\` REPLACES the whole \`props\` array, so echo
|
|
5945
|
-
back each prop's stored \`ui\` or you delete it along with the prop.
|
|
6329
|
+
back each prop's stored \`ui\` or you delete it along with the prop. Props have no fieldsets, so a
|
|
6330
|
+
prop joins a group with \`ui.group\`: the FIRST prop of a group defines \`{ id, label, kind?, icon?,
|
|
6331
|
+
collapsed? }\` (kind \`group\` or \`row\`, never \`tab\`), and every later prop sends \`{ id }\` only. A
|
|
6332
|
+
FIELD never takes \`ui.group\`; it joins a fieldset with \`fieldsetId\`.
|
|
5946
6333
|
|
|
5947
6334
|
\`\`\`jsonc
|
|
5948
6335
|
{ "key": "slides", "label": "Slides", "type": "table",
|
|
@@ -5952,11 +6339,92 @@ back each prop's stored \`ui\` or you delete it along with the prop.
|
|
|
5952
6339
|
{ "key": "caption", "type": "text", ... } ] } }
|
|
5953
6340
|
\`\`\`
|
|
5954
6341
|
|
|
6342
|
+
**Patterns \u2014 what the dock draws, and the key that asks for it.**
|
|
6343
|
+
- **leaf** \u2014 one field, one control. Nothing to declare; \`ui.control\` only when the default is wrong.
|
|
6344
|
+
- **group** \u2014 a \`kind: 'group'\` fieldset (a prop: \`ui.group\`): a disclosure that shows two values
|
|
6345
|
+
when closed. \`collapsed: true\` starts it closed.
|
|
6346
|
+
- **row** \u2014 a \`kind: 'row'\` fieldset: at most 3 short scalars (number, color, boolean, date,
|
|
6347
|
+
datetime, select) on one line. No \`ui.control\` on a row member.
|
|
6348
|
+
- **branch** \u2014 a nesting field drawn as a drill-in row with its \`ui.icon\` and a summary.
|
|
6349
|
+
- **list** \u2014 repeatable items named by \`ui.itemLabel\` and \`ui.preview\`.
|
|
6350
|
+
- **grid** \u2014 \`layout: 'grid'\`, media-led: the field needs an image or file child for \`preview.media\`.
|
|
6351
|
+
- **table** \u2014 \`layout: 'table'\`: at most 3 scalar columns (text, number, select, boolean, date,
|
|
6352
|
+
datetime, slug, email).
|
|
6353
|
+
- **section header** \u2014 a fieldset's \`name\` and \`description\` over its fields.
|
|
6354
|
+
- **conditional** \u2014 \`showIf\`, placed AFTER the field it reads.
|
|
6355
|
+
|
|
6356
|
+
**Budgets, per dock screen.** \`get_next_steps\` measures them (\`dock-budget\`):
|
|
6357
|
+
- at most **8 ungrouped leaves** at rest (the pinned fields plus any one tab);
|
|
6358
|
+
- at most **12 leaves in a group**, and branches at most **3 levels** deep;
|
|
6359
|
+
- **any value within 2 activations** of its section screen. A collapsed group, a non-first tab and a
|
|
6360
|
+
branch each cost 1, so a group inside tab 2 already costs 2: leave groups in non-first tabs open.
|
|
6361
|
+
Importance orders a screen: required fields first, then array order.
|
|
6362
|
+
|
|
6363
|
+
**Recipes \u2014 named key presets, never code.** Pick the one the section is; adapt the child keys.
|
|
6364
|
+
|
|
6365
|
+
| Recipe | Keys |
|
|
6366
|
+
|---|---|
|
|
6367
|
+
| Gallery | \`layout:'grid'\`, \`preview:{media:'image', title:'caption'}\`, \`itemLabel:'Image'\`, \`icon:'images'\` |
|
|
6368
|
+
| Logo wall | \`layout:'grid'\`, \`preview:{media:'logo', title:'name'}\`, \`itemLabel:'Logo'\`, \`icon:'image'\` |
|
|
6369
|
+
| Team | \`layout:'grid'\`, \`preview:{media:'photo', title:'name', subtitle:'role'}\`, \`itemLabel:'Person'\`, \`icon:'users'\` |
|
|
6370
|
+
| Testimonials | \`preview:{title:'author', subtitle:'role', media:'avatar'}\`, \`itemLabel:'Testimonial'\`, \`icon:'double-quote'\` |
|
|
6371
|
+
| Steps | \`preview:{title:'title'}\`, \`itemLabel:'Step'\`, \`icon:'olist'\`; \`reorderable:false\` only on Tier-1 evidence |
|
|
6372
|
+
| Pricing tiers | \`preview:{title:'name', subtitle:'price'}\`, \`itemLabel:'Tier'\`, \`icon:'tiers'\` |
|
|
6373
|
+
| FAQ | \`preview:{title:'question'}\`, \`itemLabel:'Question'\`, \`icon:'help-circle'\` |
|
|
6374
|
+
| Nav links | \`preview:{title:'label', subtitle:'href'}\`, \`itemLabel:'Link'\`, \`icon:'link'\` (never a table: a label and its href are one link) |
|
|
6375
|
+
| Stats | \`layout:'table'\` over \u22643 scalar columns (value, label, suffix), \`itemLabel:'Stat'\`, \`icon:'bar-chart'\` |
|
|
6376
|
+
| CTA | one \`kind:'group'\` fieldset holding the heading, text, button label AND href (a button's pair shares one fieldset), \`icon:'launch'\` |
|
|
6377
|
+
| Dimensions | one \`kind:'row'\` fieldset of up to 3 numbers (width, height, gap) |
|
|
6378
|
+
|
|
6379
|
+
**Tabs.** A \`kind: 'tab'\` fieldset is a tab:
|
|
6380
|
+
- at most **3** tabs, names at most 20 characters and unique;
|
|
6381
|
+
- top-level fields only: tabs never nest and props never take them;
|
|
6382
|
+
- untabbed fields stay PINNED above the strip;
|
|
6383
|
+
- a group or row fieldset joins a tab with \`tab: '<tab id>'\`; groups in a non-first tab default open;
|
|
6384
|
+
- with fewer than 2 non-empty tabs no strip renders, so a tab for one fieldset is noise.
|
|
6385
|
+
|
|
6386
|
+
\`\`\`jsonc
|
|
6387
|
+
"fieldsets": [
|
|
6388
|
+
{ "id": "content", "name": "Content", "kind": "tab" },
|
|
6389
|
+
{ "id": "style", "name": "Style", "kind": "tab" },
|
|
6390
|
+
{ "id": "cta", "name": "Button", "kind": "group", "tab": "content", "icon": "launch" },
|
|
6391
|
+
{ "id": "size", "name": "Size", "kind": "row", "tab": "style" }
|
|
6392
|
+
]
|
|
6393
|
+
\`\`\`
|
|
6394
|
+
|
|
6395
|
+
**The write path \u2014 existing fields, by path, with If-Match.**
|
|
6396
|
+
- A model's fields: \`update_content_model\` with \`fieldUi\`, \`fieldFieldset\`, \`fieldOrder\`,
|
|
6397
|
+
\`fieldLabel\`, \`fieldHelpText\` (keyed by dotted key path, \`'hero.title'\`) and \`fieldsets\`. A page's
|
|
6398
|
+
schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
|
|
6399
|
+
\`backingModel\`.
|
|
6400
|
+
- A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propLabel\` (keyed by prop key).
|
|
6401
|
+
They write the DRAFT props; no publish is needed for the editor to show them.
|
|
6402
|
+
- Always send \`ifMatch\`, the \`optimisticVersion\` you read.
|
|
6403
|
+
- A NEW field carries its chrome in the same \`add_field\` / \`add_page_field\` call.
|
|
6404
|
+
|
|
6405
|
+
**The dock's refusals, each named in the message.**
|
|
6406
|
+
- **428** \`precondition-required\` \u2014 a chrome key without \`ifMatch\`. Read, then send it.
|
|
6407
|
+
- **409** \`version-conflict\` \u2014 someone wrote since your read. Read again, re-apply; never retry blindly.
|
|
6408
|
+
- **409** \`ui_human_owned\` \u2014 a person shaped those paths. Leave them, or send \`override: true\`, and
|
|
6409
|
+
only when the user said so.
|
|
6410
|
+
- \`tab_unknown\`, \`tab_nested\`, \`tab_props\`, \`tab_max\`, \`tab_name\`, \`tab_depth\` \u2014 the tab rules above.
|
|
6411
|
+
- \`fieldset_unknown\` / \`fieldset_orphan\` \u2014 a \`fieldsetId\` naming no fieldset, or a list that drops a
|
|
6412
|
+
fieldset its fields still name.
|
|
6413
|
+
- \`row_member_type\`, \`row_member_count\`, \`row_member_control\` \u2014 the row rule.
|
|
6414
|
+
- \`pair_split\` \u2014 a button's label and href in different fieldsets.
|
|
6415
|
+
- \`ui.icon\` or \`ui.itemLabel\` on a leaf; an icon name not in \`bettercms://icons/dock\`.
|
|
6416
|
+
- \`layout_grid\` \u2014 a grid with no image or file child. \`layout_table\` \u2014 more than 3 columns, or a
|
|
6417
|
+
column that is not a scalar.
|
|
6418
|
+
- \`prop_group_unknown\` / \`prop_group_redefined\` \u2014 the \`ui.group\` define-once rule.
|
|
6419
|
+
- \`field_path_unknown\` / \`field_path_ambiguous\` / \`field_order_not_permutation\` \u2014 a path that names
|
|
6420
|
+
no field or two, or an order that is not exactly the parent's keys.
|
|
6421
|
+
|
|
5955
6422
|
**THE RECEIPT \u2014 read it back.** There is no separate verification tool and no mode to flip.
|
|
5956
6423
|
\`get_content_model\` (and \`get_page\` / \`get_component\`) return each field's \u2014 and each component
|
|
5957
6424
|
prop's \u2014 stored \`ui\`, \`helpText\` and \`showIf\`. A prop that is ABSENT from that response was not
|
|
5958
|
-
stored, whatever the write returned \u2014 \xA711 RECEIPTS rule 3, applied to this feature.
|
|
5959
|
-
|
|
6425
|
+
stored, whatever the write returned \u2014 \xA711 RECEIPTS rule 3, applied to this feature. For a page, read
|
|
6426
|
+
\`get_page\` and its \`backingModel\`. Then call \`get_next_steps\`: \`dock-budget\`, \`dock-repeater-unnamed\`
|
|
6427
|
+
and \`dock-showif-order\` must all be gone. Declare, then read back, then say it works.
|
|
5960
6428
|
|
|
5961
6429
|
**No \`order\` property. Ever.** A field's order IS its index in \`fields\`. To put a new field
|
|
5962
6430
|
somewhere other than the end, pass \`after: '<existing top-level field key>'\` to \`add_field\` /
|
|
@@ -6032,6 +6500,35 @@ Do not stop at the plan or at a partial receipt \u2014 finish the whole site, ne
|
|
|
6032
6500
|
reads mode "declared", unmatched 0, bound above 0, \`coverage.pending\` empty. Send me that report.
|
|
6033
6501
|
10. Ask me whether to commit and push to my repository, commit only, or leave it uncommitted (unpushed, the
|
|
6034
6502
|
next rebuild from my repo drops it). Touch git only as I answer.`;
|
|
6503
|
+
var FIELD_UI_PROMPT = {
|
|
6504
|
+
title: "Declare how fields LOOK in the editor (guided)",
|
|
6505
|
+
description: "Set the editor chrome of an existing schema and its component props: groups, tabs, rows, icons, item names, what titles and thumbnails a list's rows, help text, conditional visibility. Derives it from your source, then from the field shapes, and asks only what it cannot know.",
|
|
6506
|
+
request: "scope, e.g. 'the Team and Testimonials models' or 'the whole project'"
|
|
6507
|
+
};
|
|
6508
|
+
var FIELD_UI_TEXT = (request) => `Declare the editor UI for my BetterCMS fields and component props.${request ? ` Scope: "${request}".` : ""}
|
|
6509
|
+
|
|
6510
|
+
Read bettercms://playbook/schema section 14 first: the vocabulary, patterns, budgets, recipes, the
|
|
6511
|
+
tabs rule, every refusal, and the three tiers that decide each value (my source, then the field
|
|
6512
|
+
shape, then ask me, once and batched). Icon names are in bettercms://icons/dock.
|
|
6513
|
+
|
|
6514
|
+
**Order of work**
|
|
6515
|
+
1. \`get_next_steps\`: its dock-budget, dock-repeater-unnamed and dock-showif-order steps name the
|
|
6516
|
+
screens to fix. Read each target with \`get_content_model\`, \`get_page\` (its \`backingModel\`) or
|
|
6517
|
+
\`get_component\`; note its \`optimisticVersion\` and the chrome a person already set.
|
|
6518
|
+
2. Decide each field's chrome by the \xA714 tiers and write down which tier answered.
|
|
6519
|
+
3. Apply one write per model or component, always with \`ifMatch\` (428 without it; 409
|
|
6520
|
+
version-conflict when stale: read again and re-apply, never retry blindly):
|
|
6521
|
+
- fields that exist: \`update_content_model\` fieldUi / fieldFieldset / fieldOrder / fieldLabel /
|
|
6522
|
+
fieldHelpText by field path, plus \`fieldsets\`. A singleton page: send \`pageId\` instead of
|
|
6523
|
+
modelId, with its backingModel's version;
|
|
6524
|
+
- component props: \`update_component\` propUi / propGroup / propLabel;
|
|
6525
|
+
- a NEW field: ui / helpText / showIf in the same \`add_field\` / \`add_page_field\` call, with \`after\`.
|
|
6526
|
+
A 409 \`ui_human_owned\` names chrome a person set: leave it, or ask me and send \`override: true\`
|
|
6527
|
+
only on my word.
|
|
6528
|
+
4. **RECEIPT, non-negotiable.** Re-read every target and confirm the stored chrome matches what you
|
|
6529
|
+
sent: a key ABSENT from the read was not stored, whatever the write said. Call \`get_next_steps\`
|
|
6530
|
+
until no dock-* step is left. Report per field the tier that decided it, what you set, and that
|
|
6531
|
+
the read-back confirmed it; say plainly what you could not confirm.`;
|
|
6035
6532
|
var SCHEMA_PROPOSAL_FLOW = `### Whole-project design (confirm-first) \u2192 \`create_component\` / \`create_page\` / \`create_content_model\`
|
|
6036
6533
|
Design the WHOLE project from its brief or its code, and **confirm the shape with the user
|
|
6037
6534
|
BEFORE creating anything**. Never silently guess.
|
|
@@ -6576,104 +7073,12 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
6576
7073
|
server.registerPrompt(
|
|
6577
7074
|
"field-ui",
|
|
6578
7075
|
{
|
|
6579
|
-
title:
|
|
6580
|
-
description:
|
|
6581
|
-
argsSchema: {
|
|
6582
|
-
request: z2.string().optional().describe("scope, e.g. 'the Team and Testimonials models' or 'the whole project'")
|
|
6583
|
-
}
|
|
7076
|
+
title: FIELD_UI_PROMPT.title,
|
|
7077
|
+
description: FIELD_UI_PROMPT.description,
|
|
7078
|
+
argsSchema: { request: z2.string().optional().describe(FIELD_UI_PROMPT.request) }
|
|
6584
7079
|
},
|
|
6585
7080
|
({ request }) => ({
|
|
6586
|
-
messages: [
|
|
6587
|
-
{
|
|
6588
|
-
role: "user",
|
|
6589
|
-
content: {
|
|
6590
|
-
type: "text",
|
|
6591
|
-
text: `Declare the editor UI for my BetterCMS fields.${request ? ` Scope: "${request}".` : ""}
|
|
6592
|
-
|
|
6593
|
-
Read bettercms://playbook/schema section 14 first \u2014 it has the exact prop shapes and the refusals.
|
|
6594
|
-
|
|
6595
|
-
**What you are setting** (all of these ride on any field, on create_content_model / create_page /
|
|
6596
|
-
add_field / add_page_field; none of them affect delivery):
|
|
6597
|
-
ui.collapsed start a nesting field's panel closed
|
|
6598
|
-
ui.preview {title,subtitle,media} which CHILD FIELD KEY titles a collapsed row, its second line, its thumb
|
|
6599
|
-
ui.layout 'list'|'grid'|'table' how its items are arranged; table is for repeatable row schemas; omit for list
|
|
6600
|
-
ui.reorderable omit (= true) unless the ORDER IS THE MEANING; false LOCKS the list
|
|
6601
|
-
ui.control 'segmented'|'slider' a LEAF's control: segmented on a select, slider on a number with config.min < max
|
|
6602
|
-
helpText one line under the field: what to WRITE here
|
|
6603
|
-
showIf hide a field until another field has a value
|
|
6604
|
-
|
|
6605
|
-
**The same \`ui\` rides on COMPONENT PROPS** \u2014 create_component / update_component, one per entry
|
|
6606
|
-
of \`props\`. Placement differs because the vocabulary does: preview/layout on 'group', 'table'
|
|
6607
|
-
and 'slot'; reorderable on 'table' ALONE (the only prop whose value is a list). A prop's preview
|
|
6608
|
-
keys name a SUB-FIELD from its own config.fields. If this scope includes components, do them in
|
|
6609
|
-
the same pass \u2014 but note update_component REPLACES the whole props array, so read get_component
|
|
6610
|
-
first and echo every prop back WITH its stored ui.
|
|
6611
|
-
|
|
6612
|
-
**Work the three tiers in order. Stop at the first that answers \u2014 do not escalate what a tier
|
|
6613
|
-
below already settled.**
|
|
6614
|
-
|
|
6615
|
-
TIER 1 \u2014 DERIVE FROM SOURCE, if and only if you have a checkout of the site.
|
|
6616
|
-
Read the component that renders each field. \`<img src={item.photo}>\` next to
|
|
6617
|
-
\`<h3>{item.name}</h3>\` in the map over a list IS the answer: preview {title:'name',
|
|
6618
|
-
media:'photo'}. A block inside \`<details>\` or behind a "Show more" is collapsed:true.
|
|
6619
|
-
A \`grid-cols-*\` wrapper around the map is layout:'grid'. A FIXED-ARITY render \u2014
|
|
6620
|
-
steps[0]/steps[1]/steps[2], or copy that names positions ("Step 1", "finally") \u2014 is
|
|
6621
|
-
reorderable:false, and it is the ONLY evidence that earns that key.
|
|
6622
|
-
\u{1F534} THE SERVER NEVER READS YOUR SOURCE. \`pull_project_source\` / \`get_conversion_brief\` hand
|
|
6623
|
-
the repo to YOU; add_field does not look at it. This tier is your own reading, in your own
|
|
6624
|
-
checkout, before you call any tool. ON A GREENFIELD SCHEMA-FIRST PROJECT THERE IS NO SOURCE \u2014
|
|
6625
|
-
Tier 1 does not apply and TIER 2 IS THE FLOOR. Do not stall waiting for code that will
|
|
6626
|
-
never exist.
|
|
6627
|
-
|
|
6628
|
-
TIER 2 \u2014 DERIVE FROM THE FIELD SHAPE, when the code is silent or absent.
|
|
6629
|
-
One image-ish child in a repeater \u2192 that is \`media\`. The first required short-text child
|
|
6630
|
-
(name/title/heading/label) \u2192 that is \`title\`. More than ~6 children, or a label that reads
|
|
6631
|
-
optional ("Advanced", "Extras") \u2192 collapsed:true. A repeatable whose item is mostly its
|
|
6632
|
-
image (gallery, logos, team) \u2192 layout:'grid'; everything else stays list, so OMIT layout.
|
|
6633
|
-
\`reorderable\` has NO Tier-2 guess \u2014 leave it absent. The default is true, and locking a
|
|
6634
|
-
list the user did not ask to lock silently removes something they could do yesterday.
|
|
6635
|
-
GUESS HERE otherwise. A wrong preview costs one edit; a question per repeater costs the
|
|
6636
|
-
user the session.
|
|
6637
|
-
|
|
6638
|
-
TIER 3 \u2014 ASK ME, and ONLY for these three. Batch them into ONE AskUserQuestion at the end,
|
|
6639
|
-
never one field at a time:
|
|
6640
|
-
a) A panel with MANY fields, where grouping and order depend on which content I treat as
|
|
6641
|
-
primary \u2014 you cannot infer my priorities.
|
|
6642
|
-
b) CONDITIONAL VISIBILITY (showIf) \u2014 it is business logic about when a field is irrelevant.
|
|
6643
|
-
Never invent a rule.
|
|
6644
|
-
c) VARIANTS SHARING A sectionType \u2014 their prop keys must match, or swapping the layout loses
|
|
6645
|
-
content. Confirm the shared key set before declaring per-variant chrome.
|
|
6646
|
-
|
|
6647
|
-
**Order of work**
|
|
6648
|
-
1. \`list_content_models\` / \`list_pages\`, then \`get_content_model\` / \`get_page\` for each target.
|
|
6649
|
-
Note which fields ALREADY carry ui/helpText/showIf \u2014 do not overwrite a human's choice
|
|
6650
|
-
without asking.
|
|
6651
|
-
2. Decide each field's chrome by the tiers above. Write down which tier answered; you will
|
|
6652
|
-
report it.
|
|
6653
|
-
3. Apply. For a field that already exists, the declaration goes with the field: use the
|
|
6654
|
-
dashboard or a model update \u2014 \`add_field\` APPENDS and refuses an existing key, so it is
|
|
6655
|
-
the wrong tool for editing one. For a NEW field, pass ui/helpText/showIf in the same
|
|
6656
|
-
\`add_field\` / \`add_page_field\` call, and pass \`after: '<existing key>'\` if it belongs
|
|
6657
|
-
somewhere other than the bottom.
|
|
6658
|
-
4. **RECEIPT \u2014 non-negotiable.** Re-read every target with \`get_content_model\` / \`get_page\` /
|
|
6659
|
-
\`get_component\` and
|
|
6660
|
-
confirm the stored \`ui\` matches what you sent. A prop ABSENT from that response was NOT
|
|
6661
|
-
stored, whatever the write said. Report per field: the tier that decided it, what you set,
|
|
6662
|
-
and that the read-back confirmed it. Anything you could not confirm, say so plainly.
|
|
6663
|
-
|
|
6664
|
-
**Refusals you should expect and must not work around:** an unknown key inside \`ui\` is a 400
|
|
6665
|
-
(it is strict on purpose \u2014 that is your typo, named); \`ui.preview\` or \`ui.layout\` on a leaf
|
|
6666
|
-
field is a 400 (both describe ITEMS); \`ui.reorderable\` anywhere without a LIST is a 400 \u2014 a
|
|
6667
|
-
\`group\` and a non-repeatable zone hold one item, so only \`repeater\`, an \`array\` with
|
|
6668
|
-
config.zones.repeatable, and \`modular\` take it; a preview key that is not a child of that field
|
|
6669
|
-
is a 400 listing the valid child keys. Fix the declaration; never retry without it and call that
|
|
6670
|
-
success.
|
|
6671
|
-
|
|
6672
|
-
There is deliberately NO \`order\` property and no mode to turn on \u2014 order is the array index,
|
|
6673
|
-
and \`ui\` only changes editor chrome, so there is nothing to flip.`
|
|
6674
|
-
}
|
|
6675
|
-
}
|
|
6676
|
-
]
|
|
7081
|
+
messages: [{ role: "user", content: { type: "text", text: FIELD_UI_TEXT(request) } }]
|
|
6677
7082
|
})
|
|
6678
7083
|
);
|
|
6679
7084
|
}
|
|
@@ -6697,9 +7102,9 @@ function buildServer(deps) {
|
|
|
6697
7102
|
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
6698
7103
|
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
6699
7104
|
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
6700
|
-
instructions: RECEIPT_FIRST_INSTRUCTION + " When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication. " + // The default after authoring (the structure standard): organise what you made. Same
|
|
7105
|
+
instructions: RECEIPT_FIRST_INSTRUCTION + " When the user asks to make a site or all of its pages editable, to convert it, or to bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. " + LAYOUT_CHROME_INSTRUCTION + " When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 create_components, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. BetterCMS never executes a customer's Section renderer or app code. An ordinary MCP connection is not a push runner: explicitly poll list_section_validation_requests, claim one request at an exact git commit, run implementation and responsive checks inside the user's own repository and real app shell, then submit manifest + validation with that requestId and complete it\u2014or truthfully fail it when implementation/evidence is missing. Never invent a manifest, a passing validation, or visual evidence; these tools cannot grant the separate human Visual Approval required for publication. " + // The default after authoring (the structure standard): organise what you made. Same
|
|
6701
7106
|
// sentence as the hosted connector's MCP_INSTRUCTIONS.
|
|
6702
|
-
STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION
|
|
7107
|
+
STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION + " " + DOCK_UI_ROUTING_INSTRUCTION
|
|
6703
7108
|
}
|
|
6704
7109
|
);
|
|
6705
7110
|
server.registerResource(
|
|
@@ -6726,6 +7131,10 @@ function buildServer(deps) {
|
|
|
6726
7131
|
contents: [{ uri: PLAYBOOK_URI, mimeType: "text/markdown", text: SCHEMA_PLAYBOOK }]
|
|
6727
7132
|
})
|
|
6728
7133
|
);
|
|
7134
|
+
const { uri: dockIconsUri, name: dockIconsName, ...dockIconsMeta } = DOCK_ICONS_RESOURCE;
|
|
7135
|
+
server.registerResource(dockIconsName, dockIconsUri, dockIconsMeta, () => ({
|
|
7136
|
+
contents: [{ uri: dockIconsUri, mimeType: dockIconsMeta.mimeType, text: DOCK_ICONS_TEXT }]
|
|
7137
|
+
}));
|
|
6729
7138
|
registerTools(server, {
|
|
6730
7139
|
auth: deps.auth,
|
|
6731
7140
|
// `project` is the per-call target of a workspace-wide grant; the SDK sends it as
|