@omniharmonic/openspace-cli 0.1.16 → 0.1.17

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.
@@ -7579,12 +7579,18 @@ var trimSlashes = (s2) => s2.replace(/^\/+|\/+$/g, "");
7579
7579
  var LinkTargets = class {
7580
7580
  constructor(doc) {
7581
7581
  this.doc = doc;
7582
- this.slugs = new Set(Object.values(doc.pages).map((p) => p.slug));
7582
+ this.bySlug = new Map(Object.values(doc.pages).map((p) => [p.slug, p]));
7583
+ this.slugs = new Set(this.bySlug.keys());
7583
7584
  this.redirectFroms = new Set((doc.site.redirects ?? []).map((r) => trimSlashes(r.from)));
7584
7585
  }
7585
7586
  doc;
7586
7587
  slugs;
7587
7588
  redirectFroms;
7589
+ bySlug;
7590
+ /** The page at a site-internal path (`/about/#team` → About), if it is one. */
7591
+ pageAt(raw2) {
7592
+ return this.bySlug.get(trimSlashes(raw2.split(/[?#]/)[0] ?? ""));
7593
+ }
7588
7594
  hasCollection(name) {
7589
7595
  return name in this.doc.collections || name in (this.doc.collectionConfigs ?? {});
7590
7596
  }
@@ -7610,8 +7616,34 @@ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7610
7616
  function findBrokenLinks(doc) {
7611
7617
  const targets = new LinkTargets(doc);
7612
7618
  const out = [];
7619
+ const fragments = /* @__PURE__ */ new Map();
7620
+ const fragmentsOf = (page) => {
7621
+ let f = fragments.get(page);
7622
+ if (!f) {
7623
+ f = pageFragments(doc, page);
7624
+ fragments.set(page, f);
7625
+ }
7626
+ return f;
7627
+ };
7628
+ const sectionIds = new Set(
7629
+ Object.values(doc.pages).flatMap((p) => p.sections.map((s2) => s2.id))
7630
+ );
7613
7631
  const walk5 = (value, scope, path2, where2) => {
7614
7632
  const add = (kind, target, message, severity = "error") => out.push({ scope, path: path2, ...where2, kind, target, severity, message });
7633
+ const checkAnchor = (o2) => {
7634
+ const ref = anchorRef(o2, targets, scope.kind === "page" ? scope.page : void 0);
7635
+ if (!ref || fragmentsOf(ref.page.id).has(ref.fragment)) return;
7636
+ out.push({
7637
+ scope,
7638
+ path: path2,
7639
+ ...where2,
7640
+ kind: "anchor",
7641
+ target: ref.target,
7642
+ severity: "warning",
7643
+ message: `Link to ${ref.target}: no section on ${pageTitle(ref.page)} is called \u201C${ref.fragment}\u201D`,
7644
+ anchor: { page: ref.page.id, fragment: ref.fragment }
7645
+ });
7646
+ };
7615
7647
  if (typeof value === "string") {
7616
7648
  if (path2[path2.length - 1] === "body")
7617
7649
  for (const m of value.matchAll(MARKDOWN_LINK_RE)) {
@@ -7630,6 +7662,10 @@ function findBrokenLinks(doc) {
7630
7662
  if (o.type === "page" && typeof o.page === "string") {
7631
7663
  if (!(o.page in doc.pages))
7632
7664
  add("page", o.page, `Link to page ${o.page} but no such page exists`);
7665
+ else checkAnchor(o);
7666
+ } else if (o.type === "anchor" && typeof o.section === "string") {
7667
+ if (!sectionIds.has(o.section))
7668
+ add("anchor", o.section, "Link to a section that no longer exists", "warning");
7633
7669
  } else if (o.type === "item" && typeof o.collection === "string" && typeof o.slug === "string") {
7634
7670
  if (!doc.collections[o.collection]?.[o.slug])
7635
7671
  add(
@@ -7647,10 +7683,12 @@ function findBrokenLinks(doc) {
7647
7683
  } else if (o.type === "url" && typeof o.url === "string" && isInternalPath(o.url)) {
7648
7684
  if (!targets.pathResolves(o.url))
7649
7685
  add("url", o.url, `Link to ${o.url} does not lead to any page`);
7686
+ else checkAnchor(o);
7650
7687
  } else if (o.type === "link" && isObject(o.attrs) && typeof o.attrs.href === "string") {
7651
7688
  const href = o.attrs.href;
7652
7689
  if (isInternalPath(href) && !targets.pathResolves(href))
7653
7690
  add("richtext", href, `Text link to ${href} does not lead to any page`, "warning");
7691
+ else checkAnchor(o);
7654
7692
  }
7655
7693
  for (const [k, v] of Object.entries(o)) walk5(v, scope, [...path2, k], where2);
7656
7694
  };
@@ -7788,6 +7826,121 @@ function withPages(doc, pages) {
7788
7826
  for (const p of pages) all[p.id] = p;
7789
7827
  return { ...doc, pages: all };
7790
7828
  }
7829
+ var pageTitle = (p) => p.title || (p.slug === "" ? "Home" : p.slug);
7830
+ var LANDMARKS = ["main", "top"];
7831
+ var TEMPLATE_ID_RE = /\bid\s*=\s*["']([A-Za-z][\w-]*)["']/g;
7832
+ function pageFragments(doc, pageId) {
7833
+ const ids = new Set(LANDMARKS);
7834
+ const blocks2 = /* @__PURE__ */ new Set();
7835
+ const page = doc.pages[pageId];
7836
+ for (const s2 of [...page?.sections ?? [], doc.site.footer]) {
7837
+ ids.add(s2.anchor ?? s2.id);
7838
+ for (const b of s2.blocks) {
7839
+ blocks2.add(b.id);
7840
+ const name = /^component\/(.+)$/.exec(b.type)?.[1];
7841
+ const tpl = name ? doc.components?.[name]?.template : void 0;
7842
+ if (tpl) {
7843
+ for (const m of tpl.matchAll(TEMPLATE_ID_RE)) if (m[1]) ids.add(m[1]);
7844
+ }
7845
+ }
7846
+ }
7847
+ return {
7848
+ has: (f) => ids.has(f) || blocks2.has(f) || [...blocks2].some((b) => f.startsWith(`${b}-`))
7849
+ };
7850
+ }
7851
+ function anchorRef(o, targets, currentPage) {
7852
+ if (o.type === "page" && typeof o.page === "string" && typeof o.anchor === "string") {
7853
+ const page2 = targets.doc.pages[o.page];
7854
+ return page2 ? { page: page2, fragment: o.anchor, target: `${pagePath(page2)}#${o.anchor}` } : void 0;
7855
+ }
7856
+ const href = o.type === "url" && typeof o.url === "string" ? o.url : o.type === "link" && isObject(o.attrs) && typeof o.attrs.href === "string" ? o.attrs.href : void 0;
7857
+ if (!href) return void 0;
7858
+ const hash = /#([^#?]+)$/.exec(href)?.[1];
7859
+ if (!hash) return void 0;
7860
+ if (href.startsWith("#")) {
7861
+ if (o.type !== "link" || !currentPage) return void 0;
7862
+ const page2 = targets.doc.pages[currentPage];
7863
+ return page2 ? { page: page2, fragment: hash, target: href } : void 0;
7864
+ }
7865
+ if (!isInternalPath(href)) return void 0;
7866
+ const page = targets.pageAt(href);
7867
+ return page ? { page, fragment: hash, target: href } : void 0;
7868
+ }
7869
+ function readableStrings(value, out = []) {
7870
+ if (typeof value === "string") {
7871
+ if (!/^(?:\/|#|https?:|mailto:|tel:|media\/|data:)/.test(value)) out.push(value);
7872
+ } else if (Array.isArray(value)) for (const v of value) readableStrings(v, out);
7873
+ else if (isObject(value)) {
7874
+ for (const [k, v] of Object.entries(value))
7875
+ if (!["type", "id", "src", "url", "href", "page", "variant", "level", "preset"].includes(k))
7876
+ readableStrings(v, out);
7877
+ }
7878
+ return out;
7879
+ }
7880
+ function linksToAnchor(value, targets, pageId, fragment) {
7881
+ if (Array.isArray(value)) return value.some((v) => linksToAnchor(v, targets, pageId, fragment));
7882
+ if (!isObject(value)) return false;
7883
+ const ref = anchorRef(value, targets, pageId);
7884
+ if (ref && ref.page.id === pageId && ref.fragment === fragment) return true;
7885
+ return Object.values(value).some((v) => linksToAnchor(v, targets, pageId, fragment));
7886
+ }
7887
+ function suggestAnchorSection(doc, pageId, fragment) {
7888
+ const page = doc.pages[pageId];
7889
+ const wanted = words(fragment);
7890
+ if (!page || !wanted.length) return void 0;
7891
+ const targets = new LinkTargets(doc);
7892
+ const match = (s2) => {
7893
+ const own3 = words(s2);
7894
+ const hits = wanted.filter((w) => own3.some((x) => related(w, x))).length;
7895
+ return hits === wanted.length ? 2 : hits ? 1 : 0;
7896
+ };
7897
+ let best;
7898
+ let bestScore = 0;
7899
+ for (const s2 of page.sections) {
7900
+ if (linksToAnchor(s2.blocks, targets, page.id, fragment)) continue;
7901
+ const name = [
7902
+ s2.label ?? "",
7903
+ s2.anchor ?? "",
7904
+ (s2.preset ?? "").replace(/@\d+$/, ""),
7905
+ ...s2.blocks.map((b) => b.type.replace(/^component\//, ""))
7906
+ ].join(" ");
7907
+ const text7 = readableStrings(s2.blocks.map((b) => b.fields)).reduce((n, t) => n + match(t), 0);
7908
+ const score = match(name) * 4 + Math.min(text7, 4);
7909
+ if (score > bestScore) {
7910
+ best = s2;
7911
+ bestScore = score;
7912
+ }
7913
+ }
7914
+ return bestScore >= 2 ? best : void 0;
7915
+ }
7916
+ function retargetAnchor(value, doc, pageId, from, to, currentPage) {
7917
+ const targets = new LinkTargets(doc);
7918
+ let changed = 0;
7919
+ const swap = (href) => {
7920
+ const base = href.slice(0, href.length - from.length - 1);
7921
+ if (to) return `${base}#${to}`;
7922
+ return base || "#top";
7923
+ };
7924
+ const fix = (v) => {
7925
+ if (Array.isArray(v)) return v.map(fix);
7926
+ if (!isObject(v)) return v;
7927
+ const ref = anchorRef(v, targets, currentPage);
7928
+ if (ref && ref.page.id === pageId && ref.fragment === from) {
7929
+ changed++;
7930
+ if (v.type === "page") {
7931
+ const { anchor: _a3, ...rest } = v;
7932
+ return to ? { ...rest, anchor: to } : rest;
7933
+ }
7934
+ if (v.type === "url") return { ...v, url: swap(v.url) };
7935
+ const attrs = v.attrs;
7936
+ return { ...v, attrs: { ...attrs, href: swap(attrs.href) } };
7937
+ }
7938
+ const out = {};
7939
+ for (const [k, x] of Object.entries(v)) out[k] = fix(x);
7940
+ return out;
7941
+ };
7942
+ return { value: fix(value), changed };
7943
+ }
7791
7944
 
7792
7945
  // ../schema/src/secrets.ts
7793
7946
  function shannonEntropy(value) {
@@ -12385,6 +12538,11 @@ function linkPlace(doc, link2) {
12385
12538
  }
12386
12539
  function describeBrokenLink(doc, link2) {
12387
12540
  const where2 = linkPlace(doc, link2);
12541
+ if (link2.kind === "anchor") {
12542
+ const page = link2.anchor ? doc.pages[link2.anchor.page] : void 0;
12543
+ if (!link2.anchor || !page) return `${where2} links to a section that no longer exists.`;
12544
+ return `${where2} links to \u201C${link2.anchor.fragment}\u201D on ${titleOf(page)}, but no section there is called that.`;
12545
+ }
12388
12546
  switch (link2.kind) {
12389
12547
  case "page":
12390
12548
  return `${where2} links to a page that no longer exists.`;
@@ -12426,10 +12584,73 @@ function linkFix(doc, link2, meta) {
12426
12584
  if (link2.scope.kind === "site" && link2.path[0] === "nav") {
12427
12585
  const { value, changes } = resolveInternalLinks(doc.site.nav, doc);
12428
12586
  if (!changes.length) return void 0;
12429
- return { label, ops: [{ op: "site.update", ...meta(), set: { nav: value } }] };
12587
+ return { label, ops: [{ op: "nav.update", ...meta(), tree: value }] };
12430
12588
  }
12431
12589
  return void 0;
12432
12590
  }
12591
+ function sectionName(section) {
12592
+ if (section.label) return section.label;
12593
+ for (const b of section.blocks) {
12594
+ const t = b.fields.text;
12595
+ if (typeof t === "string" && t.trim()) return t.trim().slice(0, 60);
12596
+ }
12597
+ return "that";
12598
+ }
12599
+ function retargetOps(doc, link2, from, to, meta) {
12600
+ const anchorPage = link2.anchor?.page;
12601
+ if (!anchorPage) return void 0;
12602
+ if (link2.block && link2.field) {
12603
+ const container2 = link2.container === "footer" ? doc.site.footer : doc.pages[link2.container]?.sections.find((x) => x.id === link2.section);
12604
+ const block4 = container2?.blocks.find((b) => b.id === link2.block);
12605
+ if (!block4) return void 0;
12606
+ const current2 = link2.container === "footer" ? void 0 : link2.container;
12607
+ const r = retargetAnchor(block4.fields[link2.field], doc, anchorPage, from, to, current2);
12608
+ if (!r.changed) return void 0;
12609
+ return [
12610
+ {
12611
+ op: "block.update",
12612
+ ...meta(),
12613
+ page: link2.container,
12614
+ block: block4.id,
12615
+ set: { [link2.field]: r.value }
12616
+ }
12617
+ ];
12618
+ }
12619
+ if (link2.scope.kind === "site" && link2.path[0] === "nav") {
12620
+ const r = retargetAnchor(doc.site.nav, doc, anchorPage, from, to);
12621
+ if (!r.changed) return void 0;
12622
+ return [{ op: "nav.update", ...meta(), tree: r.value }];
12623
+ }
12624
+ return void 0;
12625
+ }
12626
+ function anchorFix(doc, link2, meta) {
12627
+ if (!link2.anchor) return void 0;
12628
+ const { page: pageId, fragment } = link2.anchor;
12629
+ const page = doc.pages[pageId];
12630
+ if (!page) return void 0;
12631
+ const section = suggestAnchorSection(doc, pageId, fragment);
12632
+ if (!section) {
12633
+ const ops2 = retargetOps(doc, link2, fragment, void 0, meta);
12634
+ return ops2 ? { label: `Link to the top of ${titleOf(page)} instead`, ops: ops2 } : void 0;
12635
+ }
12636
+ const label = `Connect it to the \u201C${sectionName(section)}\u201D section`;
12637
+ const taken = new Set(page.sections.map((s2) => s2.anchor).filter(Boolean));
12638
+ if (!section.anchor && SlugSchema.safeParse(fragment).success && !taken.has(fragment))
12639
+ return {
12640
+ label,
12641
+ ops: [
12642
+ {
12643
+ op: "section.update",
12644
+ ...meta(),
12645
+ page: pageId,
12646
+ section: section.id,
12647
+ set: { anchor: fragment }
12648
+ }
12649
+ ]
12650
+ };
12651
+ const ops = retargetOps(doc, link2, fragment, section.anchor ?? section.id, meta);
12652
+ return ops ? { label, ops } : void 0;
12653
+ }
12433
12654
  function linkIssues(doc, pageId, options = {}, meta = opMeta(options)) {
12434
12655
  const out = [];
12435
12656
  for (const link2 of findBrokenLinks(doc)) {
@@ -12437,7 +12658,7 @@ function linkIssues(doc, pageId, options = {}, meta = opMeta(options)) {
12437
12658
  if (pageId && s2.kind === "page" && s2.page !== pageId) continue;
12438
12659
  if (pageId && s2.kind === "item") continue;
12439
12660
  const page = link2.container ?? (s2.kind === "page" ? s2.page : void 0);
12440
- const autofix = linkFix(doc, link2, meta);
12661
+ const autofix = link2.kind === "anchor" ? anchorFix(doc, link2, meta) : linkFix(doc, link2, meta);
12441
12662
  out.push(
12442
12663
  issue({
12443
12664
  rule: "broken-link",
@@ -12447,7 +12668,7 @@ function linkIssues(doc, pageId, options = {}, meta = opMeta(options)) {
12447
12668
  ...link2.block ? { block: link2.block } : {},
12448
12669
  key: `${s2.kind === "item" ? `${s2.collection}/${s2.slug}:` : ""}${link2.path.join(".")}`,
12449
12670
  message: describeBrokenLink(doc, link2),
12450
- fix: link2.severity === "error" ? "Point the link at a page that exists, or add the missing page. The site can't be published until then." : "Point the link at a page that exists, or add the missing page.",
12671
+ fix: link2.kind === "anchor" ? "Give the section that name (Section settings \u2192 Link name), or point the link at another section." : link2.severity === "error" ? "Point the link at a page that exists, or add the missing page. The site can't be published until then." : "Point the link at a page that exists, or add the missing page.",
12451
12672
  ...autofix ? { autofix } : {}
12452
12673
  })
12453
12674
  );
@@ -13264,8 +13485,8 @@ function phoneIssuesFromProbe(doc, pageId, results, options = {}) {
13264
13485
  const at = locate(doc, pageId, f);
13265
13486
  if (f.kind === "cta-below-fold" && at.section?.preset && !at.section.preset.startsWith("hero/"))
13266
13487
  continue;
13267
- const sectionName = at.section && at.page !== FOOTER ? " in this section" : at.page === FOOTER ? " in the footer" : "";
13268
- const where2 = f.component ? ` in the \u201C${f.component}\u201D component` : sectionName;
13488
+ const sectionName2 = at.section && at.page !== FOOTER ? " in this section" : at.page === FOOTER ? " in the footer" : "";
13489
+ const where2 = f.component ? ` in the \u201C${f.component}\u201D component` : sectionName2;
13269
13490
  const [message, fix] = MESSAGE[f.kind](f, where2);
13270
13491
  const key2 = `${f.kind}:${at.block?.id ?? f.block ?? ""}:${f.kind === "tap-target" || f.kind === "photo-contrast" ? f.text ?? "" : ""}`;
13271
13492
  const i = issue({
@@ -45432,6 +45653,7 @@ Design check flags (custom components too: invalid code, budgets, motion, fixed
45432
45653
  - Styles come from Site Styles tokens (palette, pairing, density, shape, buttons, motion, image treatment). Never write inline CSS; restyle with set_styles or apply_template_look.
45433
45654
  - Section color themes: light, light-accent, dark, dark-accent, brand. The contrast solver keeps text readable in all five.
45434
45655
  - Rich text is Markdown: headings (#\u2013####), **bold**, *italic*, [links](/contact), lists, > quotes; a trailing {.lead} sets a paragraph preset.
45656
+ - Menu entries and buttons that jump to a section (one-page sites: "How it works", "Prices", "Join"): give the section a link name (anchor: insert_section / insert_component, or update_section set {"anchor": "how-it-works"}), then link to it \u2014 menu entry {type:"page", page, label, anchor}, button or field link {type:"page", page, anchor}. The same entries serve the desktop menu and the phone menu. design_check warns about links to a section name nothing has.
45435
45657
  - Edits land in the draft. The owner publishes \u2014 call publish only when they ask, with confirm: true.
45436
45658
  `;
45437
45659
  var ITERATION_GUIDANCE = `# Design iteration loop
@@ -46025,9 +46247,807 @@ var positionArg = external_exports.union([
46025
46247
  external_exports.strictObject({ before: external_exports.string() }),
46026
46248
  external_exports.strictObject({ after: external_exports.string() })
46027
46249
  ]).describe('Where: an index, "start", "end", {"before": id} or {"after": id}.');
46250
+ var anchorArg = external_exports.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase words joined by hyphens, like how-it-works.").max(60).describe(
46251
+ `The section's link name, so menu entries and buttons can jump to it: lowercase words with hyphens, e.g. "how-it-works". Link to it with {type:"page", page, anchor}.`
46252
+ );
46028
46253
  var confirmArg = external_exports.boolean().optional().describe("Must be true. Ask the owner first: this cannot be undone from here.");
46029
46254
  var deviceArg = external_exports.enum(["desktop", "tablet", "mobile"]);
46030
46255
 
46256
+ // ../mcp-tools/src/tools/read.ts
46257
+ var listSites = defineTool({
46258
+ name: "list_sites",
46259
+ title: "List sites",
46260
+ description: "Sites this connection can edit, with their titles and live addresses. Start here when you don't know the site id.",
46261
+ input: external_exports.object({}),
46262
+ annotations: { readOnlyHint: true },
46263
+ async handler(ctx) {
46264
+ const allowed = new Set(await ctx.allowedSites());
46265
+ const sites = (await ctx.store.listSites()).filter((s2) => allowed.has(s2.id));
46266
+ return { data: { sites, canPublish: ctx.grant.publish && ctx.store.capabilities.canPublish } };
46267
+ }
46268
+ });
46269
+ function stylesSummary(doc) {
46270
+ const compiled2 = compileStyles(doc.styles);
46271
+ const pairing = getPairing(doc.styles.choices.pairing);
46272
+ return {
46273
+ choices: doc.styles.choices,
46274
+ pairing: pairing ? { id: pairing.id, heading: pairing.heading.family, body: pairing.body.family } : { id: doc.styles.choices.pairing, unknown: true },
46275
+ overrides: doc.styles.overrides ?? null,
46276
+ brand: compiled2.tokens.palette.brand,
46277
+ themes: Object.fromEntries(
46278
+ Object.entries(compiled2.tokens.themes).map(([theme, c]) => [
46279
+ theme,
46280
+ {
46281
+ background: c.background,
46282
+ text: c.text,
46283
+ heading: c.heading,
46284
+ accent: c.accent,
46285
+ "accent-text": c["accent-text"]
46286
+ }
46287
+ ])
46288
+ ),
46289
+ contrast: {
46290
+ mode: compiled2.contrast.mode,
46291
+ adjustedBySolver: compiled2.contrast.adjustments.length,
46292
+ failures: compiled2.contrast.failures.length
46293
+ }
46294
+ };
46295
+ }
46296
+ function pageSummary(page) {
46297
+ return {
46298
+ id: page.id,
46299
+ title: page.title,
46300
+ slug: page.slug,
46301
+ path: pagePathOf(page),
46302
+ sections: page.sections.length,
46303
+ hidden: page.hidden ?? false
46304
+ };
46305
+ }
46306
+ var getSite = defineTool({
46307
+ name: "get_site",
46308
+ title: "Get site",
46309
+ description: "Site settings, navigation, header/footer, Site Styles summary, page list and collections. Call before editing to learn page ids and the current look.",
46310
+ input: external_exports.object({ site: siteArg }),
46311
+ annotations: { readOnlyHint: true },
46312
+ async handler(ctx, input) {
46313
+ const siteId = await ctx.siteId(input.site);
46314
+ const doc = await ctx.doc(siteId);
46315
+ const { site: site2 } = doc;
46316
+ return {
46317
+ data: {
46318
+ id: siteId,
46319
+ title: site2.title,
46320
+ tagline: site2.tagline ?? null,
46321
+ template: doc.config.template ?? null,
46322
+ url: doc.config.deploy.customDomain ? `https://${doc.config.deploy.customDomain}` : doc.config.deploy.url,
46323
+ nav: site2.nav,
46324
+ header: site2.header,
46325
+ footer: {
46326
+ sectionId: site2.footer.id,
46327
+ layout: site2.footer.layout.mode,
46328
+ blocks: site2.footer.blocks.map((b) => ({ id: b.id, type: b.type })),
46329
+ note: 'Edit footer blocks with the block tools using page "footer".'
46330
+ },
46331
+ announcement: site2.announcement ?? null,
46332
+ social: site2.social ?? [],
46333
+ seo: site2.seo ?? null,
46334
+ styles: stylesSummary(doc),
46335
+ pages: Object.values(doc.pages).map(pageSummary),
46336
+ collections: Object.entries(doc.collections).map(([name, items]) => ({
46337
+ name,
46338
+ kind: Object.values(items)[0]?.kind ?? doc.collectionConfigs?.[name]?.kind ?? null,
46339
+ items: Object.keys(items).length
46340
+ })),
46341
+ media: Object.keys(doc.media).length,
46342
+ capabilities: {
46343
+ ...ctx.store.capabilities,
46344
+ // Same answer as list_sites: publishing needs both the store and this connection.
46345
+ canPublish: ctx.grant.publish && ctx.store.capabilities.canPublish,
46346
+ grantPublish: ctx.grant.publish
46347
+ }
46348
+ }
46349
+ };
46350
+ }
46351
+ });
46352
+ function sectionView(ctx, section, detail, doc) {
46353
+ const registry3 = doc ? ctx.registryFor(doc) : ctx.registry;
46354
+ const blocks2 = section.blocks.map((b) => {
46355
+ if (detail === "outline") {
46356
+ const out = {
46357
+ id: b.id,
46358
+ type: b.type,
46359
+ text: blockPreview(registry3, b)
46360
+ };
46361
+ if (b.variant) out.variant = b.variant;
46362
+ if (b.column !== void 0) out.column = b.column;
46363
+ if (b.hidden) out.hidden = true;
46364
+ return out;
46365
+ }
46366
+ return blockToAgent(registry3, b);
46367
+ });
46368
+ const base = {
46369
+ id: section.id,
46370
+ ...section.preset ? { preset: section.preset } : {},
46371
+ ...section.label ? { label: section.label } : {},
46372
+ ...section.hidden ? { hidden: true } : {},
46373
+ ...section.locked ? { locked: true } : {},
46374
+ theme: section.style?.colorTheme ?? "light"
46375
+ };
46376
+ if (detail === "outline") {
46377
+ base.layout = section.layout.mode === "grid" ? "grid" : `stack ${JSON.stringify(section.layout.columns.d)}`;
46378
+ base.blocks = blocks2;
46379
+ return base;
46380
+ }
46381
+ return {
46382
+ ...base,
46383
+ layout: section.layout,
46384
+ style: section.style ?? {},
46385
+ ...section.anchor ? { anchor: section.anchor } : {},
46386
+ blocks: blocks2
46387
+ };
46388
+ }
46389
+ var getPage2 = defineTool({
46390
+ name: "get_page",
46391
+ title: "Get page",
46392
+ description: `A page's section/block tree with stable ids, fields (rich text as Markdown), layout and style. Use detail "outline" for a compact overview (ids, types, text previews) and "full" before editing fields or grid placements. page "footer" returns the site footer section.`,
46393
+ input: external_exports.object({
46394
+ site: siteArg,
46395
+ page: containerArg,
46396
+ detail: external_exports.enum(["full", "outline"]).optional().describe('Default "full".')
46397
+ }),
46398
+ annotations: { readOnlyHint: true },
46399
+ async handler(ctx, input) {
46400
+ const siteId = await ctx.siteId(input.site);
46401
+ const doc = await ctx.doc(siteId);
46402
+ const detail = input.detail ?? "full";
46403
+ const { id, page } = resolvePage(doc, input.page, { allowFooter: true });
46404
+ if (id === FOOTER || !page)
46405
+ return { data: { page: FOOTER, sections: [sectionView(ctx, doc.site.footer, detail, doc)] } };
46406
+ return {
46407
+ data: {
46408
+ id: page.id,
46409
+ title: page.title,
46410
+ slug: page.slug,
46411
+ path: pagePathOf(page),
46412
+ seo: page.seo ?? {},
46413
+ ...page.header ? { header: page.header } : {},
46414
+ ...page.hidden ? { hidden: true } : {},
46415
+ sections: page.sections.map((s2) => sectionView(ctx, s2, detail, doc))
46416
+ }
46417
+ };
46418
+ }
46419
+ });
46420
+ function blockMetaView(m, full) {
46421
+ const fields = Object.fromEntries(
46422
+ Object.entries(m.fields).map(([k, f]) => {
46423
+ const v = {
46424
+ kind: f.kind === "richText" ? "richText (Markdown)" : f.kind
46425
+ };
46426
+ if (f.optional) v.optional = true;
46427
+ if (f.label) v.label = f.label;
46428
+ if (full && f.help) v.help = f.help;
46429
+ if (f.ai) v.ai = f.ai;
46430
+ if (f.kind === "select") v.options = f.options.map((o) => o.value);
46431
+ if (f.kind === "text" && f.maxLength) v.maxLength = f.maxLength;
46432
+ if (f.kind === "list")
46433
+ v.itemFields = Object.fromEntries(
46434
+ Object.entries(f.fields).map(([ik, iv]) => [
46435
+ ik,
46436
+ iv.kind === "richText" ? "richText (Markdown)" : iv.kind
46437
+ ])
46438
+ );
46439
+ if (f.kind === "image") v.shape = "{ src, alt, focal? }";
46440
+ if (f.kind === "link") v.allow = f.allow;
46441
+ return [k, v];
46442
+ })
46443
+ );
46444
+ return {
46445
+ type: m.type,
46446
+ label: m.label,
46447
+ category: m.category,
46448
+ description: m.description,
46449
+ variants: m.variants,
46450
+ styleSlots: m.styleSlots,
46451
+ fields,
46452
+ ai: m.ai
46453
+ };
46454
+ }
46455
+ var listBlocks = defineTool({
46456
+ name: "list_blocks",
46457
+ title: "List blocks",
46458
+ description: "The block catalog: each block's fields (rich text as Markdown), variants, style slots and AI guidance (purpose, whenToUse, avoid). Filter by category or type. Prefer section presets for whole sections; use blocks to add or adjust pieces.",
46459
+ input: external_exports.object({
46460
+ category: external_exports.string().optional(),
46461
+ type: external_exports.string().optional().describe("One block type for full detail.")
46462
+ }),
46463
+ annotations: { readOnlyHint: true },
46464
+ async handler(ctx, input) {
46465
+ let metas = ctx.registry.meta();
46466
+ if (input.type) {
46467
+ metas = metas.filter((m) => m.type === input.type);
46468
+ if (!metas.length)
46469
+ throw new ToolError(
46470
+ `Unknown block type "${input.type}". Types: ${ctx.registry.types().join(", ")}.`,
46471
+ "not_found"
46472
+ );
46473
+ }
46474
+ if (input.category) metas = metas.filter((m) => m.category === input.category);
46475
+ return {
46476
+ data: {
46477
+ blocks: metas.map((m) => blockMetaView(m, Boolean(input.type))),
46478
+ categories: [...new Set(ctx.registry.meta().map((m) => m.category))]
46479
+ }
46480
+ };
46481
+ }
46482
+ });
46483
+ var getBlockSchema = defineTool({
46484
+ name: "get_block_schema",
46485
+ title: "Get block schema",
46486
+ description: "JSON Schema for one block type's fields (with labels and AI hints). Rich-text fields are shown as ProseMirror JSON but the edit tools also accept a Markdown string for them.",
46487
+ input: external_exports.object({ type: external_exports.string() }),
46488
+ annotations: { readOnlyHint: true },
46489
+ async handler(ctx, input) {
46490
+ const def = ctx.registry.get(input.type);
46491
+ if (!def)
46492
+ throw new ToolError(
46493
+ `Unknown block type "${input.type}". Types: ${ctx.registry.types().join(", ")}.`,
46494
+ "not_found"
46495
+ );
46496
+ return {
46497
+ data: {
46498
+ type: def.type,
46499
+ version: def.version,
46500
+ variants: def.meta.variants,
46501
+ styleSlots: def.meta.styleSlots,
46502
+ fields: blockFieldsJsonSchema(def)
46503
+ }
46504
+ };
46505
+ }
46506
+ });
46507
+ var listSectionPresets = defineTool({
46508
+ name: "list_section_presets",
46509
+ title: "List section presets",
46510
+ description: "Designed section presets by category (hero, about, services, gallery, testimonials, pricing, faq, team, stats, cta, contact, newsletter, blog, events, products, footer, header) with descriptions, layout, designed-for color themes, copy-length ranges and AI guidance. Compose pages from these with insert_section/create_page.",
46511
+ input: external_exports.object({
46512
+ category: external_exports.enum(PRESET_CATEGORIES).optional(),
46513
+ query: external_exports.string().optional().describe("Words to match in label, description or tags."),
46514
+ detail: external_exports.enum(["brief", "full"]).optional().describe('Default "brief".')
46515
+ }),
46516
+ annotations: { readOnlyHint: true },
46517
+ async handler(_ctx, input) {
46518
+ let list5 = presetCatalog.slice();
46519
+ if (input.category) list5 = list5.filter((p) => p.category === input.category);
46520
+ if (input.query) {
46521
+ const words4 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
46522
+ list5 = list5.filter((p) => {
46523
+ const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
46524
+ return words4.every((w) => hay.includes(w));
46525
+ });
46526
+ }
46527
+ const full = input.detail === "full";
46528
+ return {
46529
+ data: {
46530
+ presets: list5.map(
46531
+ (p) => full ? p : {
46532
+ id: p.id,
46533
+ label: p.label,
46534
+ description: p.description,
46535
+ target: p.target,
46536
+ layout: p.layout,
46537
+ overlap: p.overlap,
46538
+ themes: p.themes,
46539
+ whenToUse: p.ai.whenToUse
46540
+ }
46541
+ ),
46542
+ categories: PRESET_CATEGORIES
46543
+ }
46544
+ };
46545
+ }
46546
+ });
46547
+ function findPreset(id) {
46548
+ const exact = getPreset(id);
46549
+ if (exact) return exact;
46550
+ const base = id.replace(/@\d+$/, "");
46551
+ const matches = presetCatalog.filter((p) => p.id.startsWith(`${base}@`)).sort((a, b) => b.version - a.version);
46552
+ const best = matches[0] && getPreset(matches[0].id);
46553
+ if (best) return best;
46554
+ const category = base.split("/")[0] ?? "";
46555
+ const near = presetCatalog.filter((p) => p.category === category).map((p) => p.id);
46556
+ throw new ToolError(
46557
+ `Unknown preset "${id}".${near.length ? ` In ${category}: ${near.join(", ")}.` : " Call list_section_presets."}`,
46558
+ "not_found"
46559
+ );
46560
+ }
46561
+
46562
+ // ../mcp-tools/src/tools/edit.ts
46563
+ var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
46564
+ var themeArg = external_exports.enum(COLOR_THEMES).describe("Section color theme.");
46565
+ var blockSummary = (registry3, b) => ({
46566
+ id: b.id,
46567
+ type: b.type,
46568
+ ...b.variant ? { variant: b.variant } : {},
46569
+ ...b.column !== void 0 ? { column: b.column } : {},
46570
+ text: blockPreview(registry3, b)
46571
+ });
46572
+ function sectionSummary(registry3, s2) {
46573
+ return {
46574
+ id: s2.id,
46575
+ ...s2.preset ? { preset: s2.preset } : {},
46576
+ theme: s2.style?.colorTheme ?? "light",
46577
+ layout: s2.layout.mode,
46578
+ blocks: s2.blocks.map((b) => blockSummary(registry3, b))
46579
+ };
46580
+ }
46581
+ function container(doc, ref) {
46582
+ return resolvePage(doc, ref, { allowFooter: true }).id;
46583
+ }
46584
+ function pageOnly(doc, ref) {
46585
+ if (ref === FOOTER)
46586
+ throw new ToolError(
46587
+ "Sections can't be inserted, moved or deleted in the footer; replace it with a footer preset (insert_section with a footer/\u2026 preset) or edit its blocks.",
46588
+ "invalid_arguments"
46589
+ );
46590
+ return resolvePage(doc, ref).id;
46591
+ }
46592
+ function requireSection2(doc, page, section) {
46593
+ const loc = findSection(doc, page, section);
46594
+ if (!loc) {
46595
+ const ids = page === FOOTER ? [doc.site.footer.id] : doc.pages[page]?.sections.map(
46596
+ (s2) => s2.id
46597
+ ) ?? [];
46598
+ throw new ToolError(
46599
+ `No section ${section} on ${page}. Sections there: ${ids.join(", ") || "none"}.`,
46600
+ "not_found"
46601
+ );
46602
+ }
46603
+ return loc.section;
46604
+ }
46605
+ function requireBlock(doc, page, block4) {
46606
+ const loc = findBlock(doc, page, block4);
46607
+ if (loc) return { block: loc.block, section: loc.section };
46608
+ const elsewhere = locateBlock(doc, block4);
46609
+ throw new ToolError(
46610
+ elsewhere ? `Block ${block4} is on ${elsewhere.container}, not ${page}.` : `No block ${block4}. Call get_page to see block ids.`,
46611
+ "not_found"
46612
+ );
46613
+ }
46614
+ function makeBlock2(registry3, input) {
46615
+ const def = registry3.get(input.type);
46616
+ if (!def)
46617
+ throw new ToolError(
46618
+ `Unknown block type "${input.type}". Types: ${registry3.types().join(", ")}.`,
46619
+ "invalid_arguments"
46620
+ );
46621
+ const fields = {};
46622
+ for (const [k, m] of Object.entries(def.meta.fields))
46623
+ if (!m.optional && m.default !== void 0) fields[k] = structuredClone(m.default);
46624
+ Object.assign(fields, fieldsFromAgent(registry3, input.type, input.fields ?? {}));
46625
+ const { type, fields: _f, id, v: _v, ...rest } = input;
46626
+ return {
46627
+ ...rest,
46628
+ id: id ?? newId("blk"),
46629
+ type,
46630
+ v: def.version,
46631
+ fields
46632
+ };
46633
+ }
46634
+ function makeSection(registry3, raw2) {
46635
+ const blocks2 = Array.isArray(raw2.blocks) ? raw2.blocks : [];
46636
+ return {
46637
+ ...raw2,
46638
+ id: typeof raw2.id === "string" ? raw2.id : newId("sec"),
46639
+ layout: raw2.layout ?? { mode: "stack", columns: { d: [12] } },
46640
+ style: isRecord2(raw2.style) ? raw2.style : { colorTheme: "light" },
46641
+ blocks: blocks2.map((b, i) => {
46642
+ if (!isRecord2(b) || typeof b.type !== "string")
46643
+ throw new ToolError(`section.blocks[${i}] needs a "type".`, "invalid_arguments");
46644
+ return makeBlock2(registry3, b);
46645
+ })
46646
+ };
46647
+ }
46648
+ function takenAnchor(doc, page, anchor) {
46649
+ const other = doc.pages[page]?.sections.find(
46650
+ (s2) => s2.anchor === anchor
46651
+ );
46652
+ if (other)
46653
+ throw new ToolError(
46654
+ `Another section on this page is already called \u201C${anchor}\u201D (${other.id}).`,
46655
+ "invalid_arguments",
46656
+ [],
46657
+ "Pick another anchor, or link to that section."
46658
+ );
46659
+ return anchor;
46660
+ }
46661
+ var insertSection = defineTool({
46662
+ name: "insert_section",
46663
+ title: "Insert section",
46664
+ description: "Add a section to a page from a preset id (preferred \u2014 see list_section_presets) or an explicit section. Preset sections arrive with designed demo copy: replace it with the owner's real content using update_block. Header presets restyle the site header and footer presets replace the site footer (page is ignored for those). Returns the new section and block ids.",
46665
+ input: external_exports.object({
46666
+ site: siteArg,
46667
+ page: external_exports.string().optional().describe("Page id/slug/title (not needed for header/footer presets)."),
46668
+ preset: external_exports.string().optional().describe('Preset id, e.g. "hero/split-image@1" (version optional).'),
46669
+ section: external_exports.looseObject({}).optional().describe(
46670
+ "Explicit section ({layout, style, blocks:[{type, fields, variant?, column?}]}); ids are generated when omitted; rich text as Markdown."
46671
+ ),
46672
+ at: positionArg.optional(),
46673
+ colorTheme: themeArg.optional(),
46674
+ primaryLink: external_exports.unknown().optional().describe(
46675
+ "Link for the main call to action ({type:'page', page:'pg_\u2026'} or {type:'url', url})."
46676
+ ),
46677
+ secondaryLink: external_exports.unknown().optional(),
46678
+ collection: external_exports.string().optional().describe("Collection for summary/events/product presets."),
46679
+ label: external_exports.string().max(80).optional().describe("Layers label for the section."),
46680
+ anchor: anchorArg.optional()
46681
+ }),
46682
+ annotations: {},
46683
+ async handler(ctx, input) {
46684
+ const siteId = await ctx.siteId(input.site);
46685
+ const doc = await ctx.doc(siteId);
46686
+ if (!!input.preset === !!input.section)
46687
+ throw new ToolError("Give exactly one of preset or section.", "invalid_arguments");
46688
+ const links = {};
46689
+ for (const k of ["primaryLink", "secondaryLink"]) {
46690
+ if (input[k] === void 0) continue;
46691
+ const r = LinkSchema.safeParse(input[k]);
46692
+ if (!r.success)
46693
+ throw new ToolError(
46694
+ `${k} is not a valid link: ${r.error.issues.map((i) => i.message).join("; ")}`,
46695
+ "invalid_arguments"
46696
+ );
46697
+ links[k] = r.data;
46698
+ }
46699
+ if (input.preset) {
46700
+ const preset = findPreset(input.preset);
46701
+ const opts = { colorTheme: input.colorTheme, collection: input.collection, ...links };
46702
+ const linkNote = (changes2) => changes2.length ? {
46703
+ linksChanged: changes2.map((c) => ({ from: c.from, to: c.to })),
46704
+ linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
46705
+ } : {};
46706
+ if (preset.target === "header") {
46707
+ const { value: header, changes: changes2 } = resolveInternalLinks(preset.build(opts), doc);
46708
+ await ctx.apply(siteId, [{ op: "site.update", set: { header } }]);
46709
+ return {
46710
+ data: {
46711
+ applied: preset.id,
46712
+ header,
46713
+ note: "The site header now uses this preset.",
46714
+ ...linkNote(changes2)
46715
+ }
46716
+ };
46717
+ }
46718
+ const { value: section2, changes } = resolveInternalLinks(preset.build(opts), doc);
46719
+ if (input.label) section2.label = input.label;
46720
+ if (input.anchor && preset.target !== "footer" && input.page)
46721
+ section2.anchor = takenAnchor(doc, pageOnly(doc, input.page), input.anchor);
46722
+ if (preset.target === "footer") {
46723
+ await ctx.apply(siteId, [{ op: "site.update", set: { footer: section2 } }]);
46724
+ return {
46725
+ data: {
46726
+ replaced: "footer",
46727
+ section: sectionSummary(ctx.registry, section2),
46728
+ note: 'Edit footer blocks with page "footer".',
46729
+ ...linkNote(changes)
46730
+ }
46731
+ };
46732
+ }
46733
+ if (!input.page)
46734
+ throw new ToolError("page is required for page sections.", "invalid_arguments");
46735
+ const page2 = pageOnly(doc, input.page);
46736
+ await ctx.apply(siteId, [
46737
+ { op: "section.insert", page: page2, at: input.at ?? "end", section: section2 }
46738
+ ]);
46739
+ return {
46740
+ data: {
46741
+ page: page2,
46742
+ section: sectionSummary(ctx.registry, section2),
46743
+ copyGuidance: preset.copy.map((c) => `${c.label}: ${c.words[0]}\u2013${c.words[1]} words`),
46744
+ next: "Replace the demo copy with the owner's content (update_block), then run design_check.",
46745
+ ...linkNote(changes)
46746
+ }
46747
+ };
46748
+ }
46749
+ if (!input.page) throw new ToolError("page is required.", "invalid_arguments");
46750
+ const page = pageOnly(doc, input.page);
46751
+ const section = makeSection(ctx.registry, input.section);
46752
+ if (input.colorTheme)
46753
+ section.style = { ...section.style ?? {}, colorTheme: input.colorTheme };
46754
+ if (input.label) section.label = input.label;
46755
+ if (input.anchor) section.anchor = takenAnchor(doc, page, input.anchor);
46756
+ await ctx.apply(siteId, [
46757
+ { op: "section.insert", page, at: input.at ?? "end", section }
46758
+ ]);
46759
+ return { data: { page, section: sectionSummary(ctx.registry, section) } };
46760
+ }
46761
+ });
46762
+ var updateSection = defineTool({
46763
+ name: "update_section",
46764
+ title: "Update section",
46765
+ description: `Change a section's settings with dot-path patches, e.g. set {"style.colorTheme": "dark", "style.padding": {"d": "xl", "m": "lg"}, "label": "Intro", "anchor": "classes", "hidden": true, "layout": {...}}. Shortcuts: colorTheme. Blocks are edited with the block tools.`,
46766
+ input: external_exports.object({
46767
+ site: siteArg,
46768
+ page: containerArg,
46769
+ section: external_exports.string(),
46770
+ set: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
46771
+ unset: external_exports.array(external_exports.string()).optional(),
46772
+ colorTheme: themeArg.optional()
46773
+ }),
46774
+ annotations: { idempotentHint: true },
46775
+ async handler(ctx, input) {
46776
+ const siteId = await ctx.siteId(input.site);
46777
+ const doc = await ctx.doc(siteId);
46778
+ const page = container(doc, input.page);
46779
+ requireSection2(doc, page, input.section);
46780
+ const set2 = { ...input.set ?? {} };
46781
+ if (input.colorTheme) set2["style.colorTheme"] = input.colorTheme;
46782
+ const body = { op: "section.update", page, section: input.section };
46783
+ if (Object.keys(set2).length) body.set = set2;
46784
+ if (input.unset?.length) body.unset = input.unset;
46785
+ const { doc: next } = await ctx.apply(siteId, [body]);
46786
+ return {
46787
+ data: { section: sectionSummary(ctx.registry, requireSection2(next, page, input.section)) }
46788
+ };
46789
+ }
46790
+ });
46791
+ var moveSection = defineTool({
46792
+ name: "move_section",
46793
+ title: "Move section",
46794
+ description: "Reorder a section within its page.",
46795
+ input: external_exports.object({ site: siteArg, page: external_exports.string(), section: external_exports.string(), at: positionArg }),
46796
+ annotations: { idempotentHint: true },
46797
+ async handler(ctx, input) {
46798
+ const siteId = await ctx.siteId(input.site);
46799
+ const doc = await ctx.doc(siteId);
46800
+ const page = pageOnly(doc, input.page);
46801
+ requireSection2(doc, page, input.section);
46802
+ const { doc: next } = await ctx.apply(siteId, [
46803
+ { op: "section.move", page, section: input.section, at: input.at }
46804
+ ]);
46805
+ return { data: { order: next.pages[page]?.sections.map((s2) => s2.id) } };
46806
+ }
46807
+ });
46808
+ var deleteSection = defineTool({
46809
+ name: "delete_section",
46810
+ title: "Delete section",
46811
+ description: "Remove a section and its blocks from a page (recoverable from the session snapshot or discard_draft).",
46812
+ input: external_exports.object({ site: siteArg, page: external_exports.string(), section: external_exports.string() }),
46813
+ annotations: { destructiveHint: true },
46814
+ async handler(ctx, input) {
46815
+ const siteId = await ctx.siteId(input.site);
46816
+ const doc = await ctx.doc(siteId);
46817
+ const page = pageOnly(doc, input.page);
46818
+ requireSection2(doc, page, input.section);
46819
+ await ctx.apply(siteId, [{ op: "section.delete", page, section: input.section }]);
46820
+ return { data: { deleted: input.section } };
46821
+ }
46822
+ });
46823
+ var gridItemArg = external_exports.looseObject({}).describe(
46824
+ "Grid placement {d:{x,y,w,h,z?,bleed?}, t?, m?}: 24 columns on desktop/tablet, 8 on mobile, 1-based."
46825
+ );
46826
+ var insertBlock = defineTool({
46827
+ name: "insert_block",
46828
+ title: "Insert block",
46829
+ description: "Add a block to a section. Fields are validated against the block type (see list_blocks); rich text as Markdown. In stack sections pick a column (0-based); in grid sections give place (auto-placed at the bottom otherwise) and then check the mobile layout (auto_mobile_layout).",
46830
+ input: external_exports.object({
46831
+ site: siteArg,
46832
+ page: containerArg,
46833
+ section: external_exports.string(),
46834
+ block: external_exports.looseObject({
46835
+ type: external_exports.string(),
46836
+ fields: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
46837
+ variant: external_exports.string().optional(),
46838
+ style: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
46839
+ column: external_exports.int().min(0).max(3).optional(),
46840
+ label: external_exports.string().optional()
46841
+ }).describe("{type, fields, variant?, style?, column?, label?}"),
46842
+ at: positionArg.optional(),
46843
+ place: gridItemArg.optional()
46844
+ }),
46845
+ annotations: {},
46846
+ async handler(ctx, input) {
46847
+ const siteId = await ctx.siteId(input.site);
46848
+ const doc = await ctx.doc(siteId);
46849
+ const page = container(doc, input.page);
46850
+ requireSection2(doc, page, input.section);
46851
+ const made = makeBlock2(ctx.registryFor(doc), input.block);
46852
+ const block4 = resolveInternalLinks(made, doc).value;
46853
+ for (const k of Object.keys(input.block.fields ?? {}))
46854
+ if (k in made.fields) block4.fields[k] = made.fields[k];
46855
+ const body = {
46856
+ op: "block.insert",
46857
+ page,
46858
+ section: input.section,
46859
+ at: input.at ?? "end",
46860
+ block: block4
46861
+ };
46862
+ if (input.place) body.place = input.place;
46863
+ const { doc: next, warnings } = await ctx.apply(siteId, [body]);
46864
+ const placed = findBlock(next, page, block4.id);
46865
+ return {
46866
+ data: {
46867
+ block: blockSummary(ctx.registryFor(next), placed?.block ?? block4),
46868
+ ...warnings.length ? { warnings } : {}
46869
+ }
46870
+ };
46871
+ }
46872
+ });
46873
+ var updateBlock = defineTool({
46874
+ name: "update_block",
46875
+ title: "Update block",
46876
+ description: "Edit a block: set replaces whole top-level fields (rich text as Markdown; lists are replaced whole), unset removes optional fields; variant/style/column/label/hidden adjust presentation (null removes). Ids are preserved.",
46877
+ input: external_exports.object({
46878
+ site: siteArg,
46879
+ page: containerArg,
46880
+ block: external_exports.string(),
46881
+ set: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
46882
+ unset: external_exports.array(external_exports.string()).optional(),
46883
+ variant: external_exports.string().nullable().optional(),
46884
+ style: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional(),
46885
+ column: external_exports.int().min(0).max(3).nullable().optional(),
46886
+ label: external_exports.string().max(80).nullable().optional(),
46887
+ hidden: external_exports.boolean().nullable().optional()
46888
+ }),
46889
+ annotations: { idempotentHint: true },
46890
+ async handler(ctx, input) {
46891
+ const siteId = await ctx.siteId(input.site);
46892
+ const doc = await ctx.doc(siteId);
46893
+ const page = container(doc, input.page);
46894
+ const { block: block4 } = requireBlock(doc, page, input.block);
46895
+ const body = { op: "block.update", page, block: input.block };
46896
+ if (input.set) body.set = fieldsFromAgent(ctx.registryFor(doc), block4.type, input.set);
46897
+ if (input.unset?.length) body.unset = input.unset;
46898
+ for (const k of ["variant", "style", "column", "label", "hidden"])
46899
+ if (input[k] !== void 0) body[k] = input[k];
46900
+ if (Object.keys(body).length === 3)
46901
+ throw new ToolError(
46902
+ "Nothing to change: give set, unset, variant, style, column, label or hidden.",
46903
+ "invalid_arguments"
46904
+ );
46905
+ const { doc: next } = await ctx.apply(siteId, [body]);
46906
+ const updated = findBlock(next, page, input.block)?.block ?? block4;
46907
+ return { data: { block: blockSummary(ctx.registryFor(next), updated) } };
46908
+ }
46909
+ });
46910
+ var moveBlock = defineTool({
46911
+ name: "move_block",
46912
+ title: "Move block",
46913
+ description: "Move a block within its page (to another section and/or position). Give column for stack sections and place for grid sections.",
46914
+ input: external_exports.object({
46915
+ site: siteArg,
46916
+ page: containerArg,
46917
+ block: external_exports.string(),
46918
+ to: external_exports.object({
46919
+ section: external_exports.string(),
46920
+ at: positionArg.optional(),
46921
+ column: external_exports.int().min(0).max(3).nullable().optional(),
46922
+ place: gridItemArg.optional()
46923
+ })
46924
+ }),
46925
+ annotations: { idempotentHint: true },
46926
+ async handler(ctx, input) {
46927
+ const siteId = await ctx.siteId(input.site);
46928
+ const doc = await ctx.doc(siteId);
46929
+ const page = container(doc, input.page);
46930
+ requireBlock(doc, page, input.block);
46931
+ requireSection2(doc, page, input.to.section);
46932
+ const to = { section: input.to.section, at: input.to.at ?? "end" };
46933
+ if (input.to.column !== void 0) to.column = input.to.column;
46934
+ if (input.to.place) to.place = input.to.place;
46935
+ await ctx.apply(siteId, [{ op: "block.move", page, block: input.block, to }]);
46936
+ return { data: { moved: input.block, to } };
46937
+ }
46938
+ });
46939
+ var deleteBlock = defineTool({
46940
+ name: "delete_block",
46941
+ title: "Delete block",
46942
+ description: "Remove a block from its section (recoverable via the session snapshot or discard_draft).",
46943
+ input: external_exports.object({ site: siteArg, page: containerArg, block: external_exports.string() }),
46944
+ annotations: { destructiveHint: true },
46945
+ async handler(ctx, input) {
46946
+ const siteId = await ctx.siteId(input.site);
46947
+ const doc = await ctx.doc(siteId);
46948
+ const page = container(doc, input.page);
46949
+ requireBlock(doc, page, input.block);
46950
+ await ctx.apply(siteId, [{ op: "block.delete", page, block: input.block }]);
46951
+ return { data: { deleted: input.block } };
46952
+ }
46953
+ });
46954
+ var rectArg = external_exports.looseObject({}).describe(
46955
+ "{x, y, w, h, z?, bleed?: 'left'|'right'|'both'} (1-based; 24 columns desktop/tablet, 8 mobile)"
46956
+ );
46957
+ var placeInGrid2 = defineTool({
46958
+ name: "place_in_grid",
46959
+ title: "Place in grid",
46960
+ description: "Set a block's grid placement per breakpoint in a grid section. d = desktop (required on first placement), t = tablet override (null removes), m = mobile (null removes; regenerate with auto_mobile_layout). Rows grow automatically. Overlap is allowed (use z) but text must not overlap text.",
46961
+ input: external_exports.object({
46962
+ site: siteArg,
46963
+ page: containerArg,
46964
+ section: external_exports.string(),
46965
+ block: external_exports.string(),
46966
+ d: rectArg.optional(),
46967
+ t: rectArg.nullable().optional(),
46968
+ m: rectArg.nullable().optional()
46969
+ }),
46970
+ annotations: { idempotentHint: true },
46971
+ async handler(ctx, input) {
46972
+ const siteId = await ctx.siteId(input.site);
46973
+ const doc = await ctx.doc(siteId);
46974
+ const page = container(doc, input.page);
46975
+ const section = requireSection2(doc, page, input.section);
46976
+ if (section.layout.mode !== "grid")
46977
+ throw new ToolError(
46978
+ `Section ${section.id} is a stack section; set the block's column with update_block instead.`,
46979
+ "invalid_arguments"
46980
+ );
46981
+ const bodies = [];
46982
+ for (const bp of ["d", "t", "m"]) {
46983
+ const rect = input[bp];
46984
+ if (rect === void 0) continue;
46985
+ bodies.push({
46986
+ op: "grid.place",
46987
+ page,
46988
+ section: section.id,
46989
+ block: input.block,
46990
+ bp,
46991
+ rect
46992
+ });
46993
+ }
46994
+ if (!bodies.length) throw new ToolError("Give at least one of d, t or m.", "invalid_arguments");
46995
+ const { doc: next } = await ctx.apply(siteId, bodies);
46996
+ const after = requireSection2(next, page, section.id);
46997
+ return {
46998
+ data: {
46999
+ placement: after.layout.mode === "grid" ? after.layout.items[input.block] : null,
47000
+ rows: after.layout.mode === "grid" ? after.layout.rows : null,
47001
+ next: input.d && input.m === void 0 ? "Desktop changed: review mobile (auto_mobile_layout or screenshot device mobile)." : void 0
47002
+ }
47003
+ };
47004
+ }
47005
+ });
47006
+ var autoMobileLayout2 = defineTool({
47007
+ name: "auto_mobile_layout",
47008
+ title: "Auto mobile layout",
47009
+ description: "Generate mobile grid placements for a grid section by visual reading order (top-to-bottom, left-to-right on the desktop grid, layered items kept on their host). Keeps existing mobile placements unless reset is true.",
47010
+ input: external_exports.object({
47011
+ site: siteArg,
47012
+ page: containerArg,
47013
+ section: external_exports.string(),
47014
+ reset: external_exports.boolean().optional()
47015
+ }),
47016
+ annotations: { idempotentHint: true },
47017
+ async handler(ctx, input) {
47018
+ const siteId = await ctx.siteId(input.site);
47019
+ const doc = await ctx.doc(siteId);
47020
+ const page = container(doc, input.page);
47021
+ const section = requireSection2(doc, page, input.section);
47022
+ if (section.layout.mode !== "grid")
47023
+ return { data: { changed: 0, note: "Stack sections stack automatically on mobile." } };
47024
+ const ops = autoMobileLayoutOps(doc, page, section.id, { reset: input.reset });
47025
+ if (!ops.length)
47026
+ return { data: { changed: 0, note: "Mobile placements are already complete." } };
47027
+ const bodies = ops.map(({ id: _i, actor: _a3, ts: _t, ...b }) => b);
47028
+ const { doc: next } = await ctx.apply(siteId, bodies);
47029
+ const after = requireSection2(next, page, section.id);
47030
+ return {
47031
+ data: {
47032
+ changed: ops.length,
47033
+ mobile: after.layout.mode === "grid" ? Object.fromEntries(Object.entries(after.layout.items).map(([id, it]) => [id, it.m])) : null
47034
+ }
47035
+ };
47036
+ }
47037
+ });
47038
+ var editTools = [
47039
+ insertSection,
47040
+ updateSection,
47041
+ moveSection,
47042
+ deleteSection,
47043
+ insertBlock,
47044
+ updateBlock,
47045
+ moveBlock,
47046
+ deleteBlock,
47047
+ placeInGrid2,
47048
+ autoMobileLayout2
47049
+ ];
47050
+
46031
47051
  // ../mcp-tools/src/tools/components.ts
46032
47052
  var nameArg = external_exports.string().describe('Component name, lowercase-hyphenated (e.g. "editorial-hero").');
46033
47053
  var GUIDE = "Prefer section presets. Use a component for a bespoke moment presets can't express. Always expose every word, image and link as a field (owners edit fields, never code), always style with theme tokens (var(--os-color-\u2026), var(--os-font-\u2026), var(--os-space-\u2026)), use the 767/768/1199/1200 breakpoints and honour reduced motion.";
@@ -46330,13 +47350,14 @@ ${notes.join("\n")}` : ""}`,
46330
47350
  var insertComponent = defineTool({
46331
47351
  name: "insert_component",
46332
47352
  title: "Insert component",
46333
- description: "Add a section using a custom component to a page (a site component, or a Designer components library name \u2014 it is added to the site first). fields override the sample content (rich text as Markdown); replace sample copy and photos with the owner's real content. Returns the section and block ids (edit later with update_block).",
47353
+ description: "Add a section using a custom component to a page (a site component, or a Designer components library name \u2014 it is added to the site first). fields override the sample content (rich text as Markdown); replace sample copy and photos with the owner's real content. Give it an anchor when the menu or a button should jump to it. Returns the section and block ids (edit later with update_block).",
46334
47354
  input: external_exports.object({
46335
47355
  site: siteArg,
46336
47356
  page: external_exports.string().describe("Page id/slug/title."),
46337
47357
  name: nameArg,
46338
47358
  fields: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
46339
47359
  theme: external_exports.enum(COLOR_THEMES).optional(),
47360
+ anchor: anchorArg.optional(),
46340
47361
  at: positionArg.optional()
46341
47362
  }),
46342
47363
  annotations: {},
@@ -46361,6 +47382,7 @@ var insertComponent = defineTool({
46361
47382
  ).value;
46362
47383
  const block4 = section.blocks[0];
46363
47384
  block4.fields = { ...block4.fields, ...given };
47385
+ if (input.anchor) section.anchor = takenAnchor(doc, page, input.anchor);
46364
47386
  bodies.push({ op: "section.insert", page, at: input.at ?? "end", section });
46365
47387
  const { doc: next } = await ctx.apply(siteId, bodies);
46366
47388
  const target = next.pages[page];
@@ -46691,17 +47713,17 @@ function builtInTemplates() {
46691
47713
  };
46692
47714
  });
46693
47715
  }
46694
- var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
47716
+ var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
46695
47717
  function normaliseTemplate(raw2) {
46696
- if (!isRecord2(raw2)) return null;
46697
- const meta = isRecord2(raw2.template) ? { ...raw2, ...raw2.template } : raw2;
47718
+ if (!isRecord3(raw2)) return null;
47719
+ const meta = isRecord3(raw2.template) ? { ...raw2, ...raw2.template } : raw2;
46698
47720
  const id = typeof meta.id === "string" ? meta.id : null;
46699
47721
  if (!id) return null;
46700
47722
  const rawLooks = Array.isArray(meta.looks) ? meta.looks : [];
46701
47723
  const looks = [];
46702
47724
  rawLooks.forEach((l, i) => {
46703
- if (!isRecord2(l)) return;
46704
- const candidate = isRecord2(l.choices) ? l.choices : isRecord2(l.styles) && isRecord2(l.styles.choices) ? l.styles.choices : l;
47725
+ if (!isRecord3(l)) return;
47726
+ const candidate = isRecord3(l.choices) ? l.choices : isRecord3(l.styles) && isRecord3(l.styles.choices) ? l.styles.choices : l;
46705
47727
  const parsed = StyleChoicesSchema.safeParse(candidate);
46706
47728
  if (!parsed.success) return;
46707
47729
  const name = typeof l.name === "string" ? l.name : `Look ${i + 1}`;
@@ -46727,12 +47749,12 @@ async function templatesFromModule(mod) {
46727
47749
  let raws = null;
46728
47750
  const cat = mod.templateCatalog;
46729
47751
  if (Array.isArray(cat)) raws = cat;
46730
- else if (isRecord2(cat) && typeof cat.list === "function")
47752
+ else if (isRecord3(cat) && typeof cat.list === "function")
46731
47753
  raws = await cat.list();
46732
47754
  else if (typeof mod.listTemplates === "function")
46733
47755
  raws = await mod.listTemplates();
46734
47756
  else if (Array.isArray(mod.templates)) raws = mod.templates;
46735
- else if (isRecord2(mod.templates)) raws = Object.values(mod.templates);
47757
+ else if (isRecord3(mod.templates)) raws = Object.values(mod.templates);
46736
47758
  else if (typeof mod.loadTemplate === "function") {
46737
47759
  const ids = mod.TEMPLATE_IDS ?? mod.templateIds;
46738
47760
  if (Array.isArray(ids))
@@ -46757,312 +47779,6 @@ async function loadTemplates(store, importer = (spec) => import(
46757
47779
  return { source: "built-in", templates: builtInTemplates() };
46758
47780
  }
46759
47781
 
46760
- // ../mcp-tools/src/tools/read.ts
46761
- var listSites = defineTool({
46762
- name: "list_sites",
46763
- title: "List sites",
46764
- description: "Sites this connection can edit, with their titles and live addresses. Start here when you don't know the site id.",
46765
- input: external_exports.object({}),
46766
- annotations: { readOnlyHint: true },
46767
- async handler(ctx) {
46768
- const allowed = new Set(await ctx.allowedSites());
46769
- const sites = (await ctx.store.listSites()).filter((s2) => allowed.has(s2.id));
46770
- return { data: { sites, canPublish: ctx.grant.publish && ctx.store.capabilities.canPublish } };
46771
- }
46772
- });
46773
- function stylesSummary(doc) {
46774
- const compiled2 = compileStyles(doc.styles);
46775
- const pairing = getPairing(doc.styles.choices.pairing);
46776
- return {
46777
- choices: doc.styles.choices,
46778
- pairing: pairing ? { id: pairing.id, heading: pairing.heading.family, body: pairing.body.family } : { id: doc.styles.choices.pairing, unknown: true },
46779
- overrides: doc.styles.overrides ?? null,
46780
- brand: compiled2.tokens.palette.brand,
46781
- themes: Object.fromEntries(
46782
- Object.entries(compiled2.tokens.themes).map(([theme, c]) => [
46783
- theme,
46784
- {
46785
- background: c.background,
46786
- text: c.text,
46787
- heading: c.heading,
46788
- accent: c.accent,
46789
- "accent-text": c["accent-text"]
46790
- }
46791
- ])
46792
- ),
46793
- contrast: {
46794
- mode: compiled2.contrast.mode,
46795
- adjustedBySolver: compiled2.contrast.adjustments.length,
46796
- failures: compiled2.contrast.failures.length
46797
- }
46798
- };
46799
- }
46800
- function pageSummary(page) {
46801
- return {
46802
- id: page.id,
46803
- title: page.title,
46804
- slug: page.slug,
46805
- path: pagePathOf(page),
46806
- sections: page.sections.length,
46807
- hidden: page.hidden ?? false
46808
- };
46809
- }
46810
- var getSite = defineTool({
46811
- name: "get_site",
46812
- title: "Get site",
46813
- description: "Site settings, navigation, header/footer, Site Styles summary, page list and collections. Call before editing to learn page ids and the current look.",
46814
- input: external_exports.object({ site: siteArg }),
46815
- annotations: { readOnlyHint: true },
46816
- async handler(ctx, input) {
46817
- const siteId = await ctx.siteId(input.site);
46818
- const doc = await ctx.doc(siteId);
46819
- const { site: site2 } = doc;
46820
- return {
46821
- data: {
46822
- id: siteId,
46823
- title: site2.title,
46824
- tagline: site2.tagline ?? null,
46825
- template: doc.config.template ?? null,
46826
- url: doc.config.deploy.customDomain ? `https://${doc.config.deploy.customDomain}` : doc.config.deploy.url,
46827
- nav: site2.nav,
46828
- header: site2.header,
46829
- footer: {
46830
- sectionId: site2.footer.id,
46831
- layout: site2.footer.layout.mode,
46832
- blocks: site2.footer.blocks.map((b) => ({ id: b.id, type: b.type })),
46833
- note: 'Edit footer blocks with the block tools using page "footer".'
46834
- },
46835
- announcement: site2.announcement ?? null,
46836
- social: site2.social ?? [],
46837
- seo: site2.seo ?? null,
46838
- styles: stylesSummary(doc),
46839
- pages: Object.values(doc.pages).map(pageSummary),
46840
- collections: Object.entries(doc.collections).map(([name, items]) => ({
46841
- name,
46842
- kind: Object.values(items)[0]?.kind ?? doc.collectionConfigs?.[name]?.kind ?? null,
46843
- items: Object.keys(items).length
46844
- })),
46845
- media: Object.keys(doc.media).length,
46846
- capabilities: {
46847
- ...ctx.store.capabilities,
46848
- // Same answer as list_sites: publishing needs both the store and this connection.
46849
- canPublish: ctx.grant.publish && ctx.store.capabilities.canPublish,
46850
- grantPublish: ctx.grant.publish
46851
- }
46852
- }
46853
- };
46854
- }
46855
- });
46856
- function sectionView(ctx, section, detail, doc) {
46857
- const registry3 = doc ? ctx.registryFor(doc) : ctx.registry;
46858
- const blocks2 = section.blocks.map((b) => {
46859
- if (detail === "outline") {
46860
- const out = {
46861
- id: b.id,
46862
- type: b.type,
46863
- text: blockPreview(registry3, b)
46864
- };
46865
- if (b.variant) out.variant = b.variant;
46866
- if (b.column !== void 0) out.column = b.column;
46867
- if (b.hidden) out.hidden = true;
46868
- return out;
46869
- }
46870
- return blockToAgent(registry3, b);
46871
- });
46872
- const base = {
46873
- id: section.id,
46874
- ...section.preset ? { preset: section.preset } : {},
46875
- ...section.label ? { label: section.label } : {},
46876
- ...section.hidden ? { hidden: true } : {},
46877
- ...section.locked ? { locked: true } : {},
46878
- theme: section.style?.colorTheme ?? "light"
46879
- };
46880
- if (detail === "outline") {
46881
- base.layout = section.layout.mode === "grid" ? "grid" : `stack ${JSON.stringify(section.layout.columns.d)}`;
46882
- base.blocks = blocks2;
46883
- return base;
46884
- }
46885
- return {
46886
- ...base,
46887
- layout: section.layout,
46888
- style: section.style ?? {},
46889
- ...section.anchor ? { anchor: section.anchor } : {},
46890
- blocks: blocks2
46891
- };
46892
- }
46893
- var getPage2 = defineTool({
46894
- name: "get_page",
46895
- title: "Get page",
46896
- description: `A page's section/block tree with stable ids, fields (rich text as Markdown), layout and style. Use detail "outline" for a compact overview (ids, types, text previews) and "full" before editing fields or grid placements. page "footer" returns the site footer section.`,
46897
- input: external_exports.object({
46898
- site: siteArg,
46899
- page: containerArg,
46900
- detail: external_exports.enum(["full", "outline"]).optional().describe('Default "full".')
46901
- }),
46902
- annotations: { readOnlyHint: true },
46903
- async handler(ctx, input) {
46904
- const siteId = await ctx.siteId(input.site);
46905
- const doc = await ctx.doc(siteId);
46906
- const detail = input.detail ?? "full";
46907
- const { id, page } = resolvePage(doc, input.page, { allowFooter: true });
46908
- if (id === FOOTER || !page)
46909
- return { data: { page: FOOTER, sections: [sectionView(ctx, doc.site.footer, detail, doc)] } };
46910
- return {
46911
- data: {
46912
- id: page.id,
46913
- title: page.title,
46914
- slug: page.slug,
46915
- path: pagePathOf(page),
46916
- seo: page.seo ?? {},
46917
- ...page.header ? { header: page.header } : {},
46918
- ...page.hidden ? { hidden: true } : {},
46919
- sections: page.sections.map((s2) => sectionView(ctx, s2, detail, doc))
46920
- }
46921
- };
46922
- }
46923
- });
46924
- function blockMetaView(m, full) {
46925
- const fields = Object.fromEntries(
46926
- Object.entries(m.fields).map(([k, f]) => {
46927
- const v = {
46928
- kind: f.kind === "richText" ? "richText (Markdown)" : f.kind
46929
- };
46930
- if (f.optional) v.optional = true;
46931
- if (f.label) v.label = f.label;
46932
- if (full && f.help) v.help = f.help;
46933
- if (f.ai) v.ai = f.ai;
46934
- if (f.kind === "select") v.options = f.options.map((o) => o.value);
46935
- if (f.kind === "text" && f.maxLength) v.maxLength = f.maxLength;
46936
- if (f.kind === "list")
46937
- v.itemFields = Object.fromEntries(
46938
- Object.entries(f.fields).map(([ik, iv]) => [
46939
- ik,
46940
- iv.kind === "richText" ? "richText (Markdown)" : iv.kind
46941
- ])
46942
- );
46943
- if (f.kind === "image") v.shape = "{ src, alt, focal? }";
46944
- if (f.kind === "link") v.allow = f.allow;
46945
- return [k, v];
46946
- })
46947
- );
46948
- return {
46949
- type: m.type,
46950
- label: m.label,
46951
- category: m.category,
46952
- description: m.description,
46953
- variants: m.variants,
46954
- styleSlots: m.styleSlots,
46955
- fields,
46956
- ai: m.ai
46957
- };
46958
- }
46959
- var listBlocks = defineTool({
46960
- name: "list_blocks",
46961
- title: "List blocks",
46962
- description: "The block catalog: each block's fields (rich text as Markdown), variants, style slots and AI guidance (purpose, whenToUse, avoid). Filter by category or type. Prefer section presets for whole sections; use blocks to add or adjust pieces.",
46963
- input: external_exports.object({
46964
- category: external_exports.string().optional(),
46965
- type: external_exports.string().optional().describe("One block type for full detail.")
46966
- }),
46967
- annotations: { readOnlyHint: true },
46968
- async handler(ctx, input) {
46969
- let metas = ctx.registry.meta();
46970
- if (input.type) {
46971
- metas = metas.filter((m) => m.type === input.type);
46972
- if (!metas.length)
46973
- throw new ToolError(
46974
- `Unknown block type "${input.type}". Types: ${ctx.registry.types().join(", ")}.`,
46975
- "not_found"
46976
- );
46977
- }
46978
- if (input.category) metas = metas.filter((m) => m.category === input.category);
46979
- return {
46980
- data: {
46981
- blocks: metas.map((m) => blockMetaView(m, Boolean(input.type))),
46982
- categories: [...new Set(ctx.registry.meta().map((m) => m.category))]
46983
- }
46984
- };
46985
- }
46986
- });
46987
- var getBlockSchema = defineTool({
46988
- name: "get_block_schema",
46989
- title: "Get block schema",
46990
- description: "JSON Schema for one block type's fields (with labels and AI hints). Rich-text fields are shown as ProseMirror JSON but the edit tools also accept a Markdown string for them.",
46991
- input: external_exports.object({ type: external_exports.string() }),
46992
- annotations: { readOnlyHint: true },
46993
- async handler(ctx, input) {
46994
- const def = ctx.registry.get(input.type);
46995
- if (!def)
46996
- throw new ToolError(
46997
- `Unknown block type "${input.type}". Types: ${ctx.registry.types().join(", ")}.`,
46998
- "not_found"
46999
- );
47000
- return {
47001
- data: {
47002
- type: def.type,
47003
- version: def.version,
47004
- variants: def.meta.variants,
47005
- styleSlots: def.meta.styleSlots,
47006
- fields: blockFieldsJsonSchema(def)
47007
- }
47008
- };
47009
- }
47010
- });
47011
- var listSectionPresets = defineTool({
47012
- name: "list_section_presets",
47013
- title: "List section presets",
47014
- description: "Designed section presets by category (hero, about, services, gallery, testimonials, pricing, faq, team, stats, cta, contact, newsletter, blog, events, products, footer, header) with descriptions, layout, designed-for color themes, copy-length ranges and AI guidance. Compose pages from these with insert_section/create_page.",
47015
- input: external_exports.object({
47016
- category: external_exports.enum(PRESET_CATEGORIES).optional(),
47017
- query: external_exports.string().optional().describe("Words to match in label, description or tags."),
47018
- detail: external_exports.enum(["brief", "full"]).optional().describe('Default "brief".')
47019
- }),
47020
- annotations: { readOnlyHint: true },
47021
- async handler(_ctx, input) {
47022
- let list5 = presetCatalog.slice();
47023
- if (input.category) list5 = list5.filter((p) => p.category === input.category);
47024
- if (input.query) {
47025
- const words4 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
47026
- list5 = list5.filter((p) => {
47027
- const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
47028
- return words4.every((w) => hay.includes(w));
47029
- });
47030
- }
47031
- const full = input.detail === "full";
47032
- return {
47033
- data: {
47034
- presets: list5.map(
47035
- (p) => full ? p : {
47036
- id: p.id,
47037
- label: p.label,
47038
- description: p.description,
47039
- target: p.target,
47040
- layout: p.layout,
47041
- overlap: p.overlap,
47042
- themes: p.themes,
47043
- whenToUse: p.ai.whenToUse
47044
- }
47045
- ),
47046
- categories: PRESET_CATEGORIES
47047
- }
47048
- };
47049
- }
47050
- });
47051
- function findPreset(id) {
47052
- const exact = getPreset(id);
47053
- if (exact) return exact;
47054
- const base = id.replace(/@\d+$/, "");
47055
- const matches = presetCatalog.filter((p) => p.id.startsWith(`${base}@`)).sort((a, b) => b.version - a.version);
47056
- const best = matches[0] && getPreset(matches[0].id);
47057
- if (best) return best;
47058
- const category = base.split("/")[0] ?? "";
47059
- const near = presetCatalog.filter((p) => p.category === category).map((p) => p.id);
47060
- throw new ToolError(
47061
- `Unknown preset "${id}".${near.length ? ` In ${category}: ${near.join(", ")}.` : " Call list_section_presets."}`,
47062
- "not_found"
47063
- );
47064
- }
47065
-
47066
47782
  // ../mcp-tools/src/tools/design.ts
47067
47783
  var stripMeta = (op2) => {
47068
47784
  const { id: _i, actor: _a3, ts: _t, ...body } = op2;
@@ -47207,7 +47923,7 @@ ${(adjusted.ok ? left.map((i) => `- ${i.message}`) : issueLines(adjusted.errors)
47207
47923
  solverNotes
47208
47924
  };
47209
47925
  }
47210
- var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
47926
+ var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
47211
47927
  var setStyles = defineTool({
47212
47928
  name: "set_styles",
47213
47929
  title: "Set Site Styles",
@@ -47258,8 +47974,8 @@ var setStyles = defineTool({
47258
47974
  for (const [k, v] of Object.entries(c.layout ?? {}))
47259
47975
  if (v !== void 0) set2[`choices.layout.${k}`] = v;
47260
47976
  for (const [path2, v] of Object.entries(input.overrides?.set ?? {})) {
47261
- const token = isRecord3(v) && "$value" in v ? v : { $value: v };
47262
- if (path2.startsWith("color.") && isRecord3(token) && token.$type === void 0)
47977
+ const token = isRecord4(v) && "$value" in v ? v : { $value: v };
47978
+ if (path2.startsWith("color.") && isRecord4(token) && token.$type === void 0)
47263
47979
  token.$type = "color";
47264
47980
  set2[`overrides.${path2}`] = token;
47265
47981
  }
@@ -47700,478 +48416,6 @@ var updateDesignSystem = defineTool({
47700
48416
  });
47701
48417
  var designSystemTools = [getDesignSystem, updateDesignSystem];
47702
48418
 
47703
- // ../mcp-tools/src/tools/edit.ts
47704
- var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
47705
- var themeArg = external_exports.enum(COLOR_THEMES).describe("Section color theme.");
47706
- var blockSummary = (registry3, b) => ({
47707
- id: b.id,
47708
- type: b.type,
47709
- ...b.variant ? { variant: b.variant } : {},
47710
- ...b.column !== void 0 ? { column: b.column } : {},
47711
- text: blockPreview(registry3, b)
47712
- });
47713
- function sectionSummary(registry3, s2) {
47714
- return {
47715
- id: s2.id,
47716
- ...s2.preset ? { preset: s2.preset } : {},
47717
- theme: s2.style?.colorTheme ?? "light",
47718
- layout: s2.layout.mode,
47719
- blocks: s2.blocks.map((b) => blockSummary(registry3, b))
47720
- };
47721
- }
47722
- function container(doc, ref) {
47723
- return resolvePage(doc, ref, { allowFooter: true }).id;
47724
- }
47725
- function pageOnly(doc, ref) {
47726
- if (ref === FOOTER)
47727
- throw new ToolError(
47728
- "Sections can't be inserted, moved or deleted in the footer; replace it with a footer preset (insert_section with a footer/\u2026 preset) or edit its blocks.",
47729
- "invalid_arguments"
47730
- );
47731
- return resolvePage(doc, ref).id;
47732
- }
47733
- function requireSection2(doc, page, section) {
47734
- const loc = findSection(doc, page, section);
47735
- if (!loc) {
47736
- const ids = page === FOOTER ? [doc.site.footer.id] : doc.pages[page]?.sections.map(
47737
- (s2) => s2.id
47738
- ) ?? [];
47739
- throw new ToolError(
47740
- `No section ${section} on ${page}. Sections there: ${ids.join(", ") || "none"}.`,
47741
- "not_found"
47742
- );
47743
- }
47744
- return loc.section;
47745
- }
47746
- function requireBlock(doc, page, block4) {
47747
- const loc = findBlock(doc, page, block4);
47748
- if (loc) return { block: loc.block, section: loc.section };
47749
- const elsewhere = locateBlock(doc, block4);
47750
- throw new ToolError(
47751
- elsewhere ? `Block ${block4} is on ${elsewhere.container}, not ${page}.` : `No block ${block4}. Call get_page to see block ids.`,
47752
- "not_found"
47753
- );
47754
- }
47755
- function makeBlock2(registry3, input) {
47756
- const def = registry3.get(input.type);
47757
- if (!def)
47758
- throw new ToolError(
47759
- `Unknown block type "${input.type}". Types: ${registry3.types().join(", ")}.`,
47760
- "invalid_arguments"
47761
- );
47762
- const fields = {};
47763
- for (const [k, m] of Object.entries(def.meta.fields))
47764
- if (!m.optional && m.default !== void 0) fields[k] = structuredClone(m.default);
47765
- Object.assign(fields, fieldsFromAgent(registry3, input.type, input.fields ?? {}));
47766
- const { type, fields: _f, id, v: _v, ...rest } = input;
47767
- return {
47768
- ...rest,
47769
- id: id ?? newId("blk"),
47770
- type,
47771
- v: def.version,
47772
- fields
47773
- };
47774
- }
47775
- function makeSection(registry3, raw2) {
47776
- const blocks2 = Array.isArray(raw2.blocks) ? raw2.blocks : [];
47777
- return {
47778
- ...raw2,
47779
- id: typeof raw2.id === "string" ? raw2.id : newId("sec"),
47780
- layout: raw2.layout ?? { mode: "stack", columns: { d: [12] } },
47781
- style: isRecord4(raw2.style) ? raw2.style : { colorTheme: "light" },
47782
- blocks: blocks2.map((b, i) => {
47783
- if (!isRecord4(b) || typeof b.type !== "string")
47784
- throw new ToolError(`section.blocks[${i}] needs a "type".`, "invalid_arguments");
47785
- return makeBlock2(registry3, b);
47786
- })
47787
- };
47788
- }
47789
- var insertSection = defineTool({
47790
- name: "insert_section",
47791
- title: "Insert section",
47792
- description: "Add a section to a page from a preset id (preferred \u2014 see list_section_presets) or an explicit section. Preset sections arrive with designed demo copy: replace it with the owner's real content using update_block. Header presets restyle the site header and footer presets replace the site footer (page is ignored for those). Returns the new section and block ids.",
47793
- input: external_exports.object({
47794
- site: siteArg,
47795
- page: external_exports.string().optional().describe("Page id/slug/title (not needed for header/footer presets)."),
47796
- preset: external_exports.string().optional().describe('Preset id, e.g. "hero/split-image@1" (version optional).'),
47797
- section: external_exports.looseObject({}).optional().describe(
47798
- "Explicit section ({layout, style, blocks:[{type, fields, variant?, column?}]}); ids are generated when omitted; rich text as Markdown."
47799
- ),
47800
- at: positionArg.optional(),
47801
- colorTheme: themeArg.optional(),
47802
- primaryLink: external_exports.unknown().optional().describe(
47803
- "Link for the main call to action ({type:'page', page:'pg_\u2026'} or {type:'url', url})."
47804
- ),
47805
- secondaryLink: external_exports.unknown().optional(),
47806
- collection: external_exports.string().optional().describe("Collection for summary/events/product presets."),
47807
- label: external_exports.string().max(80).optional().describe("Layers label for the section.")
47808
- }),
47809
- annotations: {},
47810
- async handler(ctx, input) {
47811
- const siteId = await ctx.siteId(input.site);
47812
- const doc = await ctx.doc(siteId);
47813
- if (!!input.preset === !!input.section)
47814
- throw new ToolError("Give exactly one of preset or section.", "invalid_arguments");
47815
- const links = {};
47816
- for (const k of ["primaryLink", "secondaryLink"]) {
47817
- if (input[k] === void 0) continue;
47818
- const r = LinkSchema.safeParse(input[k]);
47819
- if (!r.success)
47820
- throw new ToolError(
47821
- `${k} is not a valid link: ${r.error.issues.map((i) => i.message).join("; ")}`,
47822
- "invalid_arguments"
47823
- );
47824
- links[k] = r.data;
47825
- }
47826
- if (input.preset) {
47827
- const preset = findPreset(input.preset);
47828
- const opts = { colorTheme: input.colorTheme, collection: input.collection, ...links };
47829
- const linkNote = (changes2) => changes2.length ? {
47830
- linksChanged: changes2.map((c) => ({ from: c.from, to: c.to })),
47831
- linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
47832
- } : {};
47833
- if (preset.target === "header") {
47834
- const { value: header, changes: changes2 } = resolveInternalLinks(preset.build(opts), doc);
47835
- await ctx.apply(siteId, [{ op: "site.update", set: { header } }]);
47836
- return {
47837
- data: {
47838
- applied: preset.id,
47839
- header,
47840
- note: "The site header now uses this preset.",
47841
- ...linkNote(changes2)
47842
- }
47843
- };
47844
- }
47845
- const { value: section2, changes } = resolveInternalLinks(preset.build(opts), doc);
47846
- if (input.label) section2.label = input.label;
47847
- if (preset.target === "footer") {
47848
- await ctx.apply(siteId, [{ op: "site.update", set: { footer: section2 } }]);
47849
- return {
47850
- data: {
47851
- replaced: "footer",
47852
- section: sectionSummary(ctx.registry, section2),
47853
- note: 'Edit footer blocks with page "footer".',
47854
- ...linkNote(changes)
47855
- }
47856
- };
47857
- }
47858
- if (!input.page)
47859
- throw new ToolError("page is required for page sections.", "invalid_arguments");
47860
- const page2 = pageOnly(doc, input.page);
47861
- await ctx.apply(siteId, [
47862
- { op: "section.insert", page: page2, at: input.at ?? "end", section: section2 }
47863
- ]);
47864
- return {
47865
- data: {
47866
- page: page2,
47867
- section: sectionSummary(ctx.registry, section2),
47868
- copyGuidance: preset.copy.map((c) => `${c.label}: ${c.words[0]}\u2013${c.words[1]} words`),
47869
- next: "Replace the demo copy with the owner's content (update_block), then run design_check.",
47870
- ...linkNote(changes)
47871
- }
47872
- };
47873
- }
47874
- if (!input.page) throw new ToolError("page is required.", "invalid_arguments");
47875
- const page = pageOnly(doc, input.page);
47876
- const section = makeSection(ctx.registry, input.section);
47877
- if (input.colorTheme)
47878
- section.style = { ...section.style ?? {}, colorTheme: input.colorTheme };
47879
- if (input.label) section.label = input.label;
47880
- await ctx.apply(siteId, [
47881
- { op: "section.insert", page, at: input.at ?? "end", section }
47882
- ]);
47883
- return { data: { page, section: sectionSummary(ctx.registry, section) } };
47884
- }
47885
- });
47886
- var updateSection = defineTool({
47887
- name: "update_section",
47888
- title: "Update section",
47889
- description: `Change a section's settings with dot-path patches, e.g. set {"style.colorTheme": "dark", "style.padding": {"d": "xl", "m": "lg"}, "label": "Intro", "anchor": "classes", "hidden": true, "layout": {...}}. Shortcuts: colorTheme. Blocks are edited with the block tools.`,
47890
- input: external_exports.object({
47891
- site: siteArg,
47892
- page: containerArg,
47893
- section: external_exports.string(),
47894
- set: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
47895
- unset: external_exports.array(external_exports.string()).optional(),
47896
- colorTheme: themeArg.optional()
47897
- }),
47898
- annotations: { idempotentHint: true },
47899
- async handler(ctx, input) {
47900
- const siteId = await ctx.siteId(input.site);
47901
- const doc = await ctx.doc(siteId);
47902
- const page = container(doc, input.page);
47903
- requireSection2(doc, page, input.section);
47904
- const set2 = { ...input.set ?? {} };
47905
- if (input.colorTheme) set2["style.colorTheme"] = input.colorTheme;
47906
- const body = { op: "section.update", page, section: input.section };
47907
- if (Object.keys(set2).length) body.set = set2;
47908
- if (input.unset?.length) body.unset = input.unset;
47909
- const { doc: next } = await ctx.apply(siteId, [body]);
47910
- return {
47911
- data: { section: sectionSummary(ctx.registry, requireSection2(next, page, input.section)) }
47912
- };
47913
- }
47914
- });
47915
- var moveSection = defineTool({
47916
- name: "move_section",
47917
- title: "Move section",
47918
- description: "Reorder a section within its page.",
47919
- input: external_exports.object({ site: siteArg, page: external_exports.string(), section: external_exports.string(), at: positionArg }),
47920
- annotations: { idempotentHint: true },
47921
- async handler(ctx, input) {
47922
- const siteId = await ctx.siteId(input.site);
47923
- const doc = await ctx.doc(siteId);
47924
- const page = pageOnly(doc, input.page);
47925
- requireSection2(doc, page, input.section);
47926
- const { doc: next } = await ctx.apply(siteId, [
47927
- { op: "section.move", page, section: input.section, at: input.at }
47928
- ]);
47929
- return { data: { order: next.pages[page]?.sections.map((s2) => s2.id) } };
47930
- }
47931
- });
47932
- var deleteSection = defineTool({
47933
- name: "delete_section",
47934
- title: "Delete section",
47935
- description: "Remove a section and its blocks from a page (recoverable from the session snapshot or discard_draft).",
47936
- input: external_exports.object({ site: siteArg, page: external_exports.string(), section: external_exports.string() }),
47937
- annotations: { destructiveHint: true },
47938
- async handler(ctx, input) {
47939
- const siteId = await ctx.siteId(input.site);
47940
- const doc = await ctx.doc(siteId);
47941
- const page = pageOnly(doc, input.page);
47942
- requireSection2(doc, page, input.section);
47943
- await ctx.apply(siteId, [{ op: "section.delete", page, section: input.section }]);
47944
- return { data: { deleted: input.section } };
47945
- }
47946
- });
47947
- var gridItemArg = external_exports.looseObject({}).describe(
47948
- "Grid placement {d:{x,y,w,h,z?,bleed?}, t?, m?}: 24 columns on desktop/tablet, 8 on mobile, 1-based."
47949
- );
47950
- var insertBlock = defineTool({
47951
- name: "insert_block",
47952
- title: "Insert block",
47953
- description: "Add a block to a section. Fields are validated against the block type (see list_blocks); rich text as Markdown. In stack sections pick a column (0-based); in grid sections give place (auto-placed at the bottom otherwise) and then check the mobile layout (auto_mobile_layout).",
47954
- input: external_exports.object({
47955
- site: siteArg,
47956
- page: containerArg,
47957
- section: external_exports.string(),
47958
- block: external_exports.looseObject({
47959
- type: external_exports.string(),
47960
- fields: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
47961
- variant: external_exports.string().optional(),
47962
- style: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
47963
- column: external_exports.int().min(0).max(3).optional(),
47964
- label: external_exports.string().optional()
47965
- }).describe("{type, fields, variant?, style?, column?, label?}"),
47966
- at: positionArg.optional(),
47967
- place: gridItemArg.optional()
47968
- }),
47969
- annotations: {},
47970
- async handler(ctx, input) {
47971
- const siteId = await ctx.siteId(input.site);
47972
- const doc = await ctx.doc(siteId);
47973
- const page = container(doc, input.page);
47974
- requireSection2(doc, page, input.section);
47975
- const made = makeBlock2(ctx.registryFor(doc), input.block);
47976
- const block4 = resolveInternalLinks(made, doc).value;
47977
- for (const k of Object.keys(input.block.fields ?? {}))
47978
- if (k in made.fields) block4.fields[k] = made.fields[k];
47979
- const body = {
47980
- op: "block.insert",
47981
- page,
47982
- section: input.section,
47983
- at: input.at ?? "end",
47984
- block: block4
47985
- };
47986
- if (input.place) body.place = input.place;
47987
- const { doc: next, warnings } = await ctx.apply(siteId, [body]);
47988
- const placed = findBlock(next, page, block4.id);
47989
- return {
47990
- data: {
47991
- block: blockSummary(ctx.registryFor(next), placed?.block ?? block4),
47992
- ...warnings.length ? { warnings } : {}
47993
- }
47994
- };
47995
- }
47996
- });
47997
- var updateBlock = defineTool({
47998
- name: "update_block",
47999
- title: "Update block",
48000
- description: "Edit a block: set replaces whole top-level fields (rich text as Markdown; lists are replaced whole), unset removes optional fields; variant/style/column/label/hidden adjust presentation (null removes). Ids are preserved.",
48001
- input: external_exports.object({
48002
- site: siteArg,
48003
- page: containerArg,
48004
- block: external_exports.string(),
48005
- set: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
48006
- unset: external_exports.array(external_exports.string()).optional(),
48007
- variant: external_exports.string().nullable().optional(),
48008
- style: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional(),
48009
- column: external_exports.int().min(0).max(3).nullable().optional(),
48010
- label: external_exports.string().max(80).nullable().optional(),
48011
- hidden: external_exports.boolean().nullable().optional()
48012
- }),
48013
- annotations: { idempotentHint: true },
48014
- async handler(ctx, input) {
48015
- const siteId = await ctx.siteId(input.site);
48016
- const doc = await ctx.doc(siteId);
48017
- const page = container(doc, input.page);
48018
- const { block: block4 } = requireBlock(doc, page, input.block);
48019
- const body = { op: "block.update", page, block: input.block };
48020
- if (input.set) body.set = fieldsFromAgent(ctx.registryFor(doc), block4.type, input.set);
48021
- if (input.unset?.length) body.unset = input.unset;
48022
- for (const k of ["variant", "style", "column", "label", "hidden"])
48023
- if (input[k] !== void 0) body[k] = input[k];
48024
- if (Object.keys(body).length === 3)
48025
- throw new ToolError(
48026
- "Nothing to change: give set, unset, variant, style, column, label or hidden.",
48027
- "invalid_arguments"
48028
- );
48029
- const { doc: next } = await ctx.apply(siteId, [body]);
48030
- const updated = findBlock(next, page, input.block)?.block ?? block4;
48031
- return { data: { block: blockSummary(ctx.registryFor(next), updated) } };
48032
- }
48033
- });
48034
- var moveBlock = defineTool({
48035
- name: "move_block",
48036
- title: "Move block",
48037
- description: "Move a block within its page (to another section and/or position). Give column for stack sections and place for grid sections.",
48038
- input: external_exports.object({
48039
- site: siteArg,
48040
- page: containerArg,
48041
- block: external_exports.string(),
48042
- to: external_exports.object({
48043
- section: external_exports.string(),
48044
- at: positionArg.optional(),
48045
- column: external_exports.int().min(0).max(3).nullable().optional(),
48046
- place: gridItemArg.optional()
48047
- })
48048
- }),
48049
- annotations: { idempotentHint: true },
48050
- async handler(ctx, input) {
48051
- const siteId = await ctx.siteId(input.site);
48052
- const doc = await ctx.doc(siteId);
48053
- const page = container(doc, input.page);
48054
- requireBlock(doc, page, input.block);
48055
- requireSection2(doc, page, input.to.section);
48056
- const to = { section: input.to.section, at: input.to.at ?? "end" };
48057
- if (input.to.column !== void 0) to.column = input.to.column;
48058
- if (input.to.place) to.place = input.to.place;
48059
- await ctx.apply(siteId, [{ op: "block.move", page, block: input.block, to }]);
48060
- return { data: { moved: input.block, to } };
48061
- }
48062
- });
48063
- var deleteBlock = defineTool({
48064
- name: "delete_block",
48065
- title: "Delete block",
48066
- description: "Remove a block from its section (recoverable via the session snapshot or discard_draft).",
48067
- input: external_exports.object({ site: siteArg, page: containerArg, block: external_exports.string() }),
48068
- annotations: { destructiveHint: true },
48069
- async handler(ctx, input) {
48070
- const siteId = await ctx.siteId(input.site);
48071
- const doc = await ctx.doc(siteId);
48072
- const page = container(doc, input.page);
48073
- requireBlock(doc, page, input.block);
48074
- await ctx.apply(siteId, [{ op: "block.delete", page, block: input.block }]);
48075
- return { data: { deleted: input.block } };
48076
- }
48077
- });
48078
- var rectArg = external_exports.looseObject({}).describe(
48079
- "{x, y, w, h, z?, bleed?: 'left'|'right'|'both'} (1-based; 24 columns desktop/tablet, 8 mobile)"
48080
- );
48081
- var placeInGrid2 = defineTool({
48082
- name: "place_in_grid",
48083
- title: "Place in grid",
48084
- description: "Set a block's grid placement per breakpoint in a grid section. d = desktop (required on first placement), t = tablet override (null removes), m = mobile (null removes; regenerate with auto_mobile_layout). Rows grow automatically. Overlap is allowed (use z) but text must not overlap text.",
48085
- input: external_exports.object({
48086
- site: siteArg,
48087
- page: containerArg,
48088
- section: external_exports.string(),
48089
- block: external_exports.string(),
48090
- d: rectArg.optional(),
48091
- t: rectArg.nullable().optional(),
48092
- m: rectArg.nullable().optional()
48093
- }),
48094
- annotations: { idempotentHint: true },
48095
- async handler(ctx, input) {
48096
- const siteId = await ctx.siteId(input.site);
48097
- const doc = await ctx.doc(siteId);
48098
- const page = container(doc, input.page);
48099
- const section = requireSection2(doc, page, input.section);
48100
- if (section.layout.mode !== "grid")
48101
- throw new ToolError(
48102
- `Section ${section.id} is a stack section; set the block's column with update_block instead.`,
48103
- "invalid_arguments"
48104
- );
48105
- const bodies = [];
48106
- for (const bp of ["d", "t", "m"]) {
48107
- const rect = input[bp];
48108
- if (rect === void 0) continue;
48109
- bodies.push({
48110
- op: "grid.place",
48111
- page,
48112
- section: section.id,
48113
- block: input.block,
48114
- bp,
48115
- rect
48116
- });
48117
- }
48118
- if (!bodies.length) throw new ToolError("Give at least one of d, t or m.", "invalid_arguments");
48119
- const { doc: next } = await ctx.apply(siteId, bodies);
48120
- const after = requireSection2(next, page, section.id);
48121
- return {
48122
- data: {
48123
- placement: after.layout.mode === "grid" ? after.layout.items[input.block] : null,
48124
- rows: after.layout.mode === "grid" ? after.layout.rows : null,
48125
- next: input.d && input.m === void 0 ? "Desktop changed: review mobile (auto_mobile_layout or screenshot device mobile)." : void 0
48126
- }
48127
- };
48128
- }
48129
- });
48130
- var autoMobileLayout2 = defineTool({
48131
- name: "auto_mobile_layout",
48132
- title: "Auto mobile layout",
48133
- description: "Generate mobile grid placements for a grid section by visual reading order (top-to-bottom, left-to-right on the desktop grid, layered items kept on their host). Keeps existing mobile placements unless reset is true.",
48134
- input: external_exports.object({
48135
- site: siteArg,
48136
- page: containerArg,
48137
- section: external_exports.string(),
48138
- reset: external_exports.boolean().optional()
48139
- }),
48140
- annotations: { idempotentHint: true },
48141
- async handler(ctx, input) {
48142
- const siteId = await ctx.siteId(input.site);
48143
- const doc = await ctx.doc(siteId);
48144
- const page = container(doc, input.page);
48145
- const section = requireSection2(doc, page, input.section);
48146
- if (section.layout.mode !== "grid")
48147
- return { data: { changed: 0, note: "Stack sections stack automatically on mobile." } };
48148
- const ops = autoMobileLayoutOps(doc, page, section.id, { reset: input.reset });
48149
- if (!ops.length)
48150
- return { data: { changed: 0, note: "Mobile placements are already complete." } };
48151
- const bodies = ops.map(({ id: _i, actor: _a3, ts: _t, ...b }) => b);
48152
- const { doc: next } = await ctx.apply(siteId, bodies);
48153
- const after = requireSection2(next, page, section.id);
48154
- return {
48155
- data: {
48156
- changed: ops.length,
48157
- mobile: after.layout.mode === "grid" ? Object.fromEntries(Object.entries(after.layout.items).map(([id, it]) => [id, it.m])) : null
48158
- }
48159
- };
48160
- }
48161
- });
48162
- var editTools = [
48163
- insertSection,
48164
- updateSection,
48165
- moveSection,
48166
- deleteSection,
48167
- insertBlock,
48168
- updateBlock,
48169
- moveBlock,
48170
- deleteBlock,
48171
- placeInGrid2,
48172
- autoMobileLayout2
48173
- ];
48174
-
48175
48419
  // ../mcp-tools/src/tools/pages.ts
48176
48420
  function requireConfirm(confirm, what) {
48177
48421
  if (confirm !== true)
@@ -48308,16 +48552,59 @@ var deletePage = defineTool({
48308
48552
  };
48309
48553
  }
48310
48554
  });
48555
+ function connectSections(doc, tree) {
48556
+ const view = { ...doc, site: { ...doc.site, nav: tree } };
48557
+ const bodies = [];
48558
+ const connected = [];
48559
+ const warnings = [];
48560
+ const claimed = /* @__PURE__ */ new Map();
48561
+ for (const l of findBrokenLinks(view)) {
48562
+ if (l.kind !== "anchor" || l.scope.kind !== "site" || l.path[0] !== "nav") continue;
48563
+ if (!l.anchor) {
48564
+ warnings.push(`${l.message}.`);
48565
+ continue;
48566
+ }
48567
+ const { page, fragment } = l.anchor;
48568
+ const key2 = `${page}#${fragment}`;
48569
+ if (claimed.has(key2)) continue;
48570
+ const section = suggestAnchorSection(view, page, fragment);
48571
+ const free = section && !section.anchor && ![...claimed.values()].includes(section.id) && SlugSchema.safeParse(fragment).success;
48572
+ if (section && free) {
48573
+ claimed.set(key2, section.id);
48574
+ bodies.push({ op: "section.update", page, section: section.id, set: { anchor: fragment } });
48575
+ connected.push({
48576
+ anchor: fragment,
48577
+ section: section.id,
48578
+ sectionLabel: section.label ?? section.preset ?? section.blocks[0]?.type ?? "section"
48579
+ });
48580
+ } else
48581
+ warnings.push(
48582
+ `The menu links to \u201C${fragment}\u201D, but no section on that page is called that. Name the section it should jump to with update_section (page, section, set {"anchor": "${fragment}"}), or change the entry.`
48583
+ );
48584
+ }
48585
+ return { bodies, connected, warnings };
48586
+ }
48311
48587
  var updateNavigation = defineTool({
48312
48588
  name: "update_navigation",
48313
48589
  title: "Update navigation",
48314
- description: 'Replace the navigation tree. Items: {type:"page", page:"pg_\u2026", label?}, {type:"collection", collection, label}, {type:"link", label, href, newTab?}, {type:"button", label, link, style:"primary"|"secondary"|"tertiary"}, {type:"folder", label, page?, items:[\u2026one level\u2026]}. Keep one button at most (the header call to action).',
48590
+ description: `Replace the navigation tree. Items: {type:"page", page:"pg_\u2026", label?, anchor?}, {type:"collection", collection, label}, {type:"link", label, href, newTab?}, {type:"button", label, link, style:"primary"|"secondary"|"tertiary"}, {type:"folder", label, page?, items:[\u2026one level\u2026]}. Keep one button at most (the header call to action). A menu entry that scrolls to a section of a page (a one-page site's "How it works", "Prices") is {type:"page", page, label, anchor:"how-it-works"} and a button to one has link {type:"page", page, anchor}; the anchor is that section's link name \u2014 set it when you add the section (insert_section / insert_component anchor) or with update_section set {"anchor": \u2026}. Entries whose anchor no section has yet are connected to the section they most likely mean (listed in connected \u2014 check them); the rest come back in warnings.`,
48315
48591
  input: external_exports.object({ site: siteArg, tree: external_exports.array(external_exports.looseObject({})) }),
48316
48592
  annotations: { idempotentHint: true },
48317
48593
  async handler(ctx, input) {
48318
48594
  const siteId = await ctx.siteId(input.site);
48319
- const { doc } = await ctx.apply(siteId, [{ op: "nav.update", tree: input.tree }]);
48320
- return { data: { nav: doc.site.nav } };
48595
+ const before = await ctx.doc(siteId);
48596
+ const { bodies, connected, warnings } = connectSections(before, input.tree);
48597
+ const { doc } = await ctx.apply(siteId, [
48598
+ { op: "nav.update", tree: input.tree },
48599
+ ...bodies
48600
+ ]);
48601
+ return {
48602
+ data: {
48603
+ nav: doc.site.nav,
48604
+ ...connected.length ? { connected } : {},
48605
+ ...warnings.length ? { warnings } : {}
48606
+ }
48607
+ };
48321
48608
  }
48322
48609
  });
48323
48610
  var SITE_FORBIDDEN = ["nav", "customCss", "headInjection", "footerInjection"];
@@ -51559,10 +51846,10 @@ function setRole(doc, theme, role, value) {
51559
51846
  doc.styles.overrides = overrides;
51560
51847
  return true;
51561
51848
  }
51562
- function sectionLabel(heading2, blueprint, pageTitle, n) {
51849
+ function sectionLabel(heading2, blueprint, pageTitle2, n) {
51563
51850
  const first2 = heading2?.trim() || blueprint?.find((i) => i.kind === "heading" && i.text?.trim())?.text || blueprint?.find((i) => i.kind === "text" && i.text?.trim())?.text;
51564
51851
  const words4 = (first2 ?? "").replace(/\s+/g, " ").trim();
51565
- if (!words4) return `${pageTitle} section ${n}`.slice(0, 60);
51852
+ if (!words4) return `${pageTitle2} section ${n}`.slice(0, 60);
51566
51853
  const short = words4.length > 40 ? `${words4.slice(0, 40).replace(/\s+\S*$/, "")}\u2026` : words4;
51567
51854
  const cased = /[a-z]/.test(short) ? short : short.charAt(0) + short.slice(1).toLowerCase();
51568
51855
  return cased.slice(0, 60);
@@ -59666,6 +59953,24 @@ function sectionElements(main, skip) {
59666
59953
  }
59667
59954
  return [];
59668
59955
  }
59956
+ function linkedFragments(pages) {
59957
+ const out = /* @__PURE__ */ new Set();
59958
+ for (const p of pages)
59959
+ for (const a of findAll(p.root, (e) => e.tagName === "a")) {
59960
+ const id = slugId(/#([^#?]+)$/.exec(attr(a, "href") ?? "")?.[1]);
59961
+ if (id) out.add(id);
59962
+ }
59963
+ return out;
59964
+ }
59965
+ function sectionAnchor(el, linked) {
59966
+ const own3 = slugId(attr(el, "id"));
59967
+ if (own3 && linked.has(own3)) return own3;
59968
+ const inner = find(el, (e) => {
59969
+ const id = slugId(attr(e, "id"));
59970
+ return !!id && linked.has(id);
59971
+ });
59972
+ return (inner ? slugId(attr(inner, "id")) : void 0) ?? own3;
59973
+ }
59669
59974
  var ROOT_SEL = /^(?::root|html|body|:root\s*,\s*html|html\s*,\s*body|:host)$/i;
59670
59975
  function serializeNodes(nodes) {
59671
59976
  return nodes.map(
@@ -60075,6 +60380,7 @@ ${await r.text()}`;
60075
60380
  const siteTitle = ((wordmark && wordmark.length <= 60 ? titleCase(wordmark) : void 0) ?? opts.title ?? homePage.siteName ?? (docTitle.split(/\s+[|·—–-]\s+/)[0] || "My site")).slice(0, 120);
60076
60381
  const logoImg = brand ? find(brand, (e) => e.tagName === "img") : void 0;
60077
60382
  const logoRef = logoImg ? resolveRef(bundle, attr(logoImg, "src") ?? "", home.path) : void 0;
60383
+ const linked = linkedFragments(pages);
60078
60384
  const segsByPage = /* @__PURE__ */ new Map();
60079
60385
  const sqPages = [];
60080
60386
  const takenNames = /* @__PURE__ */ new Set();
@@ -60106,6 +60412,7 @@ ${await r.text()}`;
60106
60412
  const segs = [];
60107
60413
  const sqSections = [];
60108
60414
  for (const el of p.sections) {
60415
+ const anchor = sectionAnchor(el, linked);
60109
60416
  const name = uniqueName(componentNameFor(el, p.sitePath), takenNames);
60110
60417
  const label2 = componentLabelFor(el);
60111
60418
  const dry = await new SectionConverter(el, dryCtx(p)).convert(name, label2);
@@ -60116,13 +60423,13 @@ ${await r.text()}`;
60116
60423
  if (i === 0) s2.id = m2;
60117
60424
  sqSections.push(s2);
60118
60425
  });
60119
- segs.push({ kind: "preset", marker: m2, reason: dry.error });
60426
+ segs.push({ kind: "preset", marker: m2, reason: dry.error, ...anchor ? { anchor } : {} });
60120
60427
  presets.push({ page: p.sitePath, reason: dry.error });
60121
60428
  continue;
60122
60429
  }
60123
60430
  takenNames.add(name);
60124
60431
  const m = marker();
60125
- segs.push({ kind: "component", el, marker: m, name, label: label2 });
60432
+ segs.push({ kind: "component", el, marker: m, name, label: label2, ...anchor ? { anchor } : {} });
60126
60433
  const blocks2 = [];
60127
60434
  for (const f of dry.forms) {
60128
60435
  const form = formFromHtml(f);
@@ -60318,6 +60625,11 @@ ${await r.text()}`;
60318
60625
  return rest;
60319
60626
  });
60320
60627
  if (seg.kind === "preset") {
60628
+ const first2 = group[0];
60629
+ if (first2 && seg.anchor && !anchors.has(seg.anchor)) {
60630
+ first2.anchor = seg.anchor;
60631
+ anchors.add(seg.anchor);
60632
+ }
60321
60633
  out.push(...group);
60322
60634
  continue;
60323
60635
  }
@@ -60333,9 +60645,10 @@ ${await r.text()}`;
60333
60645
  components[r.def.name] = r.def;
60334
60646
  const section = componentSection(r.def, { fields: r.fields, colorTheme: "light" });
60335
60647
  section.label = seg.label;
60336
- if (r.anchor && !anchors.has(r.anchor)) {
60337
- section.anchor = r.anchor;
60338
- anchors.add(r.anchor);
60648
+ const anchor = seg.anchor ?? r.anchor;
60649
+ if (anchor && !anchors.has(anchor)) {
60650
+ section.anchor = anchor;
60651
+ anchors.add(anchor);
60339
60652
  }
60340
60653
  section.style = { ...section.style, colorTheme: "light", padding: { d: "none" } };
60341
60654
  out.push(section);
@@ -62249,4 +62562,4 @@ export {
62249
62562
  createProgram,
62250
62563
  run
62251
62564
  };
62252
- //# sourceMappingURL=chunk-JDVCOOGD.js.map
62565
+ //# sourceMappingURL=chunk-HUS7TQPS.js.map