@ikas/code-components-mcp 2.6.0 → 2.7.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 (31) hide show
  1. package/data/framework.json +68 -8
  2. package/data/migration.json +9 -3
  3. package/data/section-templates/add-to-cart/index.tsx +16 -2
  4. package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +11 -3
  5. package/data/section-templates/add-to-cart/utils/cartLimits.ts +23 -0
  6. package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +57 -24
  7. package/data/section-templates/cart-section/sub-components/CartItem/styles.css +14 -0
  8. package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +11 -3
  9. package/data/section-templates/header-section/sub-components/CartItem/index.tsx +57 -24
  10. package/data/section-templates/header-section/sub-components/CartItem/styles.css +14 -0
  11. package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +11 -3
  12. package/data/section-templates/image-handling/ikas-config-snippet.json +1 -0
  13. package/data/section-templates/navigation/sub-components/CartItem/index.tsx +57 -24
  14. package/data/section-templates/navigation/sub-components/CartItem/styles.css +14 -0
  15. package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +11 -3
  16. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +16 -2
  17. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +92 -0
  18. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +159 -0
  19. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +141 -0
  20. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +47 -0
  21. package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +25 -0
  22. package/data/section-templates/product-detail-section/ikas-config-snippet.json +1 -0
  23. package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +120 -0
  24. package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +70 -0
  25. package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +11 -3
  26. package/data/section-templates/product-detail-section/utils/cartLimits.ts +23 -0
  27. package/data/storefront-api.json +82 -12
  28. package/data/storefront-types.json +2747 -22
  29. package/dist/index.js +582 -179
  30. package/dist/index.js.map +1 -1
  31. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1589,10 +1589,26 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1589
1589
  return parts.join("\n");
1590
1590
  }
1591
1591
  // --- Search helpers ---
1592
+ /**
1593
+ * Strip every non-alphanumeric character and lowercase, so the SAME identifier written in
1594
+ * different conventions collapses to one form: `PRODUCT_ATTRIBUTE`, `IkasProductAttributeDetail`
1595
+ * and the prose "product attribute detail" all become `productattribute…`. Without this a query
1596
+ * for a prop type (which is always SCREAMING_SNAKE) can never reach the function/type docs that
1597
+ * spell the same concept in camelCase or prose — `search_docs("PRODUCT_ATTRIBUTE")` used to miss
1598
+ * `getAttributeDetailValues` entirely.
1599
+ */
1600
+ function squashIdentifier(text) {
1601
+ return text.replace(/[^a-z0-9]/gi, "").toLowerCase();
1602
+ }
1603
+ /** A query word worth matching separator-insensitively: an identifier, not prose. */
1604
+ function isIdentifierLike(word) {
1605
+ return (word.length > 3 &&
1606
+ (/[_\-.]/.test(word) || /[a-z][A-Z]/.test(word) || /^[A-Z0-9_]+$/.test(word)));
1607
+ }
1592
1608
  function matchScore(text, query) {
1593
1609
  const lower = text.toLowerCase();
1594
1610
  const queryLower = query.toLowerCase();
1595
- const words = queryLower.split(/\s+/).filter(Boolean);
1611
+ const words = query.split(/\s+/).filter(Boolean);
1596
1612
  let score = 0;
1597
1613
  // Exact substring match
1598
1614
  if (lower.includes(queryLower)) {
@@ -1600,9 +1616,18 @@ function matchScore(text, query) {
1600
1616
  }
1601
1617
  // Word matches
1602
1618
  for (const word of words) {
1603
- if (lower.includes(word)) {
1619
+ const wordLower = word.toLowerCase();
1620
+ if (lower.includes(wordLower)) {
1604
1621
  score += 3;
1605
1622
  }
1623
+ else if (isIdentifierLike(word)) {
1624
+ // Same identifier, different convention (PRODUCT_ATTRIBUTE ↔ IkasProductAttributeDetail
1625
+ // ↔ "product attribute"). Scored below an exact word hit so literal matches still win.
1626
+ const squashed = squashIdentifier(word);
1627
+ if (squashed && squashIdentifier(text).includes(squashed)) {
1628
+ score += 2;
1629
+ }
1630
+ }
1606
1631
  }
1607
1632
  // Name exact match bonus
1608
1633
  if (lower === queryLower) {
@@ -1719,13 +1744,31 @@ function searchTypes(query) {
1719
1744
  const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
1720
1745
  ? 4
1721
1746
  : 0;
1722
- return { td, score: nameScore + domainScore + propScore + enumScore };
1747
+ const unionScore = td.unionValues?.some((v) => matchScore(v, query) > 0)
1748
+ ? 4
1749
+ : 0;
1750
+ return {
1751
+ td,
1752
+ score: nameScore + domainScore + propScore + enumScore + unionScore,
1753
+ };
1723
1754
  })
1724
1755
  .filter((item) => item.score > 0)
1725
1756
  .sort((a, b) => b.score - a.score);
1726
1757
  return scored.map((item) => item.td);
1727
1758
  }
1728
1759
  // --- Format helpers ---
1760
+ /**
1761
+ * Wrap a code snippet in a fenced block — unless it already carries its own fence. JSDoc `@example`
1762
+ * bodies are indexed verbatim, so every one of them arrives fenced; wrapping them again produced a
1763
+ * double ```` ``` ```` opener, which makes the block unparseable for anything extracting code from
1764
+ * the doc. Bare snippets still get fenced as typescript.
1765
+ */
1766
+ function asCodeBlock(snippet) {
1767
+ const trimmed = snippet.trim();
1768
+ return trimmed.startsWith("```")
1769
+ ? trimmed
1770
+ : ["```typescript", trimmed, "```"].join("\n");
1771
+ }
1729
1772
  function formatFunctionDoc(fn) {
1730
1773
  const lines = [];
1731
1774
  lines.push(`## ${fn.name}`);
@@ -1752,7 +1795,7 @@ function formatFunctionDoc(fn) {
1752
1795
  lines.push("", `**Returns:** \`${fn.returnType}\`${fn.returns ? ` - ${fn.returns}` : ""}`);
1753
1796
  }
1754
1797
  if (fn.example) {
1755
- lines.push("", "**Example:**", "```typescript", fn.example, "```");
1798
+ lines.push("", "**Example:**", asCodeBlock(fn.example));
1756
1799
  }
1757
1800
  if (fn.related.length > 0) {
1758
1801
  lines.push("", `**Related:** ${fn.related.map((r) => `\`${r}\``).join(", ")}`);
@@ -1773,7 +1816,12 @@ function formatTypeDefinition(td, opts = {}) {
1773
1816
  const lines = [];
1774
1817
  lines.push(`## ${td.name}`);
1775
1818
  lines.push("", `**Kind:** ${td.kind} | **Domain:** ${td.domain}`);
1776
- if (td.kind === "type") {
1819
+ if (td.kind === "type" && td.unionValues) {
1820
+ lines.push("", "```typescript");
1821
+ lines.push(`type ${td.name} = ${td.unionValues.map((v) => `"${v}"`).join(" | ")};`);
1822
+ lines.push("```");
1823
+ }
1824
+ else if (td.kind === "type") {
1777
1825
  lines.push("", "```typescript");
1778
1826
  const extendsStr = td.extends ? ` & ${td.extends.join(" & ")}` : "";
1779
1827
  lines.push(`type ${td.name} = {`);
@@ -1828,6 +1876,10 @@ function formatTypeSummary(td) {
1828
1876
  const count = td.enumValues?.length ?? 0;
1829
1877
  return `- \`${td.name}\` (enum, ${count} values) — domain: ${td.domain}`;
1830
1878
  }
1879
+ if (td.unionValues) {
1880
+ const values = td.unionValues.map((v) => `"${v}"`).join(" | ");
1881
+ return `- \`${td.name}\` (union: ${values}) — domain: ${td.domain}`;
1882
+ }
1831
1883
  const count = td.properties?.length ?? 0;
1832
1884
  const ext = td.extends ? ` extends ${td.extends.join(", ")}` : "";
1833
1885
  return `- \`${td.name}\` (${count} properties${ext}) — domain: ${td.domain}`;
@@ -2109,7 +2161,7 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
2109
2161
  }
2110
2162
  }
2111
2163
  else {
2112
- parts.push("```typescript", example.code, "```", "");
2164
+ parts.push(asCodeBlock(example.code), "");
2113
2165
  }
2114
2166
  parts.push(`**Reminder:** Use the API patterns above (function calls, imports, type handling) but create your own original HTML structure and CSS.`);
2115
2167
  parts.push("");
@@ -2227,7 +2279,7 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
2227
2279
  };
2228
2280
  });
2229
2281
  // Tool: get_type_definition
2230
- server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name. Shows all properties with types, extends, or enum values. Covers both runtime models (e.g. 'IkasProduct', 'IkasOrderStatus') and theme-globals tokens returned by the getters (e.g. 'ThemeSetting', 'DesignToken', 'TypographyToken', 'ColorSchemeToken', 'BreakpointToken', 'KeyframeToken'). If a type reports as not found, it may not be indexed yet — fall back to reading the package's `.d.ts` (e.g. `@ikas/bp-storefront-config`) rather than typing the value as `any`.", {
2282
+ server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name. Shows all properties with types, extends, enum values, or string-union values. Covers runtime models (e.g. 'IkasProduct', 'IkasOrderStatus'), theme-globals tokens returned by the getters (e.g. 'ThemeSetting', 'DesignToken', 'TypographyToken', 'ColorSchemeToken', 'BreakpointToken', 'KeyframeToken'), and style-prop TYPE shapes used by ikas.config.json props (e.g. 'BorderRadiusStyleType', 'PaddingStyleType', 'GapStyleType', 'SizeStyleType' and their '*StyleUnitType' unions). If a type reports as not found, it may not be indexed yet — fall back to reading the package's `.d.ts` (e.g. `@ikas/bp-storefront-config`) rather than typing the value as `any`.", {
2231
2283
  name: z
2232
2284
  .string()
2233
2285
  .describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
@@ -2623,7 +2675,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2623
2675
  return { content: [{ type: "text", text: parts.join("\n") }] };
2624
2676
  });
2625
2677
  // Tool: get_prop_types
