@bettercms-ai/mcp 0.80.1 → 0.82.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 CHANGED
@@ -2915,6 +2915,11 @@ var LISTING_TAIL = /* @__PURE__ */ new Set([
2915
2915
  "delete_integration",
2916
2916
  "test_integration_fetch",
2917
2917
  "run_integration_sync",
2918
+ "run_integration_function",
2919
+ "search_integration_reference",
2920
+ "start_integration_signin",
2921
+ "save_integration_template",
2922
+ "install_integration_template",
2918
2923
  // The ABM campaign lane — also hidden from the default listing (ABM_TOOLS below).
2919
2924
  "research_account",
2920
2925
  "build_abm_page",
@@ -2999,12 +3004,17 @@ var NEW_TOOL_TEXT = {
2999
3004
  editor_setup: "Make the live build editable: derives pages, collections, forms, bindings and components from the live release (skill bindings). Run it after an import or a rebuild, before the conversion brief. Idempotent: `ready` when it already ran for the live sha, else it starts (`running`; poll with `check: true`). `state`: never or stale (run it), running, ready, failed. 409 = nothing is live yet: deploy first. Then get_next_steps.",
3000
3005
  transform_image: "Edit an existing Media Library image with an instruction (e.g. 'remove the background', 'make it dusk'); the result is saved as a NEW asset and the original is kept (5 AI credits). Returns the new asset (id, url, altText).",
3001
3006
  write_alt_text: "Alt text for a Media Library image (mediaId) or an https image URL (url), written from its pixels (1 AI credit). A suggestion: save it with update_media.",
3002
- list_integrations: "List the project's custom integrations (any vendor with a public API: jobs boards, stores, CRMs, video). Each has its allowed hosts, secret NAMES with `secretsPresent` (is the env var set; never the value), `inject` (where the secrets go), syncs, panel and `approved`. Read bettercms://skill/integrations before building one.",
3003
- get_integration: "One integration (`integration`: id or slug) with its last 20 sync runs (status, counts, error).",
3004
- create_integration: "Declare a custom integration: `allowedHosts` (exact hostnames the server may call), `secrets` (template name -> project env var KEY), `inject` (the headers / query parameters that carry them on every request, e.g. {headers: {authorization: 'Bearer {{secret.TOKEN}}'}}), `allowPost` (GET only unless true), optional `auth` token exchange (inject {{token}}), `syncs` (vendor list -> collection) and `panel`. Secret VALUES never pass through here: the user sets them under Settings \u2192 Environment variables (production); list_env_vars shows the names. Until an admin approves it, vendor calls and syncs are refused: give the user `approval.approveUrl` and the `approval.changes` in plain words. You cannot approve it yourself.",
3005
- update_integration: "Change an integration (`integration`: id or slug); keys you leave out are kept, and `secrets`, `inject` and `syncs` replace the stored ones whole. Changing `allowedHosts`, `secrets`, `inject`, `allowPost` or `auth` withdraws its approval until an admin approves again (`approval.approveUrl`). Name, syncs, settings and panel never need approval.",
3007
+ list_integrations: "List the project's custom integrations (any vendor with a public API) and the workspace's integration `templates`. Each has its hosts, secret NAMES with `secretsPresent` (never a value), syncs, hooks, endpoints, functions and `approved`. Read bettercms://skill/integrations first.",
3008
+ get_integration: "One integration (`integration`: id or slug): `hookUrls`, `endpointUrls`, `oauthStatus`, its last sync runs, write-backs (`pushes`), function runs, webhook deliveries and vendor calls (the call log).",
3009
+ create_integration: "Declare a custom integration. Shapes: bettercms://skill/integrations. Secret VALUES never pass through here (the user sets env vars; list_env_vars shows names). Until an admin approves it, every vendor call, sync, webhook, endpoint and function is refused: give the user `approval.approveUrl` and `approval.changes` in plain words. You cannot approve it.",
3010
+ update_integration: "Change an integration (`integration`: id or slug); parameters as create_integration, keys left out are kept, lists replace the stored ones whole. A change to hosts, secrets, inject, allowPost, auth, oauth, hooks, endpoints, references or functions withdraws its approval (`approval.approveUrl`); name, syncs, settings and panel never need it.",
3006
3011
  delete_integration: "Delete an integration and its sync runs. Entries it synced stay (their provenance still names it); the env vars stay.",
3007
3012
  run_integration_sync: "Run one of an integration's syncs now (`integration`, `sync`: its key): every page of the vendor list is fetched and each item is created or updated as a DRAFT entry, matched on its externalKey, so a re-run only changes what changed (`unchanged` counts the rest); `publish: true` on the sync publishes what changed. Answers with the run: queued, running, done (counts: created, updated, unchanged, failed, pages, items) or failed (error). `waitSeconds` (\u2264 20) waits for it to finish; else follow it with get_integration. A sync with `schedule` 'hourly' or 'daily' also runs on its own.",
3013
+ run_integration_function: "Run an integration's function (bettercms/functions/<name>.ts, trigger 'manual') with `input`. Its fetch() reaches only the integration's approved hosts and the server adds the secrets: the code never sees them. `waitSeconds` (\u2264 20) answers with output, logs or error.",
3014
+ search_integration_reference: "Search an integration's live reference (`reference`: its key) at the vendor: up to 20 {id, title, image}. Store the pick in a json field (skill \xA712); the site resolves it live.",
3015
+ start_integration_signin: "Start the vendor sign-in of an integration with `oauth`: a `url` the USER opens to sign in with their own OAuth app (register `redirectUri` in it first). Tokens stay on the server.",
3016
+ save_integration_template: "Save an integration as a workspace template: its shape, never a secret value or its settings, with copies of the collections its syncs write into.",
3017
+ install_integration_template: "Install a workspace template on this project: missing collections are created and the integration waits for approval (`approval.approveUrl`); `secretsNeeded` lists the env vars to set.",
3008
3018
  test_integration_fetch: "Call the vendor once through the server, as the integration: an approved host only, with the integration's `inject` secrets added server-side and every secret redacted from the answer. The request itself takes no {{...}} templates, and the token endpoint cannot be called. Use it to read the response shape before writing a sync: `items`, `externalKey` and `map` are JSON Pointers into it. Bodies over 1 MB come back truncated. 409 integration_not_approved: an admin must approve it first."
3009
3019
  };
3010
3020
  var DELETE_PAGE_RUNTIME_BUNDLE = " Or, with no pageId, `runtimeBundle`: 'report' lists the duplicate pages derived from a server app's own bundle (`app/` folders); 'apply' retires them and their empty folders.";
@@ -3063,9 +3073,17 @@ var INTEGRATION_PARAM_TEXT = {
3063
3073
  secrets: "template name (UPPER_SNAKE) -> project env var KEY, e.g. {TOKEN: 'SHOPIFY_ADMIN_TOKEN'}; never a value",
3064
3074
  inject: "where the secrets go on every vendor request: {headers?: {name: template}, query?: {name: template}}, templates {{secret.NAME}} or {{token}}",
3065
3075
  allowPost: "true lets requests other than GET through (vendor writes, POST search APIs such as Apollo's); GET only when omitted",
3066
- auth: "optional token exchange: {request:{method,url,headers?,query?,form?}, tokenPath, expiresInPath?, ttlSeconds?}; {{secret.NAME}} in its values; its result is {{token}}",
3076
+ auth: "optional token exchange (skill \xA75); its result is {{token}}",
3067
3077
  settings: "non-secret JSON the panel reads and writes (store id, toggles); at most 16 KB",
3068
- syncs: "[{key, contentModelId, request:{method,url,headers?,query?}, pagination?:{style:'none'|'page'|'cursor'|'link', param?, cursorPath?, maxPages?}, items, externalKey, slug?, map:{fieldKey: pointer | {path, transform?:'string'|'number'|'boolean'|'join'}}, publish?, schedule?:'hourly'|'daily'|null}]; pointers are JSON Pointers",
3078
+ syncs: "vendor list -> collection [{key, contentModelId, request, items, externalKey, map, item?, writeBack?, publish?, schedule?}] (skill \xA76; two-way \xA715)",
3079
+ hooks: "vendor webhooks [{key, verify, action}] (skill \xA79); the vendor posts to `hookUrls`",
3080
+ endpoints: "public reads for the site [{key, request, params, pick, cacheSeconds?}] (skill \xA711); at `endpointUrls`",
3081
+ references: "live references [{key, label, search, lookup, items, id, title}] (skill \xA712)",
3082
+ functions: "[{name, triggers?, cpuMs?}]: bettercms/functions/<name>.ts (skill \xA710)",
3083
+ oauth: "sign-in with the customer's own OAuth app {authorizeUrl, tokenUrl, scopes, clientId, clientSecret} (secret NAMES; skill \xA713)",
3084
+ alertAfter: "tell admins after this many failed runs in a row (0 = never; 3 when omitted)",
3085
+ reference: "the reference's key (from the integration's references)",
3086
+ template: "the template's slug (from list_integrations `templates`)",
3069
3087
  panel: "the custom panel's slug: bettercms/panels/<panel>.tsx in the site repo; null for the stock page",
3070
3088
  url: "the full https URL on an allowed host (no templates in the URL)",
3071
3089
  sync: "the sync's key (from the integration's syncs)",
@@ -3097,6 +3115,18 @@ var SLOT_MULTIPLE_NOTE = "'slot' with config.multiple holds a list [{componentId
3097
3115
  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).";
3098
3116
  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`.";
3099
3117
  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.";
3118
+ var DELIVERY_KEY_DESCRIPTION = "Create a read-only delivery key for this project's own front end and return it once (it is never shown again: write it straight into the front end's .env under the name its SDK config reads, e.g. BCMS_API_KEY or BETTERCMS_API_KEY, never into chat or a commit). Published content needs no key (the SDK reads it by BCMS_PROJECT_ID), so create one only when the front end's config asks for a key. `scope: \"published\"` = content:read, no expiry; `scope: \"draft\"` also reads drafts and expires within the hour (a preview build's credential; editors' previews use the dashboard's preview link instead). Listed and revocable in Project \u2192 Settings \u2192 API keys like any other key. Requires artifact:write.";
3119
+ var DELIVERY_KEY_PARAMS = {
3120
+ name: "what it is for, shown in the key list (e.g. 'Front end (Vercel)')",
3121
+ scope: "published (content:read, the default) or draft (also drafts; expires within the hour)"
3122
+ };
3123
+ var FRONT_END_DESCRIPTION = 'Record where this project\'s own front end lives, so editors open drafts on it ("Preview on site") and a publish reaches it: `siteUrl` (its deployed origin), `framework` (next or astro: which preview route the SDK mounts), `draftPath` (Next only, when not /api/draft) and `revalidation` (a front end that revalidates instead of rebuilding). Call it after the front end is deployed; `npx @bettercms-ai/cli@latest deploy --to <host>` already registers the rebuild hook. Read the current values from get_project. Fields you omit are left as they are; null clears one. Requires artifact:write.';
3124
+ var FRONT_END_PARAMS = {
3125
+ siteUrl: "the deployed front end's origin, https://\u2026 (null clears it)",
3126
+ framework: "next or astro",
3127
+ draftPath: "Next only: the draft-mode route when not /api/draft (null clears it)",
3128
+ revalidation: "a front end that revalidates instead of rebuilding: { webhookUrl (https), webhookSecret (16+ chars, also stored as BETTERCMS_WEBHOOK_SECRET on the front end) } (null clears it)"
3129
+ };
3100
3130
  var MEDIA_DEFAULT_NOTE = "Leave the default (the CDN) unless the owner asked otherwise; never ask.";
3101
3131
  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.";
3102
3132
  var withWait = (path, waitSeconds) => typeof waitSeconds === "number" && waitSeconds > 0 ? `${path}?wait=${Math.min(Math.round(waitSeconds), 50)}` : path;
@@ -3359,6 +3389,7 @@ var uiObject = z.strictObject({
3359
3389
  placeholder: z.string().min(1).max(120).optional().describe("ghost text, a field of any type. A prop uses its own `placeholder`."),
3360
3390
  synced: z.strictObject({ from: z.string().min(1).max(64) }).optional().describe("the value comes from this global; edits apply everywhere."),
3361
3391
  valueShape: z.enum(["string", "object"]).optional().describe("a link / image / button writes a plain string or the object."),
3392
+ reference: z.object({ integration: z.string(), key: z.string() }).strict().optional().describe("a json field holding a live vendor item: the integration's slug and reference key."),
3362
3393
  access: z.strictObject({ view: z.literal("schema.view").optional(), edit: z.literal("schema.manage").optional() }).optional().describe("who sees (view: developer-only) and who changes (edit) it, gated in the editor. The enforced lock is readOnly:'developer'."),
3363
3394
  hidden: z.boolean().optional().describe("true: never shown in the form (a system value). A conditional hide is showIf."),
3364
3395
  blocks: z.record(z.string(), z.strictObject({ icon: dockIconArg.optional(), preview: z.record(z.string(), z.string()).optional() })).optional().describe("modular only: per block slug, {icon?, preview?} for its rows."),
@@ -3551,8 +3582,17 @@ var integrationShape = {
3551
3582
  auth: z.record(z.string(), z.unknown()).nullable().optional().describe(INTEGRATION_PARAM_TEXT.auth),
3552
3583
  settings: z.record(z.string(), z.unknown()).optional().describe(INTEGRATION_PARAM_TEXT.settings),
3553
3584
  syncs: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe(INTEGRATION_PARAM_TEXT.syncs),
3554
- panel: z.string().max(48).nullable().optional().describe(INTEGRATION_PARAM_TEXT.panel)
3585
+ panel: z.string().max(48).nullable().optional().describe(INTEGRATION_PARAM_TEXT.panel),
3586
+ hooks: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe(INTEGRATION_PARAM_TEXT.hooks),
3587
+ endpoints: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe(INTEGRATION_PARAM_TEXT.endpoints),
3588
+ references: z.array(z.record(z.string(), z.unknown())).max(10).optional().describe(INTEGRATION_PARAM_TEXT.references),
3589
+ functions: z.array(z.record(z.string(), z.unknown())).max(20).optional().describe(INTEGRATION_PARAM_TEXT.functions),
3590
+ oauth: z.record(z.string(), z.unknown()).nullable().optional().describe(INTEGRATION_PARAM_TEXT.oauth),
3591
+ alertAfter: z.number().int().min(0).max(50).optional().describe(INTEGRATION_PARAM_TEXT.alertAfter)
3555
3592
  };
3593
+ var integrationShapeBare = Object.fromEntries(
3594
+ Object.entries(integrationShape).map(([k, v]) => [k, v.describe(void 0)])
3595
+ );
3556
3596
  var DRY_RUN_SUMMARY = "Dry run: nothing was written.";
3557
3597
  var dryRunOf = async (client, method, path, body, headers = {}) => ok(DRY_RUN_SUMMARY, await client.fetchJSON(client.url(path), { method, body: JSON.stringify({ ...body, dryRun: true }), ...withHeaders(headers) }));
3558
3598
  function toField(f) {
@@ -3666,7 +3706,7 @@ var GIT_TEXT = {
3666
3706
  var HOSTING_TEXT = {
3667
3707
  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.",
3668
3708
  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.",
3669
- 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 (skill publishing). 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`). `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.",
3709
+ 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 (skill publishing). 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`). `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. A headless project is not deployed here: its own front end deploys with `npx @bettercms-ai/cli@latest deploy --to <host>`. Then poll get_host_deploy_status. Needs artifact:write, like deploy_project.",
3670
3710
  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."
3671
3711
  };
3672
3712
  async function builtinCopyId(client, builtinId) {
@@ -4251,7 +4291,7 @@ function buildToolDefs(deps) {
4251
4291
  propUi: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe(PROP_UI_ARG_NOTE),
4252
4292
  propGroup: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe("prop key \u2192 its ui.group ({id, label?, kind?, icon?, collapsed?}), null takes it out"),
4253
4293
  propTab: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe("prop key \u2192 its ui.tab ({id, label?, icon?, order?}), the V2 schema group it sits in. The FIRST top-level prop in a tab defines {id, label, icon?, order?}, later ones send {id}; \u22648 tabs; one group's members share one tab. null takes it out"),
4254
- propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label"),
4294
+ propLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("prop key \u2192 label: one or two words of meaning (playbook \xA72, Naming)"),
4255
4295
  propHelpText: z.record(z.string(), z.string().max(200).nullable()).optional().describe("prop key \u2192 help text; null = no help on purpose (stays empty, never refilled)"),
4256
4296
  override: z.boolean().optional().describe(OVERRIDE_NOTE),
4257
4297
  ifMatch: z.number().int().min(0).optional().describe(COMPONENT_IF_MATCH_NOTE),
@@ -4770,6 +4810,25 @@ ${d.outline}` : "Proposed Content structure.", d);
4770
4810
  z.object({ preference: z.enum(AUTHORING_CHOICES).describe("components or fields") }).shape,
4771
4811
  async (c, a) => ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference: a.preference }))
4772
4812
  ),
4813
+ def(
4814
+ "create_delivery_key",
4815
+ "Create a read-only delivery key for the front end",
4816
+ DELIVERY_KEY_DESCRIPTION,
4817
+ z.object({ name: z.string().min(1).describe(DELIVERY_KEY_PARAMS.name), scope: z.enum(["published", "draft"]).optional().describe(DELIVERY_KEY_PARAMS.scope) }).shape,
4818
+ async (c, a) => ok("Created the delivery key. It is shown once: write it into the front end's .env now.", await data(c, "POST", `/management/projects/current/delivery-keys`, a))
4819
+ ),
4820
+ def(
4821
+ "set_front_end",
4822
+ "Record where the project's own front end lives",
4823
+ FRONT_END_DESCRIPTION,
4824
+ z.object({
4825
+ siteUrl: z.string().nullable().optional().describe(FRONT_END_PARAMS.siteUrl),
4826
+ framework: z.enum(["next", "astro"]).optional().describe(FRONT_END_PARAMS.framework),
4827
+ draftPath: z.string().nullable().optional().describe(FRONT_END_PARAMS.draftPath),
4828
+ revalidation: z.object({ webhookUrl: z.string(), webhookSecret: z.string() }).nullable().optional().describe(FRONT_END_PARAMS.revalidation)
4829
+ }).shape,
4830
+ async (c, a) => ok("Recorded the front end.", await data(c, "PATCH", `/management/projects/current/front-end`, a))
4831
+ ),
4773
4832
  def(
4774
4833
  "set_binding_mode",
4775
4834
  "Set how the site's bindings are resolved",
@@ -4831,7 +4890,7 @@ ${d.outline}` : "Proposed Content structure.", d);
4831
4890
  fieldUi: z.record(z.string(), z.record(z.string(), z.unknown()).nullable()).optional().describe(FIELD_UI_ARG_NOTE),
4832
4891
  fieldFieldset: z.record(z.string(), z.string().min(1).nullable()).optional().describe("path \u2192 fieldset id, null takes it out"),
4833
4892
  fieldOrder: z.strictObject({ parentPath: z.string().min(1).optional(), order: z.array(z.string().min(1)).min(1).max(200) }).optional().describe("the full new order of one parent's child keys (top level without parentPath). An array with both zones needs 'KEY.zones.nonRepeatable' or 'KEY.zones.repeatable'; its bare key is a 400"),
4834
- fieldLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("path \u2192 label"),
4893
+ fieldLabel: z.record(z.string(), z.string().min(1).max(255)).optional().describe("path \u2192 label: one or two words of meaning (playbook \xA72, Naming)"),
4835
4894
  fieldHelpText: z.record(z.string(), z.string().max(500).nullable()).optional().describe("path \u2192 help text, null clears it"),
4836
4895
  override: z.boolean().optional().describe(OVERRIDE_NOTE),
4837
4896
  ifMatch: z.number().int().min(0).optional().describe(MODEL_IF_MATCH_NOTE),
@@ -5227,7 +5286,8 @@ ${d.outline}` : "Proposed Content structure.", d);
5227
5286
  "List custom integrations",
5228
5287
  NEW_TOOL_TEXT.list_integrations,
5229
5288
  z.object({}).shape,
5230
- async (c) => ok("Integrations.", await data(c, "GET", `/management/integrations`))
5289
+ // The whole body: the workspace's `templates` ride beside `data`.
5290
+ async (c) => ok("Integrations.", await c.fetchJSON(c.url(`/management/integrations`), { method: "GET" }))
5231
5291
  ),
5232
5292
  def(
5233
5293
  "get_integration",
@@ -5255,9 +5315,9 @@ ${d.outline}` : "Proposed Content structure.", d);
5255
5315
  NEW_TOOL_TEXT.update_integration,
5256
5316
  z.object({
5257
5317
  integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5258
- name: z.string().min(1).max(120).optional().describe("a label, 1-120 chars"),
5259
- vendor: z.string().max(60).nullable().optional().describe("the vendor's name"),
5260
- ...integrationShape
5318
+ name: z.string().min(1).max(120).optional(),
5319
+ vendor: z.string().max(60).nullable().optional(),
5320
+ ...integrationShapeBare
5261
5321
  }).shape,
5262
5322
  async (c, a) => {
5263
5323
  const { integration, ...body } = a;
@@ -5282,6 +5342,66 @@ ${d.outline}` : "Proposed Content structure.", d);
5282
5342
  }).shape,
5283
5343
  async (c, a) => ok("Sync run.", await data(c, "POST", withWait(`/management/integrations/${s(a.integration)}/syncs/${s(a.sync)}/run`, a.waitSeconds)))
5284
5344
  ),
5345
+ def(
5346
+ "run_integration_function",
5347
+ "Run an integration's function",
5348
+ NEW_TOOL_TEXT.run_integration_function,
5349
+ z.object({
5350
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5351
+ name: z.string().min(1).describe("the function's name"),
5352
+ input: z.unknown().optional().describe("the function's `event` (JSON)"),
5353
+ waitSeconds: z.number().min(0).max(50).optional().describe(PARAM_TEXT.waitSeconds)
5354
+ }).shape,
5355
+ async (c, a) => ok("Function run.", await data(c, "POST", withWait(`/management/integrations/${s(a.integration)}/functions/${s(a.name)}/run`, a.waitSeconds), { input: a.input }))
5356
+ ),
5357
+ def(
5358
+ "search_integration_reference",
5359
+ "Search an integration's live reference",
5360
+ NEW_TOOL_TEXT.search_integration_reference,
5361
+ z.object({
5362
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5363
+ reference: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.reference),
5364
+ query: z.string().max(200).optional().describe("what to search for")
5365
+ }).shape,
5366
+ async (c, a) => ok("Vendor items.", await data(c, "POST", `/management/integrations/${s(a.integration)}/references/${s(a.reference)}/search`, { query: a.query ?? "" }))
5367
+ ),
5368
+ def(
5369
+ "start_integration_signin",
5370
+ "Start an integration's vendor sign-in",
5371
+ NEW_TOOL_TEXT.start_integration_signin,
5372
+ z.object({ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration) }).shape,
5373
+ async (c, a) => ok("Sign-in URL: give it to the user to open.", await data(c, "POST", `/management/integrations/${s(a.integration)}/oauth/start`))
5374
+ ),
5375
+ def(
5376
+ "save_integration_template",
5377
+ "Save an integration as a template",
5378
+ NEW_TOOL_TEXT.save_integration_template,
5379
+ z.object({
5380
+ integration: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.integration),
5381
+ slug: z.string().min(1).max(48).optional().describe(INTEGRATION_PARAM_TEXT.slug),
5382
+ name: z.string().min(1).max(120).optional().describe("a label, 1-120 chars"),
5383
+ description: z.string().max(500).optional().describe("what it connects, for the people installing it")
5384
+ }).shape,
5385
+ async (c, a) => {
5386
+ const { integration, ...body } = a;
5387
+ return ok("Template saved.", await data(c, "POST", `/management/integrations/${s(integration)}/template`, body));
5388
+ }
5389
+ ),
5390
+ def(
5391
+ "install_integration_template",
5392
+ "Install an integration template",
5393
+ NEW_TOOL_TEXT.install_integration_template,
5394
+ z.object({
5395
+ template: z.string().min(1).describe(INTEGRATION_PARAM_TEXT.template),
5396
+ slug: z.string().min(1).max(48).optional().describe(INTEGRATION_PARAM_TEXT.slug),
5397
+ name: z.string().min(1).max(120).optional().describe("a label, 1-120 chars")
5398
+ }).shape,
5399
+ // The whole body: `approval`, `secretsNeeded` and `collectionsCreated` ride beside `data`.
5400
+ async (c, a) => {
5401
+ const { template, ...body } = a;
5402
+ return ok("Template installed.", await c.fetchJSON(c.url(`/management/integrations/templates/${s(template)}/install`), { method: "POST", body: JSON.stringify(body) }));
5403
+ }
5404
+ ),
5285
5405
  def(
5286
5406
  "test_integration_fetch",
5287
5407
  "Call a vendor through an integration",
@@ -7273,6 +7393,20 @@ from the builtin blueprints \`list_components\` returns (\`builtin:*\`) instead
7273
7393
  JSON. On an IMPORTED site, never materialize a builtin: read the repo's own components first
7274
7394
  (\`src/components\`, its sections folder) and make ONE CMS component per source component, named
7275
7395
  as the source names it \u2014 the site's Pricing section is "Pricing", not "Section 2" or "Simple".
7396
+
7397
+ **Naming: one or two words of MEANING, BEM-shaped, in Portable Text's words.** You choose every
7398
+ name, from what the page holds. The BLOCK is the section: what it IS (Hero, Pricing, FAQ), else
7399
+ the meaning words of its own heading ("We've spent years learning three timbers" \u2192 "Three
7400
+ Timbers", "Get in touch" \u2192 "Get In Touch"). A LIST is named by its own title, the heading or
7401
+ caption over it ("Our services" \u2192 "Services"), else by what its rows are (Plans, Questions,
7402
+ Steps, Links), never by its first row's copy. A list with its own title is a block inside the
7403
+ block: "About \u203A Services", its columns bare roles read under it (Title, Description). A field is
7404
+ its block plus its ELEMENT, said the way Portable Text titles its types (heading, paragraph,
7405
+ quote, list item, link, image, button) with a modifier only to tell two apart ("Hero headline",
7406
+ "Footer link 2 URL"). Never body copy, never a sentence. Rename anything to say its meaning better
7407
+ \u2014 \`propLabel\`, \`fieldLabel\`, or \`data-bcms-section-label\` / \`data-bcms-label\` in the source \u2014
7408
+ within the bound; a name you give is yours and is never re-derived.
7409
+
7276
7410
  On a DERIVED site run \`get_componentize_plan\` \u2192 \`componentize_sections\` FIRST \u2014 it does most
7277
7411
  of this in one call, and once \`components\` is recorded each \`editor_setup\` of a derived site
7278
7412
  runs it in bind mode and publishes the components, so expect ALREADY_COMPONENTIZED \u2014 the placements it wrote are