@bettercms-ai/mcp 0.46.0 → 0.48.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
@@ -1991,8 +1991,8 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
1991
1991
  doc: {
1992
1992
  schema: 1,
1993
1993
  folders: [
1994
- { id: "home:pages", parentId: null, name: "Static pages", icon: "files", sort: -3 },
1995
- { id: "home:collections", parentId: null, name: "Other content", icon: null, sort: -2 },
1994
+ { id: "home:pages", parentId: null, name: "Other pages", icon: "files", sort: 1e3 },
1995
+ { id: "home:collections", parentId: null, name: "Shared", icon: null, sort: 1001 },
1996
1996
  { id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "newspaper", sort: 0 },
1997
1997
  { id: "fld_std_products", parentId: null, name: "Products", icon: "shopping-bag", sort: 1 }
1998
1998
  ],
@@ -2028,30 +2028,36 @@ ever changes.
2028
2028
  2. **Pinned at the top:** the home page, then each collection's LIST page (the dynamic page that
2029
2029
  lists it, e.g. /blog). A single document opens straight into the editor, so it is a pin, not a
2030
2030
  folder with one page in it.
2031
- 3. **One folder per content family.**
2031
+ 3. **One folder per content family, named for the DETAIL type its route holds.** /blog gives
2032
+ "Blog posts", /case-studies "Case studies", /careers "Jobs".
2032
2033
  - Blog: a "Blog posts" folder holding the blog collection. A folder with exactly one
2033
2034
  collection opens that collection's table directly.
2034
2035
  - Commerce: a "Products" folder holding the product collection AND its taxonomies (product
2035
- categories, product tags, materials, colours, sizes, merch collections, ...).
2036
+ categories, product tags, merch collections, ...). Once there are more than four taxonomies,
2037
+ the VARIANT OPTIONS (colours, sizes, materials) move into an "Attributes" subfolder; the
2038
+ categories and tags stay beside the catalogue.
2036
2039
  - Decide the rest by the REFERENCE GRAPH: a collection that is referenced only by one family,
2037
2040
  or that references only one family (reviews \u2192 products), belongs in that family's folder.
2038
2041
  The family's main collection comes first, then the others in the order its fields reference
2039
2042
  them.
2040
- 4. **Other pages.** Standalone static pages go in the system folder \`home:pages\` ("Static
2041
- pages"). Code-only routes (a 404, a page built by code with nothing to edit) stay there too,
2042
- marked with the \`file-code\` icon. Pages that share a route (/legal/terms, /legal/privacy) get
2043
- a subfolder of Static pages; a large route family or one named for what it is (/docs/...) gets
2044
- its own top-level folder.
2045
- 5. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
2046
- products) goes in the system folder \`home:collections\`, named "Other content". So does
2047
- supporting content no family owns (testimonials, FAQs with no list page).
2048
- 6. **Icons** (lucide names): \`newspaper\` for posts, \`shopping-bag\` for products, \`book-open\`
2049
- for docs, \`files\` for Static pages, \`folder\` by default.
2050
- 7. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
2043
+ 4. **Family order:** the site's navigation order; failing that, entry count, then name.
2044
+ 5. **Other pages.** EVERY remaining standalone page goes in the system folder \`home:pages\`
2045
+ ("Other pages") \u2014 no standalone page ever gets a top-level folder. Code-only routes (a 404, a
2046
+ page built by code with nothing to edit) stay there too, marked with the \`file-code\` icon.
2047
+ Pages that share a route (/legal/terms, /legal/privacy, /docs/...) get a subfolder of it.
2048
+ 6. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
2049
+ products) goes in the system folder \`home:collections\`, named "Shared". So does supporting
2050
+ content no family owns (testimonials, FAQs with no list page).
2051
+ 7. **Settings closes the sidebar.** \`home:globals\` ("Settings") holds NO pages or collections \u2014
2052
+ navigation, header, footer, site settings and SEO defaults open from it in the dashboard. The
2053
+ three system folders always sort BELOW the folders you make: \u2026 Other pages, Shared, Settings.
2054
+ 8. **Icons** (lucide names): \`newspaper\` for posts, \`shopping-bag\` for products, \`book-open\`
2055
+ for docs, \`files\` for Other pages, \`folder\` by default.
2056
+ 9. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
2051
2057
  reason the user gave you.
2052
- 8. **Organisation only.** Never rename a page, change a slug, touch a schema or move content to
2058
+ 10. **Organisation only.** Never rename a page, change a slug, touch a schema or move content to
2053
2059
  make a structure fit. The structure fits the site, not the other way round.
2054
- 9. **Page-bound models are not collections.** A page with fields has a content model bound to
2060
+ 11. **Page-bound models are not collections.** A page with fields has a content model bound to
2055
2061
  it; it is never filed (400 \`NAV_PAGE_BOUND_MODEL\`). Organise the PAGE.
2056
2062
 
2057
2063
  ## The workflow
@@ -2084,14 +2090,15 @@ collection (\`cm_posts\`), a /shop list page (\`pg_shop\`, dynamic) for Products
2084
2090
  [pinned page] Home
2085
2091
  [pinned page] Blog (/blog lists Blog)
2086
2092
  [pinned page] Shop (/shop lists Products)
2087
- [system folder] Static pages icon=files
2088
- About, Contact, Not found (icon=file-code)
2089
- [system folder] Other content
2090
- Tags (referenced by Blog AND Products: shared)
2091
2093
  [folder] Blog posts icon=newspaper
2092
2094
  Blog
2093
2095
  [folder] Products icon=shopping-bag
2094
2096
  Products, Product categories, Colours (referenced only by Products)
2097
+ [system folder] Other pages icon=files
2098
+ About, Contact, Not found (icon=file-code)
2099
+ [system folder] Shared
2100
+ Tags (referenced by Blog AND Products: shared)
2101
+ [system folder] Settings (navigation, header, footer, site settings, SEO defaults)
2095
2102
  \`\`\`
2096
2103
 
2097
2104
  The exact \`set_content_structure\` payload (\`version: 0\` because the project had no structure):
@@ -2102,6 +2109,19 @@ ${JSON.stringify(STRUCTURE_EXAMPLE_PAYLOAD, null, 2)}
2102
2109
  `;
