@bettercms-ai/mcp 0.78.1 → 0.79.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 +143 -190
- package/dist/index.js.map +1 -1
- package/dist/kit-catalog.json +147 -0
- package/dist/skills/bettercms/SKILL.md +2 -2
- package/dist/skills/bettercms/references/content-modeling.md +10 -5
- package/dist/skills/bettercms/references/finish-site.md +7 -5
- package/dist/skills/bettercms/references/getting-started.md +12 -15
- package/dist/skills/bettercms/references/site-blueprints.md +4 -4
- package/dist/skills/bettercms/references/write-limits.md +0 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2541,7 +2541,8 @@ var STRUCTURE_DEFAULT_INSTRUCTION = `After you create collections or pages, orga
|
|
|
2541
2541
|
var SKILLS_ROUTING_INSTRUCTION = "Before writing code in a repo that uses BetterCMS, explore the repo, then read the installed `bettercms` skill (`npx @bettercms-ai/install --project -y` adds it if missing): its table turns what you found into the one reference the task needs \u2014 read only that. Without the skill installed, read the same files as resources: bettercms://skill/bettercms, then bettercms://skill/<reference>.";
|
|
2542
2542
|
var DOCK_UI_ROUTING_INSTRUCTION = "How fields and props look in the editor (groups, tabs, rows, icons, item names) is playbook \xA714 or the `field-ui` prompt: write it with update_content_model field* or update_component prop* plus ifMatch, read it back, and finish when get_next_steps reports no dock-* step.";
|
|
2543
2543
|
var PUBLISH_HOLD_INSTRUCTION = "An item an author held back from site publishes (`publishHeldAt` set) stays out of whole-site publishes: do not publish it unless the user names it; publish_entries `release` includes it again.";
|
|
2544
|
-
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is
|
|
2544
|
+
var RECEIPT_FIRST_INSTRUCTION = "Start every project with get_next_steps: its `receipt` lists what is done (`done`), what is missing (`missing`, each citing its count) and the one `nextAction`. Work from it instead of re-reading the site, and call it again after each batch of edits and before you report. On an imported site the gate is the definition of done: run `receipt.gate.nextAction`, read again, and repeat until `receipt.complete` is true. Record a gap you cannot close with editor_setup `exempt` only for an allowed reason that is true, with a note, never in prose.";
|
|
2545
|
+
var NO_QUESTIONS_INSTRUCTION = "Never stop a setup to ask the user: authoring (components) and media (the CDN) are recorded for you, and your prompt says whether you may push. Report what was decided.";
|
|
2545
2546
|
var NEXT_STEPS_DESCRIPTION = `Call this FIRST on a project. Returns \`receipt\` \u2014 \`done\` (live release, bound elements, conversion coverage, composed pages, published components and Layout), \`missing\`, \`nextAction\` and \`notes\` (unjudged: unknown, not done) \u2014 plus \`data\`, the unfinished items by \`ruleId\` (skill finish-site lists them). Each item cites its count and may carry \`payload\` {rows, total}, at most 20 rows. \`receipt.complete\` is the setup gate's verdict and \`receipt.gate\` its numbers per built route. Run \`receipt.gate.nextAction\`, call it again after each batch; you are finished only when \`complete\` is true. \`organise-content\`: pages and collections are in no folder \u2014 read ${STRUCTURE_PLAYBOOK_URI}, call suggest_content_structure and, once the user agrees, set_content_structure.`;
|
|
2546
2547
|
var LAYOUT_CHROME_INSTRUCTION = 'When the user asks to make the navigation, footer or other site chrome editable, or get_next_steps lists a `layout-*` or `chrome-duplicated` item: this is playbook \xA711 item 3. Chrome is the project Layout, never page fields: in the source, wrap each Section in `data-bcms-layout-section` (the slot, its base slug) and mark each value `data-bcms-layout-field="layout:<sections[slug].id>:<fieldId>"`, never `data-bcms-field` inside it, then push; the strip removes the page copies, never delete them by hand. On a site BetterCMS serves, publish_layout writes Layout VALUES into the live pages; a structure change waits for a Rebuild. Never place nav, footer or modal blocks or fields on a page: the Layout (or the site\'s own layout) draws chrome, so a page copy either duplicates it or renders nowhere (get_componentize_plan reports one as LAYOUT_CHROME).';
|
|
2547
2548
|
var CTA_BUTTON_INSTRUCTION = "A call to action is ONE `button` field (value {label, href, variant?, size?, arrow?, icon?, newTab?}), rendered with BcmsButton from @bettercms-ai/astro or @bettercms-ai/next: never a text field beside a link field. A page that already holds such a pair (primaryCtaText + primaryCtaHref) becomes one button with update_field `mergeHref`. Inside a component a CTA is a `button` block whose text and href two props target.";
|
|
@@ -3017,6 +3018,7 @@ var PARAM_TEXT = {
|
|
|
3017
3018
|
pageRef: "page id or path ('/de/about'); a bare slug only when one page has it",
|
|
3018
3019
|
editorSetupForce: "re-run a setup already ready for the live build",
|
|
3019
3020
|
editorSetupCheck: "true = only report the state; starts nothing",
|
|
3021
|
+
waitSeconds: "wait up to N seconds (max 50) for it to finish; call again while it runs",
|
|
3020
3022
|
editorSetupExempt: "instead of running: record setup-gate exceptions [{ route, check, reason, note }]; reason null removes one. Reasons: get_next_steps `receipt.gate`",
|
|
3021
3023
|
editorSetupDeclare: "instead of running: record binding decisions [{op, id, ...}], max 100, a result each; with `check: true` a dry run. Ops: skill bindings",
|
|
3022
3024
|
pageBlockJson: "an ARRAY REPLACES the page's block composition (a `component` block per section). An OBJECT edits ONE section of the stored page instead: `insertSection` adds a component (its id, or a builtin:* id: copied into the project once) at index `at`, or `before` / `after` a section's block id (none = last), with its `style` and `overrides`; `sectionStyle` sets a section's block style (a key replaces the stored one, null unsets it). `section` in the answer names the block (skill publishing)",
|
|
@@ -3061,6 +3063,10 @@ var UPDATE_PAGE_IF_MATCH_NOTE = "get_page's `optimisticVersion`: the write appli
|
|
|
3061
3063
|
var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId, overrides?}] (config.maxItems, default 12, at most 50): target.path 'props.children' on a section. V2 slots, 'table' mode 'types': playbook \xA75.";
|
|
3062
3064
|
var PROP_V2_NOTE = "Props take the fields' V2 keys (\xA714): 'object' = ONE {child: value} from config.fields, \u226410 levels (field_depth_exceeded); datetime|slug|email|phone|json|location|tags|rating|code|reference|multi-reference as that field (a reference names contentModelId, or polymorphic + allowedModelSlugs; page reads hydrate its entries); helpMore, advanced, recommendedMax, openMode, config.scale, ui.layout rows|chips|gallery, ui.preview.fallback, showIf notEmpty|empty. Send helpText: null for a prop whose /kit schema has no description (omitted, the platform writes one).";
|
|
3063
3065
|
var LAYOUT_LOCK_NOTE = "A Global field with `overridable:false` + `lockReason` (add-field `field`, update-field `patch`) refuses page overrides (LAYOUT_FIELD_NOT_OVERRIDABLE); get_layout scope page lists stored ones as `shadowed`.";
|
|
3066
|
+
var AUTHORING_DESCRIPTION = "Record which authoring architecture this site uses: 'components' (the default: reusable section components placed as blocks, which editors add, reorder and swap; on a site whose pages were DERIVED it keeps every field, value and binding, then run the componentize lane) or 'fields' (a typed field schema per page, for a blog, catalogue or directory). A deploy, a promote and every editor_setup record 'components' when nothing is set, so call this only to change it. Never ask the user: record it, say so in your final report, and tell them they can switch it in the dashboard.";
|
|
3067
|
+
var MEDIA_DEFAULT_NOTE = "Leave the default (the CDN) unless the owner asked otherwise; never ask.";
|
|
3068
|
+
var REBUILD_DESCRIPTION = "Rebuild from the connected repository, as the dashboard's Rebuild does. Use it when a get_next_steps fix names it, or after a push that started no build; then get_deploy_status { waitSeconds: 50 } until live, and editor_setup. 409 NOT_QUEUED: no repository (deploy_project the working tree) or publishing held.";
|
|
3069
|
+
var withWait = (path, waitSeconds) => typeof waitSeconds === "number" && waitSeconds > 0 ? `${path}?wait=${Math.min(Math.round(waitSeconds), 50)}` : path;
|
|
3064
3070
|
|
|
3065
3071
|
// src/entry-query.ts
|
|
3066
3072
|
var text = (v) => Array.isArray(v) ? v.map(String).join(",") : String(v);
|
|
@@ -3087,6 +3093,103 @@ function entryListSearch(args) {
|
|
|
3087
3093
|
return s ? `?${s}` : "";
|
|
3088
3094
|
}
|
|
3089
3095
|
|
|
3096
|
+
// src/kit.ts
|
|
3097
|
+
import { existsSync, readFileSync } from "fs";
|
|
3098
|
+
import { fileURLToPath } from "url";
|
|
3099
|
+
var KIT_URI = "bettercms://kit";
|
|
3100
|
+
var KIT_ENTRY_URI_PREFIX = "bettercms://kit/entry/";
|
|
3101
|
+
var KIT_ENTRY_URI_TEMPLATE = `${KIT_ENTRY_URI_PREFIX}{id}`;
|
|
3102
|
+
function loadKitCatalog(path = fileURLToPath(new URL("./kit-catalog.json", import.meta.url))) {
|
|
3103
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : null;
|
|
3104
|
+
}
|
|
3105
|
+
var list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
3106
|
+
function entryNotes(e, h = "###") {
|
|
3107
|
+
const out = [`${h} ${e.name} (\`${e.id}\`)`, "", `Tags: ${e.tags.join(", ") || "none"}. Proves: ${e.proves}`];
|
|
3108
|
+
if (!e.why && !e.mcp) out.push("", "No model notes in the kit for this entry: read its props.");
|
|
3109
|
+
if (e.why) out.push("", `**Why this shape.** ${e.why}`);
|
|
3110
|
+
if (e.htmlSignals.length) out.push("", "**HTML signals.**", list(e.htmlSignals));
|
|
3111
|
+
if (e.mcp) out.push("", `**Do.** ${e.mcp}`);
|
|
3112
|
+
if (e.pitfalls.length) out.push("", "**Pitfalls.**", list(e.pitfalls));
|
|
3113
|
+
if (e.notCarried.length) out.push("", `Not carried by a stored component: ${e.notCarried.join(", ")}.`);
|
|
3114
|
+
return out.join("\n");
|
|
3115
|
+
}
|
|
3116
|
+
function kitIndexText(k) {
|
|
3117
|
+
const byId = new Map(k.entries.map((e) => [e.id, e]));
|
|
3118
|
+
return [
|
|
3119
|
+
"# BetterCMS kit catalogue",
|
|
3120
|
+
"",
|
|
3121
|
+
`${k.entries.length} modelled website sections. Before designing a section, find the closest entry by its edge tag or category, read its notes and follow its mcp + pitfalls; its create_component props are at ${KIT_ENTRY_URI_TEMPLATE}.`,
|
|
3122
|
+
"",
|
|
3123
|
+
"## Categories",
|
|
3124
|
+
"",
|
|
3125
|
+
...k.categories.map((c) => `- ${c.name} (${c.entries.length}): ${KIT_URI}/${c.slug}`),
|
|
3126
|
+
"",
|
|
3127
|
+
"## Edge tags",
|
|
3128
|
+
"",
|
|
3129
|
+
...k.tags.map((t) => `- \`${t.tag}\` (${t.hint}): ${t.entries.join(", ") || "none"}`),
|
|
3130
|
+
"",
|
|
3131
|
+
"## Entries",
|
|
3132
|
+
"",
|
|
3133
|
+
...k.categories.flatMap((c) => [
|
|
3134
|
+
`### ${c.name}`,
|
|
3135
|
+
"",
|
|
3136
|
+
...c.entries.map((id) => {
|
|
3137
|
+
const e = byId.get(id);
|
|
3138
|
+
return `- \`${e.id}\` ${e.name} [${e.tags.join(", ")}]: ${e.proves}`;
|
|
3139
|
+
}),
|
|
3140
|
+
""
|
|
3141
|
+
])
|
|
3142
|
+
].join("\n");
|
|
3143
|
+
}
|
|
3144
|
+
function kitCategoryText(k, slug2) {
|
|
3145
|
+
const c = k.categories.find((x) => x.slug === slug2);
|
|
3146
|
+
const byId = new Map(k.entries.map((e) => [e.id, e]));
|
|
3147
|
+
return [
|
|
3148
|
+
`# Kit: ${c.name}`,
|
|
3149
|
+
"",
|
|
3150
|
+
`${c.entries.length} entries. Each one's create_component props: ${KIT_ENTRY_URI_TEMPLATE}. Index: ${KIT_URI}.`,
|
|
3151
|
+
"",
|
|
3152
|
+
...c.entries.map((id) => entryNotes(byId.get(id), "##") + "\n")
|
|
3153
|
+
].join("\n");
|
|
3154
|
+
}
|
|
3155
|
+
function kitEntryText(e) {
|
|
3156
|
+
return [
|
|
3157
|
+
entryNotes(e, "#"),
|
|
3158
|
+
"",
|
|
3159
|
+
"## create_component payload",
|
|
3160
|
+
"",
|
|
3161
|
+
"Props in the exact shape create_component accepts. Adapt keys and copy to the site; keep the types, `ui` and config.",
|
|
3162
|
+
"",
|
|
3163
|
+
"```json",
|
|
3164
|
+
JSON.stringify(e.component, null, 1),
|
|
3165
|
+
"```",
|
|
3166
|
+
""
|
|
3167
|
+
].join("\n");
|
|
3168
|
+
}
|
|
3169
|
+
function kitDocs(k = loadKitCatalog()) {
|
|
3170
|
+
if (!k) return [];
|
|
3171
|
+
const doc = (uri, name, title, description, text2) => ({ uri, name, title, description, mimeType: "text/markdown", text: text2 });
|
|
3172
|
+
return [
|
|
3173
|
+
doc(KIT_URI, "kit", "BetterCMS kit catalogue", "Index of modelled website sections by category and edge tag: find the closest one before designing a section.", kitIndexText(k)),
|
|
3174
|
+
...k.categories.map(
|
|
3175
|
+
(c) => doc(`${KIT_URI}/${c.slug}`, `kit-${c.slug}`, `Kit: ${c.name}`, `Model notes for the ${c.entries.length} ${c.name} entries: why, HTML signals, mcp, pitfalls.`, kitCategoryText(k, c.slug))
|
|
3176
|
+
)
|
|
3177
|
+
];
|
|
3178
|
+
}
|
|
3179
|
+
function kitEntryTexts(k = loadKitCatalog()) {
|
|
3180
|
+
return k ? Object.fromEntries(k.entries.map((e) => [`${KIT_ENTRY_URI_PREFIX}${e.id}`, kitEntryText(e)])) : {};
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/field-config.ts
|
|
3184
|
+
var FIELD_CONFIG = loadKitCatalog()?.fieldConfig ?? null;
|
|
3185
|
+
var SERVER = new Set(FIELD_CONFIG?.server ?? []);
|
|
3186
|
+
function withoutServerKeys(config) {
|
|
3187
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return config;
|
|
3188
|
+
const keys = Object.keys(config);
|
|
3189
|
+
return keys.some((k) => SERVER.has(k)) ? Object.fromEntries(Object.entries(config).filter(([k]) => !SERVER.has(k))) : config;
|
|
3190
|
+
}
|
|
3191
|
+
var withAgentConfig = (a) => a.config ? { ...a, config: withoutServerKeys(a.config) } : a;
|
|
3192
|
+
|
|
3090
3193
|
// src/tools.ts
|
|
3091
3194
|
async function askHuman(deps, q) {
|
|
3092
3195
|
if (!deps.elicit) return { prompt: q.prompt };
|
|
@@ -3130,55 +3233,6 @@ function askFramework(deps) {
|
|
|
3130
3233
|
});
|
|
3131
3234
|
}
|
|
3132
3235
|
var AUTHORING_CHOICES = ["components", "fields"];
|
|
3133
|
-
var AUTHORING_LABELS = {
|
|
3134
|
-
components: "Components (recommended) \u2014 reusable section components placed as blocks; editors add, reorder and swap sections without touching a schema. Recommend it for every marketing, landing, agency or product site",
|
|
3135
|
-
fields: "Fields \u2014 a typed field schema per page. Only for a blog, catalogue or directory where many rows share one shape; editors can change a page's copy but cannot add, reorder or swap its sections"
|
|
3136
|
-
};
|
|
3137
|
-
var AUTHORING_PROMPT = [
|
|
3138
|
-
"Ask the user which authoring architecture this site should use, then call set_authoring_preference again with their answer as `preference`:",
|
|
3139
|
-
...AUTHORING_CHOICES.map((c, i) => ` ${i + 1}. ${c} \u2014 ${AUTHORING_LABELS[c]}`),
|
|
3140
|
-
"",
|
|
3141
|
-
"On a site whose pages were DERIVED at import, answering 'components' redoes nothing: after set_authoring_preference, get_componentize_plan proposes one component per section (it reads the PUBLISHED pages \u2014 no deploy needed to reach it), you confirm it with the user, then componentize_sections places them with `props.bind` (dry run first \u2014 they land as drafts and the page keeps its fields, values and bindings, so click-to-edit and the coverage meter are unchanged), then publish_component each one, publish the pages, run `npx @bettercms-ai/convert --componentize` in the repo, and deploy. For a section group the plan lists as NO_GROUP_ROOT or NOT_A_SECTION there is nothing to componentize, so author that section with create_component and set_page_content.",
|
|
3142
|
-
"",
|
|
3143
|
-
"Recommend components unless the user says the site is schema-first.",
|
|
3144
|
-
"Do not choose on their behalf. This is asked once per project."
|
|
3145
|
-
].join("\n");
|
|
3146
|
-
function askAuthoring(deps) {
|
|
3147
|
-
return askHuman(deps, {
|
|
3148
|
-
key: "preference",
|
|
3149
|
-
message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
|
|
3150
|
-
property: {
|
|
3151
|
-
type: "string",
|
|
3152
|
-
title: "Authoring architecture",
|
|
3153
|
-
description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
|
|
3154
|
-
enum: [...AUTHORING_CHOICES],
|
|
3155
|
-
enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
|
|
3156
|
-
},
|
|
3157
|
-
accept: (v) => typeof v === "string" && AUTHORING_CHOICES.includes(v),
|
|
3158
|
-
prompt: AUTHORING_PROMPT
|
|
3159
|
-
});
|
|
3160
|
-
}
|
|
3161
|
-
var USE_CDN_PROMPT = [
|
|
3162
|
-
"Ask the user where this site's media should be served from, then call set_media_delivery again with their answer as `useCdn`:",
|
|
3163
|
-
" 1. true \u2014 the BetterCMS CDN (recommended, and the default). Media URLs are minted on the CDN host, or on the project's own CDN base if it stores media in its own bucket.",
|
|
3164
|
-
" 2. false \u2014 the API origin. Media URLs are minted on the API host instead. Same bytes, same service; only the hostname differs.",
|
|
3165
|
-
"",
|
|
3166
|
-
"This applies to URLs minted FROM NOW ON. Already-published content keeps the URLs it has, and they keep working."
|
|
3167
|
-
].join("\n");
|
|
3168
|
-
function askMediaDelivery(deps) {
|
|
3169
|
-
return askHuman(deps, {
|
|
3170
|
-
key: "useCdn",
|
|
3171
|
-
message: "Serve this site's media from the BetterCMS CDN (recommended), or from the API origin?",
|
|
3172
|
-
property: {
|
|
3173
|
-
type: "boolean",
|
|
3174
|
-
title: "Use the CDN",
|
|
3175
|
-
description: "On (default): media URLs are minted on the CDN \u2014 or on this project's own CDN base if it uses its own bucket. Off: they are minted on the API origin instead. Same bytes either way; only the host changes, and only for URLs minted from now on.",
|
|
3176
|
-
default: true
|
|
3177
|
-
},
|
|
3178
|
-
accept: (v) => typeof v === "boolean",
|
|
3179
|
-
prompt: USE_CDN_PROMPT
|
|
3180
|
-
});
|
|
3181
|
-
}
|
|
3182
3236
|
var fieldType = z.enum([
|
|
3183
3237
|
"text",
|
|
3184
3238
|
"richtext",
|
|
@@ -3315,7 +3369,7 @@ var fieldShape = {
|
|
|
3315
3369
|
"choices when type is 'select'. A plain string, or {label, value, icon?, description?} when the stored value differs from what the editor shows (e.g. {label:'Sold out', value:'soldOut'}); icon a dock name, description one line \u2264140."
|
|
3316
3370
|
),
|
|
3317
3371
|
config: z.record(z.string(), z.unknown()).optional().describe(
|
|
3318
|
-
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, repeater {minItems, maxItems} (the list's item limits; the editor shows 'max N' and stops Add there), array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}"
|
|
3372
|
+
"per-type config: reference {contentModelId}, multi-reference {contentModelId,min,max}, repeater {minItems, maxItems} (the list's item limits; the editor shows 'max N' and stops Add there), array {itemType: text|number|date|image|link|richtext}, date {includeTime, timezone}, modular|slot {blockSlugs: ['quote','gallery'], minItems?, maxItems?} (slot: playbook \xA75) \u2014 blockSlugs is REQUIRED, non-empty, and each slug must name an existing kind:'block' model; richtext {inline:true, hostTag:'h2', accept:['h2','p','b','i','u','link'], features:['bold','italic','underline','link','headings'], allowMultipleParagraphs:false} \u2014 for a field bound to ONE element on the page: `hostTag` is that element's tag and `accept` must name it, so the editor's block-style control opens already showing 'Heading 2' instead of 'Paragraph'. Name exactly ONE heading level: publish refuses an <h3> value bound to an <h2> rather than nesting one heading inside the other. Keep 'p' in `accept` or the sanitiser strips a block demoted to Paragraph; limits: text/longtext {minLength,maxLength,pattern,patternMessage}, richtext {minLength,maxLength}, number {validation:{min,max}}, array {minItems,maxItems}, tags {min,max}, rating {min,max,step}. Undocumented keys open the classic editor."
|
|
3319
3373
|
),
|
|
3320
3374
|
fields: z.array(z.lazy(() => fieldObject)).optional().describe(
|
|
3321
3375
|
"NESTED child fields \u2014 REQUIRED for type 'group' (one nested object, a Non-Repeatable Zone like blog_hero \u2192 heading, description, hero_image) and type 'repeater' (a repeatable array of such objects, a Repeatable Zone / section-list like testimonials \u2192 quote, author). A section with repeating items is a 'repeater'; a fixed grouped block is a 'group'. Recurse to any depth \u2014 do NOT flatten zones into separate top-level fields."
|
|
@@ -3512,7 +3566,7 @@ function toField(f) {
|
|
|
3512
3566
|
...base,
|
|
3513
3567
|
type: proseType(f),
|
|
3514
3568
|
...f.options ? { options: f.options } : {},
|
|
3515
|
-
...f.config ? { config: f.config } : {}
|
|
3569
|
+
...f.config ? { config: withoutServerKeys(f.config) } : {}
|
|
3516
3570
|
};
|
|
3517
3571
|
}
|
|
3518
3572
|
var BRAND_KIT_NOTE = "`brandKit` (colors, typography, radius and any typeScale/spacing/elevation/motion/graphics) is edited in the BetterCMS dashboard AND by set_brand_assets and set_brand_graphics; every write is a version a person can restore. ASSETS: a media asset OF THIS PROJECT by id; null clears one; a slot a person chose needs `replace: true`. GRAPHICS: `kind`, optional `angle`, 2-8 stops naming `colors.*` or `extras` keys \u2014 never a CSS string and never a hex. CONSUME, DO NOT COPY: a hosted page exposes `--brand-color-<key>`, `--brand-shadow-<key>` and `--brand-gradient-<key>`; style sections with their `style` tokens (`nav.backgroundGradient` and `footer.backgroundGradient` take a gradient key), never copied values. Never invent brand facts: read them from get_project.";
|
|
@@ -4639,7 +4693,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4639
4693
|
"update_project",
|
|
4640
4694
|
"Update the connected project",
|
|
4641
4695
|
"Update the connected project's settings \u2014 rename it, change its slug/description, SEO defaults, visibility, or `presentation`. Only the provided fields change. `subdomainAlias` renames the staging URL: 'astronapp' serves the site at astronapp.bettercms.site (3\u201355 lowercase letters, numbers, hyphens; 409 when taken; null goes back to the original). The original <handle>.bettercms.site keeps working; a replaced name is released. `presentation` themes the platform's OWN renderer (the one serving a site built in the CMS with no deployed build, and previewing structural drafts everywhere) with the same closed knobs as bcms-presentation.json \u2014 DTCG { $type, $value } tokens keyed like 'page.maxWidth', 'nav.background'. Without it that renderer is a generic 768px document theme. An invalid manifest is 422 PRESENTATION_INVALID.",
|
|
4642
|
-
z.object({ name: z.string().optional(), slug: z.string().optional(), description: z.string().optional(), visibility: z.string().optional(), seoDefaults: z.record(z.string(), z.unknown()).optional(), presentation: z.record(z.string(), z.unknown()).optional().describe("theme knobs for the platform renderer, keyed like bcms-presentation.json"), subdomainAlias: z.string().nullable().optional().describe("the staging name: 'astronapp' \u2192 astronapp.bettercms.site; null = back to the original") }).shape,
|
|
4696
|
+
z.object({ name: z.string().optional(), slug: z.string().optional(), description: z.string().optional(), visibility: z.string().optional(), seoDefaults: z.record(z.string(), z.unknown()).optional(), presentation: z.record(z.string(), z.unknown()).optional().describe("theme knobs for the platform renderer, keyed like bcms-presentation.json"), subdomainAlias: z.string().nullable().optional().describe("the staging name: 'astronapp' \u2192 astronapp.bettercms.site; null = back to the original"), structureInPlace: z.literal(true).optional().describe("true: in-place structure on (the gate fix enable-in-place-structure); never off") }).shape,
|
|
4643
4697
|
async (c, a) => ok("Updated project.", await data(c, "PATCH", `/management/projects/current`, a))
|
|
4644
4698
|
),
|
|
4645
4699
|
def(
|
|
@@ -4664,21 +4718,10 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4664
4718
|
def(
|
|
4665
4719
|
"set_authoring_preference",
|
|
4666
4720
|
"Set the site's authoring architecture",
|
|
4667
|
-
|
|
4668
|
-
//
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
// gate refuses the deploy regardless, so nothing ships on a guess.
|
|
4672
|
-
z.object({ preference: z.enum(AUTHORING_CHOICES).optional().describe("REQUIRED in effect \u2014 the architecture the USER chose. Ask them; never default it.") }).shape,
|
|
4673
|
-
async (c, a) => {
|
|
4674
|
-
let preference = a.preference;
|
|
4675
|
-
if (preference === void 0) {
|
|
4676
|
-
const asked = await askAuthoring(deps);
|
|
4677
|
-
if ("prompt" in asked) return fail(asked.prompt);
|
|
4678
|
-
preference = asked.value;
|
|
4679
|
-
}
|
|
4680
|
-
return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
|
|
4681
|
-
}
|
|
4721
|
+
AUTHORING_DESCRIPTION,
|
|
4722
|
+
// Required: an omitted answer must never overwrite one the owner gave (a deploy or setup records the default).
|
|
4723
|
+
z.object({ preference: z.enum(AUTHORING_CHOICES).describe("components or fields") }).shape,
|
|
4724
|
+
async (c, a) => ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference: a.preference }))
|
|
4682
4725
|
),
|
|
4683
4726
|
def(
|
|
4684
4727
|
"set_binding_mode",
|
|
@@ -4690,20 +4733,9 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
4690
4733
|
def(
|
|
4691
4734
|
"set_media_delivery",
|
|
4692
4735
|
"Choose where this site's media is served from",
|
|
4693
|
-
"Record whether this project's media (images, video, files) is served from the BetterCMS CDN or from the API origin. `useCdn: true` is the DEFAULT and the recommendation: URLs are minted on the CDN host, or on the project's own CDN base when it stores media in its own bucket (BYOK). `useCdn: false` mints them on the API origin instead \u2014 the same service serving the same bytes, only under a different hostname.
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
// elicitation below and leave the model guessing.
|
|
4697
|
-
z.object({ useCdn: z.boolean().optional().describe("REQUIRED in effect \u2014 the USER's answer. true = CDN (the default), false = API origin. Ask them; never default it.") }).shape,
|
|
4698
|
-
async (c, a) => {
|
|
4699
|
-
let useCdn = a.useCdn;
|
|
4700
|
-
if (useCdn === void 0) {
|
|
4701
|
-
const asked = await askMediaDelivery(deps);
|
|
4702
|
-
if ("prompt" in asked) return fail(asked.prompt);
|
|
4703
|
-
useCdn = asked.value;
|
|
4704
|
-
}
|
|
4705
|
-
return ok("Recorded the media delivery origin.", await data(c, "PATCH", `/management/projects/current/media-delivery`, { useCdn }));
|
|
4706
|
-
}
|
|
4736
|
+
"Record whether this project's media (images, video, files) is served from the BetterCMS CDN or from the API origin. `useCdn: true` is the DEFAULT and the recommendation: URLs are minted on the CDN host, or on the project's own CDN base when it stores media in its own bucket (BYOK). `useCdn: false` mints them on the API origin instead \u2014 the same service serving the same bytes, only under a different hostname. " + MEDIA_DEFAULT_NOTE + " \u{1F534} IT APPLIES TO URLs MINTED FROM NOW ON \u2014 new uploads, entry saves, get_media. It does NOT rewrite URLs already stored in published content, and it does not need to: the old URLs keep resolving. To move an existing image, re-save the field that holds it. \u{1F534} IT IS THE BACKEND HALF ONLY. A deployed frontend builds its own transform URLs with @bettercms-ai/image-url, which has its own media host \u2014 set `PUBLIC_BCMS_MEDIA_URL` (or the integration's `mediaUrl` option) to match, or that site's resized images stay on the CDN while everything else moves. Read the current answer from get_project: `useCdn` is the choice, `mediaBaseUrl` is the origin it produces \u2014 the value the frontend's `PUBLIC_BCMS_MEDIA_URL` must match. Re-callable; the last answer wins.",
|
|
4737
|
+
z.object({ useCdn: z.boolean().describe("true = CDN (the default), false = API origin") }).shape,
|
|
4738
|
+
async (c, a) => ok("Recorded the media delivery origin.", await data(c, "PATCH", `/management/projects/current/media-delivery`, { useCdn: a.useCdn }))
|
|
4707
4739
|
),
|
|
4708
4740
|
def(
|
|
4709
4741
|
"submit_conversion_receipt",
|
|
@@ -5018,7 +5050,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5018
5050
|
if (!modelId) return fail("This page has no backing content model; address its schema by modelId.");
|
|
5019
5051
|
}
|
|
5020
5052
|
if (!modelId) return fail("Pass modelId (a content model) or pageId (a page).");
|
|
5021
|
-
const plan = planFieldEdit(await data(c, "GET", `/management/content/models/${s(modelId)}`), a, a.ifMatch);
|
|
5053
|
+
const plan = planFieldEdit(await data(c, "GET", `/management/content/models/${s(modelId)}`), withAgentConfig(a), a.ifMatch);
|
|
5022
5054
|
if (!plan.ok) return fail(plan.error);
|
|
5023
5055
|
const path = a.pageId ? `/management/pages/${s(a.pageId)}/model` : `/management/content/models/${s(modelId)}`;
|
|
5024
5056
|
const updated = (await c.fetchJSON(c.url(path), {
|
|
@@ -5506,9 +5538,16 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5506
5538
|
"get_deploy_status",
|
|
5507
5539
|
"Get deploy/build status",
|
|
5508
5540
|
"Get the connected project's deploy/build status: state (idle|queued|building|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `editorSetup`: whether editing is set up for the live build (never or stale = call editor_setup). Poll this after deploy_project until state is idle with your sha live. `release`, `held` and `publishes` (each `live` or `partial`): skill publishing. `runtime` ({state: ok|crash-loop, restarts, oomKilled}) appears for a server app only: crash-loop means its container keeps restarting (often out of memory) though the build went live fine. Also returns `canvas.lane` (skill bindings) and `editorUrl`, the visual editor to hand the user.",
|
|
5509
|
-
z.object({}).shape,
|
|
5541
|
+
z.object({ waitSeconds: z.number().optional().describe(PARAM_TEXT.waitSeconds) }).shape,
|
|
5510
5542
|
// Flat body, no `data` envelope: `data()` read it as undefined.
|
|
5511
|
-
async (c) => ok("Deploy status.", await c.fetchJSON(c.url(`/management/projects/deploy-status
|
|
5543
|
+
async (c, a) => ok("Deploy status.", await c.fetchJSON(c.url(withWait(`/management/projects/deploy-status`, a.waitSeconds)), { method: "GET" }))
|
|
5544
|
+
),
|
|
5545
|
+
def(
|
|
5546
|
+
"rebuild_project",
|
|
5547
|
+
"Rebuild from the connected repository",
|
|
5548
|
+
REBUILD_DESCRIPTION,
|
|
5549
|
+
z.object({}).shape,
|
|
5550
|
+
async (c) => ok("Rebuild queued.", await c.fetchJSON(c.url(`/management/projects/rebuild`), { method: "POST" }))
|
|
5512
5551
|
),
|
|
5513
5552
|
def(
|
|
5514
5553
|
"editor_setup",
|
|
@@ -5517,13 +5556,14 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
5517
5556
|
z.object({
|
|
5518
5557
|
force: z.boolean().optional().describe(PARAM_TEXT.editorSetupForce),
|
|
5519
5558
|
check: z.boolean().optional().describe(PARAM_TEXT.editorSetupCheck),
|
|
5559
|
+
waitSeconds: z.number().optional().describe(PARAM_TEXT.waitSeconds),
|
|
5520
5560
|
exempt: z.array(z.record(z.string(), z.unknown())).optional().describe(PARAM_TEXT.editorSetupExempt),
|
|
5521
5561
|
declare: z.array(z.record(z.string(), z.unknown())).optional().describe(PARAM_TEXT.editorSetupDeclare)
|
|
5522
5562
|
}).shape,
|
|
5523
5563
|
// The route answers `{ state, … }` with no `data` envelope, so the whole body is the result.
|
|
5524
5564
|
// `exempt` records setup-gate exceptions instead of running anything (the gate's one write).
|
|
5525
5565
|
// `declare` records binding decisions; with `check: true` it is a dry run (the body's `dryRun`).
|
|
5526
|
-
async (c, a) => a.exempt ? ok("Setup gate exceptions.", await c.fetchJSON(c.url(`/management/projects/setup-gate/exceptions`), { method: "POST", body: JSON.stringify({ exempt: a.exempt }) })) : a.declare ? ok("Binding decisions.", await c.fetchJSON(c.url(`/management/projects/binding-decisions`), { method: "POST", body: JSON.stringify({ declare: a.declare, ...a.check === true ? { dryRun: true } : {} }) })) : ok("Editor setup.", await c.fetchJSON(c.url(`/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
|
|
5566
|
+
async (c, a) => a.exempt ? ok("Setup gate exceptions.", await c.fetchJSON(c.url(`/management/projects/setup-gate/exceptions`), { method: "POST", body: JSON.stringify({ exempt: a.exempt }) })) : a.declare ? ok("Binding decisions.", await c.fetchJSON(c.url(`/management/projects/binding-decisions`), { method: "POST", body: JSON.stringify({ declare: a.declare, ...a.check === true ? { dryRun: true } : {} }) })) : ok("Editor setup.", await c.fetchJSON(c.url(a.check === true ? withWait(`/management/projects/editor-setup`, a.waitSeconds) : `/management/projects/editor-setup`), a.check === true ? { method: "GET" } : { method: "POST", body: JSON.stringify({ force: a.force }) }))
|
|
5527
5567
|
),
|
|
5528
5568
|
// ── Deploy to the user's own host (parity with remote /mcp) ──
|
|
5529
5569
|
// A person connects Vercel or Netlify in the dashboard; nothing here touches OAuth or a host
|
|
@@ -6819,13 +6859,11 @@ build proves every binding declared; get_next_steps lists only what it could not
|
|
|
6819
6859
|
without CI checks (\`"build:bcms": "astro build"\` beside \`"build": "astro check && astro build"\`),
|
|
6820
6860
|
keeping any step the build needs. The convert codemod adds it; never edit \`build\` for BetterCMS.
|
|
6821
6861
|
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
out, let the user pick, call \`set_authoring_preference\`, then deploy again.
|
|
6862
|
+
The authoring architecture is never asked: a deploy, a promote and every \`editor_setup\` record
|
|
6863
|
+
\`components\` when nothing is set, and the owner can switch to \`fields\` in the dashboard (or
|
|
6864
|
+
\`set_authoring_preference\`). Say in your final report which one is recorded.
|
|
6826
6865
|
|
|
6827
|
-
|
|
6828
|
-
preference is null). Once \`components\` is recorded, each \`editor_setup\` componentizes the
|
|
6866
|
+
With \`components\` recorded, each \`editor_setup\` componentizes the
|
|
6829
6867
|
derived pages' NEW sections in bind mode; \`fields\` stops it for good. And a run over the WHOLE plan splits a section component an earlier run placed on
|
|
6830
6868
|
2 pages into one per page (the first page keeps it, the others get a clone), so two pages' heroes
|
|
6831
6869
|
stop editing each other; a run narrowed to one page or section never splits.
|
|
@@ -8286,8 +8324,8 @@ Read bettercms://playbook/schema section 11 first, then work this order:
|
|
|
8286
8324
|
1. EDITOR SETUP \u2014 the build imported only SEO/meta, media, brand and the models the repo
|
|
8287
8325
|
declares. Call editor_setup and poll editor_setup { check: true } until \`ready\`: it derives
|
|
8288
8326
|
the pages, collections, forms and bindings from the live build.
|
|
8289
|
-
2. AUTHORING
|
|
8290
|
-
ask
|
|
8327
|
+
2. AUTHORING \u2014 components is recorded by default (deploy, promote and editor_setup set it when
|
|
8328
|
+
nothing is); never ask me. Report which one is recorded.
|
|
8291
8329
|
3. COMPONENTS: get_componentize_plan -> componentize_sections (dryRun first) -> publish the
|
|
8292
8330
|
components and pages -> componentize_sections { codemod: true } (the section-order codemod in
|
|
8293
8331
|
the repo). FIELDS: the convert_site loop (get_conversion_brief, the codemod, the receipt).
|
|
@@ -8326,16 +8364,16 @@ On 401/403, the MCP key needs (re)authorizing.`
|
|
|
8326
8364
|
}
|
|
8327
8365
|
|
|
8328
8366
|
// src/skill-docs.ts
|
|
8329
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
8367
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
8330
8368
|
import { join } from "path";
|
|
8331
|
-
import { fileURLToPath } from "url";
|
|
8369
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8332
8370
|
var SKILL_URI_PREFIX = "bettercms://skill/";
|
|
8333
8371
|
var CORE_SKILL = "bettercms";
|
|
8334
8372
|
var CANDIDATES = ["./skills/bettercms/", "../../skills/bettercms/"];
|
|
8335
8373
|
function defaultSkillDir() {
|
|
8336
8374
|
for (const rel of CANDIDATES) {
|
|
8337
|
-
const dir =
|
|
8338
|
-
if (
|
|
8375
|
+
const dir = fileURLToPath2(new URL(rel, import.meta.url));
|
|
8376
|
+
if (existsSync2(join(dir, "SKILL.md"))) return dir;
|
|
8339
8377
|
}
|
|
8340
8378
|
return null;
|
|
8341
8379
|
}
|
|
@@ -8375,104 +8413,17 @@ function skillDocOf(stem, text2) {
|
|
|
8375
8413
|
}
|
|
8376
8414
|
function loadSkillDocs(dir = defaultSkillDir()) {
|
|
8377
8415
|
if (!dir) return [];
|
|
8378
|
-
const docs = [skillDocOf(CORE_SKILL,
|
|
8416
|
+
const docs = [skillDocOf(CORE_SKILL, readFileSync2(join(dir, "SKILL.md"), "utf8"))];
|
|
8379
8417
|
const refs = join(dir, "references");
|
|
8380
|
-
if (!
|
|
8418
|
+
if (!existsSync2(refs)) return docs;
|
|
8381
8419
|
for (const file of readdirSync(refs).filter((f) => f.endsWith(".md")).sort()) {
|
|
8382
8420
|
const stem = file.slice(0, -3);
|
|
8383
8421
|
if (stem === CORE_SKILL) continue;
|
|
8384
|
-
docs.push(skillDocOf(stem,
|
|
8422
|
+
docs.push(skillDocOf(stem, readFileSync2(join(refs, file), "utf8")));
|
|
8385
8423
|
}
|
|
8386
8424
|
return docs;
|
|
8387
8425
|
}
|
|
8388
8426
|
|
|
8389
|
-
// src/kit.ts
|
|
8390
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8391
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8392
|
-
var KIT_URI = "bettercms://kit";
|
|
8393
|
-
var KIT_ENTRY_URI_PREFIX = "bettercms://kit/entry/";
|
|
8394
|
-
var KIT_ENTRY_URI_TEMPLATE = `${KIT_ENTRY_URI_PREFIX}{id}`;
|
|
8395
|
-
function loadKitCatalog(path = fileURLToPath2(new URL("./kit-catalog.json", import.meta.url))) {
|
|
8396
|
-
return existsSync2(path) ? JSON.parse(readFileSync2(path, "utf8")) : null;
|
|
8397
|
-
}
|
|
8398
|
-
var list = (xs) => xs.map((x) => `- ${x}`).join("\n");
|
|
8399
|
-
function entryNotes(e, h = "###") {
|
|
8400
|
-
const out = [`${h} ${e.name} (\`${e.id}\`)`, "", `Tags: ${e.tags.join(", ") || "none"}. Proves: ${e.proves}`];
|
|
8401
|
-
if (!e.why && !e.mcp) out.push("", "No model notes in the kit for this entry: read its props.");
|
|
8402
|
-
if (e.why) out.push("", `**Why this shape.** ${e.why}`);
|
|
8403
|
-
if (e.htmlSignals.length) out.push("", "**HTML signals.**", list(e.htmlSignals));
|
|
8404
|
-
if (e.mcp) out.push("", `**Do.** ${e.mcp}`);
|
|
8405
|
-
if (e.pitfalls.length) out.push("", "**Pitfalls.**", list(e.pitfalls));
|
|
8406
|
-
if (e.notCarried.length) out.push("", `Not carried by a stored component: ${e.notCarried.join(", ")}.`);
|
|
8407
|
-
return out.join("\n");
|
|
8408
|
-
}
|
|
8409
|
-
function kitIndexText(k) {
|
|
8410
|
-
const byId = new Map(k.entries.map((e) => [e.id, e]));
|
|
8411
|
-
return [
|
|
8412
|
-
"# BetterCMS kit catalogue",
|
|
8413
|
-
"",
|
|
8414
|
-
`${k.entries.length} modelled website sections. Before designing a section, find the closest entry by its edge tag or category, read its notes and follow its mcp + pitfalls; its create_component props are at ${KIT_ENTRY_URI_TEMPLATE}.`,
|
|
8415
|
-
"",
|
|
8416
|
-
"## Categories",
|
|
8417
|
-
"",
|
|
8418
|
-
...k.categories.map((c) => `- ${c.name} (${c.entries.length}): ${KIT_URI}/${c.slug}`),
|
|
8419
|
-
"",
|
|
8420
|
-
"## Edge tags",
|
|
8421
|
-
"",
|
|
8422
|
-
...k.tags.map((t) => `- \`${t.tag}\` (${t.hint}): ${t.entries.join(", ") || "none"}`),
|
|
8423
|
-
"",
|
|
8424
|
-
"## Entries",
|
|
8425
|
-
"",
|
|
8426
|
-
...k.categories.flatMap((c) => [
|
|
8427
|
-
`### ${c.name}`,
|
|
8428
|
-
"",
|
|
8429
|
-
...c.entries.map((id) => {
|
|
8430
|
-
const e = byId.get(id);
|
|
8431
|
-
return `- \`${e.id}\` ${e.name} [${e.tags.join(", ")}]: ${e.proves}`;
|
|
8432
|
-
}),
|
|
8433
|
-
""
|
|
8434
|
-
])
|
|
8435
|
-
].join("\n");
|
|
8436
|
-
}
|
|
8437
|
-
function kitCategoryText(k, slug2) {
|
|
8438
|
-
const c = k.categories.find((x) => x.slug === slug2);
|
|
8439
|
-
const byId = new Map(k.entries.map((e) => [e.id, e]));
|
|
8440
|
-
return [
|
|
8441
|
-
`# Kit: ${c.name}`,
|
|
8442
|
-
"",
|
|
8443
|
-
`${c.entries.length} entries. Each one's create_component props: ${KIT_ENTRY_URI_TEMPLATE}. Index: ${KIT_URI}.`,
|
|
8444
|
-
"",
|
|
8445
|
-
...c.entries.map((id) => entryNotes(byId.get(id), "##") + "\n")
|
|
8446
|
-
].join("\n");
|
|
8447
|
-
}
|
|
8448
|
-
function kitEntryText(e) {
|
|
8449
|
-
return [
|
|
8450
|
-
entryNotes(e, "#"),
|
|
8451
|
-
"",
|
|
8452
|
-
"## create_component payload",
|
|
8453
|
-
"",
|
|
8454
|
-
"Props in the exact shape create_component accepts. Adapt keys and copy to the site; keep the types, `ui` and config.",
|
|
8455
|
-
"",
|
|
8456
|
-
"```json",
|
|
8457
|
-
JSON.stringify(e.component, null, 1),
|
|
8458
|
-
"```",
|
|
8459
|
-
""
|
|
8460
|
-
].join("\n");
|
|
8461
|
-
}
|
|
8462
|
-
function kitDocs(k = loadKitCatalog()) {
|
|
8463
|
-
if (!k) return [];
|
|
8464
|
-
const doc = (uri, name, title, description, text2) => ({ uri, name, title, description, mimeType: "text/markdown", text: text2 });
|
|
8465
|
-
return [
|
|
8466
|
-
doc(KIT_URI, "kit", "BetterCMS kit catalogue", "Index of modelled website sections by category and edge tag: find the closest one before designing a section.", kitIndexText(k)),
|
|
8467
|
-
...k.categories.map(
|
|
8468
|
-
(c) => doc(`${KIT_URI}/${c.slug}`, `kit-${c.slug}`, `Kit: ${c.name}`, `Model notes for the ${c.entries.length} ${c.name} entries: why, HTML signals, mcp, pitfalls.`, kitCategoryText(k, c.slug))
|
|
8469
|
-
)
|
|
8470
|
-
];
|
|
8471
|
-
}
|
|
8472
|
-
function kitEntryTexts(k = loadKitCatalog()) {
|
|
8473
|
-
return k ? Object.fromEntries(k.entries.map((e) => [`${KIT_ENTRY_URI_PREFIX}${e.id}`, kitEntryText(e)])) : {};
|
|
8474
|
-
}
|
|
8475
|
-
|
|
8476
8427
|
// src/server.ts
|
|
8477
8428
|
var SERVER_NAME = "bettercms";
|
|
8478
8429
|
var SERVER_VERSION = "1.5.0";
|
|
@@ -8498,7 +8449,7 @@ function buildServer(deps) {
|
|
|
8498
8449
|
// 🔴 The plain-language ask, routed — the stdio twin of the hosted connector's line
|
|
8499
8450
|
// (src/routes/mcp/index.ts MCP_INSTRUCTIONS). "Make my site editable" has one recipe and
|
|
8500
8451
|
// one definition of done; without this an agent converts the page it landed on and stops.
|
|
8501
|
-
instructions:
|
|
8452
|
+
instructions: RECEIPT_FIRST_INSTRUCTION + " " + NO_QUESTIONS_INSTRUCTION + " " + TOOL_LOADING_INSTRUCTION + " When the user asks to make a site or all of its pages editable and they were DERIVED at import: editor_setup, then get_next_steps until `receipt.gate.complete`. To convert its source or bind its fields: this is playbook \xA713. Read `bettercms://playbook/schema` \xA713, call get_binding_report and get_conversion_brief { complete: true }, convert EVERY route the brief lists, and finish only when get_binding_report shows coverage.pending empty on every route \u2014 not when the first page works. On a workspace-wide connection pass projectId on every call; never ask the user to re-scope the connection. " + LAYOUT_CHROME_INSTRUCTION + " " + CTA_BUTTON_INSTRUCTION + " When the user asks to componentize the whole site, to turn every section into a component, or to build a component library from the site: this is playbook \xA712. Read `bettercms://playbook/schema` \xA712, start with get_site_composition, and use the batch tools \u2014 componentize_sections, compose_pages, update_layout with `commands`, publish_components \u2014 rather than one call per component. Finish with get_site_composition and tell the user what the platform does not model (cookie banners, modals, breadcrumbs, pagination) and which components still need the owner's approval in the dashboard before they can be published. " + // The default after authoring (the structure standard): organise what you made. Same
|
|
8502
8453
|
// sentence as the hosted connector's MCP_INSTRUCTIONS.
|
|
8503
8454
|
STRUCTURE_DEFAULT_INSTRUCTION + " " + SKILLS_ROUTING_INSTRUCTION + " " + DOCK_UI_ROUTING_INSTRUCTION + " " + PUBLISH_HOLD_INSTRUCTION
|
|
8504
8455
|
}
|
|
@@ -8555,7 +8506,9 @@ function buildServer(deps) {
|
|
|
8555
8506
|
// `project` is the per-call target of a workspace-wide grant; the SDK sends it as
|
|
8556
8507
|
// X-BCMS-Project. Omitted entirely when absent so a project-scoped key's request is
|
|
8557
8508
|
// byte-identical to before.
|
|
8558
|
-
|
|
8509
|
+
// 🔴 65 s, not the SDK's 10 s: get_deploy_status / editor_setup { waitSeconds } hold the request for up to 50 s
|
|
8510
|
+
// while a build or a setup settles (tool-text.ts withWait), and a 10 s client timed every one of them out.
|
|
8511
|
+
createClient: (apiKey, project, branch) => withBranch(BetterCMS.management({ apiKey, baseUrl: deps.managementBaseUrl, timeout: 65e3, ...project ? { project } : {} }), branch)
|
|
8559
8512
|
});
|
|
8560
8513
|
registerPrompts(server);
|
|
8561
8514
|
return server;
|