@bettercms-ai/mcp 0.38.0 → 0.38.2
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 +398 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2108,6 +2108,21 @@ var fieldType = z.enum([
|
|
|
2108
2108
|
]);
|
|
2109
2109
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
2110
2110
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_]+$/, "letters, numbers, and underscores only");
|
|
2111
|
+
var uiObject = z.object({
|
|
2112
|
+
collapsed: z.boolean().optional().describe("start this panel collapsed. Use it for long optional sections."),
|
|
2113
|
+
preview: z.object({
|
|
2114
|
+
title: z.string().min(1).optional().describe("CHILD KEY whose value titles a collapsed row"),
|
|
2115
|
+
media: z.string().min(1).optional().describe("CHILD KEY whose value is the row's thumbnail (image/file/url all work)")
|
|
2116
|
+
}).optional().describe(
|
|
2117
|
+
"how ONE ITEM summarises itself when collapsed. Both slots name 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."
|
|
2118
|
+
),
|
|
2119
|
+
layout: z.enum(["list", "grid"]).optional().describe(
|
|
2120
|
+
"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`."
|
|
2121
|
+
),
|
|
2122
|
+
reorderable: z.boolean().optional().describe(
|
|
2123
|
+
"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."
|
|
2124
|
+
)
|
|
2125
|
+
});
|
|
2111
2126
|
var fieldShape = {
|
|
2112
2127
|
key: fieldKey.describe(
|
|
2113
2128
|
"machine field key. \u{1F534} UNIQUE ACROSS THE WHOLE MODEL \u2014 API IDs share ONE FLAT NAMESPACE, so a field nested inside a group must NOT reuse a key used by another field or group. Every section's heading cannot be 'title'. Prefix with the section: 'hero_title', 'pricing_title', 'faq_title'. Reusing a key is refused, and if it slips through it leaves permanent errors in the editor and breaks conditional-visibility rules, which reference fields by bare key."
|
|
@@ -2124,6 +2139,29 @@ var fieldShape = {
|
|
|
2124
2139
|
),
|
|
2125
2140
|
fields: z.array(z.lazy(() => fieldObject)).optional().describe(
|
|
2126
2141
|
"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."
|
|
2142
|
+
),
|
|
2143
|
+
// ── Authoring chrome + editor behaviour ────────────────────────────────────
|
|
2144
|
+
// These three already persist end-to-end and already have a panel in the dashboard's schema
|
|
2145
|
+
// builder. They were unreachable from MCP for ONE reason: the OutField/toField whitelist
|
|
2146
|
+
// below did not model them, so they were stripped after the model had gone to the trouble of
|
|
2147
|
+
// sending them. Declaring them here is what makes the existing panel agent-writable.
|
|
2148
|
+
helpText: z.string().max(500).optional().describe(
|
|
2149
|
+
"one line of guidance shown UNDER this field in the editor \u2014 what to write here, not what the field is. Write it for the person typing ('Roughly 60 characters; it becomes the browser tab title'), not for a developer."
|
|
2150
|
+
),
|
|
2151
|
+
showIf: z.object({
|
|
2152
|
+
match: z.enum(["all", "any"]),
|
|
2153
|
+
rules: z.array(
|
|
2154
|
+
z.object({
|
|
2155
|
+
field: z.string().describe("the BARE KEY of another field on this same model"),
|
|
2156
|
+
op: z.enum(["eq", "neq", "in"]),
|
|
2157
|
+
value: z.union([z.string(), z.array(z.string())])
|
|
2158
|
+
})
|
|
2159
|
+
)
|
|
2160
|
+
}).optional().describe(
|
|
2161
|
+
"conditional visibility \u2014 hide this field until another field has a given value (e.g. show `external_url` only when `link_type` is 'external'). Admin-only: it changes what an author SEES, never what is stored or delivered. Rules reference fields by BARE KEY, and the key namespace is flat, so the key must exist on this model."
|
|
2162
|
+
),
|
|
2163
|
+
ui: uiObject.optional().describe(
|
|
2164
|
+
"AUTHORING CHROME \u2014 how this field presents in the editor. Never a delivery effect. `preview` and `layout` are valid ONLY on a nesting field ('group', 'repeater', an 'array' with config.zones, or 'modular'); `reorderable` only where there is a LIST to lock ('repeater', an 'array' with config.zones.repeatable, or 'modular' \u2014 a 'group' holds one item). preview keys must name real children, or the write is refused with the valid keys listed. An unknown key inside `ui` is a 400, not a silent strip. Read it back with get_content_model / get_page to confirm it landed. There is deliberately no `order`: field order is the array index \u2014 use add_field's `after` to insert."
|
|
2127
2165
|
)
|
|
2128
2166
|
};
|
|
2129
2167
|
var fieldObject = z.object(fieldShape);
|
|
@@ -2164,7 +2202,10 @@ function toField(f) {
|
|
|
2164
2202
|
const base = {
|
|
2165
2203
|
key: f.key,
|
|
2166
2204
|
label: f.label,
|
|
2167
|
-
...f.required !== void 0 ? { required: f.required } : {}
|
|
2205
|
+
...f.required !== void 0 ? { required: f.required } : {},
|
|
2206
|
+
...f.helpText !== void 0 ? { helpText: f.helpText } : {},
|
|
2207
|
+
...f.showIf !== void 0 ? { showIf: f.showIf } : {},
|
|
2208
|
+
...f.ui !== void 0 ? { ui: f.ui } : {}
|
|
2168
2209
|
};
|
|
2169
2210
|
if (f.type === "group") {
|
|
2170
2211
|
return { ...base, type: "array", config: { zones: { nonRepeatable: toFields(f.fields) } } };
|
|
@@ -2358,6 +2399,18 @@ function buildToolDefs(deps) {
|
|
|
2358
2399
|
text: z.string().min(1).describe("the content to derive SEO metadata from"),
|
|
2359
2400
|
context: z.string().optional().describe("optional surrounding context, e.g. the page slug")
|
|
2360
2401
|
});
|
|
2402
|
+
const connectNativeAiInput = z.object({
|
|
2403
|
+
capabilities: z.array(z.string().min(1)).min(1).optional().describe(
|
|
2404
|
+
"optional \u2014 any of inspect, propose, implement, cancel, question. Defaults to inspect+propose+cancel."
|
|
2405
|
+
)
|
|
2406
|
+
});
|
|
2407
|
+
const nativeBridgeHeartbeatInput = z.object({
|
|
2408
|
+
protocolVersion: z.string().min(1).describe("executor protocol version; currently '1'"),
|
|
2409
|
+
capabilities: z.array(z.string().min(1)).min(1).describe(
|
|
2410
|
+
"what this executor can do: any of inspect, propose, implement, cancel, question. Must include inspect, propose and cancel. An unknown value is refused, never ignored."
|
|
2411
|
+
),
|
|
2412
|
+
session: z.object({ sessionId: z.string(), sessionEpoch: z.number().int().positive() }).optional().describe("the `session` from the previous response; omit on first contact to reconnect")
|
|
2413
|
+
});
|
|
2361
2414
|
const createModelInput = z.object({
|
|
2362
2415
|
name: z.string().min(1).describe("human model name, e.g. 'Blog Post'"),
|
|
2363
2416
|
slug: slug.describe("url-safe unique slug, e.g. 'blog-post'"),
|
|
@@ -2369,13 +2422,18 @@ function buildToolDefs(deps) {
|
|
|
2369
2422
|
"the model's typed schema fields. 'group'/'repeater' NEST their child fields (any depth) \u2014 don't flatten zones into top-level fields."
|
|
2370
2423
|
)
|
|
2371
2424
|
});
|
|
2425
|
+
const afterArg = z.string().min(1).optional().describe(
|
|
2426
|
+
"OPTIONAL insertion point: the key of an existing TOP-LEVEL field to insert this one directly after. Omit to append at the end (the default, and the behaviour before this existed). Use it to put a field where an author expects it instead of at the bottom \u2014 e.g. after:'hero_title' for a subtitle. An unknown key is refused rather than silently appended."
|
|
2427
|
+
);
|
|
2372
2428
|
const addFieldInput = z.object({
|
|
2373
2429
|
modelId: z.string().min(1).describe("id of the content model to extend"),
|
|
2374
|
-
...fieldShape
|
|
2430
|
+
...fieldShape,
|
|
2431
|
+
after: afterArg
|
|
2375
2432
|
});
|
|
2376
2433
|
const addPageFieldInput = z.object({
|
|
2377
2434
|
pageId: z.string().min(1).describe("id of the page to extend (from list_pages / create_page)"),
|
|
2378
|
-
...fieldShape
|
|
2435
|
+
...fieldShape,
|
|
2436
|
+
after: afterArg
|
|
2379
2437
|
});
|
|
2380
2438
|
const uploadAssetInput = z.object({
|
|
2381
2439
|
localPath: z.string().min(1).optional().describe("absolute path to a local file (e.g. a repo image); provide this OR url"),
|
|
@@ -2510,7 +2568,13 @@ function buildToolDefs(deps) {
|
|
|
2510
2568
|
// get_site_composition counts, never a refusal. Parity: componentPropDefSchema on the server.
|
|
2511
2569
|
required: z.boolean().optional().describe("mark the field required in the editor. An editor HINT: the API still accepts a placement that leaves it empty, and get_site_composition is what reports one."),
|
|
2512
2570
|
helpText: z.string().max(200).optional().describe("hint shown under the control, muted (\u2264 200 chars)"),
|
|
2513
|
-
placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)")
|
|
2571
|
+
placeholder: z.string().max(120).optional().describe("ghost text inside a text-ish control (\u2264 120 chars)"),
|
|
2572
|
+
// ⚠ A STRIP SITE, exactly like OutField above: this is a `z.object`, so a key the model
|
|
2573
|
+
// sends and this shape does not declare is dropped BEFORE the request leaves — a 200 with
|
|
2574
|
+
// the declaration gone. `ui` rode through the backend's looseObject `config` and died here.
|
|
2575
|
+
ui: uiObject.optional().describe(
|
|
2576
|
+
"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."
|
|
2577
|
+
)
|
|
2514
2578
|
});
|
|
2515
2579
|
const getLayoutInput = z.object({
|
|
2516
2580
|
scope: z.enum(["global", "page"]).default("global"),
|
|
@@ -2585,7 +2649,9 @@ function buildToolDefs(deps) {
|
|
|
2585
2649
|
allowedOn: allowedOn.optional(),
|
|
2586
2650
|
description: z.string().optional(),
|
|
2587
2651
|
blockJson: z.array(blockObject).default([]).describe("the component's block tree"),
|
|
2588
|
-
props: z.array(componentPropObject).default([]).describe(
|
|
2652
|
+
props: z.array(componentPropObject).default([]).describe(
|
|
2653
|
+
"overridable fields. Each one may carry `ui` to say how its control presents in the inspector \u2014 a collapsed group, what titles and thumbnails a table's rows, whether that table's rows can be dragged at all."
|
|
2654
|
+
)
|
|
2589
2655
|
});
|
|
2590
2656
|
const updateComponentInput = z.object({
|
|
2591
2657
|
componentId: z.string().min(1).describe("component id (from list_components)"),
|
|
@@ -2598,7 +2664,9 @@ function buildToolDefs(deps) {
|
|
|
2598
2664
|
allowedOn: allowedOn.optional().describe("REPLACES the placement allowlist"),
|
|
2599
2665
|
description: z.string().optional(),
|
|
2600
2666
|
blockJson: z.array(blockObject).optional().describe("REPLACES the block tree"),
|
|
2601
|
-
props: z.array(componentPropObject).optional()
|
|
2667
|
+
props: z.array(componentPropObject).optional().describe(
|
|
2668
|
+
"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."
|
|
2669
|
+
)
|
|
2602
2670
|
});
|
|
2603
2671
|
const getComponentInput = z.object({
|
|
2604
2672
|
componentId: z.string().min(1).describe("component id (from list_components)")
|
|
@@ -3016,7 +3084,7 @@ function buildToolDefs(deps) {
|
|
|
3016
3084
|
def(
|
|
3017
3085
|
"get_content_model",
|
|
3018
3086
|
"Get a content model",
|
|
3019
|
-
"Get one content model by id INCLUDING its full field schema (keys, types, nested group/repeater children)
|
|
3087
|
+
"Get one content model by id INCLUDING its full field schema (keys, types, nested group/repeater children) AND each field's stored authoring props \u2014 `helpText`, `showIf`, `ui`. Read it before add_field so you know the existing keys, and AFTER declaring `ui`/`showIf`/`helpText` as the receipt that the declaration actually landed: a prop that is missing from this response was not stored, whatever the write returned.",
|
|
3020
3088
|
z.object({ modelId: z.string().min(1) }).shape,
|
|
3021
3089
|
async (c, a) => ok("Content model.", await data(c, "GET", `/management/content/models/${s(a.modelId)}`))
|
|
3022
3090
|
),
|
|
@@ -3098,7 +3166,7 @@ function buildToolDefs(deps) {
|
|
|
3098
3166
|
def(
|
|
3099
3167
|
"get_binding_report",
|
|
3100
3168
|
"Check what on the live site is editable",
|
|
3101
|
-
"The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. `builtRoutes` lists the routes the live build has HTML for (null when unknown: runtime release, >500 routes, or a report older than this field). `canvas.lane` names the live-preview lane this build gives the editor \u2014 `bridge` (the build ships BcmsDraftBridge), `draft-route` (a node runtime rendering drafts server-side) or `none`, in which case get_next_steps carries the recipe (playbook section 11); null means the report predates the field. `unaddressable` counts visible text on the live site that no field owns
|
|
3169
|
+
"The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns what that scan found, per slot: `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), `pagesInspected`, `bound` (elements carrying a binding), and `unmatched` \u2014 per page, each path with its kind and the reason it failed (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report and this answers pages 0, mode null, refreshRequired true. It certifies exactly one thing \u2014 that every non-empty field of every page has SOME element carrying its path. It cannot see copy that was never modelled, so diff each route's visible text against its entry values yourself before calling a page done. `builtRoutes` lists the routes the live build has HTML for (null when unknown: runtime release, >500 routes, or a report older than this field). `canvas.lane` names the live-preview lane this build gives the editor \u2014 `bridge` (the build ships BcmsDraftBridge), `draft-route` (a node runtime rendering drafts server-side) or `none`, in which case get_next_steps carries the recipe (playbook section 11); null means the report predates the field. `unaddressable` counts visible text on the live site that no field owns \u2014 the one thing `unmatched` structurally cannot see, because it only ever speaks about fields that already exist. EVERY release measures it server-side on every inspected page: `{count, pages, measuredAt, routes:[{path, count, visible, buckets:[{tag, context, chars, nodes, samples}]}]}`, where `path` is the route (the empty string is the home page), `visible` its countable characters (ornaments, skip links and the platform badge excluded from both sides) and `buckets` where the unowned text is, biggest first. Anything over `count / visible > 0.02` is copy the CMS cannot see: 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 section 11 has the loop. An author turning the editor's Unbound text control on posts a bare count of text NODES for the route they opened (no `visible`, no buckets); those rows are reported only for a build the release never measured, never summed with the release's characters. `skipReasons` says why pages were not inspected (`no-html`, `route-cap`, `authored-content` = the singleton lane declined the project, so NO route of it has a page), and `coverage.error` = `nothing-bound` means not one element of this build carries a binding, so the percentage beside it describes a site this build is not. `unaddressable` is measured PER SLOT; on a promote-gated project pass slot:'current' to read the tree the editor frames. Pass `slot` ('current' or 'staging') to read the other tree; the default is the slot this project's releases land in.",
|
|
3102
3170
|
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read; defaults to the one this project deploys to") }).shape,
|
|
3103
3171
|
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
3104
3172
|
),
|
|
@@ -3427,11 +3495,26 @@ function buildToolDefs(deps) {
|
|
|
3427
3495
|
const model = await client.getModel(args.modelId);
|
|
3428
3496
|
const dupes = duplicateKeys([...flatFieldKeys(model.fields), args.key]);
|
|
3429
3497
|
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
3498
|
+
const existingFields = model.fields;
|
|
3499
|
+
let at = existingFields.length;
|
|
3500
|
+
if (args.after !== void 0) {
|
|
3501
|
+
const idx = existingFields.findIndex(
|
|
3502
|
+
(f) => f.key === args.after
|
|
3503
|
+
);
|
|
3504
|
+
if (idx === -1) {
|
|
3505
|
+
return fail(
|
|
3506
|
+
`after: '${args.after}' is not a top-level field on this model. Valid keys: ${existingFields.map((f) => `'${f.key}'`).join(", ")}. Omit \`after\` to append at the end.`
|
|
3507
|
+
);
|
|
3508
|
+
}
|
|
3509
|
+
at = idx + 1;
|
|
3510
|
+
}
|
|
3511
|
+
const nextFields = [...existingFields];
|
|
3512
|
+
nextFields.splice(at, 0, toField(args));
|
|
3430
3513
|
const updated = await client.updateModel(args.modelId, {
|
|
3431
|
-
fields:
|
|
3514
|
+
fields: nextFields
|
|
3432
3515
|
});
|
|
3433
3516
|
return ok(
|
|
3434
|
-
`Added field '${args.key}' to '${updated.name}'. Model now has ${updated.fields.length} field(s).`,
|
|
3517
|
+
`Added field '${args.key}' to '${updated.name}'${args.after ? ` after '${args.after}'` : ""}. Model now has ${updated.fields.length} field(s).`,
|
|
3435
3518
|
updated
|
|
3436
3519
|
);
|
|
3437
3520
|
})
|
|
@@ -3446,7 +3529,10 @@ function buildToolDefs(deps) {
|
|
|
3446
3529
|
},
|
|
3447
3530
|
handler: guard(
|
|
3448
3531
|
async (args) => withClient(async (client) => {
|
|
3449
|
-
const page = await client.addPageFields(args.pageId, {
|
|
3532
|
+
const page = await client.addPageFields(args.pageId, {
|
|
3533
|
+
addFields: [toField(args)],
|
|
3534
|
+
...args.after !== void 0 ? { after: args.after } : {}
|
|
3535
|
+
});
|
|
3450
3536
|
return ok(
|
|
3451
3537
|
`Added field '${args.key}' to page '${page.title}'. Page now has ${page.fields.length} field(s).`,
|
|
3452
3538
|
page
|
|
@@ -3764,7 +3850,7 @@ function buildToolDefs(deps) {
|
|
|
3764
3850
|
name: "get_component",
|
|
3765
3851
|
config: {
|
|
3766
3852
|
title: "Get a component (with its blockJson)",
|
|
3767
|
-
description: "Get one component by id INCLUDING its blockJson tree and props. Read this before update_component so you keep the existing blocks.",
|
|
3853
|
+
description: "Get one component by id INCLUDING its blockJson tree and props (with any stored `ui` authoring chrome on them). Read this before update_component so you keep the existing blocks, and after declaring field UI as the receipt that it landed \u2014 a prop absent here was not stored.",
|
|
3768
3854
|
inputSchema: getComponentInput.shape
|
|
3769
3855
|
},
|
|
3770
3856
|
handler: guard(
|
|
@@ -4013,6 +4099,55 @@ ${lines.join("\n")}`, found);
|
|
|
4013
4099
|
})
|
|
4014
4100
|
)
|
|
4015
4101
|
},
|
|
4102
|
+
// ── Native bridge (executor presence) ──────────────────────────────────────
|
|
4103
|
+
{
|
|
4104
|
+
name: "connect_native_ai",
|
|
4105
|
+
config: {
|
|
4106
|
+
title: "Connect this client to the project's Native AI Connection",
|
|
4107
|
+
description: "Connect this client to the project's Native AI Connection, so the dashboard shows it as an available executor. Call it ONCE when the user asks to connect \u2014 after that, ordinary tool calls on the same project keep it listed, and nothing needs to run on a timer. Idempotent. Returns the project it connected to and `presenceWindowMs`: the executor stays listed for that long after the last tool call, then drops off until the next one. THIS GRANTS NOTHING \u2014 it is presence only, and starting a run is still a human act in the dashboard. A 403 BRIDGE_GRANT_REQUIRED means this client is using a bare API key rather than an approved connection; a 400 means the connection covers the whole workspace and you must pass `projectId`.",
|
|
4108
|
+
inputSchema: connectNativeAiInput.shape
|
|
4109
|
+
},
|
|
4110
|
+
handler: guard(
|
|
4111
|
+
async (args) => withClient(async (client) => {
|
|
4112
|
+
const result = await client.fetchJSON(client.url("/management/orchestration/bridge/connect"), {
|
|
4113
|
+
method: "POST",
|
|
4114
|
+
body: JSON.stringify({
|
|
4115
|
+
protocolVersion: "1",
|
|
4116
|
+
...args.capabilities ? { capabilities: args.capabilities } : {}
|
|
4117
|
+
})
|
|
4118
|
+
});
|
|
4119
|
+
const mins = result.presenceWindowMs ? Math.round(result.presenceWindowMs / 6e4) : null;
|
|
4120
|
+
return ok(
|
|
4121
|
+
`Connected to project ${result.projectId}. The dashboard now lists this client as an available executor${mins ? `; it stays listed for ${mins} minutes after the last tool call` : ""}.`,
|
|
4122
|
+
result
|
|
4123
|
+
);
|
|
4124
|
+
})
|
|
4125
|
+
)
|
|
4126
|
+
},
|
|
4127
|
+
{
|
|
4128
|
+
name: "native_bridge_heartbeat",
|
|
4129
|
+
config: {
|
|
4130
|
+
title: "Register as a native bridge and report liveness",
|
|
4131
|
+
description: "Register this client as a native-bridge executor for the connected project and report that it is alive. The dashboard's 'Native AI Connection' panel shows an executor as live ONLY while heartbeats keep arriving: call this again every `nextHeartbeatMs` (25s), because liveness lapses 90s after the last call and the panel then stops offering this executor. Idempotent \u2014 registration happens on the first call, so a restart or a reconnect is just another call. Echo back the `session` from the previous response so the same connection epoch is kept; omit it to reconnect, which fences the previous one. A 422 names a refused capability or protocol; it does not retry into success. THIS GRANTS NO REPOSITORY MUTATION \u2014 it is presence only, and starting a run still requires a human in the dashboard.",
|
|
4132
|
+
inputSchema: nativeBridgeHeartbeatInput.shape
|
|
4133
|
+
},
|
|
4134
|
+
handler: guard(
|
|
4135
|
+
async (args) => withClient(async (client) => {
|
|
4136
|
+
const result = await client.fetchJSON(client.url("/management/orchestration/bridge/heartbeat"), {
|
|
4137
|
+
method: "POST",
|
|
4138
|
+
body: JSON.stringify({
|
|
4139
|
+
protocolVersion: args.protocolVersion,
|
|
4140
|
+
capabilities: args.capabilities,
|
|
4141
|
+
...args.session ? { session: args.session } : {}
|
|
4142
|
+
})
|
|
4143
|
+
});
|
|
4144
|
+
return ok(
|
|
4145
|
+
`Bridge ${result.liveness}. Call again within ${Math.round(result.nextHeartbeatMs / 1e3)}s to stay live.`,
|
|
4146
|
+
result
|
|
4147
|
+
);
|
|
4148
|
+
})
|
|
4149
|
+
)
|
|
4150
|
+
},
|
|
4016
4151
|
// ── Lifecycle tools (parity with the remote /mcp surface) ──────────────────
|
|
4017
4152
|
// Media management, form submissions (leads), redirects, SEO, AEO site-files,
|
|
4018
4153
|
// promote, and entry version history. These call the management endpoints straight
|
|
@@ -4254,7 +4389,12 @@ component placement, and the page keeps its fields, its values and its bindings.
|
|
|
4254
4389
|
1. set_authoring_preference { preference: "components" }
|
|
4255
4390
|
2. get_componentize_plan one component per section, computed live; creates nothing. It
|
|
4256
4391
|
reads this project's PUBLISHED pages, so no deploy is needed
|
|
4257
|
-
to reach it \u2014 the deploy is the last step, not the first
|
|
4392
|
+
to reach it \u2014 the deploy is the last step, not the first.
|
|
4393
|
+
On a site DERIVED at import the release hook has already run
|
|
4394
|
+
this plan in bind mode and PUBLISHED what it created, so
|
|
4395
|
+
expect ALREADY_COMPONENTIZED and skip to step 6 (publish the
|
|
4396
|
+
pages) \u2014 the lane publishes the COMPONENTS, not the placements,
|
|
4397
|
+
which are still draft blocks on the pages
|
|
4258
4398
|
3. show the user the plan and CONFIRM \u2014 it says how many components and which pages
|
|
4259
4399
|
4. componentize_sections \`dryRun: true\` first, then for real; components land as DRAFTS
|
|
4260
4400
|
and each placement carries \`props.bind\` to the page's field
|
|
@@ -4319,10 +4459,28 @@ lane, so every structural draft falls back to the platform renderer and its gene
|
|
|
4319
4459
|
\`null\` means the report predates the field; redeploy to learn. When it is \`none\`,
|
|
4320
4460
|
\`get_next_steps\` names the recipe for this project's framework (\`canvas-lane-missing\`), and
|
|
4321
4461
|
\xA713 step 5c has both. The same report carries \`unaddressable\`: how much VISIBLE text on the
|
|
4322
|
-
live site no field owns
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4462
|
+
live site no field owns \u2014 \`unmatched\` cannot see it, because it only ever speaks about fields
|
|
4463
|
+
that already exist. EVERY release measures it server-side, per route, with a denominator and the
|
|
4464
|
+
places it is (\`unaddressable.routes[]\`: \`path\`, \`count\`, \`visible\`, \`buckets[]\`). An author
|
|
4465
|
+
turning the editor's **"Unbound text"** control on posts a bare count of text NODES for the route
|
|
4466
|
+
they opened; it is reported only for a build the release never measured, and never added to
|
|
4467
|
+
the release's characters. Ornaments, skip links and the platform badge are excluded from both sides.
|
|
4468
|
+
|
|
4469
|
+
**THE LOOP, and it is not optional \u2014 a deploy is not a conversion.**
|
|
4470
|
+
|
|
4471
|
+
1. deploy, then \`get_binding_report\`;
|
|
4472
|
+
2. for each \`unaddressable.routes[]\` where \`count / visible > 0.02\`:
|
|
4473
|
+
read its \`buckets\` \u2014 each one is a TAG in a LANDMARK with a sample of the copy;
|
|
4474
|
+
3. fix it in the SOURCE: wrap that run of text in an element the codemod can bind (a
|
|
4475
|
+
\`<span>\`, a \`<p>\`, a \`<div>\` \u2014 never a bare text node between tags), or declare it
|
|
4476
|
+
yourself with \`<BcmsField path="\u2026">\`;
|
|
4477
|
+
4. deploy again and re-read. Repeat until every route is under 2%.
|
|
4478
|
+
|
|
4479
|
+
\`get_next_steps\` runs the same test and hands you the worst route with its buckets
|
|
4480
|
+
(\`unaddressable-copy\`). A \`coverage\` carrying \`error: "nothing-bound"\` means NOT ONE element
|
|
4481
|
+
of the build is bound \u2014 usually the singleton lane declined the project (\`skipReasons\`
|
|
4482
|
+
\`authored-content\`: its content models were authored by the repo), so the percentage beside it
|
|
4483
|
+
describes a site this build is not.
|
|
4326
4484
|
|
|
4327
4485
|
**A site built in the CMS with NO deployed build has only that renderer** \u2014 live and in the
|
|
4328
4486
|
editor \u2014 so the generic theme IS the site until you theme it. Theme it without a deploy:
|
|
@@ -4437,7 +4595,10 @@ still gets its own component: its own family, plus \`allowedOn: ["slug:<page>"]\
|
|
|
4437
4595
|
with NO \`sectionType\` never appears in the editor's "Add a section" picker. Start from the
|
|
4438
4596
|
builtin blueprints \`list_components\` returns (\`builtin:*\`) instead of hand-writing block JSON.
|
|
4439
4597
|
On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
|
|
4440
|
-
of this in one call
|
|
4598
|
+
of this in one call, and on a site derived at IMPORT the release hook has already run it in bind
|
|
4599
|
+
mode and published the components, so expect ALREADY_COMPONENTIZED \u2014 the placements it wrote are
|
|
4600
|
+
still DRAFT blocks on the pages, so publish the pages, then \`npx @bettercms-ai/convert
|
|
4601
|
+
--componentize\`.
|
|
4441
4602
|
|
|
4442
4603
|
**3. The hierarchy editors see is the GROUP.** \`create_component\` and \`update_component\` take
|
|
4443
4604
|
\`group\`: a NAME, resolved against the project's component groups and created when missing. That
|
|
@@ -4686,6 +4847,123 @@ conversion is yours to write.
|
|
|
4686
4847
|
**Done means.** Whichever recipe you ran: \`get_binding_report\` reads \`mode "declared"\`,
|
|
4687
4848
|
\`unmatched\` empty, \`bound\` above 0, \`coverage.pending\` empty, and \`canvas.lane\` is \`bridge\` or
|
|
4688
4849
|
\`draft-route\` when the site should show structural drafts.
|
|
4850
|
+
|
|
4851
|
+
## 14. Declare how a field LOOKS in the editor \u2014 \`ui\`, \`helpText\`, \`showIf\`
|
|
4852
|
+
|
|
4853
|
+
You can create the schema. You can also say how it PRESENTS to the person filling it in. These
|
|
4854
|
+
three props ride on any field, on any field-creating tool (\`create_content_model\`,
|
|
4855
|
+
\`create_page\`, \`add_field\`, \`add_page_field\`), and none of them can move, reshape or hide a
|
|
4856
|
+
delivered value \u2014 they change editor chrome only.
|
|
4857
|
+
|
|
4858
|
+
\`\`\`jsonc
|
|
4859
|
+
{ "key": "testimonials", "label": "Testimonials", "type": "repeater",
|
|
4860
|
+
"ui": { "collapsed": true, "preview": { "title": "author", "media": "avatar" },
|
|
4861
|
+
"layout": "grid", "reorderable": true },
|
|
4862
|
+
"helpText": "Three to five. Shortest quotes read best.",
|
|
4863
|
+
"fields": [ { "key": "author", ... }, { "key": "avatar", "type": "image", ... }, ... ] }
|
|
4864
|
+
\`\`\`
|
|
4865
|
+
|
|
4866
|
+
- **\`helpText\`** \u2014 one line UNDER the field telling the author what to write here, not what the
|
|
4867
|
+
field is. "Roughly 60 characters; it becomes the browser tab title" earns its place.
|
|
4868
|
+
"The title field" does not.
|
|
4869
|
+
- **\`showIf\`** \u2014 hide a field until another has a value:
|
|
4870
|
+
\`{match:'all',rules:[{field:'link_type',op:'eq',value:'external'}]}\`. Rules name fields by
|
|
4871
|
+
BARE KEY, and the key namespace is FLAT across the model (\xA7 the duplicate-key rule), so the
|
|
4872
|
+
key you reference must exist.
|
|
4873
|
+
- **\`ui.collapsed\`** \u2014 start a nesting field's panel closed. For long optional sections.
|
|
4874
|
+
- **\`ui.preview\`** \u2014 how ONE ITEM of a nesting field summarises itself when collapsed. Both
|
|
4875
|
+
slots name a **CHILD FIELD KEY of that field** \u2014 not a value, not a dotted path.
|
|
4876
|
+
Without it a repeater of ten testimonials reads "Item 1 \u2026 Item 10" and the author has to open
|
|
4877
|
+
each one to find the one they meant. \`media\` may point at an \`image\`, \`file\` or url-ish
|
|
4878
|
+
\`text\` child; the type is not constrained.
|
|
4879
|
+
- **\`ui.layout\`** \u2014 \`'list'\` (the default; omit it) or \`'grid'\`. Grid when the THUMBNAIL is
|
|
4880
|
+
what the author scans: a gallery, a logo wall, a team. List when the words are.
|
|
4881
|
+
- **\`ui.reorderable\`** \u2014 omit it (\u2261 \`true\`) unless the ORDER IS PART OF THE MEANING. \`false\`
|
|
4882
|
+
LOCKS the list so its items cannot be dragged: a "three steps" band that must stay three steps
|
|
4883
|
+
in that order, a nav whose order is semantic, a timeline. Locking a list nobody should reorder
|
|
4884
|
+
is the declaration there was previously no way to make; locking one out of tidiness takes a
|
|
4885
|
+
capability away from the author, so do not set it "just in case".
|
|
4886
|
+
|
|
4887
|
+
**Five refusals, all deliberate, all 400 with the fix in the message.**
|
|
4888
|
+
1. An unknown key inside \`ui\` is REJECTED, not stripped. \`ui\` is a strict object precisely so
|
|
4889
|
+
that a typo is a refusal you can read instead of a 200 with your declaration gone.
|
|
4890
|
+
2. \`ui.preview\` on a LEAF field is refused \u2014 it summarises an item, and a leaf has no items.
|
|
4891
|
+
Valid on \`group\`, \`repeater\`, an \`array\` with \`config.zones\`, or \`modular\`.
|
|
4892
|
+
3. \`preview.title\` / \`preview.media\` naming a key that is not a child of that field is refused,
|
|
4893
|
+
and the error lists the valid child keys.
|
|
4894
|
+
4. \`ui.layout\` follows the same rule as \`preview\`: nesting fields only. A leaf has no items
|
|
4895
|
+
to arrange.
|
|
4896
|
+
5. \`ui.reorderable\` is STRICTER \u2014 it needs a LIST, not merely children. Valid on \`repeater\`,
|
|
4897
|
+
an \`array\` with \`config.zones.repeatable\`, or \`modular\`. A \`group\` (and a non-repeatable
|
|
4898
|
+
zone) holds exactly ONE item, so there is nothing there to drag and the write is refused.
|
|
4899
|
+
|
|
4900
|
+
**The same \`ui\` rides on COMPONENT PROPS.** \`create_component\` / \`update_component\` take it on
|
|
4901
|
+
each entry of \`props\`, with the placement rules the prop vocabulary implies: \`preview\` and
|
|
4902
|
+
\`layout\` on \`group\`, \`table\` and \`slot\`; \`reorderable\` on \`table\` ALONE, because a table is the
|
|
4903
|
+
only prop whose value is a list (a \`group\` is one object, a \`slot\` one component instance). A
|
|
4904
|
+
prop's \`preview\` keys name a SUB-FIELD from its own \`config.fields\` and are checked against
|
|
4905
|
+
them \u2014 except on a \`slot\`, whose children belong to whichever component fills it and therefore
|
|
4906
|
+
cannot be resolved at save time. \`update_component\` REPLACES the whole \`props\` array, so echo
|
|
4907
|
+
back each prop's stored \`ui\` or you delete it along with the prop.
|
|
4908
|
+
|
|
4909
|
+
\`\`\`jsonc
|
|
4910
|
+
{ "key": "slides", "label": "Slides", "type": "table",
|
|
4911
|
+
"target": { "blockId": "slider", "path": "props.slides" },
|
|
4912
|
+
"ui": { "preview": { "title": "caption", "media": "image" }, "layout": "grid" },
|
|
4913
|
+
"config": { "fields": [ { "key": "image", "type": "image", ... },
|
|
4914
|
+
{ "key": "caption", "type": "text", ... } ] } }
|
|
4915
|
+
\`\`\`
|
|
4916
|
+
|
|
4917
|
+
**THE RECEIPT \u2014 read it back.** There is no separate verification tool and no mode to flip.
|
|
4918
|
+
\`get_content_model\` (and \`get_page\` / \`get_component\`) return each field's \u2014 and each component
|
|
4919
|
+
prop's \u2014 stored \`ui\`, \`helpText\` and \`showIf\`. A prop that is ABSENT from that response was not
|
|
4920
|
+
stored, whatever the write returned \u2014 \xA712 rule 3, applied to this feature. Declare, then read
|
|
4921
|
+
back, then say it works.
|
|
4922
|
+
|
|
4923
|
+
**No \`order\` property. Ever.** A field's order IS its index in \`fields\`. To put a new field
|
|
4924
|
+
somewhere other than the end, pass \`after: '<existing top-level field key>'\` to \`add_field\` /
|
|
4925
|
+
\`add_page_field\`; omit it and the field appends, exactly as before. \`after\` is an argument
|
|
4926
|
+
applied once at the write \u2014 a stored ordering prop would be a second source of truth that
|
|
4927
|
+
disagrees with the array the moment anyone drags a row in the builder.
|
|
4928
|
+
|
|
4929
|
+
**Where the values come from \u2014 three tiers, in order. Stop at the first that answers.**
|
|
4930
|
+
|
|
4931
|
+
TIER 1 DERIVE FROM THE SITE'S SOURCE, when you have a checkout.
|
|
4932
|
+
<img src={item.image}> + <h3>{item.name}</h3> inside the map over \`team\`
|
|
4933
|
+
\u2192 ui.preview = { title: 'name', media: 'image' }
|
|
4934
|
+
A section rendered inside a <details> or behind a "Show advanced" toggle
|
|
4935
|
+
\u2192 ui.collapsed: true
|
|
4936
|
+
grid-cols-* / display:grid on the wrapper of the .map \u2192 ui.layout: 'grid'
|
|
4937
|
+
A FIXED-ARITY render \u2014 steps[0]/steps[1]/steps[2], or copy that names the
|
|
4938
|
+
positions ("Step 1", "then", "finally") \u2192 ui.reorderable: false
|
|
4939
|
+
A prop read only when another is set \u2192 showIf
|
|
4940
|
+
\u{1F534} THE SERVER NEVER READS YOUR SOURCE on the add_field path. \`pull_project_source\`
|
|
4941
|
+
and \`get_conversion_brief\` hand YOU the repo; this tier is work you do in your own
|
|
4942
|
+
checkout before you call the tool. On a greenfield, schema-first project there is
|
|
4943
|
+
no source at all \u2014 Tier 1 is vacuous and TIER 2 IS THE FLOOR.
|
|
4944
|
+
|
|
4945
|
+
TIER 2 DERIVE FROM THE FIELD SHAPE, when the code is silent.
|
|
4946
|
+
A repeater whose children include exactly one image-ish child \u2192 that is \`media\`.
|
|
4947
|
+
Its first required short-text child (name/title/heading/label) \u2192 that is \`title\`.
|
|
4948
|
+
A nesting field with more than ~6 children, or one whose label reads as optional
|
|
4949
|
+
("Advanced", "Extras", "SEO overrides") \u2192 \`collapsed: true\`.
|
|
4950
|
+
A repeatable whose item is MOSTLY its image (gallery, logos, team) \u2192 \`layout:
|
|
4951
|
+
'grid'\`; anything whose words are the point stays 'list' \u2014 so OMIT \`layout\`.
|
|
4952
|
+
\`reorderable\` HAS NO TIER-2 GUESS. The default (true) is the safe one, and
|
|
4953
|
+
locking a list the user did not ask to lock takes a capability away from them.
|
|
4954
|
+
Leave it absent unless Tier 1 showed you a fixed arity or the user said so.
|
|
4955
|
+
Guessing here is CORRECT. A wrong preview costs one edit; asking about every
|
|
4956
|
+
repeater on a 40-field schema costs the user the session.
|
|
4957
|
+
|
|
4958
|
+
TIER 3 ASK THE USER \u2014 only when 1 and 2 are both silent, and only for these three:
|
|
4959
|
+
a) MANY fields in one panel, where the grouping and the order depend on which
|
|
4960
|
+
content the user considers primary. You cannot know their priorities.
|
|
4961
|
+
b) CONDITIONAL VISIBILITY (\`showIf\`). It encodes business logic \u2014 when a field is
|
|
4962
|
+
irrelevant \u2014 and nothing in the code or the shape tells you that.
|
|
4963
|
+
c) VARIANTS SHARING A \`sectionType\`. Their prop keys MUST match (\xA74), so a preview
|
|
4964
|
+
or a rename that diverges between two variants makes a layout swap lose content.
|
|
4965
|
+
Confirm the shared key set before you declare per-variant chrome.
|
|
4966
|
+
Ask with AskUserQuestion, batched, once \u2014 never one field at a time.
|
|
4689
4967
|
`;
|
|
4690
4968
|
|
|
4691
4969
|
// src/prompts.ts
|
|
@@ -5191,6 +5469,108 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
5191
5469
|
]
|
|
5192
5470
|
})
|
|
5193
5471
|
);
|
|
5472
|
+
server.registerPrompt(
|
|
5473
|
+
"field-ui",
|
|
5474
|
+
{
|
|
5475
|
+
title: "Declare how fields LOOK in the editor (guided)",
|
|
5476
|
+
description: "Set the authoring chrome on an existing schema \u2014 collapsed panels, what titles and thumbnails a repeater's rows, help text, conditional visibility. Derives it from your site's source where there is source, from the field shapes where there isn't, and only asks you about the three things it genuinely cannot know.",
|
|
5477
|
+
argsSchema: {
|
|
5478
|
+
request: z2.string().optional().describe("scope, e.g. 'the Team and Testimonials models' or 'the whole project'")
|
|
5479
|
+
}
|
|
5480
|
+
},
|
|
5481
|
+
({ request }) => ({
|
|
5482
|
+
messages: [
|
|
5483
|
+
{
|
|
5484
|
+
role: "user",
|
|
5485
|
+
content: {
|
|
5486
|
+
type: "text",
|
|
5487
|
+
text: `Declare the editor UI for my BetterCMS fields.${request ? ` Scope: "${request}".` : ""}
|
|
5488
|
+
|
|
5489
|
+
Read bettercms://playbook/schema section 14 first \u2014 it has the exact prop shapes and the refusals.
|
|
5490
|
+
|
|
5491
|
+
**What you are setting** (all of these ride on any field, on create_content_model / create_page /
|
|
5492
|
+
add_field / add_page_field; none of them affect delivery):
|
|
5493
|
+
ui.collapsed start a nesting field's panel closed
|
|
5494
|
+
ui.preview {title,media} which CHILD FIELD KEY titles a collapsed row, and which is its thumb
|
|
5495
|
+
ui.layout 'list'|'grid' how its items are arranged; omit for list
|
|
5496
|
+
ui.reorderable omit (= true) unless the ORDER IS THE MEANING; false LOCKS the list
|
|
5497
|
+
helpText one line under the field: what to WRITE here
|
|
5498
|
+
showIf hide a field until another field has a value
|
|
5499
|
+
|
|
5500
|
+
**The same \`ui\` rides on COMPONENT PROPS** \u2014 create_component / update_component, one per entry
|
|
5501
|
+
of \`props\`. Placement differs because the vocabulary does: preview/layout on 'group', 'table'
|
|
5502
|
+
and 'slot'; reorderable on 'table' ALONE (the only prop whose value is a list). A prop's preview
|
|
5503
|
+
keys name a SUB-FIELD from its own config.fields. If this scope includes components, do them in
|
|
5504
|
+
the same pass \u2014 but note update_component REPLACES the whole props array, so read get_component
|
|
5505
|
+
first and echo every prop back WITH its stored ui.
|
|
5506
|
+
|
|
5507
|
+
**Work the three tiers in order. Stop at the first that answers \u2014 do not escalate what a tier
|
|
5508
|
+
below already settled.**
|
|
5509
|
+
|
|
5510
|
+
TIER 1 \u2014 DERIVE FROM SOURCE, if and only if you have a checkout of the site.
|
|
5511
|
+
Read the component that renders each field. \`<img src={item.photo}>\` next to
|
|
5512
|
+
\`<h3>{item.name}</h3>\` in the map over a list IS the answer: preview {title:'name',
|
|
5513
|
+
media:'photo'}. A block inside \`<details>\` or behind a "Show more" is collapsed:true.
|
|
5514
|
+
A \`grid-cols-*\` wrapper around the map is layout:'grid'. A FIXED-ARITY render \u2014
|
|
5515
|
+
steps[0]/steps[1]/steps[2], or copy that names positions ("Step 1", "finally") \u2014 is
|
|
5516
|
+
reorderable:false, and it is the ONLY evidence that earns that key.
|
|
5517
|
+
\u{1F534} THE SERVER NEVER READS YOUR SOURCE. \`pull_project_source\` / \`get_conversion_brief\` hand
|
|
5518
|
+
the repo to YOU; add_field does not look at it. This tier is your own reading, in your own
|
|
5519
|
+
checkout, before you call any tool. ON A GREENFIELD SCHEMA-FIRST PROJECT THERE IS NO SOURCE \u2014
|
|
5520
|
+
Tier 1 does not apply and TIER 2 IS THE FLOOR. Do not stall waiting for code that will
|
|
5521
|
+
never exist.
|
|
5522
|
+
|
|
5523
|
+
TIER 2 \u2014 DERIVE FROM THE FIELD SHAPE, when the code is silent or absent.
|
|
5524
|
+
One image-ish child in a repeater \u2192 that is \`media\`. The first required short-text child
|
|
5525
|
+
(name/title/heading/label) \u2192 that is \`title\`. More than ~6 children, or a label that reads
|
|
5526
|
+
optional ("Advanced", "Extras") \u2192 collapsed:true. A repeatable whose item is mostly its
|
|
5527
|
+
image (gallery, logos, team) \u2192 layout:'grid'; everything else stays list, so OMIT layout.
|
|
5528
|
+
\`reorderable\` has NO Tier-2 guess \u2014 leave it absent. The default is true, and locking a
|
|
5529
|
+
list the user did not ask to lock silently removes something they could do yesterday.
|
|
5530
|
+
GUESS HERE otherwise. A wrong preview costs one edit; a question per repeater costs the
|
|
5531
|
+
user the session.
|
|
5532
|
+
|
|
5533
|
+
TIER 3 \u2014 ASK ME, and ONLY for these three. Batch them into ONE AskUserQuestion at the end,
|
|
5534
|
+
never one field at a time:
|
|
5535
|
+
a) A panel with MANY fields, where grouping and order depend on which content I treat as
|
|
5536
|
+
primary \u2014 you cannot infer my priorities.
|
|
5537
|
+
b) CONDITIONAL VISIBILITY (showIf) \u2014 it is business logic about when a field is irrelevant.
|
|
5538
|
+
Never invent a rule.
|
|
5539
|
+
c) VARIANTS SHARING A sectionType \u2014 their prop keys must match, or swapping the layout loses
|
|
5540
|
+
content. Confirm the shared key set before declaring per-variant chrome.
|
|
5541
|
+
|
|
5542
|
+
**Order of work**
|
|
5543
|
+
1. \`list_content_models\` / \`list_pages\`, then \`get_content_model\` / \`get_page\` for each target.
|
|
5544
|
+
Note which fields ALREADY carry ui/helpText/showIf \u2014 do not overwrite a human's choice
|
|
5545
|
+
without asking.
|
|
5546
|
+
2. Decide each field's chrome by the tiers above. Write down which tier answered; you will
|
|
5547
|
+
report it.
|
|
5548
|
+
3. Apply. For a field that already exists, the declaration goes with the field: use the
|
|
5549
|
+
dashboard or a model update \u2014 \`add_field\` APPENDS and refuses an existing key, so it is
|
|
5550
|
+
the wrong tool for editing one. For a NEW field, pass ui/helpText/showIf in the same
|
|
5551
|
+
\`add_field\` / \`add_page_field\` call, and pass \`after: '<existing key>'\` if it belongs
|
|
5552
|
+
somewhere other than the bottom.
|
|
5553
|
+
4. **RECEIPT \u2014 non-negotiable.** Re-read every target with \`get_content_model\` / \`get_page\` /
|
|
5554
|
+
\`get_component\` and
|
|
5555
|
+
confirm the stored \`ui\` matches what you sent. A prop ABSENT from that response was NOT
|
|
5556
|
+
stored, whatever the write said. Report per field: the tier that decided it, what you set,
|
|
5557
|
+
and that the read-back confirmed it. Anything you could not confirm, say so plainly.
|
|
5558
|
+
|
|
5559
|
+
**Refusals you should expect and must not work around:** an unknown key inside \`ui\` is a 400
|
|
5560
|
+
(it is strict on purpose \u2014 that is your typo, named); \`ui.preview\` or \`ui.layout\` on a leaf
|
|
5561
|
+
field is a 400 (both describe ITEMS); \`ui.reorderable\` anywhere without a LIST is a 400 \u2014 a
|
|
5562
|
+
\`group\` and a non-repeatable zone hold one item, so only \`repeater\`, an \`array\` with
|
|
5563
|
+
config.zones.repeatable, and \`modular\` take it; a preview key that is not a child of that field
|
|
5564
|
+
is a 400 listing the valid child keys. Fix the declaration; never retry without it and call that
|
|
5565
|
+
success.
|
|
5566
|
+
|
|
5567
|
+
There is deliberately NO \`order\` property and no mode to turn on \u2014 order is the array index,
|
|
5568
|
+
and \`ui\` only changes editor chrome, so there is nothing to flip.`
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
]
|
|
5572
|
+
})
|
|
5573
|
+
);
|
|
5194
5574
|
}
|
|
5195
5575
|
|
|
5196
5576
|
// src/server.ts
|