@bettercms-ai/mcp 0.66.1 → 0.66.3

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
@@ -2277,7 +2277,7 @@ var STRUCTURE_EXAMPLE_PAYLOAD = {
2277
2277
  doc: {
2278
2278
  schema: 1,
2279
2279
  folders: [
2280
- { id: "home:pages", parentId: null, name: "Other pages", icon: "documents", sort: 1e3 },
2280
+ { id: "home:pages", parentId: null, name: "Pages", icon: "documents", sort: 1e3 },
2281
2281
  { id: "home:collections", parentId: null, name: "Shared", icon: null, sort: 1001 },
2282
2282
  { id: "fld_std_blog", parentId: null, name: "Blog posts", icon: "document-text", sort: 0 },
2283
2283
  { id: "fld_std_products", parentId: null, name: "Products", icon: "basket", sort: 1 }
@@ -2327,18 +2327,20 @@ ever changes.
2327
2327
  The family's main collection comes first, then the others in the order its fields reference
2328
2328
  them.
2329
2329
  4. **Family order:** the site's navigation order; failing that, entry count, then name.
2330
- 5. **Other pages.** EVERY remaining standalone page goes in the system folder \`home:pages\`
2331
- ("Other pages") \u2014 no standalone page ever gets a top-level folder. Code-only routes (a 404, a
2330
+ 5. **Pages.** EVERY remaining standalone page goes in the system folder \`home:pages\`
2331
+ ("Pages") \u2014 no standalone page ever gets a top-level folder. Code-only routes (a 404, a
2332
2332
  page built by code with nothing to edit) stay there too, marked with the \`file-code\` icon.
2333
2333
  Pages that share a route (/legal/terms, /legal/privacy, /docs/...) get a subfolder of it.
2334
+ A site in more than one language files every page, home included, under Pages \u203A <language>
2335
+ (named in itself: English, Fran\xE7ais, Deutsch), default language first.
2334
2336
  6. **Shared taxonomies.** A vocabulary several families reference (Tags used by posts AND
2335
2337
  products) goes in the system folder \`home:collections\`, named "Shared". So does supporting
2336
2338
  content no family owns (testimonials, FAQs with no list page).
2337
2339
  7. **Settings closes the sidebar.** \`home:globals\` ("Settings") holds NO pages or collections \u2014
2338
2340
  navigation, header, footer, site settings and SEO defaults open from it in the dashboard. The
2339
- three system folders always sort BELOW the folders you make: \u2026 Other pages, Shared, Settings.
2341
+ three system folders always sort BELOW the folders you make: \u2026 Pages, Shared, Settings.
2340
2342
  8. **Icons** (kit names, bettercms://icons/dock): \`document-text\` for posts, \`basket\` for products,
2341
- \`book\` for docs, \`documents\` for Other pages, \`folder\` by default. Lucide names already stored
2343
+ \`book\` for docs, \`documents\` for Pages, \`folder\` by default. Lucide names already stored
2342
2344
  still draw.
2343
2345
  9. **Depth:** nest at most 2 levels by default. The hard limit is 4; do not use it without a
2344
2346
  reason the user gave you.
@@ -2354,15 +2356,15 @@ ever changes.
2354
2356
 
2355
2357
  What the structure writes refuse, and the code each refusal carries:
2356
2358
 
2357
- - **System folders.** \`home:pages\` (Other pages), \`home:collections\` (Shared) and
2359
+ - **System folders.** \`home:pages\` (Pages), \`home:collections\` (Shared) and
2358
2360
  \`home:globals\` (Settings) always exist and can never be moved or deleted (400
2359
2361
  \`NAV_SYSTEM_FOLDER\`); \`rename_folder\` and \`set_icon\` work on them.
2360
- - **Type purity** (400 \`NAV_TYPE_MISMATCH\`): Other pages holds only pages at any depth, Shared
2362
+ - **Type purity** (400 \`NAV_TYPE_MISMATCH\`): Pages holds only pages at any depth, Shared
2361
2363
  only collections, Settings neither \u2014 globals are not sidebar items.
2362
2364
  - **Pins** sit above the folders in their own order, pages or collections. In a document a pin is
2363
2365
  \`folderId: "home:root"\`, a virtual id with no folder record; a folder record with that id is
2364
2366
  400 \`NAV_SYSTEM_FOLDER\`. In \`move_to_folder\`, \`folderId: 'root'\` pins and \`null\` returns
2365
- the item to its home (Other pages / Shared).
2367
+ the item to its home (Pages / Shared).
2366
2368
  - **Kind.** \`move_to_folder\` and \`set_icon\` VERIFY what an id is: a page id sent as a
2367
2369
  collection is 400 \`NAV_KIND_MISMATCH\` naming the kind to use. \`set_content_structure\` never
2368
2370
  refuses a kind mismatch: it returns \`warnings\` and \`kindMismatches\` instead.
@@ -2407,7 +2409,7 @@ collection (\`cm_posts\`), a /shop list page (\`pg_shop\`, dynamic) for Products
2407
2409
  Blog
2408
2410
  [folder] Products icon=shopping-bag
2409
2411
  Products, Product categories, Colours (referenced only by Products)
2410
- [system folder] Other pages icon=files
2412
+ [system folder] Pages icon=files
2411
2413
  About, Contact, Not found (icon=file-code)
2412
2414
  [system folder] Shared
2413
2415
  Tags (referenced by Blog AND Products: shared)
@@ -2518,6 +2520,14 @@ function mergeConfig(current, patch) {
2518
2520
  }
2519
2521
  return out;
2520
2522
  }
2523
+ var wordsOf = (v) => (typeof v === "string" ? v : typeof v?.label === "string" ? v.label : "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
2524
+ function helpTextEcho(helpText, field) {
2525
+ if (typeof helpText !== "string" || !helpText.trim()) return null;
2526
+ const said = wordsOf(helpText.replace(/^\s*e\.?g\.?[:,]?\s*/i, "")).replace(/^["'“”‘’]+|["'“”‘’.…]+$/g, "");
2527
+ const value = wordsOf(field.defaultValue);
2528
+ const echoes = said === wordsOf(field.label) || value !== "" && (said === value || /…["”]?$/.test(helpText.trim()) && value.startsWith(said));
2529
+ return echoes ? `helpText repeats the field's label or value, which the editor already shows. Say what belongs here or its format in under 60 characters ("Whole number, no currency symbol."), or send null for none.` : null;
2530
+ }
2521
2531
  function planFieldEdit(model, edit, callerIfMatch) {
2522
2532
  const fields = model.fields ?? [];
2523
2533
  const keys = typeof edit.path === "string" && edit.path ? edit.path.split(".") : [];
@@ -2540,6 +2550,8 @@ function planFieldEdit(model, edit, callerIfMatch) {
2540
2550
  if (!retype && rename === void 0 && edit.config === void 0 && changes.length === 0) {
2541
2551
  return { ok: false, error: "Nothing to change: pass label, helpText, required, config, showIf, ui, placement, type or key." };
2542
2552
  }
2553
+ const echo = helpTextEcho(edit.helpText, { label: edit.label ?? current.label, defaultValue: current.defaultValue });
2554
+ if (echo) return { ok: false, error: echo };
2543
2555
  const next = patchAt(fields, keys, (f) => {
2544
2556
  let out = { ...f };
2545
2557
  for (const k of changes) {
@@ -2737,6 +2749,7 @@ function entryListSearch(args) {
2737
2749
  for (const key of ["modelId", "pageId", "status"]) if (args[key]) q.set(key, String(args[key]));
2738
2750
  for (const [k, v] of whereToQuery(args.where)) q.append(k, v);
2739
2751
  for (const key of ["sort", "page", "limit"]) if (args[key] !== void 0 && args[key] !== null) q.set(key, String(args[key]));
2752
+ if (args.detail !== true) q.set("select", "summary");
2740
2753
  const s = q.toString();
2741
2754
  return s ? `?${s}` : "";
2742
2755
  }
@@ -3128,6 +3141,7 @@ var ENTRY_MERGE_NOTE = "true = keep the fields you leave out: the entry is read,
3128
3141
  var ENTRY_IF_MATCH_NOTE = "optimisticVersion from get_content_entry: the write applies only at that version, and a 409 means it changed since: read it again and re-apply. With merge it defaults to the version merge read.";
3129
3142
  var ENTRY_SCHEDULED_AT_NOTE = "ISO datetime to publish this draft at (needs publish permission); null cancels. An edit that omits scheduledAt CANCELS a pending publish schedule, so send it again with every edit made before it fires.";
3130
3143
  var ENTRY_UNPUBLISH_AT_NOTE = "ISO datetime at which the published entry returns to draft; null cancels. Omitting it never cancels it.";
3144
+ var ENTRY_LIST_DETAIL_NOTE = "true = full rows with every field value and the published copy. Large: read one entry with get_content_entry instead.";
3131
3145
  var ENTRY_LIST_LIMIT_NOTE = "entries per page, 1-200; 50 when omitted. Read `meta.totalPages` and ask for the next `page`.";
3132
3146
  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).";
3133
3147
  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.";
@@ -3412,7 +3426,8 @@ function buildToolDefs(deps) {
3412
3426
  where: z.record(z.string(), z.unknown()).optional().describe(PARAM_TEXT.where),
3413
3427
  sort: z.string().max(100).optional().describe(PARAM_TEXT.sort),
3414
3428
  page: z.number().int().min(1).optional().describe(PARAM_TEXT.page),
3415
- limit: z.number().int().min(1).max(200).optional().describe(ENTRY_LIST_LIMIT_NOTE)
3429
+ limit: z.number().int().min(1).max(200).optional().describe(ENTRY_LIST_LIMIT_NOTE),
3430
+ detail: z.boolean().optional().describe(ENTRY_LIST_DETAIL_NOTE)
3416
3431
  });
3417
3432
  const clearableText = (max) => z.string().max(max).nullable().optional();
3418
3433
  const headPatchShape = {
@@ -3830,7 +3845,7 @@ ${d.summary.outline}` : "Content structure.", d);
3830
3845
  def(
3831
3846
  "suggest_content_structure",
3832
3847
  "Propose the standard Content structure",
3833
- `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.`,
3848
+ `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 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.`,
3834
3849
  z.object({
3835
3850
  maxDepth: z.number().int().min(1).max(4).optional().describe("folder nesting to propose; default 2 (the standard), 4 is the hard limit")
3836
3851
  }).shape,
@@ -3857,7 +3872,7 @@ ${d.outline}` : "Proposed Content structure.", d);
3857
3872
  items: z.array(z.object({
3858
3873
  kind: z.enum(["page", "collection"]),
3859
3874
  id: z.string().min(1).describe("page id or collection (content model) id"),
3860
- 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)'),
3875
+ folderId: z.string().nullable().describe('folder id; "home:root" = pinned at the top level; null = its home (Pages for a page, the Shared list for a collection)'),
3861
3876
  sort: z.number(),
3862
3877
  icon: z.string().nullable()
3863
3878
  }))
@@ -3890,13 +3905,13 @@ ${d.outline}` : "Proposed Content structure.", d);
3890
3905
  def(
3891
3906
  "move_to_folder",
3892
3907
  "Move into a Content sidebar folder",
3893
- `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_POINTER}`,
3908
+ `Move a page, a collection or a folder into a folder. folderId null moves it back to its home: 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_POINTER}`,
3894
3909
  z.object({
3895
3910
  kind: z.enum(["page", "collection", "folder"]).describe(
3896
3911
  "what the id IS on this branch \u2014 VERIFIED, not assumed: an id is either a page or a collection, never both, and a mismatch is refused with 400 NAV_KIND_MISMATCH naming the kind to use. Call it again with that kind, which also clears the wrong-kind entry."
3897
3912
  ),
3898
3913
  id: z.string().min(1).describe("the page id, collection (content model) id, or folder id"),
3899
- 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)")
3914
+ 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 (Pages, the Shared list, or the top level for a folder)")
3900
3915
  }).shape,
3901
3916
  async (c, a) => structureResult(await data(c, "POST", `/management/content-structure/move`, { kind: a.kind, id: a.id, folderId: a.folderId }))
3902
3917
  ),
@@ -5217,20 +5232,11 @@ ${notes.join("\n")}` : summary, created);
5217
5232
  name: "list_content_entries",
5218
5233
  config: {
5219
5234
  title: "List content entries (incl. drafts)",
5220
- description: "List content entries \u2014 including drafts \u2014 filtered by model and/or page, by field values (`where`), sorted and paged. Use it to SEE existing content before editing. For a singleton page, pass its pageId to get its single entry. A project-scoped connection sees its own project's entries plus workspace-level ones, which it can read but not change. Returns `limit` entries per `page` (50 by default); the summary says which page of how many.",
5235
+ description: "List content entries \u2014 including drafts \u2014 filtered by model and/or page, by field values (`where`), sorted and paged. Use it to SEE existing content before editing. For a singleton page, pass its pageId to get its single entry. A project-scoped connection sees its own project's entries plus workspace-level ones, which it can read but not change. Returns `limit` entries per `page` (50 by default); the summary says which page of how many. Each entry is a SUMMARY (id, slug, title, status, model, page, dates, optimisticVersion) \u2014 read values with get_content_entry, or pass `detail: true` for full rows.",
5221
5236
  inputSchema: listEntriesInput.shape
5222
5237
  },
5223
5238
  handler: guard(
5224
5239
  async (args) => withClient(async (client) => {
5225
- const filter = {
5226
- ...args.modelId ? { modelId: args.modelId } : {},
5227
- ...args.pageId ? { pageId: args.pageId } : {},
5228
- ...args.status ? { status: args.status } : {}
5229
- };
5230
- if ([args.limit, args.where, args.sort, args.page].every((v) => v === void 0)) {
5231
- const entries = await client.listEntries(filter);
5232
- return ok(`${entries.length} entr(y/ies).`, entries);
5233
- }
5234
5240
  const res = await client.fetchJSON(
5235
5241
  client.url(`/management/content/entries${entryListSearch(args)}`),
5236
5242
  { method: "GET" }
@@ -6132,6 +6138,10 @@ components \u2014 is \`editor_setup\`, run on demand against the live build. The
6132
6138
  \`get_deploy_status\` \`editorSetup\` (and get_next_steps) say when it has not run for the live
6133
6139
  build: \`never\` or \`stale\` \u2192 run it before anything below.
6134
6140
 
6141
+ **Builds run \`build:bcms\` instead of \`build\`** when package.json declares one: the build alone,
6142
+ without CI checks (\`"build:bcms": "astro build"\` beside \`"build": "astro check && astro build"\`),
6143
+ keeping any step the build needs. The convert codemod adds it; never edit \`build\` for BetterCMS.
6144
+
6135
6145
  \`deploy_project\` and \`promote_project\` both answer **409
6136
6146
  AUTHORING_DECISION_REQUIRED** until a human has chosen this project's architecture. This is
6137
6147
  asked ONCE per project, ever. It is not an error to retry or route around: read the message
@@ -6854,7 +6864,8 @@ Importance orders a screen: required fields first, then array order.
6854
6864
  schema: the same call with \`pageId\` instead of \`modelId\`, and the version from \`get_page\`'s
6855
6865
  \`backingModel\`.
6856
6866
  - A component's props: \`update_component\` with \`propUi\`, \`propGroup\`, \`propLabel\`, \`propHelpText\` (keyed by prop key).
6857
- Name props from the code (\`plan.cents\` \u2192 Cents), help text as \`e.g. "49"\`; repeated cards are one \`table\` prop.
6867
+ Name props from the code (\`plan.cents\` \u2192 Cents); help text guides ("Whole number, no currency symbol."), never repeats
6868
+ the value; repeated cards are one \`table\` prop. An icon (\`config.media: "icon"\`) is content: group it, never prune it.
6858
6869
  They write the DRAFT props; no publish is needed for the editor to show them.
6859
6870
  - Always send \`ifMatch\`, the \`optimisticVersion\` you read.
6860
6871
  - A NEW field carries its chrome in the same \`add_field\` call.
@@ -7649,7 +7660,7 @@ function buildServer(deps) {
7649
7660
  STRUCTURE_PLAYBOOK_URI,
7650
7661
  {
7651
7662
  title: "BetterCMS Content structure playbook",
7652
- 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.",
7663
+ 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, Pages for the rest, Settings last, at most 2 levels. With a worked set_content_structure payload.",
7653
7664
  mimeType: "text/markdown"
7654
7665
  },
7655
7666
  () => ({