2103
2110
 
2104
2111
  // src/tools.ts
2112
+ async function askHuman(deps, q) {
2113
+ if (!deps.elicit) return { prompt: q.prompt };
2114
+ try {
2115
+ const res = await deps.elicit({
2116
+ message: q.message,
2117
+ requestedSchema: { type: "object", properties: { [q.key]: q.property }, required: [q.key] }
2118
+ });
2119
+ const picked = res.action === "accept" ? res.content?.[q.key] : void 0;
2120
+ if (q.accept(picked)) return { value: picked };
2121
+ } catch {
2122
+ }
2123
+ return { prompt: q.prompt };
2124
+ }
2105
2125
  var FRAMEWORK_CHOICES = ["astro", "next", "react-ts", "other"];
2106
2126
  var FRAMEWORK_LABELS = {
2107
2127
  astro: "Astro \u2014 recommended default, static by default and fastest to publish",
@@ -2115,32 +2135,20 @@ var FRAMEWORK_PROMPT = [
2115
2135
  "",
2116
2136
  "Do not choose on their behalf. Sites cannot be built as plain HTML/CSS \u2014 every project is backed by one of these starters, which is what keeps its content editable in the CMS."
2117
2137
  ].join("\n");
2118
- async function askFramework(deps) {
2119
- if (!deps.elicit) return { prompt: FRAMEWORK_PROMPT };
2120
- try {
2121
- const res = await deps.elicit({
2122
- message: "Which technology should this site be built with?",
2123
- requestedSchema: {
2124
- type: "object",
2125
- properties: {
2126
- framework: {
2127
- type: "string",
2128
- title: "Technology",
2129
- description: "The frontend stack this project's starter is based on.",
2130
- enum: [...FRAMEWORK_CHOICES],
2131
- enumNames: FRAMEWORK_CHOICES.map((c) => FRAMEWORK_LABELS[c])
2132
- }
2133
- },
2134
- required: ["framework"]
2135
- }
2136
- });
2137
- const picked = res.action === "accept" ? res.content?.framework : void 0;
2138
- if (typeof picked === "string" && FRAMEWORK_CHOICES.includes(picked)) {
2139
- return { framework: picked };
2140
- }
2141
- } catch {
2142
- }
2143
- return { prompt: FRAMEWORK_PROMPT };
2138
+ function askFramework(deps) {
2139
+ return askHuman(deps, {
2140
+ key: "framework",
2141
+ message: "Which technology should this site be built with?",
2142
+ property: {
2143
+ type: "string",
2144
+ title: "Technology",
2145
+ description: "The frontend stack this project's starter is based on.",
2146
+ enum: [...FRAMEWORK_CHOICES],
2147
+ enumNames: FRAMEWORK_CHOICES.map((c) => FRAMEWORK_LABELS[c])
2148
+ },
2149
+ accept: (v) => typeof v === "string" && FRAMEWORK_CHOICES.includes(v),
2150
+ prompt: FRAMEWORK_PROMPT
2151
+ });
2144
2152
  }
2145
2153
  var AUTHORING_CHOICES = ["components", "fields"];
2146
2154
  var AUTHORING_LABELS = {
@@ -2156,32 +2164,41 @@ var AUTHORING_PROMPT = [
2156
2164
  "Recommend components unless the user says the site is schema-first.",
2157
2165
  "Do not choose on their behalf. This is asked once per project."
2158
2166
  ].join("\n");
2159
- async function askAuthoring(deps) {
2160
- if (!deps.elicit) return { prompt: AUTHORING_PROMPT };
2161
- try {
2162
- const res = await deps.elicit({
2163
- message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
2164
- requestedSchema: {
2165
- type: "object",
2166
- properties: {
2167
- preference: {
2168
- type: "string",
2169
- title: "Authoring architecture",
2170
- description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
2171
- enum: [...AUTHORING_CHOICES],
2172
- enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
2173
- }
2174
- },
2175
- required: ["preference"]
2176
- }
2177
- });
2178
- const picked = res.action === "accept" ? res.content?.preference : void 0;
2179
- if (typeof picked === "string" && AUTHORING_CHOICES.includes(picked)) {
2180
- return { preference: picked };
2181
- }
2182
- } catch {
2183
- }
2184
- return { prompt: AUTHORING_PROMPT };
2167
+ function askAuthoring(deps) {
2168
+ return askHuman(deps, {
2169
+ key: "preference",
2170
+ message: "Which authoring architecture should this site use? Components is recommended for a marketing site.",
2171
+ property: {
2172
+ type: "string",
2173
+ title: "Authoring architecture",
2174
+ description: "How this site's pages are composed. Components is recommended unless this is a schema-first site (a blog, catalogue or directory). Asked once per project.",
2175
+ enum: [...AUTHORING_CHOICES],
2176
+ enumNames: AUTHORING_CHOICES.map((c) => AUTHORING_LABELS[c])
2177
+ },
2178
+ accept: (v) => typeof v === "string" && AUTHORING_CHOICES.includes(v),
2179
+ prompt: AUTHORING_PROMPT
2180
+ });
2181
+ }
2182
+ var USE_CDN_PROMPT = [
2183
+ "Ask the user where this site's media should be served from, then call set_media_delivery again with their answer as `useCdn`:",
2184
+ " 1. true \u2014 the BetterCMS CDN (recommended, and the default). Media URLs are minted on the CDN host, or on the project's own CDN base if it stores media in its own bucket.",
2185
+ " 2. false \u2014 the API origin. Media URLs are minted on the API host instead. Same bytes, same service; only the hostname differs.",
2186
+ "",
2187
+ "This applies to URLs minted FROM NOW ON. Already-published content keeps the URLs it has, and they keep working."
2188
+ ].join("\n");
2189
+ function askMediaDelivery(deps) {
2190
+ return askHuman(deps, {
2191
+ key: "useCdn",
2192
+ message: "Serve this site's media from the BetterCMS CDN (recommended), or from the API origin?",
2193
+ property: {
2194
+ type: "boolean",
2195
+ title: "Use the CDN",
2196
+ description: "On (default): media URLs are minted on the CDN \u2014 or on this project's own CDN base if it uses its own bucket. Off: they are minted on the API origin instead. Same bytes either way; only the host changes, and only for URLs minted from now on.",
2197
+ default: true
2198
+ },
2199
+ accept: (v) => typeof v === "boolean",
2200
+ prompt: USE_CDN_PROMPT
2201
+ });
2185
2202
  }
2186
2203
  var fieldType = z.enum([
2187
2204
  "text",
@@ -2393,7 +2410,7 @@ function toField(f) {
2393
2410
  ...f.config ? { config: f.config } : {}
2394
2411
  };
2395
2412
  }
2396
- var STRUCTURE_NOTE = 'ORGANISATION ONLY: folders never change a URL, slug, content or publish state, and nothing is rebuilt or delivered. Folders nest at most 4 levels deep (a page or collection may sit in a level-4 folder). Icons are lucide icon names, e.g. "folder", "file-text", "rows-3", "house" (a-z, 0-9 and dashes); null clears one. SYSTEM FOLDERS (CPO-129): "home:pages" (Static pages), "home:collections" (Collections) and "home:globals" (Globals) always exist, even when the document has no record for them. They stay at the top level and can never be moved or deleted (400 NAV_SYSTEM_FOLDER), but rename_folder and set_icon work on them and create their record on first use. TYPE PURITY: anything inside Static pages, at any depth, must be a page, and anything inside Collections must be a collection, and Globals holds NO pages or collections at all, because globals are not sidebar items (subfolders under Globals are allowed but stay empty). Any of these is 400 NAV_TYPE_MISMATCH. Your own folders hold either kind. ACCESS (CPO-131): Requires Admin or Developer access to CHANGE the structure, the same access as editing the schema; anyone who can read content can call get_content_structure. A 403 SCHEMA_ACCESS_REQUIRED means the person behind this connection is not an Admin or Developer: stop and tell the user, do not retry. ROOT PINS (CPO-132): folderId \'root\' pins a page or collection at the top level; null returns it to its home (Static pages / Collections). Pins sit above the folders, in their own order, and may be 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). KIND CHECK: an id is either a page or a collection, never both, and a custom folder holds either \u2014 so move_to_folder and set_icon VERIFY that the id really is the `kind` you named on this branch, and refuse a mismatch with 400 NAV_KIND_MISMATCH naming the kind to use; call it again with that kind, which also clears the wrong-kind entry. An id that resolves to nothing is allowed (it may exist on another branch) and the result says so. set_content_structure never refuses: it returns `warnings` and `kindMismatches`. get_content_structure marks such items `kindMismatch: true` and lists them in `summary.kindMismatches`. PAGE-BOUND MODELS (CPO-135): a page with fields has a content model bound to it. That model is NOT a collection \u2014 get_content_structure never lists it among collections or in the tree, and move_to_folder / set_icon / set_content_structure refuse it with 400 NAV_PAGE_BOUND_MODEL naming the owning page (\'This model belongs to the page "\u2026"; organise the page.\'). Organise the PAGE instead, by its page id. If a stored document still references one it appears in `summary.hiddenItems` with `boundToPageId`, and the next single write drops it.';
2413
+ var STRUCTURE_NOTE = 'ORGANISATION ONLY: folders never change a URL, slug, content or publish state, and nothing is rebuilt or delivered. Folders nest at most 4 levels deep (a page or collection may sit in a level-4 folder). Icons are lucide icon names, e.g. "folder", "file-text", "rows-3", "house" (a-z, 0-9 and dashes); null clears one. SYSTEM FOLDERS (CPO-129): "home:pages" (Other pages), "home:collections" (Shared) and "home:globals" (Settings) always exist, even when the document has no record for them. They close the sidebar, below the folders you make, and can never be moved or deleted (400 NAV_SYSTEM_FOLDER), but rename_folder and set_icon work on them and create their record on first use. TYPE PURITY: anything inside Other pages, at any depth, must be a page, and anything inside Shared must be a collection, and Settings holds NO pages or collections at all, because globals are not sidebar items (subfolders under Settings are allowed but stay empty). Any of these is 400 NAV_TYPE_MISMATCH. Your own folders hold either kind. ACCESS (CPO-131): Requires Admin or Developer access to CHANGE the structure, the same access as editing the schema; anyone who can read content can call get_content_structure. A 403 SCHEMA_ACCESS_REQUIRED means the person behind this connection is not an Admin or Developer: stop and tell the user, do not retry. ROOT PINS (CPO-132): folderId \'root\' pins a page or collection at the top level; null returns it to its home (Other pages / Shared). Pins sit above the folders, in their own order, and may be 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). KIND CHECK: an id is either a page or a collection, never both, and a custom folder holds either \u2014 so move_to_folder and set_icon VERIFY that the id really is the `kind` you named on this branch, and refuse a mismatch with 400 NAV_KIND_MISMATCH naming the kind to use; call it again with that kind, which also clears the wrong-kind entry. An id that resolves to nothing is allowed (it may exist on another branch) and the result says so. set_content_structure never refuses: it returns `warnings` and `kindMismatches`. get_content_structure marks such items `kindMismatch: true` and lists them in `summary.kindMismatches`. PAGE-BOUND MODELS (CPO-135): a page with fields has a content model bound to it. That model is NOT a collection \u2014 get_content_structure never lists it among collections or in the tree, and move_to_folder / set_icon / set_content_structure refuse it with 400 NAV_PAGE_BOUND_MODEL naming the owning page (\'This model belongs to the page "\u2026"; organise the page.\'). Organise the PAGE instead, by its page id. If a stored document still references one it appears in `summary.hiddenItems` with `boundToPageId`, and the next single write drops it.';
2397
2414
  function structureResult(d) {
2398
2415
  const message = d?.message;
2399
2416
  return ok(typeof message === "string" ? message : "Updated the Content structure.", d);
@@ -3067,7 +3084,7 @@ ${d.summary.outline}` : "Content structure.", d);
3067
3084
  def(
3068
3085
  "suggest_content_structure",
3069
3086
  "Propose the standard Content structure",
3070
- `READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the platform's own implementation of the standard over the project's pages, collections, their references and the routes its build serves: the home page and each collection's list page pinned at the top, one folder per content family (Blog posts, Products with its taxonomies, grouped by the reference graph), shared taxonomies in Other content, standalone and code-only pages in Static 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.`,
3087
+ `READ-ONLY: propose the standard Content sidebar structure for the connected project (${STRUCTURE_PLAYBOOK_URI}) and write NOTHING. Runs the platform's own implementation of the standard over the project's pages, collections, their references and the routes its build serves: the home page and each collection's list page pinned at the top, one folder per content family named for the detail type its route holds (Blog posts, Case studies, Jobs, Products with its taxonomies and an Attributes subfolder for its variant options), ordered by entry count (or the site navigation, when the caller supplies its link order), shared taxonomies in Shared, every remaining standalone and code-only 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.`,
3071
3088
  z.object({
3072
3089
  maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
3073
3090
  }).shape,
@@ -3094,7 +3111,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3094
3111
  items: z.array(z.object({
3095
3112
  kind: z.enum(["page", "collection"]),
3096
3113
  id: z.string().min(1).describe("page id or collection (content model) id"),
3097
- folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Static pages for a page, the Collections list for a collection)'),
3114
+ folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Other pages for a page, the Shared list for a collection)'),
3098
3115
  sort: z.number(),
3099
3116
  icon: z.string().nullable()
3100
3117
  }))
@@ -3127,11 +3144,11 @@ ${d.outline}` : "Proposed Content structure.", d);
3127
3144
  def(
3128
3145
  "move_to_folder",
3129
3146
  "Move into a Content sidebar folder",
3130
- `Move a page, a collection or a folder into a folder. folderId null moves it back to its home: Static pages for a page, the Collections list for a collection, the top level for a folder. It lands after what is already there. Moving a folder takes its contents with it and is refused if the result would nest deeper than 4 levels or put a folder inside itself. ${STRUCTURE_NOTE}`,
3147
+ `Move a page, a collection or a folder into a folder. folderId null moves it back to its home: Other pages for a page, the Shared list for a collection, the top level for a folder. It lands after what is already there. Moving a folder takes its contents with it and is refused if the result would nest deeper than 4 levels or put a folder inside itself. ${STRUCTURE_NOTE}`,
3131
3148
  z.object({
3132
3149
  kind: z.enum(["page", "collection", "folder"]),
3133
3150
  id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
3134
- folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (Static pages, the Collections list, or the top level for a folder)")
3151
+ folderId: z.string().min(1).nullable().describe("target folder id; 'root' pins a page or collection at the top level (for a folder, 'root' is the top level); null moves it back to its home (Other pages, the Shared list, or the top level for a folder)")
3135
3152
  }).shape,
3136
3153
  async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
3137
3154
  ),
@@ -3299,7 +3316,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3299
3316
  if (framework === void 0) {
3300
3317
  const asked = await askFramework(deps);
3301
3318
  if ("prompt" in asked) return fail(asked.prompt);
3302
- framework = asked.framework;
3319
+ framework = asked.value;
3303
3320
  }
3304
3321
  return ok("Created project.", await data(c, "POST", `/management/projects`, { ...a, framework }));
3305
3322
  }
@@ -3318,7 +3335,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3318
3335
  if (preference === void 0) {
3319
3336
  const asked = await askAuthoring(deps);
3320
3337
  if ("prompt" in asked) return fail(asked.prompt);
3321
- preference = asked.preference;
3338
+ preference = asked.value;
3322
3339
  }
3323
3340
  return ok("Recorded the authoring architecture.", await data(c, "PATCH", `/management/projects/current/authoring-preference`, { preference }));
3324
3341
  }
@@ -3330,6 +3347,24 @@ ${d.outline}` : "Proposed Content structure.", d);
3330
3347
  z.object({ declaredBindings: z.boolean().describe("true = trust the template's declared bindings; false = text-match (the default)") }).shape,
3331
3348
  async (c, a) => ok("Recorded the binding mode.", await data(c, "PATCH", `/management/projects/current/binding-mode`, { declaredBindings: a.declaredBindings }))
3332
3349
  ),
3350
+ def(
3351
+ "set_media_delivery",
3352
+ "Choose where this site's media is served from",
3353
+ "Record whether this project's media (images, video, files) is served from the BetterCMS CDN or from the API origin. `useCdn: true` is the DEFAULT and the recommendation: URLs are minted on the CDN host, or on the project's own CDN base when it stores media in its own bucket (BYOK). `useCdn: false` mints them on the API origin instead \u2014 the same service serving the same bytes, only under a different hostname. ASK THE USER; do not pick for them. Called without `useCdn`, this tool asks them directly (or hands you the question to ask). \u{1F534} IT APPLIES TO URLs MINTED FROM NOW ON \u2014 new uploads, entry saves, get_media. It does NOT rewrite URLs already stored in published content, and it does not need to: the old URLs keep resolving. To move an existing image, re-save the field that holds it. \u{1F534} IT IS THE BACKEND HALF ONLY. A deployed frontend builds its own transform URLs with @bettercms-ai/image-url, which has its own media host \u2014 set `PUBLIC_BCMS_MEDIA_URL` (or the integration's `mediaUrl` option) to match, or that site's resized images stay on the CDN while everything else moves. Read the current answer from get_project (`useCdn`). Re-callable; the last answer wins.",
3354
+ // Optional in the schema for the same reason `framework` and `preference` are: a
3355
+ // required arg is rejected by the SDK before the handler runs, which would kill the
3356
+ // elicitation below and leave the model guessing.
3357
+ z.object({ useCdn: z.boolean().optional().describe("REQUIRED in effect \u2014 the USER's answer. true = CDN (the default), false = API origin. Ask them; never default it.") }).shape,
3358
+ async (c, a) => {
3359
+ let useCdn = a.useCdn;
3360
+ if (useCdn === void 0) {
3361
+ const asked = await askMediaDelivery(deps);
3362
+ if ("prompt" in asked) return fail(asked.prompt);
3363
+ useCdn = asked.value;
3364
+ }
3365
+ return ok("Recorded the media delivery origin.", await data(c, "PATCH", `/management/projects/current/media-delivery`, { useCdn }));
3366
+ }
3367
+ ),
3333
3368
  def(
3334
3369
  "submit_conversion_receipt",
3335
3370
  "Record what the conversion codemod could and could not do",
@@ -6007,7 +6042,7 @@ and \`ui\` only changes editor chrome, so there is nothing to flip.`
6007
6042
 
6008
6043
  // src/server.ts
6009
6044
  var SERVER_NAME = "bettercms";
6010
- var SERVER_VERSION = "1.4.0";
6045
+ var SERVER_VERSION = "1.5.0";
6011
6046
  var SERVER_DISPLAY = {
6012
6047
  title: "BetterCMS",
6013
6048
  websiteUrl: "https://bettercms.ai",
@@ -6034,7 +6069,7 @@ function buildServer(deps) {
6034
6069
  STRUCTURE_PLAYBOOK_URI,
6035
6070
  {
6036
6071
  title: "BetterCMS Content structure playbook",
6037
- description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family grouped by the reference graph, shared taxonomies in Other content, Static pages for the rest, at most 2 levels. With a worked set_content_structure payload.",
6072
+ description: "How to organise a project's Content sidebar: one place per document, the home page and list pages pinned, one folder per content family named for the detail type its route holds and ordered by entry count, shared taxonomies in Shared, Other pages for the rest, Settings last, at most 2 levels. With a worked set_content_structure payload.",
6038
6073
  mimeType: "text/markdown"
6039
6074
  },
6040
6075
  () => ({