@bettercms-ai/mcp 0.84.5 → 0.86.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 +277 -60
- 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)]));
|
|
@@ -2905,6 +2988,10 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
|
|
|
2905
2988
|
// Reporting, history and workflow chrome.
|
|
2906
2989
|
"get_analytics_overview",
|
|
2907
2990
|
"get_analytics_top_pages",
|
|
2991
|
+
"get_analytics_top_referrers",
|
|
2992
|
+
"get_analytics_breakdown",
|
|
2993
|
+
"get_conversions",
|
|
2994
|
+
"get_ai_traffic",
|
|
2908
2995
|
"list_activity",
|
|
2909
2996
|
"get_workflow_board",
|
|
2910
2997
|
"move_stage",
|
|
@@ -2959,6 +3046,14 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
|
|
|
2959
3046
|
"start_integration_signin",
|
|
2960
3047
|
"save_integration_template",
|
|
2961
3048
|
"install_integration_template",
|
|
3049
|
+
// CMS-stored custom dock UI: reached for once the stock controls and ui keys fall short.
|
|
3050
|
+
"write_dock_ui",
|
|
3051
|
+
"approve_dock_ui",
|
|
3052
|
+
"validate_dock_ui",
|
|
3053
|
+
"publish_dock_ui",
|
|
3054
|
+
"list_dock_ui",
|
|
3055
|
+
// A whole V2 schema at once: reached for when the field-by-field tools would take dozens of calls.
|
|
3056
|
+
"write_schema",
|
|
2962
3057
|
// The ABM campaign lane — also hidden from the default listing (ABM_TOOLS below).
|
|
2963
3058
|
"research_account",
|
|
2964
3059
|
"build_abm_page",
|
|
@@ -3010,7 +3105,7 @@ var toolMeta = (name) => ALWAYS_LOADED.has(name) ? { "anthropic/alwaysLoad": tru
|
|
|
3010
3105
|
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
3106
|
|
|
3012
3107
|
// 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?}].";
|
|
3108
|
+
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
3109
|
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
3110
|
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
3111
|
var WINDOW_NOTE = "{from?, until?}, ISO 8601 with offset: shown only between the two instants; null clears (skill publishing).";
|
|
@@ -3056,7 +3151,15 @@ var NEW_TOOL_TEXT = {
|
|
|
3056
3151
|
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
3152
|
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
3153
|
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."
|
|
3154
|
+
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.",
|
|
3155
|
+
// CMS-stored custom dock UI: write → approve (the user's yes) → validate → publish. Recipe: skill dock-input.
|
|
3156
|
+
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",
|
|
3157
|
+
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",
|
|
3158
|
+
validate_dock_ui: "Check one version (default newest) without publishing: allowed imports, size caps, a name the repo also has. Answers the build receipt",
|
|
3159
|
+
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",
|
|
3160
|
+
list_dock_ui: "The project's dock UI sources, with usage against the limits",
|
|
3161
|
+
// A V2 TypeSchema into a model (lane fields, through a change set) or a component's props (lane props).
|
|
3162
|
+
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
3163
|
};
|
|
3061
3164
|
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
3165
|
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 +3172,10 @@ var PARAM_TEXT = {
|
|
|
3069
3172
|
readOnly: "'developer' = the value and this lock change only with schema access (needs lockReason); true = an editor marker",
|
|
3070
3173
|
lockReason: "which system owns the value (\u2264 200 chars)",
|
|
3071
3174
|
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
|
|
3175
|
+
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
3176
|
helpMore: "longer help behind the \u24D8 next to the label, when one helpText line cannot carry it",
|
|
3177
|
+
fieldAi: "true = the editor offers its AI assist on this field",
|
|
3178
|
+
modelV2: "the V2 block {shape?, placement?, category?, variants?: {slug: {icon?, description?}}}; null clears it",
|
|
3074
3179
|
advanced: "true = a rare or developer setting: it collects in a closed 'Advanced' section",
|
|
3075
3180
|
recommendedMax: "a SOFT limit (characters, items or value): the editor warns past it, never refuses",
|
|
3076
3181
|
openMode: "object/array/group/repeater: how the value (or each row) opens: 'inline', 'drill', 'popover' or 'dialog'",
|
|
@@ -3106,7 +3211,12 @@ var PARAM_TEXT = {
|
|
|
3106
3211
|
focalX: "focal point, 0-1 across from the left edge; null resets to the centre",
|
|
3107
3212
|
focalY: "focal point, 0-1 down from the top edge; null resets to the centre",
|
|
3108
3213
|
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"
|
|
3214
|
+
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",
|
|
3215
|
+
dockUiUserApproved: "true only after the user said yes to this version",
|
|
3216
|
+
dockUiTarget: "per scope: field {modelId|pageId|componentId, path}, section {componentId}, page {pageId}, layout {}",
|
|
3217
|
+
writeSchemaModel: "model or component id/slug; null creates a model",
|
|
3218
|
+
writeSchemaAcceptLoss: "loss keys or paths you accept",
|
|
3219
|
+
writeSchemaIfMatch: "the dry run's `version`; needed to write over a stored row"
|
|
3110
3220
|
};
|
|
3111
3221
|
var INTEGRATION_PARAM_TEXT = {
|
|
3112
3222
|
integration: "the integration's id or slug (from list_integrations)",
|
|
@@ -3136,7 +3246,7 @@ var INTEGRATION_PARAM_TEXT = {
|
|
|
3136
3246
|
var INPUT_CONFIRMED_HEADER = "x-bcms-confirmed-input";
|
|
3137
3247
|
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
3248
|
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
|
|
3249
|
+
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
3250
|
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
3251
|
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
3252
|
var SEO_KEY_TEXT = {
|
|
@@ -3402,13 +3512,14 @@ var OPEN_MODE_INVALID = "openMode_invalid: openMode must be one of 'inline', 'dr
|
|
|
3402
3512
|
var slug = z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, numbers, and hyphens only");
|
|
3403
3513
|
var fieldKey = z.string().regex(/^[a-zA-Z0-9_-]+$/, "letters, numbers, underscores, and hyphens only");
|
|
3404
3514
|
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.
|
|
3515
|
+
var fieldCondition = z.strictObject({
|
|
3406
3516
|
match: z.enum(["all", "any"]),
|
|
3407
3517
|
rules: z.array(
|
|
3408
|
-
z.
|
|
3518
|
+
z.strictObject({
|
|
3409
3519
|
field: z.string().describe("the BARE KEY of another field on this same model"),
|
|
3410
3520
|
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`)
|
|
3521
|
+
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`),
|
|
3522
|
+
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
3523
|
})
|
|
3413
3524
|
)
|
|
3414
3525
|
});
|
|
@@ -3444,7 +3555,7 @@ var uiObject = z.strictObject({
|
|
|
3444
3555
|
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
3556
|
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
3557
|
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."),
|
|
3558
|
+
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
3559
|
input: z.object({
|
|
3449
3560
|
name: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/),
|
|
3450
3561
|
writes: z.array(z.string().min(1).max(64)).max(16).optional()
|
|
@@ -3475,7 +3586,7 @@ var propUiObject = uiObject.extend({
|
|
|
3475
3586
|
var choiceOption = z.union([z.string(), z.object({ label: z.string(), value: z.string(), icon: z.string().optional(), description: z.string().optional() })]);
|
|
3476
3587
|
var fieldShape = {
|
|
3477
3588
|
key: fieldKey.describe(
|
|
3478
|
-
"machine field key
|
|
3589
|
+
"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
3590
|
),
|
|
3480
3591
|
label: z.string().min(1).describe("human label shown in the editor"),
|
|
3481
3592
|
type: fieldType.describe(`field type. ${BUTTON_FIELD_NOTE} ${LINK_VALUE_NOTE}`),
|
|
@@ -3532,10 +3643,20 @@ var fieldShape = {
|
|
|
3532
3643
|
helpMore: z.string().max(2e3).optional().describe(PARAM_TEXT.helpMore),
|
|
3533
3644
|
advanced: z.boolean().optional().describe(PARAM_TEXT.advanced),
|
|
3534
3645
|
recommendedMax: z.number().int().positive().optional().describe(PARAM_TEXT.recommendedMax),
|
|
3535
|
-
openMode: z.enum(
|
|
3536
|
-
localized: z.boolean().optional()
|
|
3646
|
+
openMode: z.enum(OPEN_MODES, { error: OPEN_MODE_INVALID }).optional().describe(PARAM_TEXT.openMode),
|
|
3647
|
+
localized: z.boolean().optional(),
|
|
3648
|
+
ai: z.boolean().optional().describe(PARAM_TEXT.fieldAi),
|
|
3649
|
+
// Server-owned provenance (types DefSources): declared only so a client that sends it reads why.
|
|
3650
|
+
sources: z.never({ error: SOURCES_SERVER_OWNED }).optional()
|
|
3537
3651
|
};
|
|
3538
|
-
var fieldObject = z.
|
|
3652
|
+
var fieldObject = z.strictObject(fieldShape);
|
|
3653
|
+
var modelV2Arg = z.strictObject({
|
|
3654
|
+
shape: z.enum(V2_SHAPES).optional(),
|
|
3655
|
+
placement: z.enum(V2_PLACEMENTS).optional(),
|
|
3656
|
+
category: z.enum(KIT_CATEGORIES).optional(),
|
|
3657
|
+
variants: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), description: z.string().optional() })).optional()
|
|
3658
|
+
}).nullable().optional().describe(PARAM_TEXT.modelV2);
|
|
3659
|
+
var dockUiName = z.string().regex(DOCK_UI_NAME).describe("[a-z0-9-], \u226464");
|
|
3539
3660
|
var urlPatternArg = z.string().nullable().optional().describe(
|
|
3540
3661
|
"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
3662
|
);
|
|
@@ -3590,31 +3711,29 @@ var seoPatchShape = {
|
|
|
3590
3711
|
searchConfig: z.object({ enabled: z.boolean(), placeholder: z.string().max(120), aiEnabled: z.boolean().optional() }).optional().describe(SEO_KEY_TEXT.searchConfig)
|
|
3591
3712
|
};
|
|
3592
3713
|
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
|
|
3714
|
+
function siblingDuplicateKeys(fields) {
|
|
3594
3715
|
const out = [];
|
|
3595
|
-
const
|
|
3716
|
+
const levelsOf = (f) => {
|
|
3596
3717
|
const zones = f.config?.zones;
|
|
3597
|
-
|
|
3598
|
-
if (zones?.repeatable?.fields) return zones.repeatable.fields;
|
|
3599
|
-
return f.fields ?? [];
|
|
3718
|
+
return [f.fields, zones?.nonRepeatable, zones?.repeatable?.fields].filter((l) => Array.isArray(l));
|
|
3600
3719
|
};
|
|
3601
|
-
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
const
|
|
3606
|
-
if (
|
|
3720
|
+
const walk = (level) => {
|
|
3721
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3722
|
+
for (const raw of level) {
|
|
3723
|
+
const f = raw;
|
|
3724
|
+
const key = typeof f?.key === "string" ? f.key.trim() : "";
|
|
3725
|
+
if (key) {
|
|
3726
|
+
if (seen.has(key) && !out.includes(key)) out.push(key);
|
|
3727
|
+
seen.add(key);
|
|
3728
|
+
}
|
|
3729
|
+
if (f && typeof f === "object") for (const child of levelsOf(f)) walk(child);
|
|
3607
3730
|
}
|
|
3608
|
-
}
|
|
3731
|
+
};
|
|
3732
|
+
walk(Array.isArray(fields) ? fields : []);
|
|
3609
3733
|
return out;
|
|
3610
3734
|
}
|
|
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
3735
|
function duplicateKeyFailure(dupes) {
|
|
3617
|
-
return `Duplicate API ID${dupes.length === 1 ? "" : "s"}: ${dupes.map((k) => `'${k}'`).join(", ")}.
|
|
3736
|
+
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
3737
|
}
|
|
3619
3738
|
function toFields(fs) {
|
|
3620
3739
|
return (fs ?? []).map(toField);
|
|
@@ -3689,7 +3808,8 @@ function toField(f) {
|
|
|
3689
3808
|
...f.helpMore !== void 0 ? { helpMore: f.helpMore } : {},
|
|
3690
3809
|
...f.advanced !== void 0 ? { advanced: f.advanced } : {},
|
|
3691
3810
|
...f.recommendedMax !== void 0 ? { recommendedMax: f.recommendedMax } : {},
|
|
3692
|
-
...f.openMode !== void 0 ? { openMode: f.openMode } : {}
|
|
3811
|
+
...f.openMode !== void 0 ? { openMode: f.openMode } : {},
|
|
3812
|
+
...f.ai !== void 0 ? { ai: f.ai } : {}
|
|
3693
3813
|
};
|
|
3694
3814
|
if (f.type === "object") {
|
|
3695
3815
|
return { ...base, type: "object", fields: toFields(f.fields) };
|
|
@@ -4700,6 +4820,72 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4700
4820
|
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
4821
|
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
4822
|
),
|
|
4823
|
+
// ── CMS-stored custom dock UI (write → approve → validate → publish) ──
|
|
4824
|
+
def(
|
|
4825
|
+
"write_dock_ui",
|
|
4826
|
+
"Write custom dock UI",
|
|
4827
|
+
NEW_TOOL_TEXT.write_dock_ui,
|
|
4828
|
+
z.object({
|
|
4829
|
+
name: dockUiName,
|
|
4830
|
+
scope: z.enum(["field", "section", "page", "layout"]),
|
|
4831
|
+
kind: z.enum(["input", "panel"]),
|
|
4832
|
+
target: z.record(z.string(), z.unknown()).describe(PARAM_TEXT.dockUiTarget),
|
|
4833
|
+
source: z.string().min(1).describe(".tsx, \u2264256 KiB")
|
|
4834
|
+
}).shape,
|
|
4835
|
+
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 }))
|
|
4836
|
+
),
|
|
4837
|
+
def(
|
|
4838
|
+
"approve_dock_ui",
|
|
4839
|
+
"Approve custom dock UI",
|
|
4840
|
+
NEW_TOOL_TEXT.approve_dock_ui,
|
|
4841
|
+
z.object({ name: dockUiName, version: z.number().int().min(1), userApproved: z.boolean().optional().describe(PARAM_TEXT.dockUiUserApproved) }).shape,
|
|
4842
|
+
async (c, a) => ok("Approved the dock UI version.", (await c.fetchJSON(c.url(`/management/dock-ui/${encodeURIComponent(s(a.name))}/approve`), {
|
|
4843
|
+
method: "POST",
|
|
4844
|
+
body: JSON.stringify({ version: a.version }),
|
|
4845
|
+
...withHeaders(a.userApproved === true ? { [INPUT_CONFIRMED_HEADER]: "true" } : {})
|
|
4846
|
+
})).data)
|
|
4847
|
+
),
|
|
4848
|
+
def(
|
|
4849
|
+
"validate_dock_ui",
|
|
4850
|
+
"Validate custom dock UI",
|
|
4851
|
+
NEW_TOOL_TEXT.validate_dock_ui,
|
|
4852
|
+
z.object({ name: dockUiName, version: z.number().int().min(1).optional() }).shape,
|
|
4853
|
+
async (c, a) => ok("Dock UI build receipt.", await data(c, "POST", `/management/dock-ui/${encodeURIComponent(s(a.name))}/validate`, { version: a.version }))
|
|
4854
|
+
),
|
|
4855
|
+
def(
|
|
4856
|
+
"publish_dock_ui",
|
|
4857
|
+
"Publish custom dock UI",
|
|
4858
|
+
NEW_TOOL_TEXT.publish_dock_ui,
|
|
4859
|
+
z.object({ name: dockUiName, version: z.number().int().min(1).optional() }).shape,
|
|
4860
|
+
async (c, a) => ok("Published the dock UI version.", await data(c, "POST", `/management/dock-ui/${encodeURIComponent(s(a.name))}/publish`, { version: a.version }))
|
|
4861
|
+
),
|
|
4862
|
+
def(
|
|
4863
|
+
"list_dock_ui",
|
|
4864
|
+
"List custom dock UI",
|
|
4865
|
+
NEW_TOOL_TEXT.list_dock_ui,
|
|
4866
|
+
z.object({ scope: z.enum(["field", "section", "page", "layout"]).optional(), status: z.enum(["draft", "approved", "published"]).optional() }).shape,
|
|
4867
|
+
async (c, a) => ok("Dock UI sources.", await data(c, "GET", `/management/dock-ui${q({ scope: a.scope, status: a.status })}`))
|
|
4868
|
+
),
|
|
4869
|
+
// ── A V2 TypeSchema in one write (kit A9/C6): a model change set, a model create, or a component's props ──
|
|
4870
|
+
def(
|
|
4871
|
+
"write_schema",
|
|
4872
|
+
"Write a V2 schema",
|
|
4873
|
+
NEW_TOOL_TEXT.write_schema,
|
|
4874
|
+
z.object({
|
|
4875
|
+
model: z.string().min(1).nullable().describe(PARAM_TEXT.writeSchemaModel),
|
|
4876
|
+
lane: z.enum(["fields", "props"]),
|
|
4877
|
+
schema: z.record(z.string(), z.unknown()).describe("{ name, title, kind, fields, \u2026 }"),
|
|
4878
|
+
acceptLoss: z.array(z.string().min(1)).optional().describe(PARAM_TEXT.writeSchemaAcceptLoss),
|
|
4879
|
+
dryRun: z.boolean().optional(),
|
|
4880
|
+
ifMatch: z.number().int().min(0).optional().describe(PARAM_TEXT.writeSchemaIfMatch),
|
|
4881
|
+
confirmed: z.boolean().optional().describe(PARAM_TEXT.confirmedInput)
|
|
4882
|
+
}).shape,
|
|
4883
|
+
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`), {
|
|
4884
|
+
method: "POST",
|
|
4885
|
+
body: JSON.stringify({ model: a.model, lane: a.lane, schema: a.schema, acceptLoss: a.acceptLoss, dryRun: a.dryRun }),
|
|
4886
|
+
...withHeaders({ ...a.ifMatch !== void 0 ? { "If-Match": `W/"${a.ifMatch}"` } : {}, ...confirmedHeaders(a.confirmed) })
|
|
4887
|
+
})).data)
|
|
4888
|
+
),
|
|
4703
4889
|
def(
|
|
4704
4890
|
"get_seo",
|
|
4705
4891
|
"Get site SEO",
|
|
@@ -4957,6 +5143,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4957
5143
|
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
5144
|
fieldLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe('path or "<family>#section" \u2192 label'),
|
|
4959
5145
|
fieldHelpText: z.record(z.string(), z.string().max(500).nullable()).optional().describe("path \u2192 help text, null clears it"),
|
|
5146
|
+
v2: modelV2Arg,
|
|
4960
5147
|
override: z.boolean().optional().describe(OVERRIDE_NOTE),
|
|
4961
5148
|
ifMatch: z.number().int().min(0).optional().describe(MODEL_IF_MATCH_NOTE),
|
|
4962
5149
|
removeFields: z.array(z.string().min(1)).min(1).max(100).optional().describe(FIELD_REMOVAL_NOTE),
|
|
@@ -5187,8 +5374,8 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5187
5374
|
helpText: z.string().nullable().optional().describe("null clears it"),
|
|
5188
5375
|
required: z.boolean().optional(),
|
|
5189
5376
|
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:
|
|
5377
|
+
showIf: fieldCondition.nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5378
|
+
requiredIf: fieldCondition.nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5192
5379
|
compare: z.array(z.record(z.string(), z.unknown())).nullable().optional().describe("same shape as on add_field; null clears it"),
|
|
5193
5380
|
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
5381
|
placement: z.enum(["cover", "title", "excerpt", "body", "inline", "panel"]).nullable().optional(),
|
|
@@ -5197,10 +5384,12 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5197
5384
|
helpMore: z.string().max(2e3).nullable().optional().describe(PARAM_TEXT.helpMore),
|
|
5198
5385
|
advanced: z.boolean().nullable().optional().describe(PARAM_TEXT.advanced),
|
|
5199
5386
|
recommendedMax: z.number().int().positive().nullable().optional().describe(PARAM_TEXT.recommendedMax),
|
|
5200
|
-
openMode: z.enum(
|
|
5387
|
+
openMode: z.enum(OPEN_MODES, { error: OPEN_MODE_INVALID }).nullable().optional().describe(PARAM_TEXT.openMode),
|
|
5201
5388
|
localized: z.boolean().nullable().optional(),
|
|
5389
|
+
ai: z.boolean().nullable().optional().describe(PARAM_TEXT.fieldAi),
|
|
5390
|
+
sources: z.never({ error: SOURCES_SERVER_OWNED }).optional(),
|
|
5202
5391
|
type: fieldType.optional().describe("a new type; needs allowRetype"),
|
|
5203
|
-
key: fieldKey.optional().describe("a new key (
|
|
5392
|
+
key: fieldKey.optional().describe("a new key (unique among its siblings)"),
|
|
5204
5393
|
allowRetype: z.boolean().optional().describe("confirm a type change, once the user has agreed"),
|
|
5205
5394
|
allowFieldRemoval: z.boolean().optional().describe("with a rename: discard the old key's stored values"),
|
|
5206
5395
|
mergeHref: z.string().min(1).optional().describe(PARAM_TEXT.mergeHref),
|
|
@@ -5632,9 +5821,9 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5632
5821
|
def(
|
|
5633
5822
|
"get_analytics_overview",
|
|
5634
5823
|
"Get traffic overview",
|
|
5635
|
-
|
|
5636
|
-
z.object({ from: z.string().optional(), to: z.string().optional() }).shape,
|
|
5637
|
-
async (c, a) => ok("Analytics overview.", await data(c, "GET", `/management/insights/analytics/overview${q({ from: a.from, to: a.to })}`))
|
|
5824
|
+
'Traffic totals and the daily series for the connected project: views, unique visitors, bytes, sessions, bounces and responses by status class. Defaults to the last 30 days; pass `from`/`to` as YYYY-MM-DD, and `compare: "previous"` for the previous period\'s totals beside them.',
|
|
5825
|
+
z.object({ from: z.string().optional(), to: z.string().optional(), compare: z.enum(["previous"]).optional() }).shape,
|
|
5826
|
+
async (c, a) => ok("Analytics overview.", await data(c, "GET", `/management/insights/analytics/overview${q({ from: a.from, to: a.to, compare: a.compare })}`))
|
|
5638
5827
|
),
|
|
5639
5828
|
def(
|
|
5640
5829
|
"get_analytics_top_pages",
|
|
@@ -5643,6 +5832,34 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5643
5832
|
z.object({ from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
|
|
5644
5833
|
async (c, a) => ok("Top pages.", await data(c, "GET", `/management/insights/analytics/top-pages${q({ from: a.from, to: a.to, limit: a.limit })}`))
|
|
5645
5834
|
),
|
|
5835
|
+
def(
|
|
5836
|
+
"get_analytics_top_referrers",
|
|
5837
|
+
"Get top referrers",
|
|
5838
|
+
"Traffic sources: the referrer hosts sending visitors, by views. Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD) and `limit`.",
|
|
5839
|
+
z.object({ from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
|
|
5840
|
+
async (c, a) => ok("Top referrers.", await data(c, "GET", `/management/insights/analytics/top-referrers${q({ from: a.from, to: a.to, limit: a.limit })}`))
|
|
5841
|
+
),
|
|
5842
|
+
def(
|
|
5843
|
+
"get_analytics_breakdown",
|
|
5844
|
+
"Get traffic breakdown",
|
|
5845
|
+
"Views and visitors by one dimension: `country` (ISO code), `device` (desktop, mobile, tablet), `browser` or `crawler` (the bots that fetched the site). Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD) and `limit`.",
|
|
5846
|
+
z.object({ dim: z.enum(["country", "device", "browser", "crawler"]), from: z.string().optional(), to: z.string().optional(), limit: z.number().optional() }).shape,
|
|
5847
|
+
async (c, a) => ok("Traffic breakdown.", await data(c, "GET", `/management/insights/analytics/breakdown${q({ dim: a.dim, from: a.from, to: a.to, limit: a.limit })}`))
|
|
5848
|
+
),
|
|
5849
|
+
def(
|
|
5850
|
+
"get_conversions",
|
|
5851
|
+
"Get conversions",
|
|
5852
|
+
"Conversions: form submissions (the one goal a site measures) per form over the last `days` days (default 30, max 90), with last-7 vs previous-7, all-time spam, the pages each form is on, and the site's conversion rate (submissions per 100 page views). `degraded: true` means the submissions store didn't answer, so the counts are not measurements and the rate is null.",
|
|
5853
|
+
z.object({ days: z.number().int().min(1).max(90).optional() }).shape,
|
|
5854
|
+
async (c, a) => ok("Conversions.", await data(c, "GET", `/management/insights/analytics/conversions${q({ days: a.days })}`))
|
|
5855
|
+
),
|
|
5856
|
+
def(
|
|
5857
|
+
"get_ai_traffic",
|
|
5858
|
+
"Get AI traffic",
|
|
5859
|
+
"AI traffic: crawler visits for AI products (GPTBot, ClaudeBot\u2026, beside search crawlers) and visitors referred by answer engines (ChatGPT, Perplexity\u2026), with totals. Pro plan and above. Defaults to the last 30 days; pass `from`/`to` (YYYY-MM-DD).",
|
|
5860
|
+
z.object({ from: z.string().optional(), to: z.string().optional() }).shape,
|
|
5861
|
+
async (c, a) => ok("AI traffic.", await data(c, "GET", `/management/insights/analytics/ai-traffic${q({ from: a.from, to: a.to })}`))
|
|
5862
|
+
),
|
|
5646
5863
|
def(
|
|
5647
5864
|
"list_seo_issues",
|
|
5648
5865
|
"List SEO issues",
|
|
@@ -5974,12 +6191,12 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5974
6191
|
name: "create_page",
|
|
5975
6192
|
config: {
|
|
5976
6193
|
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.
|
|
6194
|
+
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
6195
|
inputSchema: createPageInput.shape
|
|
5979
6196
|
},
|
|
5980
6197
|
handler: guard(
|
|
5981
6198
|
async (args) => withClient(async (client) => {
|
|
5982
|
-
const dupes =
|
|
6199
|
+
const dupes = siblingDuplicateKeys(args.fields);
|
|
5983
6200
|
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
5984
6201
|
const page = await client.createPage({
|
|
5985
6202
|
title: args.title,
|
|
@@ -6015,7 +6232,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
6015
6232
|
},
|
|
6016
6233
|
handler: guard(
|
|
6017
6234
|
async (args) => withClient(async (client) => {
|
|
6018
|
-
const modelDupes =
|
|
6235
|
+
const modelDupes = siblingDuplicateKeys(args.fields);
|
|
6019
6236
|
if (modelDupes.length > 0) return fail(duplicateKeyFailure(modelDupes));
|
|
6020
6237
|
const input = {
|
|
6021
6238
|
name: args.name,
|
|
@@ -6079,7 +6296,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
6079
6296
|
}
|
|
6080
6297
|
const modelId = args.modelId;
|
|
6081
6298
|
const model = await client.getModel(modelId);
|
|
6082
|
-
const dupes =
|
|
6299
|
+
const dupes = (model.fields ?? []).some((f) => f?.key === args.key) ? [args.key] : [];
|
|
6083
6300
|
if (dupes.length > 0) return fail(duplicateKeyFailure(dupes));
|
|
6084
6301
|
const existingFields = model.fields;
|
|
6085
6302
|
let at = existingFields.length;
|
|
@@ -7885,7 +8102,7 @@ delivered value \u2014 they change editor chrome only.
|
|
|
7885
8102
|
|
|
7886
8103
|
- **\`helpText\`** \u2014 the exception: most fields get none. Write it only when the author needs it to
|
|
7887
8104
|
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
|
|
8105
|
+
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
8106
|
- **\`showIf\`** \u2014 hide a field until another has a value:
|
|
7890
8107
|
\`{match:'all',rules:[{field:'link_type',op:'eq',value:'external'}]}\`. Rules name fields by
|
|
7891
8108
|
BARE KEY, and the key namespace is FLAT across the model (\xA7 the duplicate-key rule), so the
|
|
@@ -8039,7 +8256,7 @@ An unknown \`openMode\` is refused with \`openMode_invalid\`, one on a leaf \`op
|
|
|
8039
8256
|
schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
|
|
8040
8257
|
\`backingModel\`.
|
|
8041
8258
|
- 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
|
|
8259
|
+
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
8260
|
the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
|
|
8044
8261
|
They write the DRAFT props; no publish is needed for the editor to show them.
|
|
8045
8262
|
- Always send \`ifMatch\`, the \`optimisticVersion\` you read.
|