@bettercms-ai/mcp 0.84.5 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +242 -57
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +149 -149
- package/dist/skills/bettercms/SKILL.md +1 -1
- package/dist/skills/bettercms/references/bindings.md +9 -0
- package/dist/skills/bettercms/references/components.md +1 -1
- package/dist/skills/bettercms/references/content-modeling.md +1 -1
- package/dist/skills/bettercms/references/dock-input.md +89 -1
- package/dist/skills/bettercms/references/editor-chrome.md +2 -2
- package/dist/skills/bettercms/references/finish-site.md +1 -1
- package/dist/skills/bettercms/references/forms-and-interactions.md +1 -1
- package/dist/skills/bettercms/references/integrations.md +13 -2
- package/dist/skills/bettercms/references/kit-catalog.md +117 -181
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2504,6 +2504,72 @@ var DOCK_ICON_NAMES = [
|
|
|
2504
2504
|
];
|
|
2505
2505
|
var DOCK_ICON_NAME_SET = new Set(DOCK_ICON_NAMES);
|
|
2506
2506
|
|
|
2507
|
+
// ../types/src/kit-v2.ts
|
|
2508
|
+
var OPEN_MODES = ["inline", "drill", "popover", "dialog"];
|
|
2509
|
+
var SHOW_IF_SCOPES = ["parent", "root"];
|
|
2510
|
+
var V2_SHAPES = ["block", "page", "entry", "global", "object"];
|
|
2511
|
+
var V2_PLACEMENTS = ["inline", "section", "page"];
|
|
2512
|
+
var KIT_CATEGORIES = [
|
|
2513
|
+
"Navigation & chrome",
|
|
2514
|
+
"Heroes",
|
|
2515
|
+
"Features & storytelling",
|
|
2516
|
+
"Social proof",
|
|
2517
|
+
"Media & galleries",
|
|
2518
|
+
"Commerce",
|
|
2519
|
+
"Pricing & plans",
|
|
2520
|
+
"Conversion & forms",
|
|
2521
|
+
"Listings & feeds",
|
|
2522
|
+
"Long-form & docs",
|
|
2523
|
+
"Company & people",
|
|
2524
|
+
"Events & places",
|
|
2525
|
+
"Data & interactive",
|
|
2526
|
+
"Advanced patterns"
|
|
2527
|
+
];
|
|
2528
|
+
var UI_REFUSAL_CODES = {
|
|
2529
|
+
schema: [
|
|
2530
|
+
"facet_unclassified",
|
|
2531
|
+
"excluded_facet",
|
|
2532
|
+
"depth_exceeded",
|
|
2533
|
+
"validation_failed",
|
|
2534
|
+
"hash_mismatch",
|
|
2535
|
+
"non_finite_number",
|
|
2536
|
+
"shape_kind_mismatch",
|
|
2537
|
+
"block_field_unrenderable",
|
|
2538
|
+
// rich text (richtext-features.ts richTextRefusal): `richtext.<code>:<detail>`
|
|
2539
|
+
"block_not_accepted",
|
|
2540
|
+
"derived_field",
|
|
2541
|
+
"feature_unknown"
|
|
2542
|
+
],
|
|
2543
|
+
build: [
|
|
2544
|
+
"peer_missing",
|
|
2545
|
+
"kit_major_unsupported",
|
|
2546
|
+
"compile_error",
|
|
2547
|
+
"nondeterministic",
|
|
2548
|
+
"size_exceeded",
|
|
2549
|
+
// CMS-stored UI (dock-ui.ts): an import off the allow-list; one name in both the repo and the CMS
|
|
2550
|
+
"import_denied",
|
|
2551
|
+
"name_conflict"
|
|
2552
|
+
],
|
|
2553
|
+
protocol: ["unsupported", "handshake_timeout", "origin_mismatch", "write_denied"],
|
|
2554
|
+
runtime: ["crashed", "blank", "csp_violation"],
|
|
2555
|
+
// not_approved: publish_dock_ui on a version no one approved
|
|
2556
|
+
policy: ["confirmation_required", "flag_off", "not_approved"]
|
|
2557
|
+
};
|
|
2558
|
+
var UI_REFUSAL_IDS = Object.keys(UI_REFUSAL_CODES).flatMap(
|
|
2559
|
+
(f) => UI_REFUSAL_CODES[f].map((c) => `${f}.${c}`)
|
|
2560
|
+
);
|
|
2561
|
+
var KEY_RESERVED = /* @__PURE__ */ new Set(["derived", "layout", "page"]);
|
|
2562
|
+
var DERIVE_MINTED_RESERVED = /* @__PURE__ */ new Set([...KEY_RESERVED, "id"]);
|
|
2563
|
+
|
|
2564
|
+
// ../types/src/dock-ui.ts
|
|
2565
|
+
var DOCK_UI_LIMITS = {
|
|
2566
|
+
sourcesPerProject: 64,
|
|
2567
|
+
bytesPerProject: 4 * 1024 * 1024,
|
|
2568
|
+
bytesPerVersion: 256 * 1024,
|
|
2569
|
+
bytesPerBundle: 512 * 1024
|
|
2570
|
+
};
|
|
2571
|
+
var DOCK_UI_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
2572
|
+
|
|
2507
2573
|
// src/tools.ts
|
|
2508
2574
|
import { BetterCMSError } from "@bettercms-ai/sdk";
|
|
2509
2575
|
|
|
@@ -2742,9 +2808,10 @@ var MODEL_UPDATE_KEYS = [
|
|
|
2742
2808
|
"fieldOrder",
|
|
2743
2809
|
"fieldLabel",
|
|
2744
2810
|
"fieldHelpText",
|
|
2745
|
-
"override"
|
|
2811
|
+
"override",
|
|
2812
|
+
"v2"
|
|
2746
2813
|
];
|
|
2747
|
-
var NOTHING_TO_UPDATE = "Nothing to update: update_content_model changes the model's name, slug, description, urlPattern, fieldsets and field chrome (fieldUi, fieldFieldset, fieldOrder, fieldLabel, fieldHelpText). `fields` is not an argument: add a field with add_field, change one with update_field, delete with removeFields (remove_page_fields for a page).";
|
|
2814
|
+
var NOTHING_TO_UPDATE = "Nothing to update: update_content_model changes the model's name, slug, description, urlPattern, fieldsets, its V2 block (v2) and field chrome (fieldUi, fieldFieldset, fieldOrder, fieldLabel, fieldHelpText). `fields` is not an argument: add a field with add_field, change one with update_field, delete with removeFields (remove_page_fields for a page).";
|
|
2748
2815
|
var updatesNothing = (args) => args.removeFields === void 0 && args.allowFieldRemoval === void 0 && !MODEL_UPDATE_KEYS.some((k) => args[k] !== void 0);
|
|
2749
2816
|
function planFieldRemoval(model, keys, callerIfMatch) {
|
|
2750
2817
|
const fields = model.fields ?? [];
|
|
@@ -2761,6 +2828,19 @@ function planFieldRemoval(model, keys, callerIfMatch) {
|
|
|
2761
2828
|
const kept = fields.filter((f) => !drop.has(f.key));
|
|
2762
2829
|
return ifMatch === void 0 ? { ok: true, fields: kept } : { ok: true, fields: kept, ifMatch };
|
|
2763
2830
|
}
|
|
2831
|
+
var SOURCES_SERVER_OWNED = "sources_server_owned: `sources` is stamped by the server on every write (who last set ui, input and ai) and is never taken from a client. Drop it and send the facet itself.";
|
|
2832
|
+
function sourcesIn(fields, at = "") {
|
|
2833
|
+
for (const f of Array.isArray(fields) ? fields : []) {
|
|
2834
|
+
if (!f || typeof f !== "object") continue;
|
|
2835
|
+
const path = at ? `${at}.${String(f.key)}` : String(f.key);
|
|
2836
|
+
if (f.sources !== void 0) return path;
|
|
2837
|
+
for (const list2 of CHILD_LISTS) {
|
|
2838
|
+
const hit = sourcesIn(getIn(f, list2), path);
|
|
2839
|
+
if (hit) return hit;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return null;
|
|
2843
|
+
}
|
|
2764
2844
|
var CHILD_LISTS = [
|
|
2765
2845
|
["fields"],
|
|
2766
2846
|
["config", "zones", "nonRepeatable"],
|
|
@@ -2793,16 +2873,17 @@ function fieldAt(fields, keys) {
|
|
|
2793
2873
|
patchAt(fields, keys, (f) => (found = f, f));
|
|
2794
2874
|
return found;
|
|
2795
2875
|
}
|
|
2796
|
-
function
|
|
2797
|
-
return fields.
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2876
|
+
function siblingKeys(fields, keys) {
|
|
2877
|
+
if (keys.length <= 1) return fields.map((f) => String(f.key));
|
|
2878
|
+
const parent = fieldAt(fields, keys.slice(0, -1));
|
|
2879
|
+
const self = keys[keys.length - 1];
|
|
2880
|
+
for (const list2 of CHILD_LISTS) {
|
|
2881
|
+
const kids = parent ? getIn(parent, list2) : void 0;
|
|
2882
|
+
if (Array.isArray(kids) && kids.some((k) => k.key === self)) return kids.map((k) => String(k.key));
|
|
2883
|
+
}
|
|
2884
|
+
return [];
|
|
2804
2885
|
}
|
|
2805
|
-
var CLEARABLE = ["helpText", "showIf", "requiredIf", "compare", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode", "localized"];
|
|
2886
|
+
var CLEARABLE = ["helpText", "showIf", "requiredIf", "compare", "ui", "placement", "readOnly", "lockReason", "helpMore", "advanced", "recommendedMax", "openMode", "localized", "ai"];
|
|
2806
2887
|
var SETTABLE = ["label", "required", ...CLEARABLE];
|
|
2807
2888
|
function mergeConfig(current, patch) {
|
|
2808
2889
|
const out = { ...current ?? {} };
|
|
@@ -2821,6 +2902,7 @@ function helpTextEcho(helpText, field) {
|
|
|
2821
2902
|
return echoes ? `helpText repeats the field's label or value, which the editor already shows. Say what belongs here or its format in under 60 characters ("Whole number, no currency symbol."), or send null for none.` : null;
|
|
2822
2903
|
}
|
|
2823
2904
|
function planFieldEdit(model, edit, callerIfMatch) {
|
|
2905
|
+
if (edit.sources !== void 0) return { ok: false, error: SOURCES_SERVER_OWNED };
|
|
2824
2906
|
const fields = model.fields ?? [];
|
|
2825
2907
|
const keys = typeof edit.path === "string" && edit.path ? edit.path.split(".") : [];
|
|
2826
2908
|
const current = keys.length ? fieldAt(fields, keys) : void 0;
|
|
@@ -2835,12 +2917,12 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2835
2917
|
};
|
|
2836
2918
|
}
|
|
2837
2919
|
const rename = typeof edit.key === "string" && edit.key !== current.key ? edit.key : void 0;
|
|
2838
|
-
if (rename !== void 0 &&
|
|
2839
|
-
return { ok: false, error: `The key '${rename}' is already used
|
|
2920
|
+
if (rename !== void 0 && siblingKeys(fields, keys).includes(rename)) {
|
|
2921
|
+
return { ok: false, error: `The key '${rename}' is already used by a field beside '${keys.join(".")}'. Two siblings can't share a key (the same key in a different group is fine).` };
|
|
2840
2922
|
}
|
|
2841
2923
|
const changes = SETTABLE.filter((k) => edit[k] !== void 0);
|
|
2842
2924
|
if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
|
|
2843
|
-
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, requiredIf, compare, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, localized, type or key." };
|
|
2925
|
+
return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, requiredIf, compare, ui, placement, readOnly, lockReason, helpMore, advanced, recommendedMax, openMode, localized, ai, type or key." };
|
|
2844
2926
|
}
|
|
2845
2927
|
const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
|
|
2846
2928
|
if (echo) return { ok: false, error: echo };
|
|
@@ -2869,6 +2951,7 @@ function planFieldEdit(model, edit, callerIfMatch) {
|
|
|
2869
2951
|
};
|
|
2870
2952
|
}
|
|
2871
2953
|
function planFieldAdd(model, field, after, callerIfMatch) {
|
|
2954
|
+
if (field.sources !== void 0 || sourcesIn(field.fields)) return { ok: false, error: SOURCES_SERVER_OWNED };
|
|
2872
2955
|
const fields = model.fields ?? [];
|
|
2873
2956
|
const keys = field.key.split(".");
|
|
2874
2957
|
const childKey = keys.pop();
|
|
@@ -2879,8 +2962,8 @@ function planFieldAdd(model, field, after, callerIfMatch) {
|
|
|
2879
2962
|
if (lists.length > 1) return { ok: false, error: `'${parentPath}' holds two lists of fields (a fixed part and a repeating part); add this field in the dashboard, where you pick which.` };
|
|
2880
2963
|
const list2 = lists[0] ?? (parent.type === "group" || parent.type === "repeater" ? ["fields"] : void 0);
|
|
2881
2964
|
if (!list2) return { ok: false, error: `'${parentPath}' is a ${String(parent.type)} field: only a group or a repeater holds fields.` };
|
|
2882
|
-
if (allKeys(fields).includes(childKey)) return { ok: false, error: `The key '${childKey}' is already used in this schema; keys share one flat namespace, nested fields included.` };
|
|
2883
2965
|
const kids = getIn(parent, list2) ?? [];
|
|
2966
|
+
if (kids.some((k) => k.key === childKey)) return { ok: false, error: `The key '${childKey}' is already used in '${parentPath}'. Two siblings can't share a key (the same key in a different group is fine).` };
|
|
2884
2967
|
const at = after === void 0 ? kids.length : kids.findIndex((f) => f.key === after) + 1;
|
|
2885
2968
|
if (after !== void 0 && at === 0) return { ok: false, error: `after: '${after}' is not a field of '${parentPath}'. Its fields: ${kids.map((f) => `'${String(f.key)}'`).join(", ") || "none"}.` };
|
|
2886
2969
|
const next = patchAt(fields, keys, (p) => setIn(p, list2, [...kids.slice(0, at), { ...field, key: childKey }, ...kids.slice(at)]));
|
|
@@ -2959,6 +3042,14 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
|
|
|
2959
3042
|
"start_integration_signin",
|
|
2960
3043
|
"save_integration_template",
|
|
2961
3044
|
"install_integration_template",
|
|
3045
|
+
// CMS-stored custom dock UI: reached for once the stock controls and ui keys fall short.
|
|
3046
|
+
"write_dock_ui",
|
|
3047
|
+
"approve_dock_ui",
|
|
3048
|
+
"validate_dock_ui",
|
|
3049
|
+
"publish_dock_ui",
|
|
3050
|
+
"list_dock_ui",
|
|
3051
|
+
// A whole V2 schema at once: reached for when the field-by-field tools would take dozens of calls.
|
|
3052
|
+
"write_schema",
|
|
2962
3053
|
// The ABM campaign lane — also hidden from the default listing (ABM_TOOLS below).
|
|
2963
3054
|
"research_account",
|
|
2964
3055
|
"build_abm_page",
|
|
@@ -3010,7 +3101,7 @@ var toolMeta = (name) => ALWAYS_LOADED.has(name) ? { "anthropic/alwaysLoad": tru
|
|
|
3010
3101
|
var TOOL_LOADING_INSTRUCTION = `TOOL SCHEMAS: a client with tool search lists most BetterCMS tools by name only. Before the first call to a tool whose schema you have not been shown, load it with the client's tool search, and load every tool the task needs in ONE search (for a renaming pass: update_field, update_content_model, update_component, list_components). A call made without the schema is refused by the client before it reaches BetterCMS ("Invalid tool parameters"): that is this, never a BetterCMS error, and never a reason to change the arguments.`;
|
|
3011
3102
|
|
|
3012
3103
|
// src/tool-text.ts
|
|
3013
|
-
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; key 'a.b' adds b inside group/repeater a (after: a sibling); `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options`; `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field. V2 keys (object, openMode, advanced, helpMore, recommendedMax, config.scale): \xA714 of bettercms://playbook/schema. Cross-field: `requiredIf` (showIf's shape) and `compare` [{op:gt|gte|lt|lte|eq|neq, field, message?}].";
|
|
3104
|
+
var ADD_FIELD_DESCRIPTION = "Append ONE field to a content model (`modelId`) or to a page's schema (`pageId`) \u2014 additive, never overwrites; a key already in use is refused. For a section/zone add ONE 'group' (fixed block) or 'repeater' (repeating list) carrying its child `fields`, not the zone's inner fields as separate top-level fields. Pass `after` to insert it after an existing top-level field instead of at the end; key 'a.b' adds b inside group/repeater a (after: a sibling); `ui`, `helpText`, `showIf`, `placement` and `fieldsetId` declare how it presents in the editor \u2014 read them back with get_content_model or get_page. On a page a 'group' is stored as type 'array' with `config.zones.nonRepeatable`: entry and delivery data carry `<key>.nonRepeatable.<sub>`, while data-bcms-field paths are `<key>.<sub>`. Schema only \u2014 never places a section; use componentize_sections to turn a group into a section. `multiselect` stores a string[] of `config.options`; `icon` is a BetterCMS icon name or an uploaded icon. To change a field that exists, use update_field. V2 keys (object, openMode, advanced, helpMore, recommendedMax, ai, config.scale): \xA714 of bettercms://playbook/schema. Cross-field: `requiredIf` (showIf's shape) and `compare` [{op:gt|gte|lt|lte|eq|neq, field, message?}].";
|
|
3014
3105
|
var V2_LEAF_TYPES_NOTE = "`tags`: string[] taxonomy (config.options suggestions, max, allowCustom). `rating`: 0..config.max (5), step 1|0.5. `code`: {language?, code} or a string (config.languages).";
|
|
3015
3106
|
var MOVE_STAGE_DESCRIPTION = "Move a content entry (`entryId`) or a page (`pageId`) to another editorial stage \u2014 this is how you take it OUT of review so it can be published. `workflowStage` is a stage key from get_workflow_board; pass `fromStage` to 409 if someone moved it first. Moving into the publish-gate stage is an approval and needs a publish-enabled connection. Publish afterwards with update_content_entry or update_page status:'published' \u2014 approving and publishing are deliberately two separate calls.";
|
|
3016
3107
|
var WINDOW_NOTE = "{from?, until?}, ISO 8601 with offset: shown only between the two instants; null clears (skill publishing).";
|
|
@@ -3056,7 +3147,15 @@ var NEW_TOOL_TEXT = {
|
|
|
3056
3147
|
start_integration_signin: "Start the vendor sign-in of an integration with `oauth`: a `url` the USER opens to sign in with their own OAuth app (register `redirectUri` in it first). Tokens stay on the server.",
|
|
3057
3148
|
save_integration_template: "Save an integration as a workspace template: its shape, never a secret value or its settings, with copies of the collections its syncs write into.",
|
|
3058
3149
|
install_integration_template: "Install a workspace template on this project: missing collections are created and the integration waits for approval (`approval.approveUrl`); `secretsNeeded` lists the env vars to set.",
|
|
3059
|
-
test_integration_fetch: "Call the vendor once through the server, as the integration: an approved host only, with the integration's `inject` secrets added server-side and every secret redacted from the answer. The request itself takes no {{...}} templates, and the token endpoint cannot be called. Use it to read the response shape before writing a sync: `items`, `externalKey` and `map` are JSON Pointers into it. Bodies over 1 MB come back truncated. 409 integration_not_approved: an admin must approve it first."
|
|
3150
|
+
test_integration_fetch: "Call the vendor once through the server, as the integration: an approved host only, with the integration's `inject` secrets added server-side and every secret redacted from the answer. The request itself takes no {{...}} templates, and the token endpoint cannot be called. Use it to read the response shape before writing a sync: `items`, `externalKey` and `map` are JSON Pointers into it. Bodies over 1 MB come back truncated. 409 integration_not_approved: an admin must approve it first.",
|
|
3151
|
+
// CMS-stored custom dock UI: write → approve (the user's yes) → validate → publish. Recipe: skill dock-input.
|
|
3152
|
+
write_dock_ui: "Save a custom dock input or panel (one .tsx `source`) as a new DRAFT version of `name` (a letter, then [a-z0-9-], \u226448), what `cms:<name>` imports resolve. Imports: react, react-dom, @bettercms-ai/kit/*, @bettercms-ai/dock-input, cms:<name>. Same bytes write nothing. Skill dock-input",
|
|
3153
|
+
approve_dock_ui: "Approve the newest `version` for publishing. Show the user the source first; userApproved: true only after their yes. A later write drops it",
|
|
3154
|
+
validate_dock_ui: "Check one version (default newest) without publishing: allowed imports, size caps, a name the repo also has. Answers the build receipt",
|
|
3155
|
+
publish_dock_ui: "Publish an APPROVED version (default newest): validates first, refusing policy.not_approved, build.name_conflict or the build's refusals, then rebuilds the project's dock UI release",
|
|
3156
|
+
list_dock_ui: "The project's dock UI sources, with usage against the limits",
|
|
3157
|
+
// A V2 TypeSchema into a model (lane fields, through a change set) or a component's props (lane props).
|
|
3158
|
+
write_schema: "Write a kit V2 TypeSchema into a model (lane fields, as a change set; model: null creates) or a component's props (lane props). Run dryRun first: a write whose `loss` is not all in acceptLoss is refused schema.loss_not_accepted"
|
|
3060
3159
|
};
|
|
3061
3160
|
var DELETE_PAGE_RUNTIME_BUNDLE = " Or, with no pageId, `runtimeBundle`: 'report' lists the duplicate pages derived from a server app's own bundle (`app/` folders); 'apply' retires them and their empty folders.";
|
|
3062
3161
|
var FIELD_LOCKED_NOTE = "A readOnly 'developer' field keeps its stored value: changing or omitting it without schema access is 403 FIELD_LOCKED.";
|
|
@@ -3069,8 +3168,10 @@ var PARAM_TEXT = {
|
|
|
3069
3168
|
readOnly: "'developer' = the value and this lock change only with schema access (needs lockReason); true = an editor marker",
|
|
3070
3169
|
lockReason: "which system owns the value (\u2264 200 chars)",
|
|
3071
3170
|
lockClears: "as on add_field; null clears it",
|
|
3072
|
-
helpText: "shown under the field. Most fields get none: write it only for a format or unit, a place the canvas cannot show, a real constraint or a fallback, as one plain sentence
|
|
3171
|
+
helpText: "shown under the field. Most fields get none: write it only for a format or unit, a place the canvas cannot show, a real constraint or a fallback, as one plain sentence of at most 160 characters with no semicolons, no dashes and no values from the page, never its label or value. Omitted: the platform writes it where one of those reasons holds.",
|
|
3073
3172
|
helpMore: "longer help behind the \u24D8 next to the label, when one helpText line cannot carry it",
|
|
3173
|
+
fieldAi: "true = the editor offers its AI assist on this field",
|
|
3174
|
+
modelV2: "the V2 block {shape?, placement?, category?, variants?: {slug: {icon?, description?}}}; null clears it",
|
|
3074
3175
|
advanced: "true = a rare or developer setting: it collects in a closed 'Advanced' section",
|
|
3075
3176
|
recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
|
|
3076
3177
|
openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
|
|
@@ -3106,7 +3207,12 @@ var PARAM_TEXT = {
|
|
|
3106
3207
|
focalX: "focal point, 0-1 across from the left edge; null resets to the centre",
|
|
3107
3208
|
focalY: "focal point, 0-1 down from the top edge; null resets to the centre",
|
|
3108
3209
|
mergeHref: "pageId only: the link field paired with `path` (the label). Both become ONE `button` field keyed `key` (default: their stem, primaryCtaHref \u2192 primaryCta). Each entry's DRAFT gets {label, href} and keeps the old values; the live copy changes on publish. ifMatch is the page's version. 409 CODE_RENDERED_ROUTE: do its `next` recipe in code",
|
|
3109
|
-
confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval"
|
|
3210
|
+
confirmedInput: "true only after the user explicitly approved adding this custom input (`ui.input`); else setting or changing one is refused input_needs_confirmation. Removing one needs no approval",
|
|
3211
|
+
dockUiUserApproved: "true only after the user said yes to this version",
|
|
3212
|
+
dockUiTarget: "per scope: field {modelId|pageId|componentId, path}, section {componentId}, page {pageId}, layout {}",
|
|
3213
|
+
writeSchemaModel: "model or component id/slug; null creates a model",
|
|
3214
|
+
writeSchemaAcceptLoss: "loss keys or paths you accept",
|
|
3215
|
+
writeSchemaIfMatch: "the dry run's `version`; needed to write over a stored row"
|
|
3110
3216
|
};
|
|
3111
3217
|
var INTEGRATION_PARAM_TEXT = {
|
|
3112
3218
|
integration: "the integration's id or slug (from list_integrations)",
|
|
@@ -3136,7 +3242,7 @@ var INTEGRATION_PARAM_TEXT = {
|
|
|
3136
3242
|
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
3137
3243
|
var BUTTON_FIELD_NOTE = "'button' is ONE call to action, value {label, href, variant? (a Button family look), size?, arrow?, icon?, newTab?, rel?, fullWidth?} (arrow:false, icon:null off): never a text field beside a link field.";
|
|
3138
3244
|
var LINK_VALUE_NOTE = "A link or button href: a URL string or {type: url|page|entry|email|phone|anchor|file, href, title?, newTab?, rel?, section?, ref: page/entry id}.";
|
|
3139
|
-
var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, requiredIf, compare, ui, placement, readOnly, lockReason, or the V2 helpMore, advanced, recommendedMax and
|
|
3245
|
+
var UPDATE_FIELD_DESCRIPTION = "Change ONE existing field of a content model (`modelId`) or of a page's schema (`pageId`), addressed by `path`: its key, or dot-separated keys through groups and repeaters ('hero.title'). Set its label, helpText, required, validation `config` (merged by key; null deletes one), showIf, requiredIf, compare, ui, placement, readOnly, lockReason, or the V2 helpMore, advanced, recommendedMax, openMode and ai (null clears those; `sources` is server-owned and refused). The rest of the schema is written back unchanged at the version read; a 409 means it changed since, so read again. `type` changes how every stored value is read and needs allowRetype: true. `key` renames it: stored values stay under the old key, so the route answers 409 with the entry count unless allowFieldRemoval: true discards them \u2014 migrate the values first. Ask the user before a retype or a rename. A page's label + link pair (primaryCtaText + primaryCtaHref) becomes one button with `mergeHref`.";
|
|
3140
3246
|
var GET_SEO_DESCRIPTION = "Get the connected project's site-wide SEO settings \u2014 every key update_seo writes: seoDefaults (metaTitle, metaDescription, ogImage, twitterHandle, og, twitter, collectionSchemaTypes), siteSchema (the site's JSON-LD), robotsConfig, sitemapConfig, rssConfig, indexnowConfig and searchConfig.";
|
|
3141
3247
|
var UPDATE_SEO_DESCRIPTION = "Set the connected project's site-wide SEO defaults and config. Each key you send is REPLACED whole (not deep-merged), so read get_seo and send the complete object. `seoDefaults` applies to every page that does not override it, and its `collectionSchemaTypes` maps a collection's model id to the schema.org type of its entries. `siteSchema` is the site's own JSON-LD (Organization/WebSite, emitted on the home page). `robotsConfig`, `sitemapConfig` and `rssConfig` shape robots.txt, sitemap.xml and the feed; `indexnowConfig` pings search engines on publish; `searchConfig` is site search. A key the plan does not include answers 403 FEATURE_LOCKED. Rebuilds the site.";
|
|
3142
3248
|
var SEO_KEY_TEXT = {
|
|
@@ -3402,13 +3508,14 @@ var OPEN_MODE_INVALID = "openMode_invalid: openMode must be one of 'inline', 'dr
|
|
|
3402
3508
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
3403
3509
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
3404
3510
|
var dockIconArg = z.string().min(1).max(40).refine((v) => DOCK_ICON_SET.has(v), `not a dock icon, see ${DOCK_ICONS_URI}`);
|
|
3405
|
-
var fieldCondition = z.
|
|
3511
|
+
var fieldCondition = z.strictObject({
|
|
3406
3512
|
match: z.enum(["all", "any"]),
|
|
3407
3513
|
rules: z.array(
|
|
3408
|
-
z.
|
|
3514
|
+
z.strictObject({
|
|
3409
3515
|
field: z.string().describe("the BARE KEY of another field on this same model"),
|
|
3410
3516
|
op: z.enum(["eq", "neq", "in", "notIn", "gt", "lt", "isSet", "notEmpty", "empty"]),
|
|
3411
|
-
value: z.union([z.string(), z.array(z.string())]).optional().describe(`omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); 'gt'/'lt' take one number as a string ("10"); required for every other op`)
|
|
3517
|
+
value: z.union([z.string(), z.array(z.string())]).optional().describe(`omit for 'isSet'/'notEmpty' (the source holds any value) and 'empty' (it holds none); 'gt'/'lt' take one number as a string ("10"); required for every other op`),
|
|
3518
|
+
scope: z.enum(SHOW_IF_SCOPES).optional().describe("where `field` is looked up: absent = a sibling, 'parent' = the object holding the siblings, 'root' = the top of the entry or component")
|
|
3412
3519
|
})
|
|
3413
3520
|
)
|
|
3414
3521
|
});
|
|
@@ -3444,7 +3551,7 @@ var uiObject = z.strictObject({
|
|
|
3444
3551
|
reference: z.object({ integration: z.string(), key: z.string() }).strict().optional().describe("a json field holding a live vendor item: the integration's slug and reference key."),
|
|
3445
3552
|
access: z.strictObject({ view: z.literal("schema.view").optional(), edit: z.literal("schema.manage").optional() }).optional().describe("who sees (view: developer-only) and who changes (edit) it, gated in the editor. The enforced lock is readOnly:'developer'."),
|
|
3446
3553
|
hidden: z.boolean().optional().describe("true: never shown in the form (a system value). A conditional hide is showIf."),
|
|
3447
|
-
blocks: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), preview: z.record(z.string(), z.string()).optional() })).optional().describe("modular only: per block slug, {icon?, preview?} for its rows."),
|
|
3554
|
+
blocks: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), preview: z.record(z.string(), z.string()).optional(), openMode: z.enum(OPEN_MODES).optional() })).optional().describe("modular only: per block slug, {icon?, preview?, openMode?} for its rows."),
|
|
3448
3555
|
input: z.object({
|
|
3449
3556
|
name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
|
|
3450
3557
|
writes: z.array(z.string().min(1).max(64)).max(16).optional()
|
|
@@ -3475,7 +3582,7 @@ var propUiObject = uiObject.extend({
|
|
|
3475
3582
|
var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
|
|
3476
3583
|
var fieldShape = {
|
|
3477
3584
|
key: fieldKey.describe(
|
|
3478
|
-
"machine field key
|
|
3585
|
+
"machine field key, unique among its SIBLINGS (the top level, or one group's children): two siblings with one key are refused, the same key in two groups ('hero.title', 'faq.title') is fine. Keys derived from an imported site may contain hyphens ('hero-h1-welcome'): address them exactly as returned."
|
|
3479
3586
|
),
|
|
3480
3587
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
3481
3588
|
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE} ${LINK_VALUE_NOTE}`),
|
|
@@ -3532,10 +3639,20 @@ var fieldShape = {
|
|
|
3532
3639
|
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3533
3640
|
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3534
3641
|
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3535
|
-
openMode: z.enum(
|
|
3536
|
-
localized: z.boolean().optional()
|
|
3642
|
+
openMode: z.enum(OPEN_MODES, { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode),
|
|
3643
|
+
localized: z.boolean().optional(),
|
|
3644
|
+
ai: z.boolean().optional().describe(PARAM_TEXT.fieldAi),
|
|
3645
|
+
// Server-owned provenance (types DefSources): declared only so a client that sends it reads why.
|
|
3646
|
+
sources: z.never({ error: SOURCES_SERVER_OWNED }).optional()
|
|
3537
3647
|
};
|
|
3538
|
-
var fieldObject = z.
|
|
3648
|
+
var fieldObject = z.strictObject(fieldShape);
|
|
3649
|
+
var modelV2Arg = z.strictObject({
|
|
3650
|
+
shape: z.enum(V2_SHAPES).optional(),
|
|
3651
|
+
placement: z.enum(V2_PLACEMENTS).optional(),
|
|
3652
|
+
category: z.enum(KIT_CATEGORIES).optional(),
|
|
3653
|
+
variants: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), description: z.string().optional() })).optional()
|
|
3654
|
+
}).nullable().optional().describe(PARAM_TEXT.modelV2);
|
|
3655
|
+
var dockUiName = z.string().regex(DOCK_UI_NAME).describe("[a-z0-9-], \u226464");
|
|
3539
3656
|
var urlPatternArg = z.string().nullable().optional().describe(
|
|
3540
3657
|
"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."
|
|
3541
3658
|
);
|
|
@@ -3590,31 +3707,29 @@ var seoPatchShape = {
|
|
|
3590
3707
|
searchConfig: z.object({ enabled: z.boolean(), placeholder: z.string().max(120), aiEnabled: z.boolean().optional() }).optional().describe(SEO_KEY_TEXT.searchConfig)
|
|
3591
3708
|
};
|
|
3592
3709
|
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_field with pageId / set_page_content), never a one-entry collection.";
|
|
3593
|
-
function
|
|
3710
|
+
function siblingDuplicateKeys(fields) {
|
|
3594
3711
|
const out = [];
|
|
3595
|
-
const
|
|
3712
|
+
const levelsOf = (f) => {
|
|
3596
3713
|
const zones = f.config?.zones;
|
|
3597
|
-
|
|
3598
|
-
if (zones?.repeatable?.fields) return zones.repeatable.fields;
|
|
3599
|
-
return f.fields ?? [];
|
|
3714
|
+
return [f.fields, zones?.nonRepeatable, zones?.repeatable?.fields].filter((l) => Array.isArray(l));
|
|
3600
3715
|
};
|
|
3601
|
-
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
const
|
|
3606
|
-
if (
|
|
3716
|
+
const walk = (level) => {
|
|
3717
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3718
|
+
for (const raw of level) {
|
|
3719
|
+
const f = raw;
|
|
3720
|
+
const key = typeof f?.key === "string" ? f.key.trim() : "";
|
|
3721
|
+
if (key) {
|
|
3722
|
+
if (seen.has(key) && !out.includes(key)) out.push(key);
|
|
3723
|
+
seen.add(key);
|
|
3724
|
+
}
|
|
3725
|
+
if (f && typeof f === "object") for (const child of levelsOf(f)) walk(child);
|
|
3607
3726
|
}
|
|
3608
|
-
}
|
|
3727
|
+
};
|
|
3728
|
+
walk(Array.isArray(fields) ? fields : []);
|
|
3609
3729
|
return out;
|
|
3610
3730
|
}
|
|
3611
|
-
function duplicateKeys(keys) {
|
|
3612
|
-
const seen = /* @__PURE__ */ new Map();
|
|
3613
|
-
for (const k of keys) seen.set(k, (seen.get(k) ?? 0) + 1);
|
|
3614
|
-
return [...new Set(keys)].filter((k) => (seen.get(k) ?? 0) > 1);
|
|
3615
|
-
}
|
|
3616
3731
|
function duplicateKeyFailure(dupes) {
|
|
3617
|
-
return `Duplicate API ID${dupes.length === 1 ? "" : "s"}: ${dupes.map((k) => `'${k}'`).join(", ")}.
|
|
3732
|
+
return `Duplicate API ID${dupes.length === 1 ? "" : "s"}: ${dupes.map((k) => `'${k}'`).join(", ")}. Two fields at the same level (the top level, or one group's children) share it, and a value is stored at its key's path, so one would overwrite the other. Rename one (e.g. 'title' and 'subtitle'); the same key in DIFFERENT groups is fine ('hero.title', 'pricing.title').`;
|
|
3618
3733
|
}
|
|
3619
3734
|
function toFields(fs) {
|
|
3620
3735
|
return (fs ?? []).map(toField);
|
|
@@ -3689,7 +3804,8 @@ function toField(f) {
|
|
|
3689
3804
|
...f.helpMore !== void 0 ? { helpMore: f.helpMore } : {},
|
|
3690
3805
|
...f.advanced !== void 0 ? { advanced: f.advanced } : {},
|
|
3691
3806
|
...f.recommendedMax !== void 0 ? { recommendedMax: f.recommendedMax } : {},
|
|
3692
|
-
...f.openMode !== void 0 ? { openMode: f.openMode } : {}
|
|
3807
|
+
...f.openMode !== void 0 ? { openMode: f.openMode } : {},
|
|
3808
|
+
...f.ai !== void 0 ? { ai: f.ai } : {}
|
|
3693
3809
|
};
|
|
3694
3810
|
if (f.type === "object") {
|
|
3695
3811
|
return { ...base, type: "object", fields: toFields(f.fields) };
|
|
@@ -4700,6 +4816,72 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4700
4816
|
z.object({ pageId: z.string().min(1), blockId: z.string().min(1).describe("a top-level section id (get_page)"), name: z.string().min(1).describe("80 characters at most; a longer one is refused as SAVED_BLOCK_NAME_TOO_LONG") }).shape,
|
|
4701
4817
|
async (c, a) => ok("Saved the section as a reusable block.", await data(c, "POST", `/management/saved-blocks`, { pageId: a.pageId, blockId: a.blockId, name: a.name }))
|
|
4702
4818
|
),
|
|
4819
|
+
// ── CMS-stored custom dock UI (write → approve → validate → publish) ──
|
|
4820
|
+
def(
|
|
4821
|
+
"write_dock_ui",
|
|
4822
|
+
"Write custom dock UI",
|
|
4823
|
+
NEW_TOOL_TEXT.write_dock_ui,
|
|
4824
|
+
z.object({
|
|
4825
|
+
name: dockUiName,
|
|
4826
|
+
scope: z.enum(["field", "section", "page", "layout"]),
|
|
4827
|
+
kind: z.enum(["input", "panel"]),
|
|
4828
|
+
target: z.record(z.string(), z.unknown()).describe(PARAM_TEXT.dockUiTarget),
|
|
4829
|
+
source: z.string().min(1).describe(".tsx, \u2264256 KiB")
|
|
4830
|
+
}).shape,
|
|
4831
|
+
async (c, a) => ok("Saved the dock UI version as a draft.", await data(c, "POST", `/management/dock-ui`, { name: a.name, scope: a.scope, kind: a.kind, target: a.target, source: a.source }))
|
|
4832
|
+
),
|
|
4833
|
+
def(
|
|
4834
|
+
"approve_dock_ui",
|
|
4835
|
+
"Approve custom dock UI",
|
|
4836
|
+
NEW_TOOL_TEXT.approve_dock_ui,
|
|
4837
|
+
z.object({ name: dockUiName, version: z.number().int().min(1), userApproved: z.boolean().optional().describe(PARAM_TEXT.dockUiUserApproved) }).shape,
|
|
4838
|
+
async (c, a) => ok("Approved the dock UI version.", (await c.fetchJSON(c.url(`/management/dock-ui/${encodeURIComponent(s(a.name))}/approve`), {
|
|
4839
|
+
method: "POST",
|
|
4840
|
+
body: JSON.stringify({ version: a.version }),
|
|
4841
|
+
...withHeaders(a.userApproved === true ? { [INPUT_CONFIRMED_HEADER]: "true" } : {})
|
|
4842
|
+
})).data)
|
|
4843
|
+
),
|
|
4844
|
+
def(
|
|
4845
|
+
"validate_dock_ui",
|
|
4846
|
+
"Validate custom dock UI",
|
|
4847
|
+
NEW_TOOL_TEXT.validate_dock_ui,
|
|
4848
|
+
z.object({ name: dockUiName, version: z.number().int().min(1).optional() }).shape,
|
|
4849
|
+
async (c, a) => ok("Dock UI build receipt.", await data(c, "POST", `/management/dock-ui/${encodeURIComponent(s(a.name))}/validate`, { version: a.version }))
|
|
4850
|
+
),
|
|
4851
|
+
def(
|
|
4852
|
+
"publish_dock_ui",
|
|
4853
|
+
"Publish custom dock UI",
|
|
4854
|
+
NEW_TOOL_TEXT.publish_dock_ui,
|
|
4855
|
+
z.object({ name: dockUiName, version: z.number().int().min(1).optional() }).shape,
|
|
4856
|
+
async (c, a) => ok("Published the dock UI version.", await data(c, "POST", `/management/dock-ui/${encodeURIComponent(s(a.name))}/publish`, { version: a.version }))
|
|
4857
|
+
),
|
|
4858
|
+
def(
|
|
4859
|
+
"list_dock_ui",
|
|
4860
|
+
"List custom dock UI",
|
|
4861
|
+
NEW_TOOL_TEXT.list_dock_ui,
|
|
4862
|
+
z.object({ scope: z.enum(["field", "section", "page", "layout"]).optional(), status: z.enum(["draft", "approved", "published"]).optional() }).shape,
|
|
4863
|
+
async (c, a) => ok("Dock UI sources.", await data(c, "GET", `/management/dock-ui${q({ scope: a.scope, status: a.status })}`))
|
|
4864
|
+
),
|
|
4865
|
+
// ── A V2 TypeSchema in one write (kit A9/C6): a model change set, a model create, or a component's props ──
|
|
4866
|
+
def(
|
|
4867
|
+
"write_schema",
|
|
4868
|
+
"Write a V2 schema",
|
|
4869
|
+
NEW_TOOL_TEXT.write_schema,
|
|
4870
|
+
z.object({
|
|
4871
|
+
model: z.string().min(1).nullable().describe(PARAM_TEXT.writeSchemaModel),
|
|
4872
|
+
lane: z.enum(["fields", "props"]),
|
|
4873
|
+
schema: z.record(z.string(), z.unknown()).describe("{ name, title, kind, fields, \u2026 }"),
|
|
4874
|
+
acceptLoss: z.array(z.string().min(1)).optional().describe(PARAM_TEXT.writeSchemaAcceptLoss),
|
|
4875
|
+
dryRun: z.boolean().optional(),
|
|
4876
|
+
ifMatch: z.number().int().min(0).optional().describe(PARAM_TEXT.writeSchemaIfMatch),
|
|
4877
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
4878
|
+
}).shape,
|
|
4879
|
+
async (c, a) => ok(a.dryRun ? "Dry run of the schema write: nothing was written." : "Wrote the schema.", (await c.fetchJSON(c.url(`/management/schema`), {
|
|
4880
|
+
method: "POST",
|
|
4881
|
+
body: JSON.stringify({ model: a.model, lane: a.lane, schema: a.schema, acceptLoss: a.acceptLoss, dryRun: a.dryRun }),
|
|
4882
|
+
...withHeaders({ ...a.ifMatch !== void 0 ? { "If-Match": `W/"${a.ifMatch}"` } : {}, ...confirmedHeaders(a.confirmed) })
|
|
4883
|
+
})).data)
|
|
4884
|
+
),
|
|
4703
4885
|
def(
|
|
4704
4886
|
"get_seo",
|
|
4705
4887
|
"Get site SEO",
|
|
@@ -4957,6 +5139,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4957
5139
|
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). An array with both zones needs 'KEY.zones.nonRepeatable' or 'KEY.zones.repeatable'; its bare key is a 400"),
|
|
4958
5140
|
fieldLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe('path or "<family>#section" \u2192 label'),
|
|
4959
5141
|
fieldHelpText: z.record(z.string(), z.string().max(500).nullable()).optional().describe("path \u2192 help text, null clears it"),
|
|
5142
|
+
v2: modelV2Arg,
|
|
4960
5143
|
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
4961
5144
|
ifMatch: z.number().int().min(0).optional().describe(MODEL_IF_MATCH_NOTE),
|
|
4962
5145
|
removeFields: z.array(z.string().min(1)).min(1).max(100).optional().describe(FIELD_REMOVAL_NOTE),
|
|
@@ -5187,8 +5370,8 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5187
5370
|
helpText: z.string().nullable().optional().describe("null clears it"),
|
|
5188
5371
|
required: z.boolean().optional(),
|
|
5189
5372
|
config: z.record(z.string(), z.unknown()).optional().describe("per-type validation/config keys, merged into the stored config; null deletes a key"),
|
|
5190
|
-
showIf:
|
|
5191
|
-
requiredIf:
|
|
5373
|
+
showIf: fieldCondition.nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5374
|
+
requiredIf: fieldCondition.nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5192
5375
|
compare: z.array(z.record(z.string(), z.unknown())).nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5193
5376
|
ui: z.record(z.string(), z.unknown()).nullable().optional().describe("same vocabulary as on add_field; merged per key, a null key or ui:null clears"),
|
|
5194
5377
|
placement: z.enum(["cover", "title", "excerpt", "body", "inline", "panel"]).nullable().optional(),
|
|
@@ -5197,10 +5380,12 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5197
5380
|
helpMore: z.string().max(2e3).nullable().optional().describe(PARAM_TEXT.helpMore),
|
|
5198
5381
|
advanced: z.boolean().nullable().optional().describe(PARAM_TEXT.advanced),
|
|
5199
5382
|
recommendedMax: z.number().int().positive().nullable().optional().describe(PARAM_TEXT.recommendedMax),
|
|
5200
|
-
openMode: z.enum(
|
|
5383
|
+
openMode: z.enum(OPEN_MODES, { error: OPEN_MODE_INVALID }).nullable().optional().describe(PARAM_TEXT.openMode),
|
|
5201
5384
|
localized: z.boolean().nullable().optional(),
|
|
5385
|
+
ai: z.boolean().nullable().optional().describe(PARAM_TEXT.fieldAi),
|
|
5386
|
+
sources: z.never({ error: SOURCES_SERVER_OWNED }).optional(),
|
|
5202
5387
|
type: fieldType.optional().describe("a new type; needs allowRetype"),
|
|
5203
|
-
key: fieldKey.optional().describe("a new key (
|
|
5388
|
+
key: fieldKey.optional().describe("a new key (unique among its siblings)"),
|
|
5204
5389
|
allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
|
|
5205
5390
|
allowFieldRemoval: z.boolean().optional().describe("with a rename: discard the old key's stored values"),
|
|
5206
5391
|
mergeHref: z.string().min(1).optional().describe(PARAM_TEXT.mergeHref),
|
|
@@ -5974,12 +6159,12 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5974
6159
|
name: "create_page",
|
|
5975
6160
|
config: {
|
|
5976
6161
|
title: "Create a page",
|
|
5977
|
-
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once.
|
|
6162
|
+
description: "Create a page with its own typed schema. Supports pageType 'singleton' (exactly one entry \u2014 Home, About, Contact) and 'dynamic' (many entries sharing the schema \u2014 Blog posts, Products). Project-scoped from the key. Additive \u2014 does not delete or overwrite existing pages. FIRST read the page's real markup and DECOMPOSE it into a destructured tree: each visual section becomes a nested field \u2014 a fixed grouped block \u2192 type 'group', a repeating list of items (cards, testimonials, features, FAQs) \u2192 type 'repeater' \u2014 each carrying its own child `fields`. Do NOT flatten sections into many flat top-level fields. Build the full nested tree, then call this once. Field keys are unique among siblings: 'title' may repeat in two sections, not twice in one. Prose fields (headings, body copy, descriptions) are created as RICH TEXT by default; pass richText:false for values that must stay bare strings \u2014 hrefs, slugs, ids, emails. That tree is the page's SCHEMA \u2014 what it holds. " + SECTION_DOCTRINE,
|
|
5978
6163
|
inputSchema: createPageInput.shape
|
|
5979
6164
|
},
|
|
5980
6165
|
handler: guard(
|
|
5981
6166
|
async (args) => withClient(async (client) => {
|
|
5982
|
-
const dupes =
|
|
6167
|
+
const dupes = siblingDuplicateKeys(args.fields);
|
|
5983
6168
|
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
5984
6169
|
const page = await client.createPage({
|
|
5985
6170
|
title: args.title,
|
|
@@ -6015,7 +6200,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
6015
6200
|
},
|
|
6016
6201
|
handler: guard(
|
|
6017
6202
|
async (args) => withClient(async (client) => {
|
|
6018
|
-
const modelDupes =
|
|
6203
|
+
const modelDupes = siblingDuplicateKeys(args.fields);
|
|
6019
6204
|
if (modelDupes.length > 0) return fail(duplicateKeyFailure(modelDupes));
|
|
6020
6205
|
const input = {
|
|
6021
6206
|
name: args.name,
|
|
@@ -6079,7 +6264,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
6079
6264
|
}
|
|
6080
6265
|
const modelId = args.modelId;
|
|
6081
6266
|
const model = await client.getModel(modelId);
|
|
6082
|
-
const dupes =
|
|
6267
|
+
const dupes = (model.fields ?? []).some((f) => f?.key === args.key) ? [args.key] : [];
|
|
6083
6268
|
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
6084
6269
|
const existingFields = model.fields;
|
|
6085
6270
|
let at = existingFields.length;
|
|
@@ -7885,7 +8070,7 @@ delivered value \u2014 they change editor chrome only.
|
|
|
7885
8070
|
|
|
7886
8071
|
- **\`helpText\`** \u2014 the exception: most fields get none. Write it only when the author needs it to
|
|
7887
8072
|
type the right value: a format or unit ("Price in cents."), a placement the canvas cannot show, a
|
|
7888
|
-
real constraint, a fallback. One plain sentence
|
|
8073
|
+
real constraint, a fallback. One plain sentence of at most 160 characters, no semicolons, no dashes, no values copied from the page. Never restate the label ("The title field"), never coach ("Keep it short").
|
|
7889
8074
|
- **\`showIf\`** \u2014 hide a field until another has a value:
|
|
7890
8075
|
\`{match:'all',rules:[{field:'link_type',op:'eq',value:'external'}]}\`. Rules name fields by
|
|
7891
8076
|
BARE KEY, and the key namespace is FLAT across the model (\xA7 the duplicate-key rule), so the
|
|
@@ -8039,7 +8224,7 @@ An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`op
|
|
|
8039
8224
|
schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
|
|
8040
8225
|
\`backingModel\`.
|
|
8041
8226
|
- A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propTab\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
|
|
8042
|
-
Name props from the code (\`plan.cents\` \u2192 Cents); help text only where a format, a hidden placement, a constraint or a fallback needs it ("Whole number, no currency symbol."), one plain sentence
|
|
8227
|
+
Name props from the code (\`plan.cents\` \u2192 Cents); help text only where a format, a hidden placement, a constraint or a fallback needs it ("Whole number, no currency symbol."), one plain sentence of at most 160 characters with no semicolons or dashes, never repeating
|
|
8043
8228
|
the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
|
|
8044
8229
|
They write the DRAFT props; no publish is needed for the editor to show them.
|
|
8045
8230
|
- Always send \`ifMatch\`, the \`optimisticVersion\` you read.
|