2626
- server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. NOTE: these are the TypeScript prop-definition types for authoring a component — they are NOT the runtime JSON value shapes you write via `update_section_prop` (e.g. a TEXT prop is typed `string` here but written as `{ \"value\": \"...\" }`, and an IMAGE is `IkasImage | null` here but written as `{ \"id\": \"...\" }`). For the value shapes to pass to `update_section_prop`, see that tool's description. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
2678
+ server.tool("get_prop_types", 'Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. NOTE: these are the TypeScript prop-definition types for authoring a component — they are NOT the runtime JSON value shapes you write via `update_section_prop` (e.g. a TEXT prop is typed `string` here but written as `{ "value": "..." }`, and an IMAGE is `IkasImage | null` here but written as `{ "id": "..." }`). For the value shapes to pass to `update_section_prop`, see that tool\'s description. Tip: Use `npx ikas-component config add-component --props \'[...]\'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.', {}, async () => {
2627
2679
  const propTypesTopic = frameworkData.topics["prop-types"];
2628
2680
  if (propTypesTopic) {
2629
2681
  return {
@@ -3615,7 +3667,7 @@ server.tool("import_section", "Import a built section-type code component into t
3615
3667
  return callEditorAction(project_root, args);
3616
3668
  });
3617
3669
  // Tool: add_section_to_page
3618
- server.tool("add_section_to_page", 'Place a SINGLE already-imported section-type code component on a page. Use this ONLY when adding exactly one section. If you are adding more than one section — or building/populating a page — call `add_sections_to_page` instead: it places them all AND fills their props in ONE call. Calling this tool repeatedly (one section per call) is the slow path and should be avoided. Equivalent to right-clicking the section in the dev-components panel and choosing "Add to Page". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section\'s prop values with `update_section_prop` / `update_page_sections` (use `list_page_sections` to get the placement\'s `elementId` and prop names).', {
3670
+ server.tool("add_section_to_page", "Place a SINGLE already-imported section-type code component on a page. Use this ONLY when adding exactly one section. If you are adding more than one section — or building/populating a page — call `add_sections_to_page` instead: it places them all AND fills their props in ONE call. Calling this tool repeatedly (one section per call) is the slow path and should be avoided. Equivalent to right-clicking the section in the dev-components panel and choosing \"Add to Page\". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section's prop values with `update_section_prop` / `update_page_sections` (use `list_page_sections` to get the placement's `elementId` and prop names).", {
3619
3671
  project_root: z
3620
3672
  .string()
3621
3673
  .describe("Absolute path to the code-component project."),
@@ -3647,11 +3699,15 @@ server.tool("add_section_to_page", 'Place a SINGLE already-imported section-type
3647
3699
  });
3648
3700
  // Tool: add_sections_to_page
3649
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.", {
3650
- project_root: z.string().describe("Absolute path to the code-component project."),
3702
+ project_root: z
3703
+ .string()
3704
+ .describe("Absolute path to the code-component project."),
3651
3705
  page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3652
3706
  sections: z
3653
3707
  .array(z.object({
3654
- component_id: z.string().describe("Imported (or built) section-type code component id."),
3708
+ component_id: z
3709
+ .string()
3710
+ .describe("Imported (or built) section-type code component id."),
3655
3711
  index: z
3656
3712
  .number()
3657
3713
  .int()
@@ -3660,22 +3716,33 @@ server.tool("add_sections_to_page", "Place MANY sections on a page in a SINGLE c
3660
3716
  .describe("Zero-based insertion index; appends when omitted."),
3661
3717
  updates: z
3662
3718
  .array(z.object({
3663
- prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
3664
- prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
3665
- value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
3719
+ prop_id: z
3720
+ .string()
3721
+ .optional()
3722
+ .describe("Blueprint prop id (provide this or prop_name)."),
3723
+ prop_name: z
3724
+ .string()
3725
+ .optional()
3726
+ .describe("Blueprint prop name (alternative to prop_id)."),
3727
+ value: z
3728
+ .any()
3729
+ .describe("Prop value, same shape update_section_prop expects for that type."),
3666
3730
  }))
3667
3731
  .optional()
3668
3732
  .describe("Optional prop values to set on this section right after placing it."),
3669
3733
  }))
3670
3734
  .describe("Non-empty array of sections to place (in order), each with its componentId and optional prop updates."),
3671
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3735
+ port: z
3736
+ .number()
3737
+ .optional()
3738
+ .describe("Dev server WebSocket port (default 5201)."),
3672
3739
  }, async ({ project_root, page_id, sections, port }) => {
3673
- const normalized = (sections || []).map(s => ({
3740
+ const normalized = (sections || []).map((s) => ({
3674
3741
  componentId: s.component_id,
3675
3742
  ...(typeof s.index === "number" ? { index: s.index } : {}),
3676
3743
  ...(s.updates
3677
3744
  ? {
3678
- updates: s.updates.map(u => ({
3745
+ updates: s.updates.map((u) => ({
3679
3746
  ...(u.prop_id ? { propId: u.prop_id } : {}),
3680
3747
  ...(u.prop_name ? { propName: u.prop_name } : {}),
3681
3748
  value: u.value,
@@ -3714,11 +3781,16 @@ server.tool("list_page_sections", "List the sections placed on a page — a LEAN
3714
3781
  });
3715
3782
  // Tool: get_component_props
3716
3783
  server.tool("get_component_props", "Get prop blueprints straight from the editor (the source of truth) for one or MANY components in a single call — for any section OR child code component ids. Returns `{ components: [...] }`, each prop carrying its `id`, `name`, `type`, `required`, `default`, and crucially: a ready-to-use `writeExample` (the EXACT value shape to pass to update_section_prop / update_page_sections — e.g. PRODUCT_LIST, BLOG_LIST, COMPONENT_LIST, LINK) plus a `writeNote` for variants, so you do NOT have to guess or trial-write the JSON; for ENUM props the `options` (valid `value`s); and for COMPONENT/COMPONENT_LIST props the `allowedComponentIds` (which children the slot permits — when empty, an `allowedComponentsNote` says any imported code component is allowed). Use this (its `writeExample`/`options`/`allowedComponentIds`) instead of guessing or reading ikas.config.json. Pass ALL the section/child ids you need at once (batch). Child component ids come from a parent prop's `allowedComponentIds` (or get_section_template).", {
3717
- project_root: z.string().describe("Absolute path to the code-component project."),
3784
+ project_root: z
3785
+ .string()
3786
+ .describe("Absolute path to the code-component project."),
3718
3787
  component_ids: z
3719
3788
  .array(z.string())
3720
3789
  .describe("Section/child code-component ids to resolve (batch). From list_imported_sections or a COMPONENT_LIST prop's allowedComponentIds."),
3721
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3790
+ port: z
3791
+ .number()
3792
+ .optional()
3793
+ .describe("Dev server WebSocket port (default 5201)."),
3722
3794
  }, async ({ project_root, component_ids, port }) => {
3723
3795
  const args = [
3724
3796
  "get-component-props",
@@ -3730,12 +3802,17 @@ server.tool("get_component_props", "Get prop blueprints straight from the editor
3730
3802
  });
3731
3803
  // Tool: get_section_values
3732
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).", {
3733
- project_root: z.string().describe("Absolute path to the code-component project."),
3805
+ project_root: z
3806
+ .string()
3807
+ .describe("Absolute path to the code-component project."),
3734
3808
  page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3735
3809
  element_ids: z
3736
3810
  .array(z.string())
3737
3811
  .describe("Placed-section elementIds to read (batch). From `list_page_sections`."),
3738
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3812
+ port: z
3813
+ .number()
3814
+ .optional()
3815
+ .describe("Dev server WebSocket port (default 5201)."),
3739
3816
  }, async ({ project_root, page_id, element_ids, port }) => {
3740
3817
  const args = [
3741
3818
  "get-section-values",
@@ -3748,7 +3825,8 @@ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY pl
3748
3825
  return callEditorAction(project_root, args);
3749
3826
  });
3750
3827
  // Tool: update_section_prop
3751
- 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). 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>", ... ] }`; 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`; 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' +
3828
+ 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' +
3829
+ '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' +
3752
3830
  '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' +
3753
3831
  '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): ' +
3754
3832
  `{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
@@ -3769,12 +3847,21 @@ server.tool("update_section_prop", 'Change a single prop value of a section plac
3769
3847
  .describe("Blueprint prop name to update (alternative to `prop_id`)."),
3770
3848
  value: z
3771
3849
  .any()
3772
- .describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing value from `get_section_values` for non-scalar prop types.'),
3850
+ .optional()
3851
+ .describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing value from `get_section_values` for non-scalar prop types. Omit when using `bind_global`.'),
3852
+ bind_global: z
3853
+ .string()
3854
+ .optional()
3855
+ .describe("Bind this prop to a project GLOBAL variable (Theme Setting) as a LIVE dynamic value instead of writing a static `value` — the prop then always reflects that global's current value (edited in Theme Settings). Pass the global's `name` (its `variableName`, the `_<id>` form) from `list_theme_globals`. Provide `value` OR `bind_global`, not both. Bindable globals include TEXT / RICH_TEXT / NUMBER / BOOLEAN / IMAGE / COLOR / BORDER / SHADOW settings; pick a global whose type matches the prop. (This is the general theme-global binding; a COLOR prop can alternatively take an inline `{ \"patternValueId\" }` for theme colors.)"),
3856
+ list_path: z
3857
+ .array(z.object({ propId: z.string(), index: z.number() }))
3858
+ .optional()
3859
+ .describe("Only with `bind_global`: bind a prop of a COMPONENT_LIST child instead of a section-level prop. An array of `{ propId, index }` steps from the section down to the target child; `prop_id` is then the CHILD's prop id (from `get_component_props`)."),
3773
3860
  port: z
3774
3861
  .number()
3775
3862
  .optional()
3776
3863
  .describe("Dev server WebSocket port (default 5201)."),
3777
- }, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
3864
+ }, async ({ project_root, page_id, element_id, prop_id, prop_name, value, bind_global, list_path, port, }) => {
3778
3865
  const args = [
3779
3866
  "update-section-prop",
3780
3867
  "--page-id",
@@ -3783,17 +3870,21 @@ server.tool("update_section_prop", 'Change a single prop value of a section plac
3783
3870
  element_id,
3784
3871
  ...(prop_id ? ["--prop-id", prop_id] : []),
3785
3872
  ...(prop_name ? ["--prop-name", prop_name] : []),
3786
- "--value",
3787
- // The CLI JSON.parses --value. If `value` is already a JSON string, pass
3788
- // it through verbatim — re-stringifying it would double-encode it and the
3789
- // editor would store a string instead of the object/array.
3790
- typeof value === "string" ? value : JSON.stringify(value),
3873
+ ...(bind_global
3874
+ ? ["--bind-global", bind_global, ...(list_path ? ["--list-path", JSON.stringify(list_path)] : [])]
3875
+ : [
3876
+ "--value",
3877
+ // The CLI JSON.parses --value. If `value` is already a JSON string, pass
3878
+ // it through verbatim — re-stringifying it would double-encode it and the
3879
+ // editor would store a string instead of the object/array.
3880
+ typeof value === "string" ? value : JSON.stringify(value),
3881
+ ]),
3791
3882
  ...(port ? ["--port", String(port)] : []),
3792
3883
  ];
3793
3884
  return callEditorAction(project_root, args);
3794
3885
  });
3795
3886
  // Tool: upload_image
3796
- server.tool("upload_image", "Upload an image to the connected editor's project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. Use the returned `id` as the `id` of an IMAGE prop value passed to `update_section_prop` (e.g. `{ \"id\": \"<returned-id>\", \"altText\": \"...\", \"isVideo\": false }`). This is the way to set an image prop — `update_section_prop` does not upload, it only references an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).", {
3887
+ server.tool("upload_image", 'Upload an image to the connected editor\'s project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. This is the ONLY way to obtain a real image id — an id cannot be invented. Use the returned `id`: (1) as an IMAGE prop value for `update_section_prop` (e.g. `{ "id": "<returned-id>", "altText": "...", "isVideo": false }`), or (2) as an IMAGE theme global\'s value for `create_theme_global`/`update_theme_global` (e.g. `{ "id": "<returned-id>" }`). An id cannot be invented — an unreal one is stored as given and renders as a broken image, so always use a `upload_image` id or one already listed by `list_theme_globals`. Neither tool uploads; both only reference an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).', {
3797
3888
  project_root: z
3798
3889
  .string()
3799
3890
  .describe("Absolute path to the code-component project."),
@@ -3805,7 +3896,10 @@ server.tool("upload_image", "Upload an image to the connected editor's project a
3805
3896
  .string()
3806
3897
  .optional()
3807
3898
  .describe("Image URL to fetch and upload (alternative to `file_path`)."),
3808
- alt_text: z.string().optional().describe("Alt text to store with the image."),
3899
+ alt_text: z
3900
+ .string()
3901
+ .optional()
3902
+ .describe("Alt text to store with the image."),
3809
3903
  port: z
3810
3904
  .number()
3811
3905
  .optional()
@@ -3821,29 +3915,55 @@ server.tool("upload_image", "Upload an image to the connected editor's project a
3821
3915
  return callEditorAction(project_root, args);
3822
3916
  });
3823
3917
  // Tool: update_page_sections
3824
- 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. Prefer this over many update_section_prop(s) calls.", {
3825
- project_root: z.string().describe("Absolute path to the code-component project."),
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.", {
3919
+ project_root: z
3920
+ .string()
3921
+ .describe("Absolute path to the code-component project."),
3826
3922
  page_id: z.string().describe("Target page id (from list_editor_pages)."),
3827
3923
  sections: z
3828
3924
  .array(z.object({
3829
- element_id: z.string().describe("Placed-section elementId (from list_page_sections)."),
3925
+ element_id: z
3926
+ .string()
3927
+ .describe("Placed-section elementId (from list_page_sections)."),
3830
3928
  updates: z
3831
3929
  .array(z.object({
3832
- prop_id: z.string().optional().describe("Blueprint prop id (provide this or prop_name)."),
3833
- prop_name: z.string().optional().describe("Blueprint prop name (alternative to prop_id)."),
3834
- value: z.any().describe("Prop value, same shape update_section_prop expects for that type."),
3930
+ prop_id: z
3931
+ .string()
3932
+ .optional()
3933
+ .describe("Blueprint prop id (provide this or prop_name)."),
3934
+ prop_name: z
3935
+ .string()
3936
+ .optional()
3937
+ .describe("Blueprint prop name (alternative to prop_id)."),
3938
+ value: z
3939
+ .any()
3940
+ .optional()
3941
+ .describe("Prop value, same shape update_section_prop expects for that type. Omit when using bind_global."),
3942
+ bind_global: z
3943
+ .string()
3944
+ .optional()
3945
+ .describe("Bind this prop to a project GLOBAL variable (Theme Setting) as a live dynamic value instead of a static value. The global's `name` (variableName) from list_theme_globals. Provide value OR bind_global."),
3946
+ list_path: z
3947
+ .array(z.object({ propId: z.string(), index: z.number() }))
3948
+ .optional()
3949
+ .describe("Only with bind_global: `{ propId, index }` steps down to a COMPONENT_LIST child; prop_id is then the child's prop id."),
3835
3950
  }))
3836
3951
  .describe("Non-empty array of prop updates for this section."),
3837
3952
  }))
3838
3953
  .describe("Non-empty array of sections to fill, each with its elementId and prop updates."),
3839
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3954
+ port: z
3955
+ .number()
3956
+ .optional()
3957
+ .describe("Dev server WebSocket port (default 5201)."),
3840
3958
  }, async ({ project_root, page_id, sections, port }) => {
3841
- const normalized = (sections || []).map(s => ({
3959
+ const normalized = (sections || []).map((s) => ({
3842
3960
  elementId: s.element_id,
3843
- updates: (s.updates || []).map(u => ({
3961
+ updates: (s.updates || []).map((u) => ({
3844
3962
  ...(u.prop_id ? { propId: u.prop_id } : {}),
3845
3963
  ...(u.prop_name ? { propName: u.prop_name } : {}),
3846
- value: u.value,
3964
+ ...(u.bind_global
3965
+ ? { bindGlobal: u.bind_global, ...(u.list_path ? { listPath: u.list_path } : {}) }
3966
+ : { value: u.value }),
3847
3967
  })),
3848
3968
  }));
3849
3969
  const args = [
@@ -3858,17 +3978,28 @@ server.tool("update_page_sections", "Fill MANY placed sections of a page in a SI
3858
3978
  });
3859
3979
  // Tool: upload_images
3860
3980
  server.tool("upload_images", "Upload MANY images in one call (batch upload_image) and get their ids back in order — the host uploads them all in a single round-trip. Pass `images`, each `{ file_path?, image_url?, alt_text? }`. Returns an array of { id, fileName, altText, isVideo } in the same order; use each id in an IMAGE prop value. Prefer this over multiple upload_image calls when a section/page needs several images. Supported: png/jpg/jpeg/webp/gif, max 10MB each. Requires the editor embedded in the ikas admin panel.", {
3861
- project_root: z.string().describe("Absolute path to the code-component project."),
3981
+ project_root: z
3982
+ .string()
3983
+ .describe("Absolute path to the code-component project."),
3862
3984
  images: z
3863
3985
  .array(z.object({
3864
- file_path: z.string().optional().describe("Local image file path (provide this or image_url)."),
3865
- image_url: z.string().optional().describe("Image URL to fetch (alternative to file_path)."),
3986
+ file_path: z
3987
+ .string()
3988
+ .optional()
3989
+ .describe("Local image file path (provide this or image_url)."),
3990
+ image_url: z
3991
+ .string()
3992
+ .optional()
3993
+ .describe("Image URL to fetch (alternative to file_path)."),
3866
3994
  alt_text: z.string().optional().describe("Alt text for this image."),
3867
3995
  }))
3868
3996
  .describe("Non-empty array of images to upload."),
3869
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3997
+ port: z
3998
+ .number()
3999
+ .optional()
4000
+ .describe("Dev server WebSocket port (default 5201)."),
3870
4001
  }, async ({ project_root, images, port }) => {
3871
- const manifest = (images || []).map(im => ({
4002
+ const manifest = (images || []).map((im) => ({
3872
4003
  ...(im.file_path ? { file: im.file_path } : {}),
3873
4004
  ...(im.image_url ? { url: im.image_url } : {}),
3874
4005
  ...(im.alt_text ? { alt: im.alt_text } : {}),
@@ -3882,48 +4013,81 @@ server.tool("upload_images", "Upload MANY images in one call (batch upload_image
3882
4013
  return callEditorAction(project_root, args);
3883
4014
  });
3884
4015
  // Tool: search_products
3885
- server.tool("search_products", "Search the connected store's products to obtain a real productId (and first variantId) for a PRODUCT prop value. Returns products with productId, variantId, name, slug, variantCount, and `imageSrc` (the full-resolution CDN URL of the product's main image). To use that product image in an IMAGE prop, you CANNOT reuse a product image id directly (it is not a theme asset) — pass the `imageSrc` URL to `upload_image(image_url: <imageSrc>)` (or `upload_images`), then use the returned id in the IMAGE value. This gives real product imagery instead of external/placeholder URLs. Use the result in update_section_prop as `{ \"productId\": \"<productId>\", \"variantId\": \"<variantId>\" }`. Pass `query` for free-text search, or `product_ids` to resolve specific ids. Requires the editor embedded in the ikas admin panel (the admin panel runs the search; a standalone editor cannot).", {
4016
+ server.tool("search_products", 'Search the connected store\'s products to obtain a real productId (and first variantId) for a PRODUCT prop value. Returns products with productId, variantId, name, slug, variantCount, and `imageSrc` (the full-resolution CDN URL of the product\'s main image). To use that product image in an IMAGE prop, you CANNOT reuse a product image id directly (it is not a theme asset) — pass the `imageSrc` URL to `upload_image(image_url: <imageSrc>)` (or `upload_images`), then use the returned id in the IMAGE value. This gives real product imagery instead of external/placeholder URLs. Use the result in update_section_prop as `{ "productId": "<productId>", "variantId": "<variantId>" }`. Pass `query` for free-text search, or `product_ids` to resolve specific ids. Requires the editor embedded in the ikas admin panel (the admin panel runs the search; a standalone editor cannot).', {
3886
4017
  project_root: z
3887
4018
  .string()
3888
4019
  .describe("Absolute path to the code-component project."),
3889
- query: z.string().optional().describe("Free-text product search (name, sku, barcode)."),
4020
+ query: z
4021
+ .string()
4022
+ .optional()
4023
+ .describe("Free-text product search (name, sku, barcode)."),
3890
4024
  product_ids: z
3891
4025
  .array(z.string())
3892
4026
  .optional()
3893
4027
  .describe("Specific product ids to resolve directly (alternative to `query`)."),
3894
4028
  per_page: z.number().optional().describe("Results per page."),
3895
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4029
+ port: z
4030
+ .number()
4031
+ .optional()
4032
+ .describe("Dev server WebSocket port (default 5201)."),
3896
4033
  }, async ({ project_root, query, product_ids, per_page, port }) => {
3897
4034
  const args = [
3898
4035
  "search-products",
3899
4036
  ...(query ? ["--query", query] : []),
3900
- ...(product_ids && product_ids.length ? ["--ids", product_ids.join(",")] : []),
4037
+ ...(product_ids && product_ids.length
4038
+ ? ["--ids", product_ids.join(",")]
4039
+ : []),
3901
4040
  ...(typeof per_page === "number" ? ["--per-page", String(per_page)] : []),
3902
4041
  ...(port ? ["--port", String(port)] : []),
3903
4042
  ];
3904
4043
  return callEditorAction(project_root, args);
3905
4044
  });
3906
4045
  // Tool: list_categories
3907
- server.tool("list_categories", "List the store's product categories and their ids, to fill a CATEGORY prop value `{ \"categoryId\": \"<id>\" }`. Returns categoryId, name, slug, parentId for each. Returns all categories (no query). To link to a category from a LINK prop, also call `get_page_by_type(\"CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"CATEGORY\", \"itemId\": <categoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3908
- project_root: z.string().describe("Absolute path to the code-component project."),
3909
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4046
+ server.tool("list_categories", 'List the store\'s product categories and their ids, to fill a CATEGORY prop value `{ "categoryId": "<id>" }`. Returns categoryId, name, slug, parentId for each. Returns all categories (no query). To link to a category from a LINK prop, also call `get_page_by_type("CATEGORY")` for the page id, then build `{ "linkType": "PAGE", "pageId": <pageId>, "pageType": "CATEGORY", "itemId": <categoryId>, "id": <unique>, "label": "..." }`. Requires the editor embedded in the ikas admin panel.', {
4047
+ project_root: z
4048
+ .string()
4049
+ .describe("Absolute path to the code-component project."),
4050
+ port: z
4051
+ .number()
4052
+ .optional()
4053
+ .describe("Dev server WebSocket port (default 5201)."),
3910
4054
  }, async ({ project_root, port }) => {
3911
- const args = ["list-entities", "--kind", "category", ...(port ? ["--port", String(port)] : [])];
4055
+ const args = [
4056
+ "list-entities",
4057
+ "--kind",
4058
+ "category",
4059
+ ...(port ? ["--port", String(port)] : []),
4060
+ ];
3912
4061
  return callEditorAction(project_root, args);
3913
4062
  });
3914
4063
  // Tool: list_brands
3915
- server.tool("list_brands", "List the store's product brands and their ids, to fill a BRAND prop value `{ \"brandId\": \"<id>\" }`. Returns brandId, name, slug for each. Returns all brands (no query). To link to a brand from a LINK prop, also call `get_page_by_type(\"BRAND\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BRAND\", \"itemId\": <brandId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3916
- project_root: z.string().describe("Absolute path to the code-component project."),
3917
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4064
+ server.tool("list_brands", 'List the store\'s product brands and their ids, to fill a BRAND prop value `{ "brandId": "<id>" }`. Returns brandId, name, slug for each. Returns all brands (no query). To link to a brand from a LINK prop, also call `get_page_by_type("BRAND")` for the page id, then build `{ "linkType": "PAGE", "pageId": <pageId>, "pageType": "BRAND", "itemId": <brandId>, "id": <unique>, "label": "..." }`. Requires the editor embedded in the ikas admin panel.', {
4065
+ project_root: z
4066
+ .string()
4067
+ .describe("Absolute path to the code-component project."),
4068
+ port: z
4069
+ .number()
4070
+ .optional()
4071
+ .describe("Dev server WebSocket port (default 5201)."),
3918
4072
  }, async ({ project_root, port }) => {
3919
- const args = ["list-entities", "--kind", "brand", ...(port ? ["--port", String(port)] : [])];
4073
+ const args = [
4074
+ "list-entities",
4075
+ "--kind",
4076
+ "brand",
4077
+ ...(port ? ["--port", String(port)] : []),
4078
+ ];
3920
4079
  return callEditorAction(project_root, args);
3921
4080
  });
3922
4081
  // Tool: list_blogs
3923
- server.tool("list_blogs", "List the store's blogs and their ids, to fill a BLOG prop value `{ \"blogId\": \"<id>\" }`. Returns blogId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog from a LINK prop, also call `get_page_by_type(\"BLOG\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG\", \"itemId\": <blogId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3924
- project_root: z.string().describe("Absolute path to the code-component project."),
4082
+ server.tool("list_blogs", 'List the store\'s blogs and their ids, to fill a BLOG prop value `{ "blogId": "<id>" }`. Returns blogId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog from a LINK prop, also call `get_page_by_type("BLOG")` for the page id, then build `{ "linkType": "PAGE", "pageId": <pageId>, "pageType": "BLOG", "itemId": <blogId>, "id": <unique>, "label": "..." }`. Requires the editor embedded in the ikas admin panel.', {
4083
+ project_root: z
4084
+ .string()
4085
+ .describe("Absolute path to the code-component project."),
3925
4086
  query: z.string().optional().describe("Free-text blog search."),
3926
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4087
+ port: z
4088
+ .number()
4089
+ .optional()
4090
+ .describe("Dev server WebSocket port (default 5201)."),
3927
4091
  }, async ({ project_root, query, port }) => {
3928
4092
  const args = [
3929
4093
  "list-entities",
@@ -3935,10 +4099,15 @@ server.tool("list_blogs", "List the store's blogs and their ids, to fill a BLOG
3935
4099
  return callEditorAction(project_root, args);
3936
4100
  });
3937
4101
  // Tool: list_blog_categories
3938
- server.tool("list_blog_categories", "List the store's blog categories and their ids, to fill a BLOG_CATEGORY prop value `{ \"blogCategoryId\": \"<id>\" }`. Returns blogCategoryId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog category from a LINK prop, also call `get_page_by_type(\"BLOG_CATEGORY\")` for the page id, then build `{ \"linkType\": \"PAGE\", \"pageId\": <pageId>, \"pageType\": \"BLOG_CATEGORY\", \"itemId\": <blogCategoryId>, \"id\": <unique>, \"label\": \"...\" }`. Requires the editor embedded in the ikas admin panel.", {
3939
- project_root: z.string().describe("Absolute path to the code-component project."),
4102
+ server.tool("list_blog_categories", 'List the store\'s blog categories and their ids, to fill a BLOG_CATEGORY prop value `{ "blogCategoryId": "<id>" }`. Returns blogCategoryId, name, slug for each. Accepts an optional `query` for free-text search. To link to a blog category from a LINK prop, also call `get_page_by_type("BLOG_CATEGORY")` for the page id, then build `{ "linkType": "PAGE", "pageId": <pageId>, "pageType": "BLOG_CATEGORY", "itemId": <blogCategoryId>, "id": <unique>, "label": "..." }`. Requires the editor embedded in the ikas admin panel.', {
4103
+ project_root: z
4104
+ .string()
4105
+ .describe("Absolute path to the code-component project."),
3940
4106
  query: z.string().optional().describe("Free-text blog-category search."),
3941
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4107
+ port: z
4108
+ .number()
4109
+ .optional()
4110
+ .describe("Dev server WebSocket port (default 5201)."),
3942
4111
  }, async ({ project_root, query, port }) => {
3943
4112
  const args = [
3944
4113
  "list-entities",
@@ -3950,12 +4119,17 @@ server.tool("list_blog_categories", "List the store's blog categories and their
3950
4119
  return callEditorAction(project_root, args);
3951
4120
  });
3952
4121
  // Tool: get_page_by_type
3953
- server.tool("get_page_by_type", "Resolve a theme page's id from its pageType (CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, …). Use this to get the `pageId` needed to build a PAGE link to an entity: pair it with the entity id from list_categories/list_brands/list_blogs/list_blog_categories — `{ linkType:\"PAGE\", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }`. Returns `pageId: null` (with a note) if no page of that type exists yet — in that case call `create_page(page_type)` to add it, then use the returned pageId (do not guess a slug/EXTERNAL URL).", {
3954
- project_root: z.string().describe("Absolute path to the code-component project."),
4122
+ server.tool("get_page_by_type", 'Resolve a theme page\'s id from its pageType (CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, …). Use this to get the `pageId` needed to build a PAGE link to an entity: pair it with the entity id from list_categories/list_brands/list_blogs/list_blog_categories — `{ linkType:"PAGE", pageId:<this pageId>, pageType:<type>, itemId:<entityId>, id:<unique>, label:... }`. Returns `pageId: null` (with a note) if no page of that type exists yet — in that case call `create_page(page_type)` to add it, then use the returned pageId (do not guess a slug/EXTERNAL URL).', {
4123
+ project_root: z
4124
+ .string()
4125
+ .describe("Absolute path to the code-component project."),
3955
4126
  page_type: z
3956
4127
  .string()
3957
4128
  .describe("Page type to resolve, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX."),
3958
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4129
+ port: z
4130
+ .number()
4131
+ .optional()
4132
+ .describe("Dev server WebSocket port (default 5201)."),
3959
4133
  }, async ({ project_root, page_type, port }) => {
3960
4134
  const args = [
3961
4135
  "get-page-by-type",
@@ -3966,14 +4140,25 @@ server.tool("get_page_by_type", "Resolve a theme page's id from its pageType (CA
3966
4140
  return callEditorAction(project_root, args);
3967
4141
  });
3968
4142
  // Tool: create_page
3969
- 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.", {
3970
- project_root: z.string().describe("Absolute path to the code-component project."),
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.', {
4144
+ project_root: z
4145
+ .string()
4146
+ .describe("Absolute path to the code-component project."),
3971
4147
  page_type: z
3972
4148
  .string()
3973
4149
  .describe("Page type to create, e.g. CATEGORY, PRODUCT, BRAND, BLOG, BLOG_CATEGORY, INDEX, CUSTOM."),
3974
- name: z.string().optional().describe("Page name. Required for CUSTOM pages; ignored otherwise."),
3975
- slug: z.string().optional().describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
3976
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4150
+ name: z
4151
+ .string()
4152
+ .optional()
4153
+ .describe("Page name. Required for CUSTOM pages; ignored otherwise."),
4154
+ slug: z
4155
+ .string()
4156
+ .optional()
4157
+ .describe("Page slug. Required (and unique) for CUSTOM pages; ignored otherwise."),
4158
+ port: z
4159
+ .number()
4160
+ .optional()
4161
+ .describe("Dev server WebSocket port (default 5201)."),
3977
4162
  }, async ({ project_root, page_type, name, slug, port }) => {
3978
4163
  const args = [
3979
4164
  "create-page",
@@ -3987,7 +4172,9 @@ server.tool("create_page", "Create a theme page of a given pageType in the conne
3987
4172
  });
3988
4173
  // Tool: publish_theme
3989
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.", {
3990
- project_root: z.string().describe("Absolute path to the code-component project."),
4175
+ project_root: z
4176
+ .string()
4177
+ .describe("Absolute path to the code-component project."),
3991
4178
  confirm: z
3992
4179
  .boolean()
3993
4180
  .optional()
@@ -3996,7 +4183,10 @@ server.tool("publish_theme", "Publish the current theme LIVE — the same action
3996
4183
  .boolean()
3997
4184
  .optional()
3998
4185
  .describe("Required IN ADDITION to confirm when the target is the MAIN/production theme (live customer-facing site)."),
3999
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4186
+ port: z
4187
+ .number()
4188
+ .optional()
4189
+ .describe("Dev server WebSocket port (default 5201)."),
4000
4190
  }, async ({ project_root, confirm, confirm_production, port }) => {
4001
4191
  const args = [
4002
4192
  "publish-theme",
@@ -4029,21 +4219,21 @@ const EDITOR_WORKFLOW_GUIDE = [
4029
4219
  "- 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.",
4030
4220
  "",
4031
4221
  "## Value shapes by prop type (what to pass as `value`)",
4032
- "- Scalars are WRAPPED: TEXT/RICH_TEXT { \"value\": \"...\" }, BOOLEAN { \"value\": true }, NUMBER { \"value\": 12 }, COLOR { \"value\": \"#FF0000\" }, ENUM { \"value\": \"<key>\" }, DATE { \"value\": ... }.",
4033
- "- SVG is value-wrapped: { \"value\": \"<svg markup>\" }. NUMBER_RANGE: { \"value\": <number>, \"unit\": \"px\"|null }.",
4034
- "- SVG naming: the editor shows each svg's `class` attribute as its display name in the list; add class=\"my-icon\" to name it. SVG_LIST items with no class render as svg-1, svg-2, ….",
4035
- "- Object props are NOT wrapped (no { \"value\": ... }):",
4036
- " - IMAGE: { \"id\": \"<asset-id>\", \"altText\"?: \"...\", \"isVideo\"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { \"images\": [ <image>, ... ] }.",
4037
- " - VIDEO: { \"video\": { \"id\": \"<asset-id>\" }, \"thumbnailImage\"?: { \"id\": \"...\" }, \"autoplay\"?: false, \"controls\"?: true, \"loop\"?: false, \"muted\"?: false } — NOT a bare { \"id\": ... }. SVG_LIST: { \"svgs\": [ \"<svg>\", ... ] }.",
4038
- " - LINK: { \"id\": \"<unique>\", \"linkType\": \"PAGE\"|\"EXTERNAL\"|\"FILE\", \"label\": \"...\", \"subLinks\": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, 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, linkType:'PAGE', label, pageId:<pageId>, pageType:'<TYPE>', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { \"links\": [ <link>, ... ] }.",
4039
- " - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { \"<entity>Id\": \"...\", \"usePageData\"?: false } — e.g. { \"productId\": \"...\", \"variantId\"?: \"...\" }, { \"categoryId\": \"...\" }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)",
4222
+ '- Scalars are WRAPPED: TEXT/RICH_TEXT { "value": "..." }, BOOLEAN { "value": true }, NUMBER { "value": 12 }, COLOR { "value": "#FF0000" }, ENUM { "value": "<key>" }, DATE { "value": ... }.',
4223
+ '- SVG is value-wrapped: { "value": "<svg markup>" }. NUMBER_RANGE: { "value": <number>, "unit": "px"|null }.',
4224
+ '- SVG naming: the editor shows each svg\'s `class` attribute as its display name in the list; add class="my-icon" to name it. SVG_LIST items with no class render as svg-1, svg-2, ….',
4225
+ '- Object props are NOT wrapped (no { "value": ... }):',
4226
+ ' - IMAGE: { "id": "<asset-id>", "altText"?: "...", "isVideo"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { "images": [ <image>, ... ] }.',
4227
+ ' - VIDEO: { "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false } — NOT a bare { "id": ... }. SVG_LIST: { "svgs": [ "<svg>", ... ] }.',
4228
+ ' - LINK: { "id": "<unique>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "subLinks": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, 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, linkType:\'PAGE\', label, pageId:<pageId>, pageType:\'<TYPE>\', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { "links": [ <link>, ... ] }.',
4229
+ ' - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { "<entity>Id": "...", "usePageData"?: false } — e.g. { "productId": "...", "variantId"?: "..." }, { "categoryId": "..." }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)',
4040
4230
  " - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): typed objects keyed by a <entity>ListType discriminator plus initialSort/initialLimit. The id field DEPENDS on the type — for PRODUCT_LIST: STATIC → productIds:[{productId,variantId}] (from search_products); CATEGORY → category:'<categoryId>' (from list_categories) — the field is `category`, NOT `categoryId` (categoryId is ONLY the standalone CATEGORY entity prop above); ALL/DISCOUNTED/RECOMMENDED/etc → no id field. Get the exact field set + writeNote from get_component_props, or read+edit an existing value via get_section_values. Unknown sub-fields (e.g. categoryId inside a PRODUCT_LIST) are REJECTED.",
4041
- " - COMPONENT_LIST: { \"components\": [ { \"id\": \"<unique>\", \"codeComponentId\": \"<child-id>\", \"propValues\": { \"<childProp>\": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.",
4231
+ ' - COMPONENT_LIST: { "components": [ { "id": "<unique>", "codeComponentId": "<child-id>", "propValues": { "<childProp>": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.',
4042
4232
  "",
4043
4233
  "## COMPONENT_LIST rules (the part most often gotten wrong)",
4044
4234
  "- READ-MODIFY-WRITE: update_section_prop REPLACES the whole prop value. To add/edit/remove one child, take the CURRENT { components: [...] } from get_section_values, edit it, and resend the ENTIRE array (with all existing entries and their ids). Sending only your new entry wipes the rest.",
4045
4235
  "- Each entry needs a UNIQUE id (a short string you generate; it does not need to match anything). Do not reuse another entry's id.",
4046
- "- Reference a code component with \"codeComponentId\" and a built-in theme component with \"componentId\" — not the other way around. Use \"propValues\" (not \"props\").",
4236
+ '- Reference a code component with "codeComponentId" and a built-in theme component with "componentId" — not the other way around. Use "propValues" (not "props").',
4047
4237
  "- Child ids are opaque — take them from get_section_template / list_imported_sections; never invent them.",
4048
4238
  "- Auto-import: a referenced child that is BUILT but not yet imported is imported automatically (returned under autoImported). A child that is not built at all errors — build it first.",
4049
4239
  "",
@@ -4057,39 +4247,133 @@ const EDITOR_WORKFLOW_GUIDE = [
4057
4247
  "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.",
4058
4248
  ].join("\n");
4059
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 () => {
4060
- return { content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }] };
4250
+ return {
4251
+ content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }],
4252
+ };
4061
4253
  });
4062
4254
  // Tool: list_theme_globals
4063
- 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. (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.", {
4064
- project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
4065
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
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.", {
4256
+ project_root: z
4257
+ .string()
4258
+ .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
4259
+ port: z
4260
+ .number()
4261
+ .optional()
4262
+ .describe("Dev server WebSocket port (default 5201)."),
4066
4263
  }, async ({ project_root, port }) => {
4067
- const args = ["list-theme-globals", ...(port ? ["--port", String(port)] : [])];
4264
+ const args = [
4265
+ "list-theme-globals",
4266
+ ...(port ? ["--port", String(port)] : []),
4267
+ ];
4068
4268
  return callEditorAction(project_root, args);
4069
4269
  });
4270
+ // Shared text-style (typography) font-property schema — kept in lock-step with the editor's
4271
+ // "Update Text Style" modal fields (no color/text-align, since that modal doesn't expose them).
4272
+ // Each enum prop documents its accepted values so a bad value is caught with a clear error. Spread
4273
+ // into both create_theme_global (typography) and update_text_style so the two never drift apart.
4274
+ const textStyleFontParams = {
4275
+ font_family: z
4276
+ .string()
4277
+ .optional()
4278
+ .describe('A real font family the editor offers — a Google Fonts name with EXACT casing (e.g. "Inter", "Roboto", ' +
4279
+ '"Poppins"), or a font already used by an existing text style in list_theme_globals. An unknown font ' +
4280
+ "(or wrong casing) is rejected; do NOT invent a family name."),
4281
+ font_size: z.string().optional(),
4282
+ font_weight: z
4283
+ .string()
4284
+ .optional()
4285
+ .describe('font-weight as a number (e.g. "600"); append \'i\' for italic (e.g. "600i"). A PLAIN number with no \'i\' ' +
4286
+ 'is upright and REMOVES italic if the text style was italic (italic is carried by the weight — re-send the ' +
4287
+ 'weight without \'i\' to turn it off). Must be a weight the chosen ' +
4288
+ 'font ships — if it isn\'t, the error lists the supported weights; when unsure use "400".'),
4289
+ line_height: z.string().optional(),
4290
+ letter_spacing: z.string().optional(),
4291
+ text_decoration: z
4292
+ .string()
4293
+ .optional()
4294
+ .describe("text-decoration: none | underline | line-through"),
4295
+ text_transform: z
4296
+ .string()
4297
+ .optional()
4298
+ .describe("text-transform: none | uppercase | lowercase | capitalize"),
4299
+ text_overflow: z
4300
+ .string()
4301
+ .optional()
4302
+ .describe("text-overflow: clip | ellipsis"),
4303
+ white_space: z
4304
+ .string()
4305
+ .optional()
4306
+ .describe("white-space: normal | pre | pre-wrap | pre-line | wrap | nowrap | collapse"),
4307
+ line_break: z
4308
+ .string()
4309
+ .optional()
4310
+ .describe("line-break: auto | loose | normal | strict | anywhere"),
4311
+ word_break: z
4312
+ .string()
4313
+ .optional()
4314
+ .describe("word-break: normal | break-all | keep-all"),
4315
+ text_wrap_style: z
4316
+ .string()
4317
+ .optional()
4318
+ .describe("text-wrap-style: auto | balance | pretty | stable"),
4319
+ };
4320
+ // Turn the supplied text-style font params into `ikas-component` CLI flags (omitted ones dropped).
4321
+ // The editor validates each value and rejects an unsupported one with the offending value + allowed set.
4322
+ function textStyleFontArgs(input) {
4323
+ const map = {
4324
+ "--font-family": input.font_family,
4325
+ "--font-size": input.font_size,
4326
+ "--font-weight": input.font_weight,
4327
+ "--line-height": input.line_height,
4328
+ "--letter-spacing": input.letter_spacing,
4329
+ "--text-decoration": input.text_decoration,
4330
+ "--text-transform": input.text_transform,
4331
+ "--text-overflow": input.text_overflow,
4332
+ "--white-space": input.white_space,
4333
+ "--line-break": input.line_break,
4334
+ "--word-break": input.word_break,
4335
+ "--text-wrap-style": input.text_wrap_style,
4336
+ };
4337
+ return Object.entries(map).flatMap(([flag, value]) => value ? [flag, value] : []);
4338
+ }
4070
4339
  // Tool: create_theme_global
4071
4340
  server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
4072
- "- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Pass `value` as a REAL JSON value, never a stringified JSON. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: real JSON boolean true/false (NOT the string \"true\"); IMAGE: an image REFERENCE object { id: \"theme-images/<uuid>\" } (NOT { url }) — get a real id from list_theme_globals; do NOT invent/fabricate an id, omit value instead if you don't have one; BORDER: { width: { value, unit }, style, color }; SHADOW: { x, y, blur, spread, color, position: \"outside\"|\"inside\" }. For object types (IMAGE/BORDER/SHADOW) pass an actual JSON object, not a JSON-encoded string.\n" +
4073
- "- color: requires name + value — a CONCRETE color (hex e.g. \"#ff0000\", rgb()/hsl() or a CSS color name). A color token cannot alias another color, so a \"var(...)\" reference is REJECTED here (to reuse another theme color, reference its cssVar in component code, or in a colorScheme slot). TIP: a \"/\" in the `name` GROUPS the color in the editor UI (e.g. \"Brand/Primary\", \"Brand/Accent\" appear under a \"Brand\" group); use this to organize related colors. To CHANGE an existing color later, use `update_theme_color` (edits in place, keeps the cssVar stable) — do NOT delete+recreate.\n" +
4074
- "- typography: requires name + at least one of font_family/font_size/font_weight/line_height/letter_spacing. VALUE FORMATS (validated): font_weight is one of 100,200,300,400,500,600,700,800,900 (append 'i' for italic, e.g. \"600i\") AND must be a weight the chosen font actually ships (many fonts provide only a subset, e.g. \"Modak\" only ships 400) — if unavailable the call fails and lists the supported weights, so when unsure use 400; stored as a numeric weight + a font-style entry; font_size is a number WITH a unit (e.g. \"16px\", \"1.2rem\", \"80%\"); line_height is \"normal\", a unitless number (e.g. \"1.5\") or a length; letter_spacing is \"normal\" or a length (e.g. \"0.02em\", \"0.5px\"); font_family is the font name. TIP: a \"/\" in the `name` GROUPS the text style in the editor UI (e.g. \"Heading/H1\", \"Heading/H2\" appear under a \"Heading\" group); use this to organize related styles. To CHANGE an existing text style later, use `update_text_style` (edits in place, keeps the `className` stable) — do NOT delete+recreate, which changes the id/className.\n" +
4341
+ '- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Pass `value` as a REAL JSON value, never a stringified JSON. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: real JSON boolean true/false (NOT the string "true"); IMAGE: an image REFERENCE object { id: "theme-images/<uuid>" } (NOT { url }) — the id MUST come from a REAL upload: call `upload_image` (file_path or image_url) and use the id it returns, or reuse an id already listed by `list_theme_globals`. NEVER invent an id — it is stored as given and renders as a broken image, so if you have no image, omit `value` and let the merchant set it from the "Theme Settings" panel; BORDER: { width: { value, unit }, style, color } (width.value must be a number); SHADOW: { x, y, blur, spread, color, position: "outside"|"inside" }. For object types (IMAGE/BORDER/SHADOW) pass an actual JSON object, not a JSON-encoded string. Every value is validated server-side and a wrong shape is REJECTED, not silently stored.\n' +
4342
+ '- color: requires name + value — a CONCRETE color (hex e.g. "#ff0000", rgb()/hsl() or a CSS color name). A color token cannot alias another color, so a "var(...)" reference is REJECTED here (to reuse another theme color, reference its cssVar in component code, or in a colorScheme slot). TIP: a "/" in the `name` GROUPS the color in the editor UI (e.g. "Brand/Primary", "Brand/Accent" appear under a "Brand" group); use this to organize related colors. To CHANGE an existing color later, use `update_theme_color` (edits in place, keeps the cssVar stable) — do NOT delete+recreate.\n' +
4343
+ '- typography: requires name + at least one text-style property. These are exactly the fields the editor\'s "Update Text Style" modal exposes — nothing else (in particular there is NO color or text-align on a text style). VALUE FORMATS (all validated; a bad value is rejected with the offending value and the allowed set): font_weight is one of 100,200,300,400,500,600,700,800,900 (append \'i\' for italic, e.g. "600i") AND must be a weight the chosen font actually ships (many fonts provide only a subset, e.g. "Modak" only ships 400) — if unavailable the call fails and lists the supported weights, so when unsure use 400; stored as a numeric weight + a font-style entry; font_size is a number WITH a unit (e.g. "16px", "1.2rem", "80%"); line_height is "normal", a unitless number (e.g. "1.5") or a length; letter_spacing is "normal" or a length (e.g. "0.02em", "0.5px"); font_family is a real font the editor offers (a Google Fonts name with EXACT casing, e.g. "Inter") — an unknown/invented family is rejected; text_decoration is none|underline|line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. TIP: a "/" in the `name` GROUPS the text style in the editor UI (e.g. "Heading/H1", "Heading/H2" appear under a "Heading" group); use this to organize related styles. To CHANGE an existing text style later, use `update_text_style` (edits in place, keeps the `className` stable) — do NOT delete+recreate, which changes the id/className.\n' +
4075
4344
  "- 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" +
4076
4345
  "- 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" +
4077
- "- 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" +
4346
+ '- 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' +
4078
4347
  "Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
4079
- project_root: z.string().describe("Absolute path to the code-component project."),
4348
+ project_root: z
4349
+ .string()
4350
+ .describe("Absolute path to the code-component project."),
4080
4351
  kind: z
4081
- .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
4352
+ .enum([
4353
+ "globalVariable",
4354
+ "color",
4355
+ "typography",
4356
+ "breakpoint",
4357
+ "keyframe",
4358
+ "colorScheme",
4359
+ ])
4082
4360
  .describe("What to create."),
4083
4361
  name: z.string().optional().describe("Name (design tokens)."),
4084
- display_name: z.string().optional().describe("Human label (globalVariable)."),
4362
+ display_name: z
4363
+ .string()
4364
+ .optional()
4365
+ .describe("Human label (globalVariable)."),
4085
4366
  type: z.string().optional().describe("globalVariable value type."),
4086
- value: z.any().optional().describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
4087
- font_family: z.string().optional(),
4088
- font_size: z.string().optional(),
4089
- font_weight: z.string().optional(),
4090
- line_height: z.string().optional(),
4091
- letter_spacing: z.string().optional(),
4092
- width: z.number().int().optional().describe("breakpoint width in px (positive integer, no decimals)."),
4367
+ value: z
4368
+ .any()
4369
+ .optional()
4370
+ .describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
4371
+ ...textStyleFontParams,
4372
+ width: z
4373
+ .number()
4374
+ .int()
4375
+ .optional()
4376
+ .describe("breakpoint width in px (positive integer, no decimals)."),
4093
4377
  keyframe_type: z.enum(["keyframe", "transition"]).optional(),
4094
4378
  points: z.any().optional().describe("keyframe points array."),
4095
4379
  settings: z
@@ -4099,8 +4383,11 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4099
4383
  colors: z
4100
4384
  .any()
4101
4385
  .optional()
4102
- .describe("colorScheme colors: array of { slotId? | newSlotName?, value } — target an EXISTING slot by `slotId` (its id from list_theme_globals → colorSchemes.schemes / colorsByScheme keys) OR create/reuse one by `newSlotName` (use the standard slot names, \"/\" to group, e.g. \"PrimaryButton/Background\"). Exactly one per entry. value = a literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" to store a linked reference."),
4103
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4386
+ .describe('colorScheme colors: array of { slotId? | newSlotName?, value } — target an EXISTING slot by `slotId` (its id from list_theme_globals → colorSchemes.schemes / colorsByScheme keys) OR create/reuse one by `newSlotName` (use the standard slot names, "/" to group, e.g. "PrimaryButton/Background"). Exactly one per entry. value = a literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" to store a linked reference.'),
4387
+ port: z
4388
+ .number()
4389
+ .optional()
4390
+ .describe("Dev server WebSocket port (default 5201)."),
4104
4391
  }, async (input) => {
4105
4392
  const p = input.port ? ["--port", String(input.port)] : [];
4106
4393
  let args;
@@ -4112,28 +4399,40 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4112
4399
  String(input.display_name ?? ""),
4113
4400
  "--type",
4114
4401
  String(input.type ?? ""),
4115
- ...(input.value !== undefined ? ["--value", JSON.stringify(input.value)] : []),
4402
+ ...(input.value !== undefined
4403
+ ? ["--value", JSON.stringify(input.value)]
4404
+ : []),
4116
4405
  ...p,
4117
4406
  ];
4118
4407
  break;
4119
4408
  case "color":
4120
- args = ["create-color", "--name", String(input.name ?? ""), "--value", String(input.value ?? ""), ...p];
4409
+ args = [
4410
+ "create-color",
4411
+ "--name",
4412
+ String(input.name ?? ""),
4413
+ "--value",
4414
+ String(input.value ?? ""),
4415
+ ...p,
4416
+ ];
4121
4417
  break;
4122
4418
  case "typography":
4123
4419
  args = [
4124
4420
  "create-text-style",
4125
4421
  "--name",
4126
4422
  String(input.name ?? ""),
4127
- ...(input.font_family ? ["--font-family", input.font_family] : []),
4128
- ...(input.font_size ? ["--font-size", input.font_size] : []),
4129
- ...(input.font_weight ? ["--font-weight", input.font_weight] : []),
4130
- ...(input.line_height ? ["--line-height", input.line_height] : []),
4131
- ...(input.letter_spacing ? ["--letter-spacing", input.letter_spacing] : []),
4423
+ ...textStyleFontArgs(input),
4132
4424
  ...p,
4133
4425
  ];
4134
4426
  break;
4135
4427
  case "breakpoint":
4136
- args = ["create-breakpoint", "--name", String(input.name ?? ""), "--width", String(input.width ?? ""), ...p];
4428
+ args = [
4429
+ "create-breakpoint",
4430
+ "--name",
4431
+ String(input.name ?? ""),
4432
+ "--width",
4433
+ String(input.width ?? ""),
4434
+ ...p,
4435
+ ];
4137
4436
  break;
4138
4437
  case "keyframe":
4139
4438
  args = [
@@ -4143,7 +4442,9 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4143
4442
  ...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
4144
4443
  "--points",
4145
4444
  toJsonArrayArg(input.points),
4146
- ...(input.settings !== undefined ? ["--settings", toJsonArrayArg(input.settings)] : []),
4445
+ ...(input.settings !== undefined
4446
+ ? ["--settings", toJsonArrayArg(input.settings)]
4447
+ : []),
4147
4448
  ...p,
4148
4449
  ];
4149
4450
  break;
@@ -4158,18 +4459,38 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4158
4459
  ];
4159
4460
  break;
4160
4461
  default:
4161
- return { content: [{ type: "text", text: `Error: unknown kind "${input.kind}"` }] };
4462
+ return {
4463
+ content: [
4464
+ {
4465
+ type: "text",
4466
+ text: `Error: unknown kind "${input.kind}"`,
4467
+ },
4468
+ ],
4469
+ };
4162
4470
  }
4163
4471
  return callEditorAction(input.project_root, args);
4164
4472
  });
4165
4473
  // Tool: update_theme_global
4166
- server.tool("update_theme_global", "Update an existing global variable in the connected editor (e.g. fix its value or change its type). Identify it by `name` (the runtime key from `list_theme_globals`). Only the fields you pass are changed. Value shapes follow `create_theme_global` — pass `value` as REAL JSON (objects/booleans, never stringified); IMAGE is an image REFERENCE object { id: \"theme-images/<uuid>\" } (NOT { url }), BOOLEAN is a real boolean, BORDER/SHADOW are real objects.", {
4167
- project_root: z.string().describe("Absolute path to the code-component project."),
4168
- name: z.string().describe("The variable's runtime key (from `list_theme_globals`)."),
4474
+ server.tool("update_theme_global", 'Update an existing global variable in the connected editor (e.g. fix its value or change its type). Identify it by `name` (the runtime key from `list_theme_globals`). Only the fields you pass are changed. Value shapes follow `create_theme_global` — pass `value` as REAL JSON (objects/booleans, never stringified); BOOLEAN is a real boolean, BORDER/SHADOW are real objects; IMAGE is an image REFERENCE object { id: "theme-images/<uuid>" } whose id MUST come from `upload_image` or an existing `list_theme_globals` entry — an invented id renders as a broken image. Values are validated server-side; a wrong shape is REJECTED, not silently stored.', {
4475
+ project_root: z
4476
+ .string()
4477
+ .describe("Absolute path to the code-component project."),
4478
+ name: z
4479
+ .string()
4480
+ .describe("The variable's runtime key (from `list_theme_globals`)."),
4169
4481
  display_name: z.string().optional().describe("New label."),
4170
- type: z.string().optional().describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
4171
- value: z.any().optional().describe('New value as REAL JSON (shape depends on type; e.g. IMAGE: { id: "theme-images/<uuid>" }, BOOLEAN: true/false — see create_theme_global).'),
4172
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4482
+ type: z
4483
+ .string()
4484
+ .optional()
4485
+ .describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
4486
+ value: z
4487
+ .any()
4488
+ .optional()
4489
+ .describe('New value as REAL JSON (shape depends on type; e.g. BOOLEAN: true/false, NUMBER: 12, IMAGE: { id: "theme-images/<uuid>" } from upload_image — see create_theme_global). An invented IMAGE id renders as a broken image.'),
4490
+ port: z
4491
+ .number()
4492
+ .optional()
4493
+ .describe("Dev server WebSocket port (default 5201)."),
4173
4494
  }, async ({ project_root, name, display_name, type, value, port }) => {
4174
4495
  const args = [
4175
4496
  "update-global-variable",
@@ -4189,26 +4510,38 @@ server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE
4189
4510
  "`list_theme_globals`); name-based lookup is intentionally NOT supported here because names can collide across " +
4190
4511
  "design assets. To rename, pass `name` (the new name) together with `id`. " +
4191
4512
  "`points` REPLACES the keyframe's points: an array of { point, styles? } where `point` is a CSS " +
4192
- "keyframe offset (e.g. \"0%\", \"50%\", \"100%\") and each style is { property, value } using a CSS property " +
4513
+ 'keyframe offset (e.g. "0%", "50%", "100%") and each style is { property, value } using a CSS property ' +
4193
4514
  "name (opacity, transform, filter, background, color, …). Pass `points` as REAL JSON (not a stringified array). " +
4194
4515
  "For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% " +
4195
4516
  "point is ignored (the start state is the element's own normal state). " +
4196
4517
  "Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
4197
4518
  "see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
4198
4519
  "Requires `ikas-component dev` running with the editor connected.", {
4199
- project_root: z.string().describe("Absolute path to the code-component project."),
4200
- id: z.string().describe("Keyframe id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4520
+ project_root: z
4521
+ .string()
4522
+ .describe("Absolute path to the code-component project."),
4523
+ id: z
4524
+ .string()
4525
+ .describe("Keyframe id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4201
4526
  name: z
4202
4527
  .string()
4203
4528
  .optional()
4204
4529
  .describe("Optional NEW name to rename the keyframe to. NOT used to find the token — lookup is always by id."),
4205
- keyframe_type: z.enum(["keyframe", "transition"]).optional().describe("Optional new keyframe type."),
4206
- points: z.any().describe("New keyframe points array of { point, styles? } — REPLACES the existing points."),
4530
+ keyframe_type: z
4531
+ .enum(["keyframe", "transition"])
4532
+ .optional()
4533
+ .describe("Optional new keyframe type."),
4534
+ points: z
4535
+ .any()
4536
+ .describe("New keyframe points array of { point, styles? } — REPLACES the existing points."),
4207
4537
  settings: z
4208
4538
  .any()
4209
4539
  .optional()
4210
4540
  .describe("New animation settings { property, value } array (type-dependent — see create_theme_global). Omit to keep existing; [] to clear."),
4211
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4541
+ port: z
4542
+ .number()
4543
+ .optional()
4544
+ .describe("Dev server WebSocket port (default 5201)."),
4212
4545
  }, async ({ project_root, id, name, keyframe_type, points, settings, port }) => {
4213
4546
  const args = [
4214
4547
  "update-keyframe",
@@ -4218,7 +4551,9 @@ server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE
4218
4551
  ...(keyframe_type ? ["--type", keyframe_type] : []),
4219
4552
  "--points",
4220
4553
  toJsonArrayArg(points),
4221
- ...(settings !== undefined ? ["--settings", toJsonArrayArg(settings)] : []),
4554
+ ...(settings !== undefined
4555
+ ? ["--settings", toJsonArrayArg(settings)]
4556
+ : []),
4222
4557
  ...(port ? ["--port", String(port)] : []),
4223
4558
  ];
4224
4559
  return callEditorAction(project_root, args);
@@ -4228,41 +4563,52 @@ server.tool("update_text_style", "Update an existing theme typography token (tex
4228
4563
  "delete+recreate: it keeps the text style's id, so its `className` (`_<id>`) — and every section bound to it — " +
4229
4564
  "stays STABLE. Identify the text style by `id` (REQUIRED — globally unique, from `list_theme_globals`); " +
4230
4565
  "name-based lookup is intentionally NOT supported here because names can collide across design assets. " +
4231
- "To rename, pass `name` (the new name) together with `id`. Pass only the " +
4232
- "font properties you want to change (font_family, font_size, font_weight, line_height, letter_spacing); omitted " +
4233
- "ones keep their existing values. VALUE FORMATS (validated): font_weight is one " +
4234
- "of 100…900 (append 'i' for italic, e.g. \"600i\") AND must be a weight the chosen font actually ships — many fonts " +
4235
- "only provide a subset (e.g. \"Modak\" only ships 400); if a weight isn't available the call fails and the error " +
4236
- "lists the supported weights, so when unsure use 400; font_size is a number with a unit (e.g. \"16px\"); line_height " +
4237
- "is \"normal\", a unitless number or a length; letter_spacing is \"normal\" or a length (e.g. \"0.02em\"). Requires " +
4566
+ "To rename, pass `name` (the new name) together with `id`. Pass only the properties you want to change; omitted " +
4567
+ 'ones keep their existing values. The settable properties are exactly the editor\'s "Update Text Style" modal ' +
4568
+ "fields — there is NO color or text-align on a text style. VALUE FORMATS (all validated; a bad value is rejected " +
4569
+ "with the offending value and the allowed set): font_weight is one of 100…900 (append 'i' for italic, e.g. " +
4570
+ '"600i") AND must be a weight the chosen font actually ships — many fonts only provide a subset (e.g. "Modak" ' +
4571
+ "only ships 400); if a weight isn't available the call fails and the error lists the supported weights, so when " +
4572
+ 'unsure use 400; font_size is a number with a unit (e.g. "16px"); line_height is "normal", a unitless number ' +
4573
+ 'or a length; letter_spacing is "normal" or a length (e.g. "0.02em"); font_family is a real font the editor ' +
4574
+ 'offers (a Google Fonts name with EXACT casing, e.g. "Inter") — an unknown/invented family is rejected; ' +
4575
+ "text_decoration is none|underline|" +
4576
+ "line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space " +
4577
+ "is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break " +
4578
+ "is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. Set `is_default: true` to make " +
4579
+ "this text style the theme's default/global style (the editor's right-click \"Make Default Style\" / \"Global " +
4580
+ "Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
4581
+ "current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
4238
4582
  "`ikas-component dev` running with the editor connected.", {
4239
- project_root: z.string().describe("Absolute path to the code-component project."),
4240
- id: z.string().describe("Text style id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4583
+ project_root: z
4584
+ .string()
4585
+ .describe("Absolute path to the code-component project."),
4586
+ id: z
4587
+ .string()
4588
+ .describe("Text style id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4241
4589
  name: z
4242
4590
  .string()
4243
4591
  .optional()
4244
4592
  .describe("Optional NEW name to rename the text style to. NOT used to find the token — lookup is always by id."),
4245
- font_family: z.string().optional(),
4246
- font_size: z.string().optional(),
4247
- font_weight: z
4248
- .string()
4593
+ ...textStyleFontParams,
4594
+ is_default: z
4595
+ .boolean()
4249
4596
  .optional()
4250
- .describe("font-weight as a number (e.g. \"600\"); append 'i' for italic (e.g. \"600i\"). Must be a weight the chosen " +
4251
- "font ships — if it isn't, the error lists the supported weights; when unsure use \"400\"."),
4252
- line_height: z.string().optional(),
4253
- letter_spacing: z.string().optional(),
4254
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4255
- }, async ({ project_root, id, name, font_family, font_size, font_weight, line_height, letter_spacing, port }) => {
4597
+ .describe("true → make this the theme's default/global text style (clears any other default); false → remove the " +
4598
+ "default flag. Omit to leave the default state unchanged."),
4599
+ port: z
4600
+ .number()
4601
+ .optional()
4602
+ .describe("Dev server WebSocket port (default 5201)."),
4603
+ }, async (input) => {
4604
+ const { project_root, id, name, is_default, port } = input;
4256
4605
  const args = [
4257
4606
  "update-text-style",
4258
4607
  "--id",
4259
4608
  id,
4260
4609
  ...(name !== undefined ? ["--name", name] : []),
4261
- ...(font_family ? ["--font-family", font_family] : []),
4262
- ...(font_size ? ["--font-size", font_size] : []),
4263
- ...(font_weight ? ["--font-weight", font_weight] : []),
4264
- ...(line_height ? ["--line-height", line_height] : []),
4265
- ...(letter_spacing ? ["--letter-spacing", letter_spacing] : []),
4610
+ ...textStyleFontArgs(input),
4611
+ ...(is_default === true ? ["--default"] : is_default === false ? ["--no-default"] : []),
4266
4612
  ...(port ? ["--port", String(port)] : []),
4267
4613
  ];
4268
4614
  return callEditorAction(project_root, args);
@@ -4273,11 +4619,15 @@ server.tool("update_theme_color", "Update an existing theme color token IN PLACE
4273
4619
  "Identify it by `id` (REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally " +
4274
4620
  "NOT supported here because a theme can have two colors with the same name (e.g. from two different design assets). " +
4275
4621
  "To rename, pass `name` (the new name) together with `id`. Pass `value` to change the color — a CONCRETE " +
4276
- "color (hex/rgb/hsl or a CSS color name), NOT a \"var(...)\" reference (a color token can't alias another " +
4622
+ 'color (hex/rgb/hsl or a CSS color name), NOT a "var(...)" reference (a color token can\'t alias another ' +
4277
4623
  "color); omit it to only rename. Requires `ikas-component " +
4278
4624
  "dev` running with the editor connected.", {
4279
- project_root: z.string().describe("Absolute path to the code-component project."),
4280
- id: z.string().describe("Color id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4625
+ project_root: z
4626
+ .string()
4627
+ .describe("Absolute path to the code-component project."),
4628
+ id: z
4629
+ .string()
4630
+ .describe("Color id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4281
4631
  name: z
4282
4632
  .string()
4283
4633
  .optional()
@@ -4286,7 +4636,10 @@ server.tool("update_theme_color", "Update an existing theme color token IN PLACE
4286
4636
  .string()
4287
4637
  .optional()
4288
4638
  .describe('New color value — a concrete color (e.g. "#ff0000", rgb/hsl/name), not a "var(...)" reference. Omit to only rename.'),
4289
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4639
+ port: z
4640
+ .number()
4641
+ .optional()
4642
+ .describe("Dev server WebSocket port (default 5201)."),
4290
4643
  }, async ({ project_root, id, name, value, port }) => {
4291
4644
  const args = [
4292
4645
  "update-color",
@@ -4304,14 +4657,25 @@ server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN P
4304
4657
  "because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
4305
4658
  "Pass `width` (px) to change it; omit it to only rename. " +
4306
4659
  "Requires `ikas-component dev` running with the editor connected.", {
4307
- project_root: z.string().describe("Absolute path to the code-component project."),
4308
- id: z.string().describe("Breakpoint id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4660
+ project_root: z
4661
+ .string()
4662
+ .describe("Absolute path to the code-component project."),
4663
+ id: z
4664
+ .string()
4665
+ .describe("Breakpoint id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4309
4666
  name: z
4310
4667
  .string()
4311
4668
  .optional()
4312
4669
  .describe("Optional NEW name to rename the breakpoint to. NOT used to find the token — lookup is always by id."),
4313
- width: z.number().int().optional().describe("New breakpoint width in px (positive integer, no decimals). Omit to only rename."),
4314
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4670
+ width: z
4671
+ .number()
4672
+ .int()
4673
+ .optional()
4674
+ .describe("New breakpoint width in px (positive integer, no decimals). Omit to only rename."),
4675
+ port: z
4676
+ .number()
4677
+ .optional()
4678
+ .describe("Dev server WebSocket port (default 5201)."),
4315
4679
  }, async ({ project_root, id, name, width, port }) => {
4316
4680
  const args = [
4317
4681
  "update-breakpoint",
@@ -4331,8 +4695,8 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
4331
4695
  "`colors` is an array of { slotId? | newSlotName?, value } — each entry targets ONE slot, EITHER by " +
4332
4696
  "`slotId` (an EXISTING slot's id, from list_theme_globals colorsByScheme keys / colorSchemes.schemes — the " +
4333
4697
  "id-keyed path; slot names ARE listed in colorSchemes.schemes but are not unique, so prefer the scheme's colorsByScheme keys to get the right id) OR by `newSlotName` (create/reuse " +
4334
- "a slot by name; use the standard slot names, \"/\" to group, e.g. \"PrimaryButton/Background\"); value = a " +
4335
- "literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" from " +
4698
+ 'a slot by name; use the standard slot names, "/" to group, e.g. "PrimaryButton/Background"); value = a ' +
4699
+ 'literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" from ' +
4336
4700
  "list_theme_globals to store it as a LINKED reference — pass the cssVar, not a hex copied from a global color) " +
4337
4701
  "and MERGES by slot — only the slots you pass change. Pass `colors` as REAL " +
4338
4702
  "JSON (not a stringified array). Set `is_default: true` to make this the theme's DEFAULT scheme (the " +
@@ -4340,8 +4704,12 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
4340
4704
  "automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
4341
4705
  "one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
4342
4706
  "is_default to no-op. Requires `ikas-component dev` running with the editor connected.", {
4343
- project_root: z.string().describe("Absolute path to the code-component project."),
4344
- id: z.string().describe("Color scheme id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4707
+ project_root: z
4708
+ .string()
4709
+ .describe("Absolute path to the code-component project."),
4710
+ id: z
4711
+ .string()
4712
+ .describe("Color scheme id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4345
4713
  name: z
4346
4714
  .string()
4347
4715
  .optional()
@@ -4349,12 +4717,15 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
4349
4717
  colors: z
4350
4718
  .any()
4351
4719
  .optional()
4352
- .describe("Array of { slotId? | newSlotName?, value } to set — target an existing slot by `slotId` (from list_theme_globals colorsByScheme keys / colorSchemes.schemes) or create/reuse by `newSlotName`; value = a literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" (linked reference). Merges by slot. Omit to only rename."),
4720
+ .describe('Array of { slotId? | newSlotName?, value } to set — target an existing slot by `slotId` (from list_theme_globals colorsByScheme keys / colorSchemes.schemes) or create/reuse by `newSlotName`; value = a literal color hex/rgb/hsl/name, OR an existing theme color\'s cssVar "var(--<colorId>)" (linked reference). Merges by slot. Omit to only rename.'),
4353
4721
  is_default: z
4354
4722
  .boolean()
4355
4723
  .optional()
4356
4724
  .describe("true = make this the default scheme (exclusive — the editor clears the default flag from every other scheme in its group); false = remove the default flag. Omit to leave the default unchanged. The result echoes the persisted `isDefault`."),
4357
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4725
+ port: z
4726
+ .number()
4727
+ .optional()
4728
+ .describe("Dev server WebSocket port (default 5201)."),
4358
4729
  }, async ({ project_root, id, name, colors, is_default, port }) => {
4359
4730
  const args = [
4360
4731
  "update-color-scheme",
@@ -4362,23 +4733,29 @@ server.tool("update_theme_color_scheme", "Update an existing theme color scheme
4362
4733
  id,
4363
4734
  ...(name !== undefined ? ["--name", name] : []),
4364
4735
  ...(colors !== undefined ? ["--colors", toJsonArrayArg(colors)] : []),
4365
- ...(is_default === true ? ["--default"] : is_default === false ? ["--no-default"] : []),
4736
+ ...(is_default === true
4737
+ ? ["--default"]
4738
+ : is_default === false
4739
+ ? ["--no-default"]
4740
+ : []),
4366
4741
  ...(port ? ["--port", String(port)] : []),
4367
4742
  ];
4368
4743
  return callEditorAction(project_root, args);
4369
4744
  });
4370
4745
  // Tool: rename_theme_color_scheme_slot
4371
- server.tool("rename_theme_color_scheme_slot", "Rename a color scheme SLOT (a scheme key like \"Background\" or \"PrimaryButton/Text\") IN PLACE in the " +
4746
+ server.tool("rename_theme_color_scheme_slot", 'Rename a color scheme SLOT (a scheme key like "Background" or "PrimaryButton/Text") IN PLACE in the ' +
4372
4747
  "connected editor. A slot's id is STABLE, so renaming keeps every runtime reference intact " +
4373
4748
  "(`var(--<slotId>)` in CSS, `colorsByScheme[slotId]` in component code) — only the human display name " +
4374
4749
  "changes; nothing rebinds. Identify the slot by `id` (REQUIRED — the slot id from `list_theme_globals` → " +
4375
4750
  "colorSchemes.schemes[].id or a `colorsByScheme` key); name-based lookup is intentionally NOT supported " +
4376
- "because slot names can collide across design assets. `name` is the NEW slot name (a \"/\" groups it in the " +
4377
- "editor UI, e.g. \"PrimaryButton/Background\"). Only non-design-asset (user/theme) slots can be renamed here. " +
4751
+ 'because slot names can collide across design assets. `name` is the NEW slot name (a "/" groups it in the ' +
4752
+ 'editor UI, e.g. "PrimaryButton/Background"). Only non-design-asset (user/theme) slots can be renamed here. ' +
4378
4753
  "This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
4379
4754
  "scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas-component dev` running with the " +
4380
4755
  "editor connected.", {
4381
- project_root: z.string().describe("Absolute path to the code-component project."),
4756
+ project_root: z
4757
+ .string()
4758
+ .describe("Absolute path to the code-component project."),
4382
4759
  id: z
4383
4760
  .string()
4384
4761
  .min(1, "id is REQUIRED — the slot id from list_theme_globals (colorSchemes.schemes[].id / colorsByScheme keys).")
@@ -4386,8 +4763,11 @@ server.tool("rename_theme_color_scheme_slot", "Rename a color scheme SLOT (a sch
4386
4763
  name: z
4387
4764
  .string()
4388
4765
  .min(1, "name is REQUIRED — the new slot name (non-empty).")
4389
- .describe("The NEW slot name (non-empty). A \"/\" groups it in the editor UI (e.g. \"PrimaryButton/Background\")."),
4390
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4766
+ .describe('The NEW slot name (non-empty). A "/" groups it in the editor UI (e.g. "PrimaryButton/Background").'),
4767
+ port: z
4768
+ .number()
4769
+ .optional()
4770
+ .describe("Dev server WebSocket port (default 5201)."),
4391
4771
  }, async ({ project_root, id, name, port }) => {
4392
4772
  const args = [
4393
4773
  "rename-color-scheme-slot",
@@ -4400,22 +4780,45 @@ server.tool("rename_theme_color_scheme_slot", "Rename a color scheme SLOT (a sch
4400
4780
  return callEditorAction(project_root, args);
4401
4781
  });
4402
4782
  // Tool: delete_theme_global
4403
- server.tool("delete_theme_global", "Delete a theme global setting from the connected editor. For `kind: globalVariable` pass `name` (the runtime key); for design-token kinds (color | typography | breakpoint | keyframe | colorScheme) pass the token `id`. For `kind: colorSchemeSlot` pass the color SLOT id (from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key) — this deletes the slot itself across ALL schemes (use it to clean up a junk slot created by a typo); it CASCADES: the slot's color is removed from every scheme and any element/component style bound to that slot is cleared, so only delete a slot you are sure is unused. Only non-design-asset (user/theme) slots can be deleted here. Get names/ids from `list_theme_globals`. Deleting a non-existent id fails with a \"not found\" error (it does NOT report a false success), so a success result means the token really existed and was removed.", {
4404
- project_root: z.string().describe("Absolute path to the code-component project."),
4783
+ server.tool("delete_theme_global", 'Delete a theme global setting from the connected editor. For `kind: globalVariable` pass `name` (the runtime key); for design-token kinds (color | typography | breakpoint | keyframe | colorScheme) pass the token `id`. For `kind: colorSchemeSlot` pass the color SLOT id (from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key) — this deletes the slot itself across ALL schemes (use it to clean up a junk slot created by a typo); it CASCADES: the slot\'s color is removed from every scheme and any element/component style bound to that slot is cleared, so only delete a slot you are sure is unused. Only non-design-asset (user/theme) slots can be deleted here. Get names/ids from `list_theme_globals`. Deleting a non-existent id fails with a "not found" error (it does NOT report a false success), so a success result means the token really existed and was removed.', {
4784
+ project_root: z
4785
+ .string()
4786
+ .describe("Absolute path to the code-component project."),
4405
4787
  kind: z
4406
- .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme", "colorSchemeSlot"])
4788
+ .enum([
4789
+ "globalVariable",
4790
+ "color",
4791
+ "typography",
4792
+ "breakpoint",
4793
+ "keyframe",
4794
+ "colorScheme",
4795
+ "colorSchemeSlot",
4796
+ ])
4407
4797
  .describe("What to delete."),
4408
- name: z.string().optional().describe("globalVariable runtime key (from `list_theme_globals`)."),
4798
+ name: z
4799
+ .string()
4800
+ .optional()
4801
+ .describe("globalVariable runtime key (from `list_theme_globals`)."),
4409
4802
  id: z
4410
4803
  .string()
4411
4804
  .optional()
4412
4805
  .describe("design-token id, or for colorSchemeSlot the slot id from `list_theme_globals` → colorSchemes.schemes."),
4413
- port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4806
+ port: z
4807
+ .number()
4808
+ .optional()
4809
+ .describe("Dev server WebSocket port (default 5201)."),
4414
4810
  }, async ({ project_root, kind, name, id, port }) => {
4415
4811
  const p = port ? ["--port", String(port)] : [];
4416
4812
  const args = kind === "globalVariable"
4417
4813
  ? ["delete-global-variable", "--name", String(name ?? ""), ...p]
4418
- : ["delete-design-token", "--token-type", kind, "--id", String(id ?? ""), ...p];
4814
+ : [
4815
+ "delete-design-token",
4816
+ "--token-type",
4817
+ kind,
4818
+ "--id",
4819
+ String(id ?? ""),
4820
+ ...p,
4821
+ ];
4419
4822
  return callEditorAction(project_root, args);
4420
4823
  });
4421
4824
  // --- Start server ---