@bettercms-ai/mcp 0.59.1 → 0.59.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +39 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2350,6 +2350,7 @@ var fieldsetsArg = z.array(
|
|
|
2350
2350
|
"the model's editor cards: [{id, name, description?}], at most 30, ids unique, order = array index. `description` is one line under the card title. A field joins one with `fieldsetId`. Editor-only: never changes what is stored or delivered."
|
|
2351
2351
|
);
|
|
2352
2352
|
var RICH_TEXT_NOTE = "\u{1F534} A field of type 'text' is created as RICH TEXT unless you send `richText: false` on it. Send `richText: false` for every value that is not prose: a name used as a title, a URL, slug, id, SKU, email, phone, CSS class or icon name.";
|
|
2353
|
+
var PAGES_NOT_COLLECTIONS_NOTE = "A collection is only for items one template route repeats (/blog/:slug: set urlPattern). A single page's copy (home, about, contact, pricing, legal) is page fields in sections (add_page_field / set_page_content), never a one-entry collection.";
|
|
2353
2354
|
function flatFieldKeys(fields) {
|
|
2354
2355
|
const out = [];
|
|
2355
2356
|
const childrenOf = (f) => {
|
|
@@ -2431,11 +2432,11 @@ function toField(f) {
|
|
|
2431
2432
|
...f.config ? { config: f.config } : {}
|
|
2432
2433
|
};
|
|
2433
2434
|
}
|
|
2434
|
-
var BRAND_KIT_NOTE = "`brandKit`
|
|
2435
|
+
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.";
|
|
2435
2436
|
var MODEL_IF_MATCH_NOTE = "optimisticVersion from get_content_model. When sent, the update applies only if the model is still at that version; a 409 means someone changed it since \u2014 read it again and re-apply, never retry blindly. Omitted, the last write wins.";
|
|
2436
2437
|
var ENTRY_META_NOTE = "SEO is native: set this entry's metaTitle, metaDescription, noindex, canonical, og, twitter, schemaType or schema in `meta` (never as model fields). `meta` MERGES into what is stored: a key you leave out is kept, null or an empty string clears it.";
|
|
2437
2438
|
var PAGE_HEAD_NOTE = "noindex, canonical, og and twitter set the page's head extras; each MERGES into what is stored (a key you leave out is kept, null or an empty string clears it).";
|
|
2438
|
-
var STRUCTURE_NOTE = 'ORGANISATION ONLY: Content sidebar folders never change a URL, slug, content or publish state; URL folders (page folders) are set in the dashboard. Folders nest at most 4 levels deep. Icons are lucide
|
|
2439
|
+
var STRUCTURE_NOTE = 'ORGANISATION ONLY: Content sidebar folders never change a URL, slug, content or publish state; URL folders (page folders) are set in the dashboard. Folders nest at most 4 levels deep. Icons are lucide names (a-z, 0-9, dashes); null clears one. SYSTEM FOLDERS "home:pages" (Other pages), "home:collections" (Shared) and "home:globals" (Settings) always exist and can never be moved or deleted (400 NAV_SYSTEM_FOLDER); rename_folder and set_icon work on them. TYPE PURITY (400 NAV_TYPE_MISMATCH): Other pages holds only pages at any depth, Shared only collections, Settings neither \u2014 globals are not sidebar items. ACCESS: changing the structure needs Admin or Developer, reading only content access; a 403 SCHEMA_ACCESS_REQUIRED means neither: stop and tell the user, do not retry. PINS sit above the folders in their own order, pages or collections; in a document a pin is folderId "home:root", a virtual id with no folder record (a folder record with that id is 400 NAV_SYSTEM_FOLDER). set_content_structure never refuses a kind mismatch: it returns `warnings` and `kindMismatches` (the single writes do \u2014 see `kind`). PAGE-BOUND MODELS: a page with fields has a bound model that is NOT a collection: never listed or filed, and the writes refuse it with 400 NAV_PAGE_BOUND_MODEL naming its page. Organise the PAGE instead, by its page id. ';
|
|
2439
2440
|
var STRUCTURE_FENCE = "ORGANISATION ONLY: Content sidebar folders never change a URL, slug, content or publish state, and nothing is rebuilt or delivered; URL folders (page folders) are set in the dashboard. Read the resource bettercms://playbook/structure before you organise anything: it holds the rules this refuses on \u2014 system folders, type purity, the Admin/Developer access check, root pins and page-bound models. A resource is never truncated; a long description is.";
|
|
2440
2441
|
function structureResult(d) {
|
|
2441
2442
|
const message = d?.message;
|
|
@@ -2443,18 +2444,18 @@ function structureResult(d) {
|
|
|
2443
2444
|
}
|
|
2444
2445
|
var GIT_TEXT = {
|
|
2445
2446
|
list_github_repos: "Every GitHub repository this workspace's connected GitHub App installations can reach, grouped by account. Use it to find the repository to import: each ACCOUNT carries the `installationId` that import_github_repo and fork_github_repo take, and each repository under it carries `owner`, `repo` and `defaultBranch`. An empty `accounts` list means nobody has connected GitHub to this workspace yet \u2014 tell the user to connect it from the dashboard (Project \u2192 Hosting \u2192 Connect GitHub) and call this again.",
|
|
2446
|
-
import_github_repo: "Connect an existing GitHub repository to the connected project
|
|
2447
|
+
import_github_repo: "Connect an existing GitHub repository to the connected project (the dashboard's Import from GitHub) \u2014 do it BEFORE writing any code for a user who already has a repo. It wires the project API key and variables into the repository, records the connection, assigns the site handle, imports the repo's bcms-content.json as content models when it has one, and queues the first build. Pass `installationId`, `owner` and `repo` from list_github_repos; `branch` defaults to the repository's own default branch and becomes the branch every deploy builds from. It only works on a repository this workspace's installations can already reach \u2014 for anyone else's (a public starter, a template, another account's site) use fork_github_repo. Read `warning` / `contentWarning` / `envWarning` back to the user verbatim when they come back (`envWarning` names the environment variables the repo documents in .env.example and the project lacks \u2014 the site renders empty or crashes until they are set) \u2014 the repository connected but cannot build, or will leave the Pages tab empty. Re-importing replaces the project's existing connection.",
|
|
2447
2448
|
fork_github_repo: "Fork ANY GitHub repository into the user's own account and import it in one step \u2014 how a user starts from a repository that is not theirs (a public starter, a template, another account's site). BetterCMS cannot wire a repository it has no installation on, so the copy has to live on their account first; this makes that copy, keeps the upstream link, and then connects it exactly as import_github_repo does. `installationId` names the DESTINATION account (from list_github_repos); `owner` and `repo` name the SOURCE, anywhere on GitHub. `name` renames the fork; `connect: false` forks without connecting. Forking is asynchronous: a 202 means the fork exists but GitHub is still copying it, and the answer tells you to call import_github_repo with the fork's owner and repo a moment later \u2014 never fork a second time.",
|
|
2448
2449
|
list_github_branches: "The connected repository's branches, plus `buildBranch` \u2014 the one the provisioned Action builds from. A commit on any other branch never reaches the live site, so check this before you push.",
|
|
2449
2450
|
list_github_commits: "The connected repository's commit history, newest first: sha, message, author, date and url. Defaults to the build branch; pass `branch` for another, `path` to narrow it to one file or directory, and `limit` (1-100, default 20). Read the head sha here before a push you want to be safe, and hand it back as push_to_github's `expectedHeadSha`.",
|
|
2450
|
-
push_to_github: "Commit files to the connected repository \u2014 a real git commit
|
|
2451
|
+
push_to_github: "Commit files to the connected repository \u2014 a real git commit written server-side by the GitHub App: no clone, remote or token of your own. Send `files` ([{path, content, encoding}]) and/or `deletePaths`, plus a `message`; both go into ONE commit, so a rename is atomic. It targets the BUILD branch by default, where the push starts the rebuild that goes live \u2014 poll get_deploy_status after it, and read the response's `note`, which says whether this push deploys. Pass `branch` to push somewhere else (a branch that does not exist yet is created from the build branch, or from `baseBranch`) and then open a pull request with create_github_pull_request. Pass `expectedHeadSha` from list_github_commits \u2014 a full sha or a prefix of one \u2014 to be refused with a 409 rather than silently overwrite a branch someone else moved while you worked. Limits, enforced BEFORE anything is written: 200 paths and 10 MB per commit, and `.git`, `.github/workflows`, `.env` and `.npmrc` are never writable \u2014 a refused patch writes nothing at all and names every offending path.",
|
|
2451
2452
|
create_github_pull_request: "Open a pull request on the connected repository. `head` is the branch you pushed; `base` defaults to the build branch, so merging it is what ships the change. Use this instead of pushing straight to the build branch whenever a human should review first, or when that branch is protected. Returns the number and the url to hand the user.",
|
|
2452
2453
|
list_github_pull_requests: "Pull requests on the connected repository: number, title, state, whether it merged, draft, head, base and url. `state` is 'open' (the default), 'closed' or 'all'. Poll it to find out whether the pull request you opened has landed."
|
|
2453
2454
|
};
|
|
2454
2455
|
var HOSTING_TEXT = {
|
|
2455
2456
|
list_env_vars: "The project's environment variables by NAME and scope (never a value), and `missing`: the ones the repository's .env.example declares that the project does not set. A site missing them builds and serves, then renders empty or crashes in the browser. Resolve `missing` by running `npx @bettercms-ai/cli@latest env push` in the repository: it uploads those values from the user's own .env files straight to BetterCMS, so they never pass through this conversation. Never read, print or ask the user to paste a value. When the values do not exist locally because the site reads its content from another CMS, move that content into BetterCMS instead (the bettercms skill's external-content reference). BetterCMS's own BCMS_* identifiers are supplied to every build and never need setting.",
|
|
2456
2457
|
list_hosting_connections: "The Vercel, Netlify and Cloudflare accounts connected to this workspace (id, provider, accountLabel, status; never a token), plus `connectUrl`, the dashboard page where a person connects one. Call it before deploy_to_host. If the provider the user wants has no connection with status `ok`, give the user `connectUrl` to connect it in their browser and stop until they have: you never handle OAuth or a host token yourself.",
|
|
2457
|
-
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build
|
|
2458
|
+
deploy_to_host: "Deploy this project to the user's own Vercel or Netlify account through their connection. BetterCMS builds the site and the host serves that build: no host CLI, no token to paste, no vercel.json rewrite; forms keep posting to the BetterCMS API. The first call creates the site on the host and makes it the project's host; a later call for the same provider redeploys that site (`redeployed: true`) instead of creating another. `connectionId` (from list_hosting_connections) defaults to the newest working one, `siteName` to the project's handle. A refusal carries a `code` and a `fixUrl` (not_connected, reconnect, pick_existing_site, grant_required, ADMIN_REQUIRED): each is fixed by the user at `fixUrl`, so relay it and stop. `deploymentQueued: false` means the project has no live build yet: deploy_project first, and that release reaches the host on its own. Never run the Vercel or Netlify CLI while a connection exists. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
|
|
2458
2459
|
get_host_deploy_status: "The connected project's recent deployments to Vercel, Netlify or Cloudflare, newest first: state (queued|deploying|live|failed), providerUrl, error, sha and targetName. Poll it after deploy_to_host until the newest one is `live`, whose `providerUrl` is the live URL to hand the user, or `failed`, whose `error` you read back to them."
|
|
2459
2460
|
};
|
|
2460
2461
|
function ok(summary, data) {
|
|
@@ -2754,7 +2755,8 @@ function buildToolDefs(deps) {
|
|
|
2754
2755
|
data: z.record(z.string(), z.unknown()).describe(
|
|
2755
2756
|
"field values keyed by field key. A nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value, \u2026 }, repeatable: [ { childKey: value }, \u2026 ] } \u2014 nonRepeatable holds the fixed-block values, repeatable is the list of item objects (omit a zone you didn't define). A primitive 'array' (itemType) is a plain list. An 'image' value is an asset URL or asset id (from upload_asset) \u2014 the server resolves it to { id, url, name, altText }. Read get_page first to see each field's zones."
|
|
2756
2757
|
),
|
|
2757
|
-
status: z.enum(["draft", "published"]).optional().describe("omit to leave status unchanged")
|
|
2758
|
+
status: z.enum(["draft", "published"]).optional().describe("omit to leave status unchanged"),
|
|
2759
|
+
replace: z.boolean().optional().describe("true replaces the whole entry data, removing omitted fields; default false merges by top-level key")
|
|
2758
2760
|
});
|
|
2759
2761
|
const getEntryInput = z.object({
|
|
2760
2762
|
entryId: z.string().min(1).describe("content entry id")
|
|
@@ -3248,7 +3250,7 @@ ${d.summary.outline}` : "Content structure.", d);
|
|
|
3248
3250
|
def(
|
|
3249
3251
|
"suggest_content_structure",
|
|
3250
3252
|
"Propose the standard Content structure",
|
|
3251
|
-
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the
|
|
3253
|
+
`READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the standard over the project's pages, collections, references and built routes: the home page and each collection's list page pinned, one folder per content family named for its detail type (Blog posts, Case studies, Products with its taxonomies and an Attributes subfolder), ordered by entry count (or the caller's nav order), shared taxonomies in Shared, every remaining page in Other pages, at most 2 levels deep. Page-bound models are never filed. Returns { doc, version, hasDocument, outline, rationale }. To apply it: show the user the outline, then call set_content_structure with this doc and this version (the If-Match; a 412 means someone changed the sidebar, so suggest again). When hasDocument is true the project already has a structure somebody made: never replace it without the user's yes; file only what is new with move_to_folder instead.`,
|
|
3252
3254
|
z.object({
|
|
3253
3255
|
maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
|
|
3254
3256
|
}).shape,
|
|
@@ -3321,7 +3323,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3321
3323
|
def(
|
|
3322
3324
|
"set_brand_assets",
|
|
3323
3325
|
"Point the brand's logo or favicon at a media asset",
|
|
3324
|
-
`Point the brand's logo (mark) or favicon at a media asset of this project
|
|
3326
|
+
`Point the brand's logo (mark) or favicon at a media asset of this project \u2014 upload it to this project's media library first if it is not there. A slot a PERSON chose is refused without replace: true, and the refusal says so. ${BRAND_KIT_NOTE}`,
|
|
3325
3327
|
z.object({
|
|
3326
3328
|
mark: z.string().min(1).nullable().optional().describe("media asset id for the logo; null clears it"),
|
|
3327
3329
|
favicon: z.string().min(1).nullable().optional().describe("media asset id for the favicon; null clears it"),
|
|
@@ -3332,7 +3334,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3332
3334
|
def(
|
|
3333
3335
|
"set_brand_graphics",
|
|
3334
3336
|
"Add, replace or remove the brand's gradients",
|
|
3335
|
-
`Add, replace or remove the brand's gradients \u2014 the blobs and washes a site uses as backgrounds. They are TOKENS, not media
|
|
3337
|
+
`Add, replace or remove the brand's gradients \u2014 the blobs and washes a site uses as backgrounds. They are TOKENS, not media. A stop naming a colour the kit does not have is refused; add it to \`extras\` first. Upsert matches BY KEY. ${BRAND_KIT_NOTE}`,
|
|
3336
3338
|
z.object({
|
|
3337
3339
|
upsert: z.array(z.object({
|
|
3338
3340
|
key: z.string().min(1).describe("token key: lowercase letters, digits and hyphens. Emitted as --brand-gradient-<key>"),
|
|
@@ -3541,7 +3543,7 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3541
3543
|
def(
|
|
3542
3544
|
"set_binding_mode",
|
|
3543
3545
|
"Set how the site's bindings are resolved",
|
|
3544
|
-
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which works once (at import, when the CMS values equal the built copy) and breaks the first time anyone edits a value. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order is push \u2192 release \u2192 get_binding_report shows mode 'text-match' with 0 unmatched \u2192 set_binding_mode \u2192 release again \u2192 get_binding_report shows mode 'declared'. Flipping back is the same call. REQUIRES
|
|
3546
|
+
"Switch this project between the two binding resolvers, from the NEXT release on. `declaredBindings: true` makes the annotator trust the template's own data-bcms-field / data-bcms-props and never guess from rendered text \u2014 the durable state; `false` returns to text-matching, which works once (at import, when the CMS values equal the built copy) and breaks the first time anyone edits a value. Call it ONLY after every page's copy is declared in the template: undeclared fields stop being editable. The order is push \u2192 release \u2192 get_binding_report shows mode 'text-match' with 0 unmatched \u2192 set_binding_mode \u2192 release again \u2192 get_binding_report shows mode 'declared'. Flipping back is the same call. REQUIRES artifact:write (the authority that deploys the site): this decides what every future release does to every page. On a workspace-wide connection pass `projectId`: the scope is there, but the human who authorized the connection must be able to publish THAT project or it is refused with 403 PROJECT_PUBLISH_DENIED. Playbook section 13.",
|
|
3545
3547
|
z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
|
|
3546
3548
|
async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
|
|
3547
3549
|
),
|
|
@@ -3566,10 +3568,10 @@ ${d.outline}` : "Proposed Content structure.", d);
|
|
|
3566
3568
|
def(
|
|
3567
3569
|
"submit_conversion_receipt",
|
|
3568
3570
|
"Record what the conversion codemod could and could not do",
|
|
3569
|
-
"Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert
|
|
3571
|
+
"Hand BetterCMS the codemod's own account of a run, so the meter can say WHY a path is undeclared. Submit the receipt `npx @bettercms-ai/convert --receipt out.json` wrote as `{ briefDigest, receipt }`, for the SAME `briefDigest` get_conversion_brief { complete: true } returned (its shape: see `receipt`). \u{1F534} A RECEIPT WITH PENDING PATHS IS A PROGRESS REPORT, NOT A FINISH LINE: the response answers `complete` (`sourceComplete` is the receipt's own verdict; `liveChecks` gives the last release's `structureUnstamped` route count and `reportSha`) and `pendingTotal`, and echoes the first 40 pending rows with their `fix` \u2014 `{ action, file, line, col?, snippet, why?, kind? }`, `action` one of `wrap-span`, `declare-attr`, `bind-expression`, `declare-richtext`, `bind-data`, `manual`; `snippet` and `why` say what to change there. On a `declare-richtext` with `kind: document`, bind the ONE element wrapping every block of the Body, never the paragraph holding its first. Apply every fix, rerun, resubmit: only `complete: true` ends a conversion \u2014 never report a site converted on less. It is a RECORD, not a release: nothing on the site changes; the meter reads it on the get_binding_report after the next deploy. A 404 `unknown-brief` means that digest was never issued here. \u{1F534} SUBMIT THE `--forms` RECEIPT TOO, AS A SECOND CALL: a `--forms` run's receipt has all-zero `paths` and a `forms` block; write it to its own file (`--receipt forms-receipt.json`) and submit it under the same `briefDigest` (stored beside the binding receipt, never counted in coverage), then publish every form its `forms.pending` names. \u{1F534} THE SITE'S DESIGN IS NOT YOURS TO CHANGE: run `npx @bettercms-ai/convert --verify-styles --base <commit before the conversion> --root .` and include its `styleEdits` in the receipt; one listing any changed class, className or style is refused with 409 `style-edits` naming every token \u2014 restore them. Requires artifact:write.",
|
|
3570
3572
|
z.object({
|
|
3571
3573
|
briefDigest: z.string().min(1).describe("The `briefDigest` get_conversion_brief { complete: true } returned. Must match the receipt's own."),
|
|
3572
|
-
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim.")
|
|
3574
|
+
receipt: z.record(z.string(), z.unknown()).describe("The receipt `npx @bettercms-ai/convert --receipt out.json` wrote, verbatim: `paths: { declared, rewritten, alreadyDeclared, pending[{ route, scope, path, kind, file, reason, message, fix }] }` plus `styleEdits`. `paths.declared` must equal rewritten + alreadyDeclared + pending.length; each pending `reason` is the converter's own (IN_EXPRESSION, AMBIGUOUS_LITERAL, REPEATER_FIXED_LENGTH, PARSE_ERROR, \u2026); a path with no row reads `not-declared`.")
|
|
3573
3575
|
}).shape,
|
|
3574
3576
|
async (c, a) => ok("Recorded the conversion receipt.", await data(c, "POST", `/management/projects/current/conversion-receipt`, { briefDigest: a.briefDigest, receipt: a.receipt }))
|
|
3575
3577
|
),
|
|
@@ -3664,7 +3666,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3664
3666
|
def(
|
|
3665
3667
|
"deploy_project",
|
|
3666
3668
|
"Deploy new source/build",
|
|
3667
|
-
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git
|
|
3669
|
+
"Deploy new source/build for the connected project and make it live at its <handle>.bettercms.site. Pass a .tgz or .zip of the project as a base64 string in `data`: SOURCE (has package.json) is built server-side in an isolated sandbox; a prebuilt static site is served as-is. Returns the release id + sha \u2014 then poll get_deploy_status until it is live. IMPORTANT \u2014 you MUST exclude node_modules, .git and build output/caches (dist, build, .next, .astro, .cache) from the archive: the source is reinstalled and built server-side, and the ~100 MB upload ceiling is enforced in transit (a 413/502 with no server-side detail) \u2014 server-side stripping runs AFTER the upload and cannot rescue an over-limit body. For a LARGE archive (or a 413/502) use create_deploy_upload + deploy_from_upload, which uploads straight to storage with no size ceiling. Returns `canvas.lane` (the editor's live-preview lane: `bridge` / `draft-route` / `none`; playbook section 11) and `editorUrl`, the visual editor to hand the user. On a workspace-wide connection pass `projectId` (from list_projects) to say which site this ships to.",
|
|
3668
3670
|
z.object({ data: z.string().min(1).describe("base64 .tgz/.zip of the project"), mimeType: z.string().optional() }).shape,
|
|
3669
3671
|
async (c, a) => ok("Deploy queued.", await raw(c, `/management/projects/deploy`, s(a.data), a.mimeType))
|
|
3670
3672
|
),
|
|
@@ -3803,14 +3805,14 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3803
3805
|
def(
|
|
3804
3806
|
"get_binding_report",
|
|
3805
3807
|
"Check what on the live site is editable",
|
|
3806
|
-
"The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; this returns
|
|
3807
|
-
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in.") }).shape,
|
|
3808
|
+
"The receipt for 'is this site actually EDITABLE?'. Every release scans the built HTML for the element that renders each CMS field value; per slot this returns `mode` ('text-match' = bindings guessed from rendered text, 'declared' = the template declares them), what was bound, and `unmatched` \u2014 each path with its reason (not-declared / ambiguous-text / no-element). DEPLOY FIRST: before any release there is no report (pages 0, mode null, refreshRequired true). `refreshReason`: `never-generated` \u2014 only a release BetterCMS builds and serves writes a report, so a HEADLESS site never gets one, redeploy or not (verify it by fetching it); `outdated` \u2014 the next release rewrites it. It certifies only that every non-empty field has SOME element carrying its path; it cannot see copy that was never modelled, so diff each route's visible text against its entry values before calling a page done. `unaddressable` is that measure: visible text on the live site no field owns, measured server-side on every inspected page each release, by route and bucket, biggest first. Over `count / visible > 0.02` is copy the CMS cannot see: fix it in the SOURCE (wrap the run in an element the codemod can bind, or declare `<BcmsField path=\u2026>`), deploy, re-read \u2014 playbook section 11. `builtRoutes` lists the routes the live build has HTML for (null = cannot say). `canvas.lane` is the editor's live-preview lane \u2014 `bridge`, `draft-route` or `none` (then get_next_steps carries the recipe). `skipReasons` says why pages were not inspected; `coverage.error` = `nothing-bound` means no element of this build carries a binding, so the percentage beside it describes a site this build is not.",
|
|
3809
|
+
z.object({ slot: z.enum(["current", "staging"]).optional().describe("which release tree to read \u2014 everything here, `unaddressable` included, is measured PER SLOT, so on a promote-gated project pass 'current' to read the tree the editor frames. Defaults to the slot this project's releases land in. The response's `sha` is the build the report describes; `refreshRequired` is true when that is not the build this slot serves.") }).shape,
|
|
3808
3810
|
async (c, a) => ok("Binding report.", await data(c, "GET", `/management/projects/current/binding-report${q({ slot: a.slot })}`))
|
|
3809
3811
|
),
|
|
3810
3812
|
def(
|
|
3811
3813
|
"get_conversion_brief",
|
|
3812
3814
|
"Get the brief for making this site's bindings durable",
|
|
3813
|
-
"The per-project brief for making this site's bindings DURABLE \u2014 read it before you touch the templates. Returns what already exists in the CMS: every live page with its route,
|
|
3815
|
+
"The per-project brief for making this site's bindings DURABLE \u2014 read it before you touch the templates. Returns what already exists in the CMS: every live page with its route, every bindable field path with its `label`, `kind`, the CMS value (`current`) and the copy the repo renders today (`original`), the exact attributes to declare, and the ordered steps. Call it for any site whose pages were DERIVED at import, and whenever get_next_steps reports `bindings-not-declared`. It REPLACES re-registering a schema: these pages, fields and values exist already, so create_page / add_page_field / create_content_model would build a second schema over the first \u2014 edit values with set_page_content instead. `lane`: 'git-connected' = pull_project_source returns a repo; 'archive' = a tarball. Full recipe: section 13 of bettercms://playbook/schema; get_binding_report is the receipt that says you finished. Pass `complete: true` before running the CODEMOD (`npx @bettercms-ai/convert`): the brief UNCAPPED and paged, its full path list pinned under a `briefDigest` \u2014 what the coverage meter measures the build against. The complete brief also carries `forms` (id, status, fields, submitUrl), which `npx @bettercms-ai/convert --forms --brief brief.json --root . --receipt forms-receipt.json` wires into the repository's own <form> elements after the binding pass; read its `forms.pending` and publish every form its `forms.notes` names in the Forms tab (a draft form rejects every submission with a 403).",
|
|
3814
3816
|
z.object({
|
|
3815
3817
|
complete: z.boolean().optional().describe("true = the COMPLETE brief for a codemod: nothing truncated, no page omitted, paged 50 pages at a time. Page 1 pins the path list the coverage meter measures against."),
|
|
3816
3818
|
cursor: z.string().optional().describe("The `cursor` from the previous page. Implies complete. A 409 BRIEF_CHANGED means the brief was re-derived mid-pagination \u2014 start again with no cursor.")
|
|
@@ -3820,21 +3822,21 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3820
3822
|
def(
|
|
3821
3823
|
"get_conversion_plan",
|
|
3822
3824
|
"Get the approved conversion to apply",
|
|
3823
|
-
"The APPROVED conversion a human reviewed in the BetterCMS dashboard: the exact new contents of each template file, already checked against this project's real field paths. Apply it instead of writing the bindings by hand. Check out `baseHeadOid` (the exact commit it was written against
|
|
3825
|
+
"The APPROVED conversion a human reviewed in the BetterCMS dashboard: the exact new contents of each template file, already checked against this project's real field paths. Apply it instead of writing the bindings by hand. Check out `baseHeadOid` (the exact commit it was written against), branch from there, write each file's `content` verbatim (whole file, no merge, no reformatting), then push or deploy however this project ships. `stale.head` / `stale.brief` say the repository or the CMS moved since it was approved: stop and ask for a fresh proposal. `receipt.paths.pending` lists every field the codemod could NOT bind, each with a reason (`DIALECT_UNSUPPORTED`, `PROP_TARGET_NOT_FOUND`, `REPEATER_FIXED_LENGTH`, \u2026) \u2014 apply the plan first, then bind those by hand. Then finish as a hand conversion does: get_binding_report until `unmatched` is empty, then set_binding_mode { declaredBindings: true } and release once more. 404 with `code: \"no-approved-plan\"` means nobody has approved one: use get_conversion_brief and do the conversion yourself. Requires artifact:write; on a workspace-wide connection pass `projectId`.",
|
|
3824
3826
|
z.object({}).shape,
|
|
3825
3827
|
async (c) => ok("Approved conversion plan.", await data(c, "GET", `/management/projects/current/conversion-plan`))
|
|
3826
3828
|
),
|
|
3827
3829
|
def(
|
|
3828
3830
|
"get_site_composition",
|
|
3829
3831
|
"Check every page is assembled from registered components",
|
|
3830
|
-
"The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live
|
|
3832
|
+
"The receipt for 'is every page assembled from registered components?' \u2014 read-only, computed live. Call it before a whole-site componentize run (playbook \xA712) and after it, as the proof. Per page: `kind` ('blocks' | 'fields-only' | 'empty'), a block census, `nonComponentBlocks` (loose TOP-LEVEL bands the editor's section lane cannot outline), and every `component` placement with the component's `status` ('published' | 'draft' | 'missing' \u2014 a draft renders as an EMPTY STRING on the live site), whether it is `inPicker` (a component with no sectionType is dropped from 'Add a section'), and any `emptyRequiredProps` \u2014 `required` is not enforced on write, so only this read reports it. Site-wide: `components` counts published/draft, `awaitingEvidence` and `awaitingApproval` (a HUMAN approves in the BetterCMS dashboard \u2014 no tool here can, and there is no bulk approve), plus `notInPicker` and `unused` ids; `layout.placements` is the chrome placed in the Global Layout draft. `verdict.everyPageComposed` with `verdict.reasons` is the answer to give the user. Pages are read DRAFT-inclusive: check each page's own `status` before claiming the site is live.",
|
|
3831
3833
|
z.object({}).shape,
|
|
3832
3834
|
async (c) => ok("Site composition.", await data(c, "GET", `/management/projects/current/composition`))
|
|
3833
3835
|
),
|
|
3834
3836
|
def(
|
|
3835
3837
|
"get_componentize_plan",
|
|
3836
3838
|
"Get the plan for turning this site's sections into components",
|
|
3837
|
-
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing
|
|
3839
|
+
"What this site's SECTIONS would become as components \u2014 a proposal that creates nothing and is computed live. For a site whose pages were DERIVED at import, each top-level field GROUP is one section: `hero-*` and `faq-*` keys, and the repeaters the import folded (`group-*`). Per page: each section's `groupKey`, `sectionType` family, leaf `fields` with their CMS values, `shapeHash`, and either the component already rendering it (`reuse.componentId`) or the proposed one (`reuse.proposedSlug`). Under `components`, a NEW one carries the exact `props` and `blockJson` create_component would take; an EXISTING one only `componentId`, `slug` and `name` (nothing is written for it). Every page gets its OWN components; a locale copy (`/fr`) reuses its default-locale page's. A COLLECTION TEMPLATE (dynamic page, `pattern: \"{slug}\"`) is planned ONCE, every prop reading the rendered entry: only its prose body and object-row lists are bands; frontmatter (author, images, tags, dates) stays entry fields. `pending` says why a group is not offered: `NO_GROUP_ROOT` (keys are still derive-lane names like `h1-welcome` \u2014 rename them into families first), `NOT_A_SECTION` (a lone scalar or the page's own metadata; `detail: ENTRY_METADATA` = collection-template frontmatter, `detail: DECLINED` = declined with componentize_sections `decline`), `IMAGE_AS_TEXT` (a text field holding an image path \u2014 make it an image field), `ALREADY_COMPONENTIZED`, `EMPTY_GROUP`, `NESTED_REPEATER` (three deep; two levels become a nested `table` sub-field), `PAGE_NOT_EMPTY` (blocks this lane does not own and will not overwrite). Chrome (`nav-`/`footer-` keys, anything promoted into the Layout) is NEVER a section: edit it through the Layout. READ `summary: true` FIRST \u2014 the full plan runs to hundreds of KB; `paged` pages it (409 PLAN_CHANGED = start again); the unpaged full read is what `--plan plan.json` takes. Keep the `digest` \u2014 componentize_sections refuses any other.",
|
|
3838
3840
|
z.object({
|
|
3839
3841
|
summary: z.boolean().optional().describe("true = no values: each section's groupKey, family, pending and component slug. Read this first."),
|
|
3840
3842
|
paged: z.boolean().optional().describe("true = 10 pages at a time; the response carries `page`, `pageCount` and a `cursor` for the next."),
|
|
@@ -3845,7 +3847,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3845
3847
|
def(
|
|
3846
3848
|
"componentize_sections",
|
|
3847
3849
|
"Turn this site's derived sections into components",
|
|
3848
|
-
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections
|
|
3850
|
+
"Turn this site's derived sections into components. CONFIRM WITH THE USER FIRST: show them get_componentize_plan's sections, how many components it will create and which pages it will rewrite; `dryRun` first. It creates each proposed component as a DRAFT and replaces each page's DRAFT blocks with ordered `component` instances, one per group, each carrying `props.bind: \"<groupKey>\"` \u2014 the page field group that already holds the copy. Nothing is copied or moved: the page keeps its `fields`, values and bindings, so click-to-edit and the coverage meter do not change (`copy: \"instance\"` moves the words onto each placement instead \u2014 see `copy`). Pass the plan's `digest`; a 409 `stale-plan` means the site changed: read the plan again, show the user what changed and confirm again. Read `components.wouldDuplicate` before applying: each id is an existing same-family component this run would sit a NEW one beside, because reuse is by identity and slug, never by family. Re-running is safe: an already-placed group returns in `sections.pending` as ALREADY_COMPONENTIZED, with no second component. DRAFTS ONLY \u2014 an unpublished component renders as an EMPTY STRING on the live site, so publish_component each one and publish the pages, then run `npx @bettercms-ai/convert --componentize` in the repo so its templates render these sections from `pages[].blocks`. Every component it CREATES is filed into a Component Group (see `group`). When the user says a proposed group is NOT a section, pass `decline: true` with those `sections` (and `pageIds`): nothing is componentized, the plan reports them `NOT_A_SECTION` / `DECLINED` and stops proposing them, and a plan that proposes nothing undeclined is done; `decline: false` offers them again.",
|
|
3849
3851
|
z.object({
|
|
3850
3852
|
digest: z.string().min(1).describe("The `digest` get_componentize_plan returned. A different one is refused with 409 stale-plan."),
|
|
3851
3853
|
pageIds: z.array(z.string().min(1)).optional().describe("Componentize only these pages (ids from the plan). Omit for every page the plan lists."),
|
|
@@ -3853,7 +3855,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
3853
3855
|
sections: z.array(z.string().min(1)).optional().describe("Act only on these section groupKeys (from the plan). Every other section on the page keeps the placement it already has."),
|
|
3854
3856
|
copy: z.enum(["bind", "instance"]).optional().describe("Who owns each section's copy. 'bind' (default) leaves it in the page fields. 'instance' is the page-builder model: each placement takes that group's CURRENT draft values into its own `props.overrides` (a repeater becomes one table prop holding every row), records which page path each came from in `props.source`, and drops `props.bind`; the page's fields are KEPT and marked `origin: \"componentized\"`, so nothing is lost and the editor stops showing a second place to type the same words."),
|
|
3855
3857
|
dryRun: z.boolean().optional().describe("true = return the receipt without writing anything. Do this first."),
|
|
3856
|
-
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (by NAME
|
|
3858
|
+
group: z.string().min(1).max(100).optional().describe("File the components this run creates into this Component Group (a folder of the dashboard's Components tab; by NAME, created when missing), else its section family: nav, header, footer or menu \u2192 Layout, an unnamed section \u2192 Sections, otherwise the family name (Hero, FAQ). A reused component keeps its Group; the receipt's `components.groups` lists the Groups filed into."),
|
|
3857
3859
|
decline: z.boolean().optional().describe("true = record `sections` as NOT sections (nothing is componentized; the plan stops proposing them). false = offer them again. Needs `sections`.")
|
|
3858
3860
|
}).shape,
|
|
3859
3861
|
async (c, a) => ok("Componentized the sections.", await data(c, "POST", `/management/projects/current/componentize`, { digest: a.digest, pageIds: a.pageIds, pages: a.pages, sections: a.sections, copy: a.copy, dryRun: a.dryRun, group: a.group, decline: a.decline }))
|
|
@@ -4168,7 +4170,7 @@ ${res.warnings.join("\n")}` : summary, res.data);
|
|
|
4168
4170
|
def(
|
|
4169
4171
|
"get_deploy_status",
|
|
4170
4172
|
"Get deploy/build status",
|
|
4171
|
-
"Get the connected project's deploy/build status: state (idle|queued|building|categorizing|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `categorizing` means the bytes are live but the import's setup lanes (content, pages, bindings) are still running, so `publishing` is still true and the site is not ready to edit yet. Poll this after deploy_project until state is idle with your sha live. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
|
|
4173
|
+
"Get the connected project's deploy/build status: state (idle|queued|building|categorizing|failed), whether it's publishing, the live commit sha, when it went live, and any build error. `categorizing` means the bytes are live but the import's setup lanes (content, pages, bindings) are still running, so `publishing` is still true and the site is not ready to edit yet. Poll this after deploy_project until state is idle with your sha live. `release` is the newest release (id, sha, status, phaseMarks, error, createdAt): while an uploaded build ingests or releases, state is `building`, `release.sha` the new sha and `lastLiveSha` still the old one; a failed deploy shows `release.status` failed with its `error`, and `held` state (`held.by`: staff, archived or no-subdomain) means the release waits until that is cleared. Also returns `canvas.lane` (the editor's live-preview lane for this site: `bridge` / `draft-route` / `none`) and `editorUrl`, the visual editor to hand the user.",
|
|
4172
4174
|
z.object({}).shape,
|
|
4173
4175
|
async (c) => ok("Deploy status.", await data(c, "GET", `/management/projects/deploy-status`))
|
|
4174
4176
|
),
|
|
@@ -4289,7 +4291,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4289
4291
|
name: "create_content_model",
|
|
4290
4292
|
config: {
|
|
4291
4293
|
title: "Create a content model (reusable schema)",
|
|
4292
|
-
description: "Create a content model \u2014 a reusable schema for a dynamic collection (Blog, Products, Testimonials). `fields` may NEST: type 'group' = one nested object of child fields; type 'repeater' = a repeatable array of child objects. Put child fields in each group/repeater's own `fields` (any depth). Pass kind:'block' to create a BLOCK type instead \u2014 see that argument. `fieldsets` ([{id, name}]) are the model's editor cards; a field joins one with `fieldsetId`. The slug must be unique in this project and branch: a taken one is refused with 409 `slug_taken` and free `suggestions`. " + RICH_TEXT_NOTE,
|
|
4294
|
+
description: "Create a content model \u2014 a reusable schema for a dynamic collection (Blog, Products, Testimonials). " + PAGES_NOT_COLLECTIONS_NOTE + " `fields` may NEST: type 'group' = one nested object of child fields; type 'repeater' = a repeatable array of child objects. Put child fields in each group/repeater's own `fields` (any depth). Pass kind:'block' to create a BLOCK type instead \u2014 see that argument. `fieldsets` ([{id, name}]) are the model's editor cards; a field joins one with `fieldsetId`. The slug must be unique in this project and branch: a taken one is refused with 409 `slug_taken` and free `suggestions`. " + RICH_TEXT_NOTE,
|
|
4293
4295
|
inputSchema: createModelInput.shape
|
|
4294
4296
|
},
|
|
4295
4297
|
handler: guard(
|
|
@@ -4353,7 +4355,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4353
4355
|
name: "add_page_field",
|
|
4354
4356
|
config: {
|
|
4355
4357
|
title: "Add a field to a page",
|
|
4356
|
-
description: "Append a field to an existing page's schema (Home, About, a blog template, etc.). Additive \u2014 the API rejects a key that already exists and never overwrites or retypes existing fields. Use this when the target is a page (singleton or dynamic); use add_field when the target is a content model. For a section/zone, add ONE 'group' (fixed block) or 'repeater' (repeating list) field carrying its child `fields` \u2014 don't add the zone's inner fields as separate top-level fields.",
|
|
4358
|
+
description: "Append a field to an existing page's schema (Home, About, a blog template, etc.). Additive \u2014 the API rejects a key that already exists and never overwrites or retypes existing fields. Use this when the target is a page (singleton or dynamic); use add_field when the target is a content model. For a section/zone, add ONE 'group' (fixed block) or 'repeater' (repeating list) field carrying its child `fields` \u2014 don't add the zone's inner fields as separate top-level fields. 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.",
|
|
4357
4359
|
inputSchema: addPageFieldInput.shape
|
|
4358
4360
|
},
|
|
4359
4361
|
handler: guard(
|
|
@@ -4375,7 +4377,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4375
4377
|
name: "create_content_entry",
|
|
4376
4378
|
config: {
|
|
4377
4379
|
title: "Create a content entry",
|
|
4378
|
-
description: "Create a content entry under a model. Pass its field VALUES in `data`, keyed by field key \u2014 INCLUDE ALL REQUIRED FIELDS (create validates them). New entries are drafts; pass status:'published' to take it live. Read get_content_model first for the field keys and which are required.",
|
|
4380
|
+
description: "Create a content entry under a model. " + PAGES_NOT_COLLECTIONS_NOTE + " Pass its field VALUES in `data`, keyed by field key \u2014 INCLUDE ALL REQUIRED FIELDS (create validates them). New entries are drafts; pass status:'published' to take it live. Read get_content_model first for the field keys and which are required.",
|
|
4379
4381
|
inputSchema: createEntryInput.shape
|
|
4380
4382
|
},
|
|
4381
4383
|
handler: guard(
|
|
@@ -4399,14 +4401,15 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4399
4401
|
name: "set_page_content",
|
|
4400
4402
|
config: {
|
|
4401
4403
|
title: "Set a page's field values (content)",
|
|
4402
|
-
description: "Set a page's field VALUES \u2014 the actual content. For a SINGLETON page (Home, About, Site Settings) this creates or updates its one entry, so call it again to edit. `data` is keyed by field key: a nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value }, repeatable: [ { childKey: value } ] }; a primitive 'array' is a plain list; an 'image' value is an asset URL. Read the schema first with get_page. This is how you populate Home/About/Settings \u2014 create_content_entry is for dynamic collections only.",
|
|
4404
|
+
description: "Set a page's field VALUES \u2014 the actual content. For a SINGLETON page (Home, About, Site Settings) this creates or updates its one entry, so call it again to edit. `data` is keyed by field key: a nested 'array' (zone) value is an OBJECT { nonRepeatable: { childKey: value }, repeatable: [ { childKey: value } ] }; a primitive 'array' is a plain list; an 'image' value is an asset URL. Read the schema first with get_page. This is how you populate Home/About/Settings \u2014 create_content_entry is for dynamic collections only. Merges by top-level key (a zone value replaces that whole zone); pass replace:true to remove omitted fields.",
|
|
4403
4405
|
inputSchema: setPageContentInput.shape
|
|
4404
4406
|
},
|
|
4405
4407
|
handler: guard(
|
|
4406
4408
|
async (args) => withClient(async (client) => {
|
|
4407
4409
|
const entry = await client.setPageContent(args.pageId, {
|
|
4408
4410
|
data: args.data,
|
|
4409
|
-
...args.status !== void 0 ? { status: args.status } : {}
|
|
4411
|
+
...args.status !== void 0 ? { status: args.status } : {},
|
|
4412
|
+
...args.replace !== void 0 ? { replace: args.replace } : {}
|
|
4410
4413
|
});
|
|
4411
4414
|
return ok(
|
|
4412
4415
|
`Set content on page ${args.pageId} (entry ${entry.id}, status ${entry.status}).`,
|
|
@@ -4743,7 +4746,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4743
4746
|
name: "create_components",
|
|
4744
4747
|
config: {
|
|
4745
4748
|
title: "Create many components in one call",
|
|
4746
|
-
description: "Create up to 50 components in ONE call \u2014 the same input as create_component, once per item. Prefer it over create_component
|
|
4749
|
+
description: "Create up to 50 components in ONE call \u2014 the same input as create_component, once per item. Prefer it over create_component for more than three: a whole-site componentize run (playbook \xA712) is dozens. Each item reports its own `{ ok, id, slug, error }`: a failure does NOT stop the run and the successful rows stay, so read the receipt and retry only the failures (a 409 on `slug` means that name is taken \u2014 change it, do not re-run the batch). Every component lands as a DRAFT, exactly as create_component does \u2014 it renders as NOTHING on the live site until it is published, which needs the owner's approval in the dashboard. Pass `group` on each item to file it into the folder editors browse. DECLARE A PROP \u2014 WITH A `target` \u2014 for every string, link and image a marketer will ever touch: only the leaves a declared prop TARGETS are click-to-edit; copy no prop points at is unreachable from canvas and dock alike. A page is composed of SECTIONS; the full doctrine is in the bettercms://playbook/schema resource, \xA712.",
|
|
4747
4750
|
inputSchema: createComponentsInput.shape
|
|
4748
4751
|
},
|
|
4749
4752
|
handler: guard(
|
|
@@ -4769,7 +4772,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4769
4772
|
name: "publish_components",
|
|
4770
4773
|
config: {
|
|
4771
4774
|
title: "Publish many components in one call",
|
|
4772
|
-
description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it, and it has no bulk form. It is never a component you just made with create_component in no variant group \u2014 that is dashboard-managed and publishes on the preflight alone. The refusal is for a REPO-managed component without exact validation evidence and a human's approval, or for a variant-group member (the provisioned navigation and footer) whose group's canonical-input contract is not published.
|
|
4775
|
+
description: "Publish up to 100 components in ONE call \u2014 the same act as publish_component, once per item, copying each DRAFT definition to the live copy. Each item reports `{ ok, status, error, readiness }` and a failure never stops the run. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it, and it has no bulk form. It is never a component you just made with create_component in no variant group \u2014 that is dashboard-managed and publishes on the preflight alone. The refusal is for a REPO-managed component without exact validation evidence and a human's approval, or for a variant-group member (the provisioned navigation and footer) whose group's canonical-input contract is not published. The item's `readiness` names what is missing \u2014 relay that to the user as the list of what only they can approve. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish; 422 means the publish would break a Layout, and the item names which.",
|
|
4773
4776
|
inputSchema: publishComponentsInput.shape
|
|
4774
4777
|
},
|
|
4775
4778
|
handler: guard(
|
|
@@ -4804,7 +4807,7 @@ ${notes.join("\n")}` : summary, created);
|
|
|
4804
4807
|
name: "compose_pages",
|
|
4805
4808
|
config: {
|
|
4806
4809
|
title: "Set many pages' block composition in one call",
|
|
4807
|
-
description: "Give up to 50 pages their block composition in ONE call \u2014 the same write as update_page's `blockJson`, once per item, and it REPLACES each page's whole block array. Address a page by `pageId` or by `slug`. Each item reports `{ ok, pageId, slug, error }` and a failure does not stop the run. BEFORE writing anything it reads this project's component catalogue once and REFUSES any item placing a componentId the project does not have \u2014
|
|
4810
|
+
description: "Give up to 50 pages their block composition in ONE call \u2014 the same write as update_page's `blockJson`, once per item, and it REPLACES each page's whole block array. Address a page by `pageId` or by `slug`. Each item reports `{ ok, pageId, slug, error }` and a failure does not stop the run. BEFORE writing anything it reads this project's component catalogue once and REFUSES any item placing a componentId the project does not have \u2014 its missing sections would render as empty strings with no error anywhere. Writes DRAFTS: publish each page with update_page status:'published' afterwards. A page is composed of SECTIONS: a recurring band is a component with a `sectionType`, a one-off band is a `section` block whose `props.children` hold its blocks \u2014 never loose top-level heading/text/image blocks, which no editor can move, name or swap as a unit. The full doctrine is in the bettercms://playbook/schema resource, \xA712.",
|
|
4808
4811
|
inputSchema: composePagesInput.shape
|
|
4809
4812
|
},
|
|
4810
4813
|
handler: guard(
|
|
@@ -4908,7 +4911,7 @@ ${lines.join("\n")}`, found);
|
|
|
4908
4911
|
name: "set_component_source",
|
|
4909
4912
|
config: {
|
|
4910
4913
|
title: "Record which file implements a component",
|
|
4911
|
-
description: "Call this RIGHT AFTER you write or locate
|
|
4914
|
+
description: "Call this RIGHT AFTER you write or locate a component's code in the app's repository: it records which file renders it, so the dashboard's Output button can preview and validate it with no setup (without it nothing is validated). Record ONLY a file that renders exactly this component from its own fields: a component whose props are this component's field keys (`kind: 'file'`, the default), or a section the codemod extracted with `npx @bettercms-ai/convert --componentize` \u2014 a file starting `// @bettercms-ai/convert section` whose props are `{ blockId, bind, overrides, page }` (`kind: 'section'`). Never record a different component that merely contains this markup (e.g. a card that needs a `post` object): it cannot render from these fields and validation fails. If the markup is inline in a page, extract it into a section first. `path` is relative to the app root (e.g. 'src/components/sections/Hero.astro'); `export` is the export name, omitted for a default export. Call it again if the file moves; call clear_component_source if a recorded file is wrong.",
|
|
4912
4915
|
inputSchema: setComponentSourceInput.shape
|
|
4913
4916
|
},
|
|
4914
4917
|
handler: guard(
|
|
@@ -5013,7 +5016,7 @@ ${lines.join("\n")}`, found);
|
|
|
5013
5016
|
name: "publish_component",
|
|
5014
5017
|
config: {
|
|
5015
5018
|
title: "Publish a component",
|
|
5016
|
-
description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. create_component
|
|
5019
|
+
description: "Publish one project-scoped component variant after its exact implementation evidence and human approval are ready: copy its DRAFT definition to the live copy and re-bake every published page that embeds it. This is the ONLY way a component reaches the live site. Built-in `builtin:*` rows are locked blueprints, not implementations; materialize one with create_component first. Workspace-global component publication is currently fail-closed. Drafts (create_component, update_component) render as NOTHING live, with no error: publish every component you place on a page. 403 PUBLISH_NOT_GRANTED means this connection may author but not publish: say so and let the user publish from the dashboard. \u{1F534} A 409 COMPONENT_IMPLEMENTATION_NOT_READY is NOT retried and must not be: no tool on this connection can clear it. It is never a component you just made with create_component in no variant group \u2014 that is dashboard-managed and publishes on the preflight alone. The refusal is for a REPO-managed component without exact validation evidence and a human's approval, or for a variant-group member (the provisioned navigation and footer) whose group's canonical-input contract is not published. Its `readiness` names what is missing \u2014 relay it to the user instead of retrying or routing around it. 422 means publishing would break a Layout that uses it \u2014 the response names which.",
|
|
5017
5020
|
inputSchema: getComponentInput.shape
|
|
5018
5021
|
},
|
|
5019
5022
|
handler: guard(
|
|
@@ -5170,7 +5173,10 @@ DERIVED at import, where the componentize lane in \xA710 reuses every field.
|
|
|
5170
5173
|
Repeated visual region on a page? -> a COMPONENT with sectionType (a Section)
|
|
5171
5174
|
...and it comes in more than one look? -> siblings sharing that sectionType = VARIANTS
|
|
5172
5175
|
A stack of mixed, reorderable content? -> kind:'block' models + a \`modular\` field
|
|
5173
|
-
A row in a list (post, author, tier)? -> a collection (kind:'model')
|
|
5176
|
+
A row in a list (post, author, tier)? -> a collection (kind:'model'); rows one template
|
|
5177
|
+
route renders get urlPattern, e.g. '/blog/:slug'
|
|
5178
|
+
Copy for ONE page (home, about, legal)?-> page fields in sections (add_page_field /
|
|
5179
|
+
set_page_content) \u2014 never a one-entry collection
|
|
5174
5180
|
The one body of an article? -> type:'document' (exactly one, top level)
|
|
5175
5181
|
A fixed cluster of fields? -> group (never a 1-item repeater)
|
|
5176
5182
|
A repeating cluster? -> repeater
|