@ikas/code-components-mcp 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/data/framework.json +53 -26
  2. package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +13 -0
  3. package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +1 -1
  4. package/data/section-templates/account-info-section/ikas-config-snippet.json +19 -0
  5. package/data/section-templates/add-to-cart/ikas-config-snippet.json +17 -0
  6. package/data/section-templates/add-to-cart/utils/cartLimits.ts +4 -3
  7. package/data/section-templates/blog-home-section/ikas-config-snippet.json +2 -0
  8. package/data/section-templates/blog-home-section/index.tsx +2 -1
  9. package/data/section-templates/blog-post-section/ikas-config-snippet.json +2 -0
  10. package/data/section-templates/blog-post-section/index.tsx +3 -2
  11. package/data/section-templates/bundle-products/hooks/useBundleProducts.ts +7 -6
  12. package/data/section-templates/bundle-products/ikas-config-snippet.json +20 -0
  13. package/data/section-templates/cart-section/components/EmptyState/index.tsx +1 -1
  14. package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +9 -0
  15. package/data/section-templates/cart-section/utils/cartLimits.ts +24 -0
  16. package/data/section-templates/cart-section/utils/cartLineLimits.ts +100 -0
  17. package/data/section-templates/category-images-section/children/CategoryImageItem/ikas-config-snippet.json +4 -0
  18. package/data/section-templates/category-images-section/ikas-config-snippet.json +15 -0
  19. package/data/section-templates/category-list-section/children/CardProductName/ikas-config-snippet.json +2 -1
  20. package/data/section-templates/category-list-section/ikas-config-snippet.json +21 -0
  21. package/data/section-templates/category-list-section/index.tsx +2 -1
  22. package/data/section-templates/component-renderer/additional/ProductDetail/index.tsx +5 -1
  23. package/data/section-templates/component-renderer/ikas-config-snippet.json +13 -0
  24. package/data/section-templates/favorites/ikas-config-snippet.json +18 -1
  25. package/data/section-templates/features-section/ikas-config-snippet.json +13 -0
  26. package/data/section-templates/footer-section/ikas-config-snippet.json +15 -0
  27. package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/index.tsx +1 -1
  28. package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +7 -0
  29. package/data/section-templates/header-section/children/CookieBar/ikas-config-snippet.json +3 -0
  30. package/data/section-templates/header-section/children/Navbar/components/CartSidebar/index.tsx +2 -1
  31. package/data/section-templates/header-section/children/Navbar/components/SearchModal/index.tsx +2 -1
  32. package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +27 -0
  33. package/data/section-templates/header-section/children/Navbar/index.tsx +6 -5
  34. package/data/section-templates/header-section/ikas-config-snippet.json +13 -0
  35. package/data/section-templates/header-section/sub-components/CartItem/index.tsx +9 -0
  36. package/data/section-templates/header-section/utils/cartLimits.ts +24 -0
  37. package/data/section-templates/header-section/utils/cartLineLimits.ts +100 -0
  38. package/data/section-templates/hero-slider-section/children/HeroSliderItem/ikas-config-snippet.json +18 -0
  39. package/data/section-templates/hero-slider-section/children/HeroSliderItem/index.tsx +12 -7
  40. package/data/section-templates/hero-slider-section/ikas-config-snippet.json +13 -0
  41. package/data/section-templates/image-handling/ikas-config-snippet.json +52 -0
  42. package/data/section-templates/image-handling/index.tsx +5 -1
  43. package/data/section-templates/login-section/ikas-config-snippet.json +2 -0
  44. package/data/section-templates/navigation/components/CartSidebar/index.tsx +2 -1
  45. package/data/section-templates/navigation/components/SearchModal/index.tsx +2 -1
  46. package/data/section-templates/navigation/ikas-config-snippet.json +27 -0
  47. package/data/section-templates/navigation/index.tsx +6 -5
  48. package/data/section-templates/navigation/sub-components/CartItem/index.tsx +9 -0
  49. package/data/section-templates/navigation/utils/cartLimits.ts +24 -0
  50. package/data/section-templates/navigation/utils/cartLineLimits.ts +100 -0
  51. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/ikas-config-snippet.json +17 -0
  52. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +16 -0
  53. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/ikas-config-snippet.json +18 -0
  54. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/ikas-config-snippet.json +20 -0
  55. package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +27 -0
  56. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +29 -0
  57. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/ikas-config-snippet.json +18 -1
  58. package/data/section-templates/product-detail-section/children/ProductDetailOffer/ikas-config-snippet.json +20 -0
  59. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/ikas-config-snippet.json +16 -0
  60. package/data/section-templates/product-detail-section/children/ProductDetailPrices/ikas-config-snippet.json +16 -0
  61. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/ikas-config-snippet.json +16 -0
  62. package/data/section-templates/product-detail-section/children/ProductDetailSku/ikas-config-snippet.json +16 -0
  63. package/data/section-templates/product-detail-section/children/ProductDetailVariant/ikas-config-snippet.json +17 -0
  64. package/data/section-templates/product-detail-section/hooks/useBundleProducts.ts +7 -6
  65. package/data/section-templates/product-detail-section/ikas-config-snippet.json +52 -0
  66. package/data/section-templates/product-detail-section/index.tsx +5 -1
  67. package/data/section-templates/product-detail-section/utils/cartLimits.ts +4 -3
  68. package/data/section-templates/product-pricing/ikas-config-snippet.json +16 -0
  69. package/data/section-templates/product-slider-section/children/CardProductName/ikas-config-snippet.json +2 -1
  70. package/data/section-templates/product-slider-section/ikas-config-snippet.json +17 -0
  71. package/data/section-templates/register-section/ikas-config-snippet.json +2 -0
  72. package/data/section-templates/variant-selection/ikas-config-snippet.json +17 -0
  73. package/data/storefront-api.json +301 -17
  74. package/data/storefront-types.json +221 -20
  75. package/dist/index.js +408 -26
  76. package/dist/index.js.map +1 -1
  77. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1614,7 +1614,9 @@ function matchScore(text, query) {
1614
1614
  if (lower.includes(queryLower)) {
1615
1615
  score += 10;
1616
1616
  }
1617
- // Word matches
1617
+ // Word matches. Squashing the (potentially guide-sized) text is the expensive half of the
1618
+ // identifier check — compute it lazily, once per call, not once per query word.
1619
+ let squashedText = null;
1618
1620
  for (const word of words) {
1619
1621
  const wordLower = word.toLowerCase();
1620
1622
  if (lower.includes(wordLower)) {
@@ -1624,8 +1626,12 @@ function matchScore(text, query) {
1624
1626
  // Same identifier, different convention (PRODUCT_ATTRIBUTE ↔ IkasProductAttributeDetail
1625
1627
  // ↔ "product attribute"). Scored below an exact word hit so literal matches still win.
1626
1628
  const squashed = squashIdentifier(word);
1627
- if (squashed && squashIdentifier(text).includes(squashed)) {
1628
- score += 2;
1629
+ if (squashed) {
1630
+ if (squashedText === null)
1631
+ squashedText = squashIdentifier(text);
1632
+ if (squashedText.includes(squashed)) {
1633
+ score += 2;
1634
+ }
1629
1635
  }
1630
1636
  }
1631
1637
  }
@@ -1682,6 +1688,73 @@ function suggestClosestNames(input, candidates, opts) {
1682
1688
  .sort((a, b) => a.d - b.d);
1683
1689
  return edits.slice(0, max).map((s) => s.name);
1684
1690
  }
1691
+ /**
1692
+ * Formatted ` — matching sections: ...` suffix naming the guide sections that match the query,
1693
+ * so a hit buried mid-guide (e.g. "Quantity Limits" inside cart-patterns) is visible without
1694
+ * opening the full guide. Section bodies are scored on purpose: a query for an identifier that
1695
+ * appears only inside a section (an error code, a prop name) must still surface that section's
1696
+ * heading. Returns "" when the query matches most of the guide's sections — matchScore is
1697
+ * presence-based, so a broad query ("cart" against cart-patterns) ties every section and a
1698
+ * top-3 would just echo document order, hinting nothing.
1699
+ */
1700
+ // Topic content is loaded once at startup and never mutates, so the section parse is
1701
+ // memoized on the content string — a query only pays for scoring.
1702
+ const parsedSectionsCache = new Map();
1703
+ function parseSections(content) {
1704
+ const cached = parsedSectionsCache.get(content);
1705
+ if (cached)
1706
+ return cached;
1707
+ // Line walk instead of a heading-regex split: a `## ` line inside a fenced code block must
1708
+ // not become a pseudo-section (it would surface a heading that doesn't exist in the guide
1709
+ // and swallow the rest of the real section's body).
1710
+ const sections = [];
1711
+ let fenceOpen = false;
1712
+ for (const line of content.split("\n")) {
1713
+ if (/^\s*```/.test(line))
1714
+ fenceOpen = !fenceOpen;
1715
+ const headingMatch = fenceOpen ? null : line.match(/^#{2,4} (.+)/);
1716
+ if (headingMatch)
1717
+ sections.push({ heading: headingMatch[1].trim(), body: "" });
1718
+ else if (sections.length > 0)
1719
+ sections[sections.length - 1].body += line + "\n";
1720
+ }
1721
+ parsedSectionsCache.set(content, sections);
1722
+ return sections;
1723
+ }
1724
+ function sectionHint(content, query, topicTitle) {
1725
+ const sections = parseSections(content);
1726
+ const scored = [];
1727
+ for (const { heading, body } of sections) {
1728
+ // Heading hits weigh double. The floor of 3 drops sections whose only signal is a single
1729
+ // squashed-identifier body match (worth 2) — a literal word hit (3) or any heading match
1730
+ // clears it.
1731
+ const score = matchScore(heading, query) * 2 + matchScore(body, query);
1732
+ if (score >= 3)
1733
+ scored.push({ heading, score });
1734
+ }
1735
+ if (scored.length === 0)
1736
+ return "";
1737
+ scored.sort((a, b) => b.score - a.score);
1738
+ // The tie-guard below must only fire when the field is genuinely flat. A single common
1739
+ // query word ("cart") pushes every section past the floor, so counting raw matches would
1740
+ // also suppress steep fields where one section clearly won. Sections below 60% of the top
1741
+ // score are noise from such a word, not real matches — drop them before counting.
1742
+ const strong = scored.filter((s) => s.score >= scored[0].score * 0.6);
1743
+ if (strong.length > sections.length / 2)
1744
+ return "";
1745
+ // The guide's own title is usually its first `##` line, so it registers as a section.
1746
+ // Its heading is the exact text the result line already prints — listing it would burn
1747
+ // one of the three slots on a pure echo. Keep it in scoring (its body can be a genuine
1748
+ // hit and it counts toward the tie-guard), drop it from the output only. Headings are
1749
+ // already trimmed at parse time; trim the title too so stray whitespace in the JSON
1750
+ // cannot silently re-admit the echo.
1751
+ const title = topicTitle.trim();
1752
+ const named = strong.filter((s) => s.heading !== title);
1753
+ if (named.length === 0)
1754
+ return "";
1755
+ const top = named.slice(0, 3);
1756
+ return ` — matching sections: ${top.map((s) => `"${s.heading}"`).join(", ")}`;
1757
+ }
1685
1758
  function searchFunctions(query) {
1686
1759
  const scored = storefrontData.functions
1687
1760
  .map((fn) => {
@@ -1816,6 +1889,9 @@ function formatTypeDefinition(td, opts = {}) {
1816
1889
  const lines = [];
1817
1890
  lines.push(`## ${td.name}`);
1818
1891
  lines.push("", `**Kind:** ${td.kind} | **Domain:** ${td.domain}`);
1892
+ if (td.description) {
1893
+ lines.push("", td.description);
1894
+ }
1819
1895
  if (td.kind === "type" && td.unionValues) {
1820
1896
  lines.push("", "```typescript");
1821
1897
  lines.push(`type ${td.name} = ${td.unionValues.map((v) => `"${v}"`).join(" | ")};`);
@@ -1828,6 +1904,9 @@ function formatTypeDefinition(td, opts = {}) {
1828
1904
  if (td.properties) {
1829
1905
  for (const p of td.properties) {
1830
1906
  const opt = p.optional ? "?" : "";
1907
+ if (p.description) {
1908
+ lines.push(" /**", ...p.description.split("\n").map((l) => ` * ${l.trim()}`), " */");
1909
+ }
1831
1910
  lines.push(` ${p.name}${opt}: ${p.type};`);
1832
1911
  }
1833
1912
  }
@@ -1890,12 +1969,14 @@ const server = new McpServer({
1890
1969
  version: "0.1.0",
1891
1970
  }, {
1892
1971
  instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
1893
- "Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish). For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
1972
+ "Live-editor actions (require `ikas theme dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place one section (`add_sections_to_page` places MANY at once and can set their props in the same call — fastest way to build a page); `list_page_sections` → the sections placed on a page (LEAN roster: per-placement `elementId`, `componentId`, `propCount`, which props are filled — NO schema/values, so it never truncates); `move_page_section` → reorder a section already placed on a page (by `direction` up/down or an absolute `to_index`) — the only way to change section order, since the editor UI offers it solely as a drag; `get_component_props` → a component's prop blueprint (types, ENUM valid `options`, COMPONENT_LIST `allowedComponentIds`) for any section OR child id; `get_section_values` → one section's current prop values (for read-modify-write); `get_page_by_type` → a theme page id by pageType (e.g. CATEGORY), to build PAGE links to entities (`create_page` adds that page if it does not exist yet, so you never guess a slug); `update_section_prop` → change a single prop value of a placed section (`update_page_sections` fills MANY sections/props of a page in ONE call — strongly prefer it to cut round-trips when filling content); `upload_image` → upload one image (file or URL) and get an image id (`upload_images` uploads many in one call — prefer it for several); `search_products` / `list_categories` / `list_brands` / `list_blogs` / `list_blog_categories` → find real entity ids for PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY prop values; `publish_theme` → publish the theme LIVE and get back the preview URL to review (guarded — needs `confirm:true`, and `confirm_production:true` for the main theme; only call it when the user explicitly asks to publish); `list_ready_made_pages` / `update_ready_made_page_prop` / `enable_ready_made_pages` → the ikas-rendered ready-made account/membership pages (login, register, my account, orders, …) — these are NOT sections and the section tools cannot edit them; use these three to enable the groups, read their prop schemas, and customize/localize them (logo, colors, texts). When the user asks to BUILD a login/register/password/my-account/orders/addresses page, FIRST offer these ready-made pages instead of assembling one from sections — they are fully functional out of the box and customizable (logo, colors, texts); build a custom section page only if the user prefers a custom design. Offer, don't flip: `enable_ready_made_pages` creates/removes pages, so get the user's explicit OK before calling it. For the full end-to-end process (placing a section and filling its content, with per-prop value shapes and COMPONENT_LIST rules), call `get_editor_workflow` first.\n\n" +
1973
+ "READY-MADE PAGES FIRST — HARD RULE for account/membership pages: USER-FACING NAME: name these in the USER'S OWN LANGUAGE — English: \"ikas's ready-made pages\", Turkish: \"ikas'ın hazır sayfaları\"; never mix the English term into a sentence in another language — the editor UI lists them as Membership Pages / My Account Pages; \"built-in\" is an internal term — keep it out of everything the user sees, including question/option labels. When the user asks for a login, register, forgot/recover-password, my-account, orders, order-detail, addresses, favorites or email-verification page, do NOT start coding a custom section page. STOP and ask the user FIRST whether to use ikas's ready-made version of that page (`list_ready_made_pages` shows it — fully functional out of the box, customized/localized via `update_ready_made_page_prop`, the whole group turned on with `enable_ready_made_pages`). Only build a custom section page AFTER the user explicitly chooses custom. Never call `enable_ready_made_pages` without the user's OK — it creates/removes pages. Ready-made pages are born with ONLY their own ikas-rendered element: the theme's header/footer are NOT on them (the page draws its own top bar — logo, back-to-store link — and account nav). If the user wants the theme header/footer on a ready-made page, PLACE it there with add_section_to_page — sections can sit above/below that element — and suggest turning the page's own `showBrandBar` prop off to avoid double chrome.\n\n" +
1894
1974
  "To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
1895
1975
  "TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
1896
1976
  "(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
1897
1977
  "(B) SETTING prop VALUES / filling content = giving values to props that ALREADY exist on a section placed on a page. This is pure data entry via `list_page_sections` + `update_section_prop` (and `upload_image` for images). It writes NO code and needs NO rebuild.\n" +
1898
- "When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.",
1978
+ "When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.\n\n" +
1979
+ "COMMON (GLOBAL) SECTIONS — HEADER & FOOTER: these render on every page that CARRIES A PLACEMENT of them — pages created in the editor are seeded with one, EXCEPT ikas ready-made (built-in) account/membership pages, which are born WITHOUT the theme header/footer (see the ready-made rule above) — and their prop values are shared theme-wide, stored in ONE record on the INDEX (Home) page's placement. Reads and writes from any page are resolved to that record, and every response about such a section carries a `common` block (`isCommon`, `commonSectionType`, `canonicalPageId`, `canonicalElementId`, `valuesArePageSpecific: false`). Consequences: changing a header/footer prop from, say, a product-detail page changes it on ALL pages; and since the header/footer normally already holds the merchant's content, you must `get_section_values` first and change only what the user asked — never treat a prop as empty and never blind-fill 'missing' props, or you overwrite live content everywhere. If the INDEX page has no placement of the section there is no record at all: reads report nothing and writes fail until you add it to the INDEX page.",
1899
1980
  });
1900
1981
  // Tool: search_docs
1901
1982
  server.tool("search_docs", "Search across all ikas storefront API docs, framework guides, and migration guides. Returns matching functions, framework topics, and migration topics ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
@@ -1915,7 +1996,7 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1915
1996
  if (topics.length > 0) {
1916
1997
  parts.push("\n## Matching Framework Topics\n");
1917
1998
  for (const item of topics) {
1918
- parts.push(`- **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
1999
+ parts.push(`- **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}${sectionHint(item.topic.content, query, item.topic.title)}`);
1919
2000
  }
1920
2001
  parts.push("");
1921
2002
  parts.push("Use `get_framework_guide(topic)` to get full content for any topic.");
@@ -1931,7 +2012,7 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1931
2012
  if (migrationTopics.length > 0) {
1932
2013
  parts.push("\n## Matching Migration Topics\n");
1933
2014
  for (const item of migrationTopics) {
1934
- parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}`);
2015
+ parts.push(`- [migration] **${item.topic.title}** (key: \`${item.key}\`) - ${item.topic.description}${sectionHint(item.topic.content, query, item.topic.title)}`);
1935
2016
  }
1936
2017
  parts.push("");
1937
2018
  parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
@@ -2697,6 +2778,23 @@ server.tool("get_prop_types", 'Get all available ikas.config.json prop types wit
2697
2778
  };
2698
2779
  });
2699
2780
  // Tool: get_section_template
2781
+ // Starter templates that rebuild a page ikas already ships as a ready-made BUILT-IN page. Asked to
2782
+ // "build a login page", the agent reaches for the template before any other tool — so the
2783
+ // ask-the-user gate rides this response, where it cannot be skimmed past.
2784
+ const BUILTIN_COVERED_TEMPLATES = new Set([
2785
+ "login-section",
2786
+ "register-section",
2787
+ "forgot-password-section",
2788
+ "recover-password-section",
2789
+ "email-verification-section",
2790
+ "account-info-section",
2791
+ "favorites",
2792
+ ]);
2793
+ const BUILTIN_COVERED_BANNER = [
2794
+ "STOP — ASK THE USER BEFORE CODING THIS.",
2795
+ "This template rebuilds a page ikas already ships as a ready-made page: fully functional auth/account flows out of the box, customized and localized via `update_ready_made_page_prop`, the whole group enabled with `enable_ready_made_pages` (see `list_ready_made_pages`).",
2796
+ "If the user has not EXPLICITLY chosen a custom-coded page, do not write code yet. Ask them now: (a) ikas's ready-made page — instant, maintained by ikas — or (b) a custom section with full design freedom. Continue with this template ONLY after they explicitly choose custom.",
2797
+ ].join("\n");
2700
2798
  // Build the enum dynamically from the on-disk section templates directory
2701
2799
  const sectionTemplateKeys = sectionTemplateNames.length > 0
2702
2800
  ? sectionTemplateNames
@@ -2781,6 +2879,9 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2781
2879
  (bundle.componentNames.length > 0 && !componentsFullyIncluded) ||
2782
2880
  (bundle.subComponentNames.length > 0 && !subComponentsFullyIncluded);
2783
2881
  const parts = [
2882
+ ...(BUILTIN_COVERED_TEMPLATES.has(normalizedType)
2883
+ ? [BUILTIN_COVERED_BANNER, ""]
2884
+ : []),
2784
2885
  `## ${bundle.title} — API Integration Pattern Reference`,
2785
2886
  "",
2786
2887
  ];
@@ -3147,7 +3248,10 @@ server.tool("list_section_types", "List all available `get_section_template` sec
3147
3248
  ];
3148
3249
  for (const name of sectionTemplateNames) {
3149
3250
  const meta = loadSectionMeta(name);
3150
- lines.push(`- \`${name}\`${meta ? ` — ${meta.title}` : ""}`);
3251
+ const builtinMark = BUILTIN_COVERED_TEMPLATES.has(name)
3252
+ ? " [ikas ships a ready-made page for this — ask the user before coding a custom one; see list_ready_made_pages]"
3253
+ : "";
3254
+ lines.push(`- \`${name}\`${meta ? ` — ${meta.title}` : ""}${builtinMark}`);
3151
3255
  }
3152
3256
  lines.push("", "Call `get_section_template(sectionType)` to fetch one.");
3153
3257
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -3582,6 +3686,27 @@ function parseCliJson(stdout) {
3582
3686
  function toJsonArrayArg(value) {
3583
3687
  return typeof value === "string" ? value : JSON.stringify(value ?? []);
3584
3688
  }
3689
+ /**
3690
+ * The `confirm` parameter shared by the destructive editor tools.
3691
+ *
3692
+ * Spelled out at length on purpose: a one-word "confirmation" flag reads to a model as a
3693
+ * formality to fill in, and the whole point is that it stands for a human's answer.
3694
+ */
3695
+ const CONFIRM_PARAM_DESCRIPTION = "Must be `true`, and only after the USER has explicitly agreed to this specific deletion. It stands for their consent, not for your own confidence \u2014 never set it to clear up something you merely believe is unused, and never re-run with it after a refusal without asking first. The merchant's content goes with the deletion and only the editor's undo stack can bring it back.";
3696
+ /** Reply for a destructive tool called without consent. The dev-server action refuses too. */
3697
+ function destructiveConfirmRefusal() {
3698
+ return {
3699
+ content: [
3700
+ {
3701
+ type: "text",
3702
+ text: JSON.stringify({
3703
+ success: false,
3704
+ error: "Refused: `confirm` was not true. This deletes the merchant's content and can only be undone from the editor's undo stack. Ask the user whether to proceed, and pass confirm: true only after they agree.",
3705
+ }, null, 2),
3706
+ },
3707
+ ],
3708
+ };
3709
+ }
3585
3710
  async function callEditorAction(projectRoot, args) {
3586
3711
  try {
3587
3712
  const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
@@ -3616,7 +3741,7 @@ async function callEditorAction(projectRoot, args) {
3616
3741
  }
3617
3742
  //
3618
3743
  // Tool: list_editor_pages
3619
- server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas-component dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
3744
+ server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas theme dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
3620
3745
  project_root: z
3621
3746
  .string()
3622
3747
  .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
@@ -3698,7 +3823,7 @@ server.tool("add_section_to_page", "Place a SINGLE already-imported section-type
3698
3823
  return callEditorAction(project_root, args);
3699
3824
  });
3700
3825
  // Tool: add_sections_to_page
3701
- server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE call — and optionally set each section's prop values AT placement time, so you skip the separate fill step. The fastest way to build a page: one round-trip instead of one add (and one fill) per section. Pass `sections`, each `{ component_id, index?, updates?: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. Returns the new `elementId` for each placement (plus written prop results). Built-but-unimported section AND child code components are auto-imported. All update values are validated before anything is placed. Use `list_editor_pages` for page ids and `list_imported_sections` for component ids.", {
3826
+ server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE call — and optionally set each section's prop values AT placement time, so you skip the separate fill step. The fastest way to build a page: one round-trip instead of one add (and one fill) per section. Pass `sections`, each `{ component_id, index?, updates?: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. Returns the new `elementId` for each placement (plus written prop results). Built-but-unimported section AND child code components are auto-imported. All update values are validated before anything is placed. Use `list_editor_pages` for page ids and `list_imported_sections` for component ids. Placing a COMMON (global) section — the theme's header/footer — is special: it has ONE shared set of prop values stored on the INDEX (Home) page's placement, so `updates` land there and change that section on every page (the result carries a `common` block), and placing it with `updates` on a non-INDEX page while the INDEX page does not have it is rejected — place and fill it on the INDEX page instead.", {
3702
3827
  project_root: z
3703
3828
  .string()
3704
3829
  .describe("Absolute path to the code-component project."),
@@ -3761,20 +3886,76 @@ server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE c
3761
3886
  return callEditorAction(project_root, args);
3762
3887
  });
3763
3888
  // Tool: list_page_sections
3764
- server.tool("list_page_sections", "List the sections placed on a page — a LEAN ROSTER that never truncates: per-placement `elementId` (the identity of THIS placement — there can be multiple of the same section), `sectionId`, `componentId`, `name`, `propCount`, and `setPropNames` (which props already have a value). It deliberately omits the prop SCHEMA and the prop VALUES (both can be large — Header alone can have ~80 props). To get a section's prop schema (types, ENUM options, allowed children) call `get_component_props(componentId)`; to read its current values call `get_section_values(element_id)`. Use the returned `elementId` with `update_section_prop` / `update_page_sections`. Use `list_editor_pages` for page ids.", {
3889
+ server.tool("list_page_sections", "List the sections placed on a page — a LEAN ROSTER that never truncates: per-placement `elementId` (the identity of THIS placement — there can be multiple of the same section), `sectionId`, `componentId`, `name`, `propCount`, and `setPropNames` (which props already have a value). It deliberately omits the prop SCHEMA and the prop VALUES (both can be large — Header alone can have ~80 props). To get a section's prop schema (types, ENUM options, allowed children) call `get_component_props(componentId)`; to read its current values call `get_section_values(element_id)`. Use the returned `elementId` with `update_section_prop` / `update_page_sections`. Use `list_editor_pages` for page ids. COMMON (global) sections — the theme's header and footer — carry an extra `common` field (`isCommon`, `commonSectionType`, `canonicalPageId`/`canonicalElementId`, `valuesArePageSpecific: false`): their prop values are NOT page-specific, they are shared by every page and stored on the INDEX (Home) page's placement, so `setPropNames` for them reflects that one shared record and any write from any page changes the header/footer everywhere. A page customized per storefront-routing/specification holds a SEPARATE set of placements with its own section order in each state: this lists the BASE layer by default and reports the available states in `pageStates` (plus a `scope` field) — pass `sf_routing_id`/`specification_id` to list a state's own placements instead, which is what `move_page_section` needs when reordering that state.", {
3765
3890
  project_root: z
3766
3891
  .string()
3767
3892
  .describe("Absolute path to the code-component project."),
3768
3893
  page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3894
+ sf_routing_id: z
3895
+ .string()
3896
+ .optional()
3897
+ .describe("List a routing-customized state of the page instead of the base layer. Ids come from the `pageStates` block this tool returns for a customized page."),
3898
+ specification_id: z
3899
+ .string()
3900
+ .optional()
3901
+ .describe("Specification of the customized state to list (used together with `sf_routing_id`)."),
3769
3902
  port: z
3770
3903
  .number()
3771
3904
  .optional()
3772
3905
  .describe("Dev server WebSocket port (default 5201)."),
3773
- }, async ({ project_root, page_id, port }) => {
3906
+ }, async ({ project_root, page_id, sf_routing_id, specification_id, port }) => {
3774
3907
  const args = [
3775
3908
  "list-page-sections",
3776
3909
  "--page-id",
3777
3910
  page_id,
3911
+ ...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
3912
+ ...(specification_id ? ["--specification-id", specification_id] : []),
3913
+ ...(port ? ["--port", String(port)] : []),
3914
+ ];
3915
+ return callEditorAction(project_root, args);
3916
+ });
3917
+ // Tool: move_page_section
3918
+ server.tool("move_page_section", "Reorder a section that is ALREADY placed on a page — the only way to change section order, since the editor UI offers it solely as a drag in the layers tree. Target the placement by its `element_id` from `list_page_sections` (NOT the sectionId/componentId — a page can hold several placements of the same section and each moves independently). Give EITHER `direction` (\"up\"/\"down\", one position — use this for a simple nudge) OR `to_index` (absolute 0-based position), never both. Indices are 0-based and count ONLY placed sections, in exactly the order `list_page_sections` returns them, so you can read that list, pick a target position from it, and pass it straight back. The result returns `fromIndex`, `resultIndex` (where the section ACTUALLY ended up) and `sections` (the full resulting order) — assert on `resultIndex`/`sections` rather than assuming the move landed where you asked; if it lands somewhere other than requested a `note` says so. A reorder that does not take effect comes back as an ERROR, never as a success that changed nothing, so a successful result means the section really moved. Out-of-range indices are rejected with the valid range (and the section's current index) instead of being clamped. Passing a `sectionId`/`componentId` where an `element_id` belongs is detected and the error names the elementId you actually wanted. COMMON (global) sections — the theme's header and footer — are REJECTED: their position is fixed by the theme, so there is nothing to reorder. SCOPE: by default this reorders the page's BASE layer. A page customized per storefront-routing/specification keeps a SEPARATE set of placements and its own section order in each of those states — pass `sf_routing_id` (and `specification_id`) to reorder one of them instead, and read that state's current order with `list_page_sections` using the SAME ids. Whenever the page has any state the result carries `scope`, `pageStates` and `pageStatesNote`: do not report a reorder as verified on such a page without checking them, since reordering the base layer leaves every state exactly as it was. The state must ALREADY exist — creating one duplicates the whole page and is a deliberate editor action, so an unknown routing/specification is rejected with the list of existing states rather than created. To place a NEW section at a specific position use `add_sections_to_page` with its `index` instead; this tool only moves existing placements.", {
3919
+ project_root: z
3920
+ .string()
3921
+ .describe("Absolute path to the code-component project."),
3922
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3923
+ element_id: z
3924
+ .string()
3925
+ .describe("Placed-section elementId to move (from `list_page_sections`)."),
3926
+ direction: z
3927
+ .enum(["up", "down"])
3928
+ .optional()
3929
+ .describe("Move one position up or down. Provide this or `to_index`, not both."),
3930
+ to_index: z
3931
+ .number()
3932
+ .int()
3933
+ .min(0)
3934
+ .optional()
3935
+ .describe("Absolute 0-based target position among the page's placed sections. Provide this or `direction`, not both."),
3936
+ sf_routing_id: z
3937
+ .string()
3938
+ .optional()
3939
+ .describe("Reorder a routing-customized state of the page instead of the base layer. Ids come from the `pageStates` block returned by `list_page_sections`. The state must already exist."),
3940
+ specification_id: z
3941
+ .string()
3942
+ .optional()
3943
+ .describe("Specification of the customized state to reorder (used together with `sf_routing_id`)."),
3944
+ port: z
3945
+ .number()
3946
+ .optional()
3947
+ .describe("Dev server WebSocket port (default 5201)."),
3948
+ }, async ({ project_root, page_id, element_id, direction, to_index, sf_routing_id, specification_id, port, }) => {
3949
+ const args = [
3950
+ "move-page-section",
3951
+ "--page-id",
3952
+ page_id,
3953
+ "--element-id",
3954
+ element_id,
3955
+ ...(direction ? ["--direction", direction] : []),
3956
+ ...(to_index !== undefined ? ["--to-index", String(to_index)] : []),
3957
+ ...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
3958
+ ...(specification_id ? ["--specification-id", specification_id] : []),
3778
3959
  ...(port ? ["--port", String(port)] : []),
3779
3960
  ];
3780
3961
  return callEditorAction(project_root, args);
@@ -3801,7 +3982,7 @@ server.tool("get_component_props", "Get prop blueprints straight from the editor
3801
3982
  return callEditorAction(project_root, args);
3802
3983
  });
3803
3984
  // Tool: get_section_values
3804
- server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch).", {
3985
+ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch). COMMON (global) sections — the theme's header and footer — keep ONE shared set of prop values for the whole theme, stored on the INDEX (Home) page's placement; for those this tool reads the values FROM that canonical placement (a non-INDEX placement stores nothing of its own) and returns a `common` block naming `canonicalPageId`/`canonicalElementId`. So the values you get for a header/footer are the live ones the page actually renders no matter which page_id you ask from — do NOT read defaults as \"empty\" and blind-fill them, and remember any write goes to that same shared record and changes all pages.", {
3805
3986
  project_root: z
3806
3987
  .string()
3807
3988
  .describe("Absolute path to the code-component project."),
@@ -3826,6 +4007,7 @@ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY pl
3826
4007
  });
3827
4008
  // Tool: update_section_prop
3828
4009
  server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). A COLOR prop can ALSO bind to a global theme color instead of a literal hex — pass `{ "patternValueId": "<id>" }` (the color id from `list_theme_globals` → `colors[].id`) and the prop tracks that global color live (resolved to `var(--<id>)` at render); the id is validated and an unknown one is REJECTED. Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; TYPE (style props such as BorderRadiusStyleType/PaddingStyleType/GapStyleType) = the type-shaped object ITSELF, e.g. `{ "value": 8, "unit": "px" }` — get the exact field structure from the prop\'s `writeExample`/`typeName` in `get_component_props` or `get_type_definition("<typeName>")`; unknown fields, wrong field types, and invalid union values (e.g. `"unit": "banana"`) are REJECTED; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; PRODUCT_ATTRIBUTE = `{ "productId": "...", "variantId": "...", "attributeId": "..." }` — `variantId` is REQUIRED alongside `productId` (without it the value resolves with `product: null` and the component renders nothing), or bind to the page product with `{ "attributeId": "...", "usePageData": true }` on a product detail page; PRODUCT_ATTRIBUTE_LIST = the same with `"attributeIds": [ "..." ]`, and note the component receives a SINGLE wrapper object, never an array (read both with `getAttributeDetailValues()` / `getAttributeListValues()` — see `get_framework_guide("prop-types")`); list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<id from list_categories>" }` (the field is `category`, NOT `categoryId` — `categoryId` is only the standalone CATEGORY entity prop; no productIds here) or `"ALL"` (no ids). Unknown sub-fields inside a PRODUCT_LIST (e.g. `categoryId`) are REJECTED, not silently stored. CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
4010
+ "COMMON (global) sections — the theme's HEADER and FOOTER: these render on every page that carries a placement of them (ikas ready-made account/membership pages are born without one) and have ONE shared set of prop values, stored on the INDEX (Home) page's placement. Whichever page_id you write from, the value lands on that single record and changes the header/footer on ALL pages; the response carries a `common` block (`commonSectionType`, `canonicalPageId`, `canonicalElementId`) saying so, and the echoed `value` is read back from that canonical placement. Therefore: ALWAYS `get_section_values` first and change only the prop the user asked about — the props are already filled with the merchant's content even when you are on a page you have not inspected, so blind-filling \"empty-looking\" props overwrites the live header/footer everywhere. If the INDEX page has no placement of the section, there is no record to write to and the call fails telling you to add it to the INDEX page first.\n\n" +
3829
4011
  'BIND TO A GLOBAL VARIABLE (Theme Setting): instead of a static `value`, pass `bind_global` = the global\'s `name` (its `variableName`, the `_<id>` form) from `list_theme_globals`. The prop becomes a LIVE dynamic value that always reflects that global\'s current value — editing the global in Theme Settings updates every bound prop. Use this to wire content to a project-wide setting (e.g. a shared announcement text, brand color, or logo image) rather than duplicating a literal. Pick a global whose type matches the prop (TEXT↔TEXT/RICH_TEXT, NUMBER↔NUMBER, BOOLEAN↔BOOLEAN, IMAGE↔IMAGE, COLOR↔COLOR, …). Provide `value` OR `bind_global`, not both. To bind a prop of a COMPONENT_LIST CHILD, also pass `list_path` (the `{ propId, index }` steps down to the child) and set `prop_id` to the child\'s prop id. Works for every section type, including built-in / code-component sections that have no blueprint.\n\n' +
3830
4012
  'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `get_section_values`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits — call `get_component_props(parentComponentId)` to read the COMPONENT_LIST prop\'s `allowedComponentIds`, then `get_component_props(childId)` to learn that child\'s props (names, types, ENUM `options`/valid values) instead of guessing or reading ikas.config.json. The allowed set is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
3831
4013
  'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
@@ -3915,7 +4097,7 @@ server.tool("upload_image", 'Upload an image to the connected editor\'s project
3915
4097
  return callEditorAction(project_root, args);
3916
4098
  });
3917
4099
  // Tool: update_page_sections
3918
- server.tool("update_page_sections", "Fill MANY placed sections of a page in a SINGLE call — the fastest way to populate a whole page (one round-trip for the entire page instead of one call per section/prop). Takes `sections`, each `{ element_id, updates: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. All-or-nothing across the whole page: every value is resolved and deeply validated (and built-but-unimported child code components auto-imported) before anything is written — one invalid value rejects the entire page update. An update item may instead bind a prop to a project GLOBAL variable (Theme Setting) as a live dynamic value: use `{ prop_id|prop_name, bind_global }` where `bind_global` is the global's `name` (its `variableName`) from `list_theme_globals` — omit `value`. Add `list_path` (an array of `{ propId, index }` steps) to bind a prop of a COMPONENT_LIST child (then `prop_id` is the child's prop id). Static value writes are applied first, then bindings. Prefer this over many update_section_prop(s) calls.", {
4100
+ server.tool("update_page_sections", "Fill MANY placed sections of a page in a SINGLE call — the fastest way to populate a whole page (one round-trip for the entire page instead of one call per section/prop). Takes `sections`, each `{ element_id, updates: [{ prop_name|prop_id, value }] }`; values use the same per-type shapes as update_section_prop / get_editor_workflow. All-or-nothing across the whole page: every value is resolved and deeply validated (and built-but-unimported child code components auto-imported) before anything is written — one invalid value rejects the entire page update. An update item may instead bind a prop to a project GLOBAL variable (Theme Setting) as a live dynamic value: use `{ prop_id|prop_name, bind_global }` where `bind_global` is the global's `name` (its `variableName`) from `list_theme_globals` — omit `value`. Add `list_path` (an array of `{ propId, index }` steps) to bind a prop of a COMPONENT_LIST child (then `prop_id` is the child's prop id). Static value writes are applied first, then bindings. Prefer this over many update_section_prop(s) calls. If any targeted section is a COMMON (global) one — the theme's header/footer — its values are shared by every page and stored on the INDEX (Home) page's placement: the write goes there and changes that section on ALL pages, the result's `common` block says so, and the echoed values are read back from that canonical placement. Read those sections with `get_section_values` first and only change what was asked; a header/footer missing from the INDEX page has no record to write to and rejects the whole call.", {
3919
4101
  project_root: z
3920
4102
  .string()
3921
4103
  .describe("Absolute path to the code-component project."),
@@ -4140,7 +4322,7 @@ server.tool("get_page_by_type", 'Resolve a theme page\'s id from its pageType (C
4140
4322
  return callEditorAction(project_root, args);
4141
4323
  });
4142
4324
  // Tool: create_page
4143
- server.tool("create_page", 'Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:"PAGE", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas-component dev` running with the editor connected.', {
4325
+ server.tool("create_page", 'Create a theme page of a given pageType in the connected editor. Use this when get_page_by_type returns `pageId: null` for a dynamic page (CATEGORY/PRODUCT/BRAND/BLOG/BLOG_CATEGORY) you need to link to: create the page, then build a PAGE link `{ linkType:"PAGE", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }` — do NOT guess a slug/EXTERNAL URL. Non-CUSTOM page types are unique per theme: if one already exists it is returned unchanged (`alreadyExisted: true`). CUSTOM pages require `name` and a unique `slug`. Account sub-pages (ADDRESSES/ORDERS/ORDER_DETAIL/FAVORITE_PRODUCTS/RAFFLE_ACCOUNT) need an ACCOUNT page first. Returns `{ pageType, pageId, name, slug }`. Requires `ikas theme dev` running with the editor connected.', {
4144
4326
  project_root: z
4145
4327
  .string()
4146
4328
  .describe("Absolute path to the code-component project."),
@@ -4170,8 +4352,196 @@ server.tool("create_page", 'Create a theme page of a given pageType in the conne
4170
4352
  ];
4171
4353
  return callEditorAction(project_root, args);
4172
4354
  });
4355
+ // Tool: delete_page
4356
+ server.tool("delete_page", 'Delete a theme page and every section placed on it. DESTRUCTIVE and effectively one-way from here (only the editor\'s own undo stack can bring it back) — ALWAYS get the user\'s explicit OK before calling it, and never call it to "clean up" a page you merely think is unused. Takes the page id from `list_editor_pages` / `get_page_by_type`; there is no name resolution. Refused for the pages a theme cannot run without — INDEX (home), CHECKOUT and NOT_FOUND (404) — and for an ikas-rendered ready-made account page, which must be turned off by its group with `enable_ready_made_pages` so the cleanup that flow does actually runs. Deleting the page currently open in the editor moves it to the home page first. Returns the deleted page\'s `{ pageId, pageType, name, slug }` plus `remainingPages`. Requires `ikas theme dev` running with the editor connected.', {
4357
+ project_root: z
4358
+ .string()
4359
+ .describe("Absolute path to the code-component project."),
4360
+ page_id: z
4361
+ .string()
4362
+ .describe("Id of the page to delete (from `list_editor_pages` or `get_page_by_type`). Strict — no name resolution."),
4363
+ confirm: z.boolean().describe(CONFIRM_PARAM_DESCRIPTION),
4364
+ port: z
4365
+ .number()
4366
+ .optional()
4367
+ .describe("Dev server WebSocket port (default 5201)."),
4368
+ }, async ({ project_root, page_id, confirm, port }) => {
4369
+ if (confirm !== true)
4370
+ return destructiveConfirmRefusal();
4371
+ const args = [
4372
+ "delete-page",
4373
+ "--id",
4374
+ page_id,
4375
+ "--yes",
4376
+ ...(port ? ["--port", String(port)] : []),
4377
+ ];
4378
+ return callEditorAction(project_root, args);
4379
+ });
4380
+ // Tool: remove_page_section
4381
+ server.tool("remove_page_section", 'Remove ONE section placement from a page — the delete counterpart of `add_section_to_page` / `move_page_section`. DESTRUCTIVE: it takes the merchant\'s content in that section with it, so get the user\'s explicit OK first. Target the PLACEMENT by `element_id` from `list_page_sections`, never the `sectionId` or `componentId` — the same section placed twice has two independent placements and each is removed on its own (passing the wrong id is rejected with the elementId you meant). A common (header/footer) section on the home page is HIDDEN rather than deleted, because the INDEX placement is where a common section\'s prop values live for EVERY page; the reply says so with `hidden: true`, and `add_section_to_page` brings it back with its values intact. A page customized per routing/specification holds its own placements per state: pass `sf_routing_id`/`specification_id` to remove from that state, otherwise only the BASE layer is touched (the reply lists the states it left alone). Returns the removed placement plus the page\'s remaining `sections`. Requires `ikas theme dev` running with the editor connected.', {
4382
+ project_root: z
4383
+ .string()
4384
+ .describe("Absolute path to the code-component project."),
4385
+ page_id: z
4386
+ .string()
4387
+ .describe("Id of the page holding the placement (from `list_editor_pages`)."),
4388
+ element_id: z
4389
+ .string()
4390
+ .describe("The PLACEMENT's elementId from `list_page_sections` — not the sectionId and not the componentId."),
4391
+ sf_routing_id: z
4392
+ .string()
4393
+ .optional()
4394
+ .describe("Remove from this customized routing state instead of the page's base layer. The state must already exist."),
4395
+ specification_id: z
4396
+ .string()
4397
+ .optional()
4398
+ .describe("Remove from this customized specification state instead of the page's base layer. The state must already exist."),
4399
+ confirm: z.boolean().describe(CONFIRM_PARAM_DESCRIPTION),
4400
+ port: z
4401
+ .number()
4402
+ .optional()
4403
+ .describe("Dev server WebSocket port (default 5201)."),
4404
+ }, async ({ project_root, page_id, element_id, sf_routing_id, specification_id, confirm, port }) => {
4405
+ if (confirm !== true)
4406
+ return destructiveConfirmRefusal();
4407
+ const args = [
4408
+ "remove-page-section",
4409
+ "--page-id",
4410
+ page_id,
4411
+ "--element-id",
4412
+ element_id,
4413
+ "--yes",
4414
+ ...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
4415
+ ...(specification_id ? ["--specification-id", specification_id] : []),
4416
+ ...(port ? ["--port", String(port)] : []),
4417
+ ];
4418
+ return callEditorAction(project_root, args);
4419
+ });
4173
4420
  // Tool: publish_theme
4174
- server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas-component dev` running with the editor connected.", {
4421
+ // Tool: list_ready_made_pages
4422
+ server.tool("list_ready_made_pages", "List the ikas-rendered ready-made (\"built-in\") account/membership pages (login, register, forgot/recover password, my account, orders, addresses, favorites, email verification) and how to customize them. These pages are rendered by ikas's own components, NOT by sections: a ready-made page carries no sections, so `list_page_sections` shows nothing and `update_section_prop` cannot touch it — `update_ready_made_page_prop` is the write tool. Without arguments returns the roster: per-group (`membership` = login/register/password flows, `account` = my-account pages) enabled state, every ready-made page type with its theme pageId and which props the merchant already edited, plus the SHARED SCOPES (logo, palette, account-menu labels — values edited once that reach every page in the scope's group). Pass `page_type` (e.g. LOGIN) or `scope_id` for that page's/scope's FULL prop schema (types, defaults, ENUM options, translations) and current values (`values` = what the page renders with; `ownValues` = what this page's own layer stores). Pass `sf_routing_id` to read what one storefront routing/locale sees — the no-argument call lists the store's routings (id, path, locale; `isDefault: true` = the base, needing no sf_routing_id). Typical branding flow: read the theme's palette via `list_theme_globals`, then set the shared scope's colors as `{ \"patternValueId\": \"<global color id>\" }` bindings so the pages follow the theme's colors live.", {
4423
+ project_root: z
4424
+ .string()
4425
+ .describe("Absolute path to the code-component project."),
4426
+ page_type: z
4427
+ .string()
4428
+ .optional()
4429
+ .describe("One ready-made page's full prop schema and values (e.g. LOGIN, ACCOUNT, ORDERS)."),
4430
+ scope_id: z
4431
+ .string()
4432
+ .optional()
4433
+ .describe("One shared scope's full prop schema and values (scope ids come from the no-argument call)."),
4434
+ sf_routing_id: z
4435
+ .string()
4436
+ .optional()
4437
+ .describe("Read the values one storefront routing/locale sees (default: the base routing)."),
4438
+ port: z
4439
+ .number()
4440
+ .optional()
4441
+ .describe("Dev server WebSocket port (default 5201)."),
4442
+ }, async ({ project_root, page_type, scope_id, sf_routing_id, port }) => {
4443
+ const args = [
4444
+ "list-builtin-pages",
4445
+ ...(page_type ? ["--page-type", page_type] : []),
4446
+ ...(scope_id ? ["--scope-id", scope_id] : []),
4447
+ ...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
4448
+ ...(port ? ["--port", String(port)] : []),
4449
+ ];
4450
+ return callEditorAction(project_root, args);
4451
+ });
4452
+ // Tool: update_ready_made_page_prop
4453
+ server.tool("update_ready_made_page_prop", 'Set, bind, or clear ONE prop of an ikas-rendered ready-made account/membership page — the write half of `list_ready_made_pages`, and the ONLY way to customize those pages (they have no sections, so `update_section_prop` cannot reach them). Target a page\'s own prop with `page_type` (e.g. LOGIN) or a SHARED prop — logo, palette, menu labels, edited once for every page in the scope\'s group — with `scope_id`. Identify the prop by `prop_id` or `prop_name` from `list_ready_made_pages`. VALUES ARE RAW (no `{ "value": ... }` envelope — an envelope is tolerated and unwrapped): TEXT = the plain string; BOOLEAN = true/false; NUMBER = a number; ENUM = one of the prop\'s `enumOptions` VALUES; COLOR = "#rrggbb" OR `{ "patternValueId": "<global color id>" }` to follow a theme color live (ids from `list_theme_globals` → colors[].id — prefer the binding when branding, so a palette change updates the pages); IMAGE = `{ "id": "<image id>" }` (id from `upload_image`); LINK/LIST_OF_LINK = copy the shape from `list_ready_made_pages` values. `bind_global` (a global\'s `name` from `list_theme_globals`) makes the prop follow a Theme Setting instead of holding a value; `clear: true` deletes the stored value so the prop falls back to the shared value or the manifest default. `sf_routing_id` writes one routing\'s override — use it to localize texts per language; without it you write the base value every routing inherits. Writes are undoable in the editor.', {
4454
+ project_root: z
4455
+ .string()
4456
+ .describe("Absolute path to the code-component project."),
4457
+ page_type: z
4458
+ .string()
4459
+ .optional()
4460
+ .describe("Page type whose OWN prop to write (e.g. LOGIN). Pass this or scope_id."),
4461
+ scope_id: z
4462
+ .string()
4463
+ .optional()
4464
+ .describe("Shared scope whose prop to write — reaches every ready-made page in its group."),
4465
+ prop_id: z
4466
+ .string()
4467
+ .optional()
4468
+ .describe("Prop id from list_ready_made_pages. Provide this or prop_name."),
4469
+ prop_name: z
4470
+ .string()
4471
+ .optional()
4472
+ .describe("Prop name (alternative to prop_id)."),
4473
+ value: z
4474
+ .any()
4475
+ .optional()
4476
+ .describe("The RAW value (see the tool description for per-type shapes). Omit when using bind_global or clear."),
4477
+ bind_global: z
4478
+ .string()
4479
+ .optional()
4480
+ .describe("Bind the prop to a global variable (Theme Setting) — pass the global's `name` from list_theme_globals."),
4481
+ clear: z
4482
+ .boolean()
4483
+ .optional()
4484
+ .describe("true deletes the stored value; the prop falls back to the shared value or the manifest default."),
4485
+ sf_routing_id: z
4486
+ .string()
4487
+ .optional()
4488
+ .describe("Write one storefront routing's override (localization). Omit to write the base value."),
4489
+ port: z
4490
+ .number()
4491
+ .optional()
4492
+ .describe("Dev server WebSocket port (default 5201)."),
4493
+ }, async ({ project_root, page_type, scope_id, prop_id, prop_name, value, bind_global, clear, sf_routing_id, port, }) => {
4494
+ const args = [
4495
+ "update-builtin-page-prop",
4496
+ ...(page_type ? ["--page-type", page_type] : []),
4497
+ ...(scope_id ? ["--scope-id", scope_id] : []),
4498
+ ...(prop_id ? ["--prop-id", prop_id] : []),
4499
+ ...(prop_name ? ["--prop-name", prop_name] : []),
4500
+ ...(bind_global
4501
+ ? ["--bind-global", bind_global]
4502
+ : clear
4503
+ ? ["--clear"]
4504
+ : value !== undefined
4505
+ ? [
4506
+ "--value",
4507
+ // ALWAYS JSON-encode: the CLI JSON.parses --value (raw-string fallback), so a
4508
+ // verbatim TEXT string that happens to parse — "2024", "true", "null" — would
4509
+ // arrive as a number/boolean/clear. Encoded, it round-trips as the string it is.
4510
+ JSON.stringify(value),
4511
+ ]
4512
+ : []),
4513
+ ...(sf_routing_id ? ["--sf-routing-id", sf_routing_id] : []),
4514
+ ...(port ? ["--port", String(port)] : []),
4515
+ ];
4516
+ return callEditorAction(project_root, args);
4517
+ });
4518
+ // Tool: enable_ready_made_pages
4519
+ server.tool("enable_ready_made_pages", 'Turn one GROUP of ikas-rendered ready-made pages on or off: "membership" (login, register, forgot/recover password, email verification) or "account" (my account, orders, order detail, addresses, favorites). Enabling records the choice and creates any page of the group the theme does not have yet — each born rendered by ikas\'s own component; customize them right after with `update_ready_made_page_prop`. Disabling removes ONLY the untouched pages the toggle itself created — a page the merchant built on is never deleted. The flip is one undo step in the editor. Check the current state first with `list_ready_made_pages`.', {
4520
+ project_root: z
4521
+ .string()
4522
+ .describe("Absolute path to the code-component project."),
4523
+ group: z
4524
+ .enum(["membership", "account"])
4525
+ .describe('"membership" = login/register/password flows; "account" = my-account pages.'),
4526
+ enabled: z
4527
+ .boolean()
4528
+ .describe("true to enable the group, false to disable it."),
4529
+ port: z
4530
+ .number()
4531
+ .optional()
4532
+ .describe("Dev server WebSocket port (default 5201)."),
4533
+ }, async ({ project_root, group, enabled, port }) => {
4534
+ const args = [
4535
+ "enable-builtin-pages",
4536
+ "--group",
4537
+ group,
4538
+ "--enabled",
4539
+ String(enabled),
4540
+ ...(port ? ["--port", String(port)] : []),
4541
+ ];
4542
+ return callEditorAction(project_root, args);
4543
+ });
4544
+ server.tool("publish_theme", "Publish the current theme LIVE — the same action as the editor's Publish button (uploads the whole project to the storefront). This is REAL and customer-facing: the published site changes. SAFETY: it publishes nothing unless `confirm: true` is passed; if you omit it you get a dry-run with the `previewUrl` and a warning describing the impact. The MAIN/production theme additionally requires `confirm_production: true` (its URL is the live customer-facing domain). Use this when the user explicitly asks to publish/go live — do NOT publish on your own initiative. On success returns `{ published: true, previewUrl, isMainTheme }`; open `previewUrl` to review the result. Requires `ikas theme dev` running with the editor connected.", {
4175
4545
  project_root: z
4176
4546
  .string()
4177
4547
  .describe("Absolute path to the code-component project."),
@@ -4200,7 +4570,10 @@ server.tool("publish_theme", "Publish the current theme LIVE — the same action
4200
4570
  const EDITOR_WORKFLOW_GUIDE = [
4201
4571
  "# Editor workflow: placing sections and filling their content on a page",
4202
4572
  "",
4203
- "Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas-component dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
4573
+ "Use this when the user wants to add a section to a page and/or fill in its content (heading, text, image, link, slides, etc.) in the live editor. These are LIVE-EDITOR actions and require `ikas theme dev` running with the editor connected; image upload additionally requires the editor to be embedded in the ikas admin panel (the admin panel performs the upload — a standalone editor cannot upload).",
4574
+ "",
4575
+ "## Account & membership pages: ASK before you build",
4576
+ "If the page the user wants is a login, register, forgot/recover-password, my-account, orders, order-detail, addresses, favorites or email-verification page: do NOT code a custom section page yet. ikas ships ready-made versions of exactly these pages — fully functional (real auth/order/address flows), customizable (logo, colors, texts) and localizable via update_ready_made_page_prop, the whole group enabled at once with enable_ready_made_pages. FIRST ask the user which they want: the ready-made page (fast, maintained by ikas) or a custom section page (full design freedom). Only start coding after the user explicitly chooses custom — and never call enable_ready_made_pages without the user's OK, it creates/removes pages. Start discovery with list_ready_made_pages. USER-FACING NAME: name these in the USER'S OWN LANGUAGE — English: \"ikas's ready-made pages\", Turkish: \"ikas'ın hazır sayfaları\"; never mix the English term into a sentence in another language — the editor UI lists them as Membership Pages / My Account Pages; \"built-in\" is an internal term — keep it out of everything the user sees, including question/option labels. Note: a ready-made page is born with ONLY its own ikas-rendered element — the theme's header/footer are NOT on it (the page draws its own top bar — logo, back-to-store link — and account nav). If the user wants the theme header/footer there, place it with add_section_to_page (sections can sit above/below that element) and suggest turning the page's `showBrandBar` prop off to avoid double chrome.",
4204
4577
  "",
4205
4578
  "## Two distinct jobs — do not confuse them",
4206
4579
  "- (A) DEFINE props / author component code — changing WHICH props a component has. Edits component source/config (config add-prop/add-component, index.tsx) and needs a rebuild. Do this ONLY when a needed prop does not exist yet, or the user explicitly asks to build/modify the component.",
@@ -4209,14 +4582,23 @@ const EDITOR_WORKFLOW_GUIDE = [
4209
4582
  "",
4210
4583
  "## Step-by-step (JOB B — fill content)",
4211
4584
  "1. list_editor_pages → choose the target page_id.",
4212
- "2. list_page_sections(page_id) → for each placed section: its per-placement elementId, blueprint props (id, name, type), and setPropNames (which props already have a value). It is LEAN — no prop VALUES, so it never truncates. To read a section's current values (e.g. to read-modify-write a COMPONENT_LIST), call get_section_values(page_id, element_id). (If the section is not on the page yet, see 'Placing a section' below.)",
4585
+ "2. list_page_sections(page_id) → for each placed section: its per-placement elementId, blueprint props (id, name, type), and setPropNames (which props already have a value). It is LEAN — no prop VALUES, so it never truncates. To read a section's current values (e.g. to read-modify-write a COMPONENT_LIST), call get_section_values(page_id, element_id). A section returned with a `common` field is the theme's header/footer — its values are theme-wide, not this page's; read the 'COMMON (global) sections' section below BEFORE touching it. (If the section is not on the page yet, see 'Placing a section' below.)",
4213
4586
  "3. To set props (fewest round-trips = fastest): update_page_sections(page_id, sections:[{element_id, updates:[{prop_name,value}]}]) fills the WHOLE page (or one section, or just one prop — it scales to all cases) in ONE call (best). update_section_prop sets a single prop, for quick one-offs. Pass each value as a parsed JSON object/array, NEVER a JSON string.",
4214
4587
  "4. For image props: upload_images(images:[{file_path|image_url, alt_text}]) uploads many in ONE call (preferred); upload_image for a single one. Use the returned id(s) in IMAGE values. Do uploads BEFORE the update call so you have the ids.",
4215
4588
  "Speed tip: each tool call is a round-trip — gather first (one list_page_sections), batch your uploads (upload_images) and your writes (update_page_sections), rather than calling per-prop/per-image.",
4216
4589
  "",
4590
+ "## COMMON (global) sections: the header and the footer",
4591
+ "The theme's header and footer are COMMON sections: they render on every page that carries a placement of them (pages created in the editor are seeded with one — EXCEPT ikas ready-made (built-in) account/membership pages, which are born without the theme header/footer) and their prop values are SHARED by the whole theme — there is exactly ONE record, kept on the INDEX (Home) page's placement. A placement of the same section on any other page stores NOTHING of its own.",
4592
+ "- How to recognize one: list_page_sections / get_section_values / the write results return a `common` block for it — `isCommon: true`, `commonSectionType: \"header\"|\"footer\"`, `valuesArePageSpecific: false`, plus `canonicalPageId`/`canonicalElementId` (the INDEX placement that holds the values). A page containing one also gets a top-level `commonSectionsNote`.",
4593
+ "- READS are redirected for you: get_section_values / list_page_sections report a common section's values FROM the INDEX placement, whatever page_id you pass. So what you see is what the page actually renders — no need to re-read from the INDEX page.",
4594
+ "- WRITES are redirected too: update_section_prop / update_page_sections / add_sections_to_page store the value on that INDEX record, so the change applies to the header/footer on ALL pages. The echoed value is read back from the canonical placement, so it reflects what was really stored.",
4595
+ "- THE TRAP to avoid: a header/footer is normally already filled with the merchant's content. Never conclude a prop is empty and never blind-fill \"missing\" props — you would overwrite live content on every page, including pages you never opened. Always get_section_values first and change ONLY the prop the user asked about.",
4596
+ "- If the INDEX page has no placement of the section, there is no record: reads return nothing and writes are REJECTED (\"Index page does not have the common section!\"). Fix it by placing the section on the INDEX page (add_section_to_page with the INDEX page id — get it from list_editor_pages / get_page_by_type('INDEX')) and setting its props there; it then renders on the other pages too.",
4597
+ "",
4217
4598
  "## Building a whole page fast (placing + filling)",
4218
4599
  "- add_sections_to_page(page_id, sections:[{component_id, index?, updates:[{prop_name,value}]}]) places MANY sections AND sets their props in ONE call, returning each new elementId. This is the fastest path — prefer it over add_section_to_page per section followed by separate fills. Built-but-unimported section/child components are auto-imported.",
4219
4600
  "- For a single section: list_imported_sections (confirm imported; else import_section, after building) → add_section_to_page(component_id, page_id, index?) → then list_page_sections to get its elementId.",
4601
+ "- REORDERING an already-placed section is a different tool: move_page_section(page_id, element_id, direction:'up'|'down' | to_index:<n>). The `index` on add_* only positions a NEW placement; it cannot move an existing one, and the editor UI exposes reordering solely as a drag, so this tool is the only way. Indices are 0-based over the placed sections in list_page_sections order; the result returns `resultIndex` + the full resulting `sections` order — assert on those. Header/footer (COMMON) sections cannot be reordered and are rejected.",
4220
4602
  "",
4221
4603
  "## Value shapes by prop type (what to pass as `value`)",
4222
4604
  '- Scalars are WRAPPED: TEXT/RICH_TEXT { "value": "..." }, BOOLEAN { "value": true }, NUMBER { "value": 12 }, COLOR { "value": "#FF0000" }, ENUM { "value": "<key>" }, DATE { "value": ... }.',
@@ -4246,13 +4628,13 @@ const EDITOR_WORKFLOW_GUIDE = [
4246
4628
  "## Validation",
4247
4629
  "Values are validated server-side (deeply, including nested children). Wrong shape or wrong semantics (double-encoded string, { value } wrapper on an object prop, props vs propValues, missing/duplicate id, wrong reference key, nonexistent component id, unknown prop name) are REJECTED with an explanatory error instead of being stored silently. Read the error and fix the value; if unsure of a shape, read it with get_section_values and match it.",
4248
4630
  ].join("\n");
4249
- server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
4631
+ server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, how COMMON (global) header/footer sections keep one theme-wide set of values on the INDEX page (and how not to overwrite them), and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
4250
4632
  return {
4251
4633
  content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }],
4252
4634
  };
4253
4635
  });
4254
4636
  // Tool: list_theme_globals
4255
- server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas-component dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
4637
+ server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas theme dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
4256
4638
  project_root: z
4257
4639
  .string()
4258
4640
  .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
@@ -4344,7 +4726,7 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4344
4726
  "- breakpoint: requires name + width (a positive INTEGER, in px — no decimals). To CHANGE an existing breakpoint later, use `update_theme_breakpoint` (edits in place, keeps the id).\n" +
4345
4727
  "- keyframe: requires name + points (array of { point, styles? }) where each style is { property, value } using a CSS property name (opacity, transform, filter, background, color, …); keyframe_type defaults to \"keyframe\". Optional `settings` = animation-level config as { property, value } entries, TYPE-DEPENDENT: for keyframe — animation-duration, animation-iteration-count, animation-play-state, animation-delay, animation-timing-function, animation-direction, animation-fill-mode, animation-timeline, animation-range, transform-origin, backface-visibility; for transition — transition, backface-visibility. Setting these means the editor drives the animation timing (you don't have to hand-write `animation:` CSS). Use the keyframe's `ref` as the CSS animation-name. For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% point is ignored (the start state is the element's own normal state). To CHANGE an existing keyframe later, use `update_theme_keyframe` (edits in place, keeps the `ref` stable) — do NOT delete+recreate, which changes the ref and breaks references.\n" +
4346
4728
  '- colorScheme: requires name + colors (array of { slotId? | newSlotName?, value }) — each color targets ONE color SLOT, EITHER by `slotId` (an EXISTING slot\'s id, from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key — the id-keyed path) OR by `newSlotName` (a name to CREATE-or-reuse a slot; write-only, for the standard vocabulary and brand-new slots). Pass EXACTLY ONE of the two per entry. `value` is EITHER a literal color (hex/rgb/hsl or a CSS color name, e.g. "#0b1120") OR a REFERENCE to an existing theme color, passed as that color\'s cssVar from `list_theme_globals` (e.g. "var(--<colorId>)"). A `var(--id)` value is stored as a linked reference (editing the global color updates the scheme too and the dependency is tracked) — do NOT inline a hex you copied from a global color; pass its cssVar. SLOT IDENTITY: `colorSchemes.schemes` lists each slot as `{id, name}`. To set a color on an EXISTING slot, find it there by name and pass its `slotId` (names are NOT unique — two design assets can each have a "Background" — so when ambiguous disambiguate via the scheme\'s existing `colorsByScheme` keys); use `newSlotName` only to add a slot the scheme does not have yet. When creating slots use the theme\'s STANDARD slot names so components resolve them: "Background", "Heading", "Text", "Link", "HoverLink", "Border", and per button "PrimaryButton/Background", "PrimaryButton/Text", "PrimaryButton/Border", "PrimaryButton/HoverBackground", "PrimaryButton/HoverText", "PrimaryButton/HoverBorder" (and the same six under "SecondaryButton/"); a "/" groups the slot in the editor UI. A scheme should normally define EVERY slot it uses; omitted slots fall back to the default scheme\'s color. A mistaken slot can be removed with `delete_theme_global` kind `colorSchemeSlot` (by slot id), or renamed in place (id stays stable) with `rename_theme_color_scheme_slot`. To CHANGE an existing scheme later, use `update_theme_color_scheme` (edits in place, keeps the className stable; merges colors by slot, and can set/clear its default flag via `is_default`).\n' +
4347
- "Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
4729
+ "Read created items back with `list_theme_globals`. Requires `ikas theme dev` running with the editor connected.", {
4348
4730
  project_root: z
4349
4731
  .string()
4350
4732
  .describe("Absolute path to the code-component project."),
@@ -4516,7 +4898,7 @@ server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE
4516
4898
  "point is ignored (the start state is the element's own normal state). " +
4517
4899
  "Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
4518
4900
  "see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
4519
- "Requires `ikas-component dev` running with the editor connected.", {
4901
+ "Requires `ikas theme dev` running with the editor connected.", {
4520
4902
  project_root: z
4521
4903
  .string()
4522
4904
  .describe("Absolute path to the code-component project."),
@@ -4579,7 +4961,7 @@ server.tool("update_text_style", "Update an existing theme typography token (tex
4579
4961
  "this text style the theme's default/global style (the editor's right-click \"Make Default Style\" / \"Global " +
4580
4962
  "Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
4581
4963
  "current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
4582
- "`ikas-component dev` running with the editor connected.", {
4964
+ "`ikas theme dev` running with the editor connected.", {
4583
4965
  project_root: z
4584
4966
  .string()
4585
4967
  .describe("Absolute path to the code-component project."),
@@ -4656,7 +5038,7 @@ server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN P
4656
5038
  "(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
4657
5039
  "because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
4658
5040
  "Pass `width` (px) to change it; omit it to only rename. " +
4659
- "Requires `ikas-component dev` running with the editor connected.", {
5041
+ "Requires `ikas theme dev` running with the editor connected.", {
4660
5042
  project_root: z
4661
5043
  .string()
4662
5044
  .describe("Absolute path to the code-component project."),
@@ -4703,7 +5085,7 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
4703
5085
  "fallback whose colors fill any slot a non-default scheme leaves unset); this is exclusive — the editor " +
4704
5086
  "automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
4705
5087
  "one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
4706
- "is_default to no-op. Requires `ikas-component dev` running with the editor connected.", {
5088
+ "is_default to no-op. Requires `ikas theme dev` running with the editor connected.", {
4707
5089
  project_root: z
4708
5090
  .string()
4709
5091
  .describe("Absolute path to the code-component project."),
@@ -4751,7 +5133,7 @@ server.tool("rename_theme_color_scheme_slot", 'Rename a color scheme SLOT (a sch
4751
5133
  'because slot names can collide across design assets. `name` is the NEW slot name (a "/" groups it in the ' +
4752
5134
  'editor UI, e.g. "PrimaryButton/Background"). Only non-design-asset (user/theme) slots can be renamed here. ' +
4753
5135
  "This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
4754
- "scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas-component dev` running with the " +
5136
+ "scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas theme dev` running with the " +
4755
5137
  "editor connected.", {
4756
5138
  project_root: z
4757
5139
  .string()