@ikas/code-components-mcp 1.4.0-beta.202 → 1.4.0-beta.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1603,9 +1603,7 @@ function squashIdentifier(text) {
1603
1603
  /** A query word worth matching separator-insensitively: an identifier, not prose. */
1604
1604
  function isIdentifierLike(word) {
1605
1605
  return (word.length > 3 &&
1606
- (/[_\-.]/.test(word) ||
1607
- /[a-z][A-Z]/.test(word) ||
1608
- /^[A-Z0-9_]+$/.test(word)));
1606
+ (/[_\-.]/.test(word) || /[a-z][A-Z]/.test(word) || /^[A-Z0-9_]+$/.test(word)));
1609
1607
  }
1610
1608
  function matchScore(text, query) {
1611
1609
  const lower = text.toLowerCase();
@@ -3991,7 +3989,7 @@ server.tool("get_component_props", "Get prop blueprints straight from the editor
3991
3989
  return callEditorAction(project_root, args);
3992
3990
  });
3993
3991
  // Tool: get_section_values
3994
- server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch). COMMON (global) sections — the theme's header and footer — keep ONE shared set of prop values for the whole theme, stored on the INDEX (Home) page's placement; for those this tool reads the values FROM that canonical placement (a non-INDEX placement stores nothing of its own) and returns a `common` block naming `canonicalPageId`/`canonicalElementId`. So the values you get for a header/footer are the live ones the page actually renders no matter which page_id you ask from — do NOT read defaults as \"empty\" and blind-fill them, and remember any write goes to that same shared record and changes all pages. A prop bound to a theme GLOBAL variable stores no value in `propValues` — it is listed in the section's `bindings` map instead (`{ \"<propName>\": { boundGlobal: \"_<id>\", displayName, type } }`); treat such a prop as SET, not empty, and do not overwrite it with a static value unless asked (a static write removes the binding).", {
3992
+ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY placed sections (each with its full `propValues` plus blueprint `props` — every prop carrying a `writeExample`/`writeNote` for the exact value shape, plus ENUM `options` and COMPONENT_LIST `allowedComponentIds`). `list_page_sections` is lean and omits values to avoid truncation; use this to read sections' values — needed for read-modify-write of a COMPONENT_LIST, or to inspect the exact stored shape of an existing value. Returns `{ sections: [...] }`. Pass all the elementIds you need at once (batch). COMMON (global) sections — the theme's header and footer — keep ONE shared set of prop values for the whole theme, stored on the INDEX (Home) page's placement; for those this tool reads the values FROM that canonical placement (a non-INDEX placement stores nothing of its own) and returns a `common` block naming `canonicalPageId`/`canonicalElementId`. So the values you get for a header/footer are the live ones the page actually renders no matter which page_id you ask from — do NOT read defaults as \"empty\" and blind-fill them, and remember any write goes to that same shared record and changes all pages.", {
3995
3993
  project_root: z
3996
3994
  .string()
3997
3995
  .describe("Absolute path to the code-component project."),
@@ -4017,7 +4015,7 @@ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY pl
4017
4015
  // Tool: update_section_prop
4018
4016
  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' +
4019
4017
  "COMMON (global) sections — the theme's HEADER and FOOTER: these render on every page that carries a placement of them (ikas ready-made account/membership pages are born without one) and have ONE shared set of prop values, stored on the INDEX (Home) page's placement. Whichever page_id you write from, the value lands on that single record and changes the header/footer on ALL pages; the response carries a `common` block (`commonSectionType`, `canonicalPageId`, `canonicalElementId`) saying so, and the echoed `value` is read back from that canonical placement. Therefore: ALWAYS `get_section_values` first and change only the prop the user asked about — the props are already filled with the merchant's content even when you are on a page you have not inspected, so blind-filling \"empty-looking\" props overwrites the live header/footer everywhere. If the INDEX page has no placement of the section, there is no record to write to and the call fails telling you to add it to the INDEX page first.\n\n" +
4020
- "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, ENUM↔ENUM of the SAME enum type — the prop's `enumTypeId` must equal the setting's `enumTypeId`; a mismatch is REJECTED, …). Provide `value` OR `bind_global`, not both. Writing a static `value` to a prop that is bound to a global (see `bindings` in `get_section_values`) REPLACES the binding — the prop stops following the global, and the result carries `unboundGlobal` + a `warning`. To change what a bound prop shows, update the global (`update_theme_global`) instead. 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" +
4018
+ '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' +
4021
4019
  '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' +
4022
4020
  '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): ' +
4023
4021
  `{ "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" } } } ] } } } ] }`, {
@@ -4043,7 +4041,7 @@ server.tool("update_section_prop", 'Change a single prop value of a section plac
4043
4041
  bind_global: z
4044
4042
  .string()
4045
4043
  .optional()
4046
- .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. Writing a static `value` to a prop that is bound to a global (see `bindings` in `get_section_values`) REPLACES the binding — the prop stops following the global, and the result carries `unboundGlobal` + a `warning`. To change what a bound prop shows, update the global (`update_theme_global`) instead. Bindable globals include TEXT / RICH_TEXT / NUMBER / BOOLEAN / IMAGE / COLOR / BORDER / SHADOW / ENUM settings; pick a global whose type matches the prop (an ENUM prop only binds an ENUM setting of the same enum type). (This is the general theme-global binding; a COLOR prop can alternatively take an inline `{ \"patternValueId\" }` for theme colors.)"),
4044
+ .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.)"),
4047
4045
  list_path: z
4048
4046
  .array(z.object({ propId: z.string(), index: z.number() }))
4049
4047
  .optional()
@@ -4062,11 +4060,7 @@ server.tool("update_section_prop", 'Change a single prop value of a section plac
4062
4060
  ...(prop_id ? ["--prop-id", prop_id] : []),
4063
4061
  ...(prop_name ? ["--prop-name", prop_name] : []),
4064
4062
  ...(bind_global
4065
- ? [
4066
- "--bind-global",
4067
- bind_global,
4068
- ...(list_path ? ["--list-path", JSON.stringify(list_path)] : []),
4069
- ]
4063
+ ? ["--bind-global", bind_global, ...(list_path ? ["--list-path", JSON.stringify(list_path)] : [])]
4070
4064
  : [
4071
4065
  "--value",
4072
4066
  // The CLI JSON.parses --value. If `value` is already a JSON string, pass
@@ -4157,10 +4151,7 @@ server.tool("update_page_sections", "Fill MANY placed sections of a page in a SI
4157
4151
  ...(u.prop_id ? { propId: u.prop_id } : {}),
4158
4152
  ...(u.prop_name ? { propName: u.prop_name } : {}),
4159
4153
  ...(u.bind_global
4160
- ? {
4161
- bindGlobal: u.bind_global,
4162
- ...(u.list_path ? { listPath: u.list_path } : {}),
4163
- }
4154
+ ? { bindGlobal: u.bind_global, ...(u.list_path ? { listPath: u.list_path } : {}) }
4164
4155
  : { value: u.value }),
4165
4156
  })),
4166
4157
  }));
@@ -4369,7 +4360,7 @@ server.tool("create_page", 'Create a theme page of a given pageType in the conne
4369
4360
  return callEditorAction(project_root, args);
4370
4361
  });
4371
4362
  // Tool: delete_page
4372
- server.tool("delete_page", "Delete a theme page and every section placed on it. DESTRUCTIVE and effectively one-way from here (only the editor's own undo stack can bring it back) — ALWAYS get the user's explicit OK before calling it, and never call it to \"clean up\" a page you merely think is unused. Takes the page id from `list_editor_pages` / `get_page_by_type`; there is no name resolution. Refused for the pages a theme cannot run without — INDEX (home), CHECKOUT and NOT_FOUND (404) — and for an ikas-rendered ready-made account page, which must be turned off by its group with `enable_ready_made_pages` so the cleanup that flow does actually runs. Deleting the page currently open in the editor moves it to the home page first. Returns the deleted page's `{ pageId, pageType, name, slug }` plus `remainingPages`. Requires `ikas theme dev` running with the editor connected.", {
4363
+ server.tool("delete_page", 'Delete a theme page and every section placed on it. DESTRUCTIVE and effectively one-way from here (only the editor\'s own undo stack can bring it back) — ALWAYS get the user\'s explicit OK before calling it, and never call it to "clean up" a page you merely think is unused. Takes the page id from `list_editor_pages` / `get_page_by_type`; there is no name resolution. Refused for the pages a theme cannot run without — INDEX (home), CHECKOUT and NOT_FOUND (404) — and for an ikas-rendered ready-made account page, which must be turned off by its group with `enable_ready_made_pages` so the cleanup that flow does actually runs. Deleting the page currently open in the editor moves it to the home page first. Returns the deleted page\'s `{ pageId, pageType, name, slug }` plus `remainingPages`. Requires `ikas theme dev` running with the editor connected.', {
4373
4364
  project_root: z
4374
4365
  .string()
4375
4366
  .describe("Absolute path to the code-component project."),
@@ -4394,7 +4385,7 @@ server.tool("delete_page", "Delete a theme page and every section placed on it.
4394
4385
  return callEditorAction(project_root, args);
4395
4386
  });
4396
4387
  // Tool: remove_page_section
4397
- server.tool("remove_page_section", "Remove ONE section placement from a page — the delete counterpart of `add_section_to_page` / `move_page_section`. DESTRUCTIVE: it takes the merchant's content in that section with it, so get the user's explicit OK first. The checkout section on the CHECKOUT page is refused — the checkout page requires it, so never offer to remove it. Target the PLACEMENT by `element_id` from `list_page_sections`, never the `sectionId` or `componentId` — the same section placed twice has two independent placements and each is removed on its own (passing the wrong id is rejected with the elementId you meant). A common (header/footer) section on the home page is HIDDEN rather than deleted, because the INDEX placement is where a common section's prop values live for EVERY page; the reply says so with `hidden: true`, and `add_section_to_page` brings it back with its values intact. A page customized per routing/specification holds its own placements per state: pass `sf_routing_id`/`specification_id` to remove from that state, otherwise only the BASE layer is touched (the reply lists the states it left alone). Returns the removed placement plus the page's remaining `sections`. Requires `ikas theme dev` running with the editor connected.", {
4388
+ server.tool("remove_page_section", 'Remove ONE section placement from a page — the delete counterpart of `add_section_to_page` / `move_page_section`. DESTRUCTIVE: it takes the merchant\'s content in that section with it, so get the user\'s explicit OK first. Target the PLACEMENT by `element_id` from `list_page_sections`, never the `sectionId` or `componentId` — the same section placed twice has two independent placements and each is removed on its own (passing the wrong id is rejected with the elementId you meant). A common (header/footer) section on the home page is HIDDEN rather than deleted, because the INDEX placement is where a common section\'s prop values live for EVERY page; the reply says so with `hidden: true`, and `add_section_to_page` brings it back with its values intact. A page customized per routing/specification holds its own placements per state: pass `sf_routing_id`/`specification_id` to remove from that state, otherwise only the BASE layer is touched (the reply lists the states it left alone). Returns the removed placement plus the page\'s remaining `sections`. Requires `ikas theme dev` running with the editor connected.', {
4398
4389
  project_root: z
4399
4390
  .string()
4400
4391
  .describe("Absolute path to the code-component project."),
@@ -4417,7 +4408,7 @@ server.tool("remove_page_section", "Remove ONE section placement from a page —
4417
4408
  .number()
4418
4409
  .optional()
4419
4410
  .describe("Dev server WebSocket port (default 5201)."),
4420
- }, async ({ project_root, page_id, element_id, sf_routing_id, specification_id, confirm, port, }) => {
4411
+ }, async ({ project_root, page_id, element_id, sf_routing_id, specification_id, confirm, port }) => {
4421
4412
  if (confirm !== true)
4422
4413
  return destructiveConfirmRefusal();
4423
4414
  const args = [
@@ -4605,10 +4596,10 @@ const EDITOR_WORKFLOW_GUIDE = [
4605
4596
  "",
4606
4597
  "## COMMON (global) sections: the header and the footer",
4607
4598
  "The theme's header and footer are COMMON sections: they render on every page that carries a placement of them (pages created in the editor are seeded with one — EXCEPT ikas ready-made (built-in) account/membership pages, which are born without the theme header/footer) and their prop values are SHARED by the whole theme — there is exactly ONE record, kept on the INDEX (Home) page's placement. A placement of the same section on any other page stores NOTHING of its own.",
4608
- '- How to recognize one: list_page_sections / get_section_values / the write results return a `common` block for it — `isCommon: true`, `commonSectionType: "header"|"footer"`, `valuesArePageSpecific: false`, plus `canonicalPageId`/`canonicalElementId` (the INDEX placement that holds the values). A page containing one also gets a top-level `commonSectionsNote`.',
4599
+ "- How to recognize one: list_page_sections / get_section_values / the write results return a `common` block for it — `isCommon: true`, `commonSectionType: \"header\"|\"footer\"`, `valuesArePageSpecific: false`, plus `canonicalPageId`/`canonicalElementId` (the INDEX placement that holds the values). A page containing one also gets a top-level `commonSectionsNote`.",
4609
4600
  "- READS are redirected for you: get_section_values / list_page_sections report a common section's values FROM the INDEX placement, whatever page_id you pass. So what you see is what the page actually renders — no need to re-read from the INDEX page.",
4610
4601
  "- WRITES are redirected too: update_section_prop / update_page_sections / add_sections_to_page store the value on that INDEX record, so the change applies to the header/footer on ALL pages. The echoed value is read back from the canonical placement, so it reflects what was really stored.",
4611
- '- THE TRAP to avoid: a header/footer is normally already filled with the merchant\'s content. Never conclude a prop is empty and never blind-fill "missing" props — you would overwrite live content on every page, including pages you never opened. Always get_section_values first and change ONLY the prop the user asked about.',
4602
+ "- THE TRAP to avoid: a header/footer is normally already filled with the merchant's content. Never conclude a prop is empty and never blind-fill \"missing\" props — you would overwrite live content on every page, including pages you never opened. Always get_section_values first and change ONLY the prop the user asked about.",
4612
4603
  "- If the INDEX page has no placement of the section, there is no record: reads return nothing and writes are REJECTED (\"Index page does not have the common section!\"). Fix it by placing the section on the INDEX page (add_section_to_page with the INDEX page id — get it from list_editor_pages / get_page_by_type('INDEX')) and setting its props there; it then renders on the other pages too.",
4613
4604
  "",
4614
4605
  "## Building a whole page fast (placing + filling)",
@@ -4650,7 +4641,7 @@ server.tool("get_editor_workflow", "Read this FIRST when the user wants to place
4650
4641
  };
4651
4642
  });
4652
4643
  // Tool: list_theme_globals
4653
- server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas theme dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together. (7) ENUM settings: a setting with `type: \"ENUM\"` selects one option of a PROJECT enum and carries `enumTypeId` (the enum's id — the same one an ENUM prop declares as `enumTypeId`) and `options` (`{ label: value }`); its `value` is always one of those option VALUES (never a label), or null when unset. The response's top-level `enumTypes` array lists the project enums a new ENUM setting may select from (`{ id, name, options }`) — use an `id` from it as `enum_type_id` in `create_theme_global`. An ENUM prop can be bound (`bind_global`) only to an ENUM setting of the SAME enum.", {
4644
+ server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas theme dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. Each typography entry also carries `isDefault` (true for the theme's default/global text style — the editor's \"Make Default Style\"); toggle it via `update_text_style`'s `is_default`. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
4654
4645
  project_root: z
4655
4646
  .string()
4656
4647
  .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
@@ -4680,9 +4671,9 @@ const textStyleFontParams = {
4680
4671
  font_weight: z
4681
4672
  .string()
4682
4673
  .optional()
4683
- .describe("font-weight as a number (e.g. \"600\"); append 'i' for italic (e.g. \"600i\"). A PLAIN number with no 'i' " +
4684
- "is upright and REMOVES italic if the text style was italic (italic is carried by the weight — re-send the " +
4685
- "weight without 'i' to turn it off). Must be a weight the chosen " +
4674
+ .describe('font-weight as a number (e.g. "600"); append \'i\' for italic (e.g. "600i"). A PLAIN number with no \'i\' ' +
4675
+ 'is upright and REMOVES italic if the text style was italic (italic is carried by the weight — re-send the ' +
4676
+ 'weight without \'i\' to turn it off). Must be a weight the chosen ' +
4686
4677
  'font ships — if it isn\'t, the error lists the supported weights; when unsure use "400".'),
4687
4678
  line_height: z.string().optional(),
4688
4679
  letter_spacing: z.string().optional(),
@@ -4736,7 +4727,7 @@ function textStyleFontArgs(input) {
4736
4727
  }
4737
4728
  // Tool: create_theme_global
4738
4729
  server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
4739
- '- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW|ENUM); 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" }; ENUM: a setting that selects ONE option of a PROJECT enum — pass `enum_type_id` (an enum id from `list_theme_globals` → `enumTypes[]`; the same enum an ENUM prop declares as `enumTypeId`; built-in @ikas/* enums are REJECTED) and `value` = one of that enum\'s option VALUES (the strings in its `options`, e.g. "bottom"), NOT a label; omit `value` to leave it unset. Once created, an ENUM prop of the SAME enum can be bound to it with `update_section_prop` `bind_global`, and in component code `themeValue("_<id>")` from `./global-types` narrows to the enum\'s union type. 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' +
4730
+ '- 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' +
4740
4731
  '- 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' +
4741
4732
  '- 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' +
4742
4733
  "- 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" +
@@ -4766,10 +4757,6 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4766
4757
  .any()
4767
4758
  .optional()
4768
4759
  .describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
4769
- enum_type_id: z
4770
- .string()
4771
- .optional()
4772
- .describe("globalVariable of type ENUM only: id of the project enum the setting selects from (from `list_theme_globals` → `enumTypes[]`). Required for ENUM, invalid for other types."),
4773
4760
  ...textStyleFontParams,
4774
4761
  width: z
4775
4762
  .number()
@@ -4804,9 +4791,6 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4804
4791
  ...(input.value !== undefined
4805
4792
  ? ["--value", JSON.stringify(input.value)]
4806
4793
  : []),
4807
- ...(input.enum_type_id
4808
- ? ["--enum-type-id", String(input.enum_type_id)]
4809
- : []),
4810
4794
  ...p,
4811
4795
  ];
4812
4796
  break;
@@ -4876,7 +4860,7 @@ server.tool("create_theme_global", "Create a theme global setting in the connect
4876
4860
  return callEditorAction(input.project_root, args);
4877
4861
  });
4878
4862
  // Tool: update_theme_global
4879
- 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; ENUM `value` is one of the enum's option VALUES (see the setting's `options` in `list_theme_globals`), and `enum_type_id` re-points the setting at another project enum. Changing `type` or `enum_type_id` of a global that props are bound to is REJECTED (the error names the places) — unbind those props first. Values are validated server-side; a wrong shape is REJECTED, not silently stored.", {
4863
+ 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.', {
4880
4864
  project_root: z
4881
4865
  .string()
4882
4866
  .describe("Absolute path to the code-component project."),
@@ -4887,20 +4871,16 @@ server.tool("update_theme_global", "Update an existing global variable in the co
4887
4871
  type: z
4888
4872
  .string()
4889
4873
  .optional()
4890
- .describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW|ENUM)."),
4874
+ .describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
4891
4875
  value: z
4892
4876
  .any()
4893
4877
  .optional()
4894
- .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, ENUM: an option VALUE string — see create_theme_global). An invented IMAGE id renders as a broken image.'),
4895
- enum_type_id: z
4896
- .string()
4897
- .optional()
4898
- .describe('ENUM only: the project enum to select from (from `list_theme_globals` → `enumTypes[]`); may be passed alone to re-point an unbound ENUM global, or together with `type: "ENUM"`.'),
4878
+ .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.'),
4899
4879
  port: z
4900
4880
  .number()
4901
4881
  .optional()
4902
4882
  .describe("Dev server WebSocket port (default 5201)."),
4903
- }, async ({ project_root, name, display_name, type, value, enum_type_id, port, }) => {
4883
+ }, async ({ project_root, name, display_name, type, value, port }) => {
4904
4884
  const args = [
4905
4885
  "update-global-variable",
4906
4886
  "--name",
@@ -4908,7 +4888,6 @@ server.tool("update_theme_global", "Update an existing global variable in the co
4908
4888
  ...(display_name !== undefined ? ["--display-name", display_name] : []),
4909
4889
  ...(type !== undefined ? ["--type", type] : []),
4910
4890
  ...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
4911
- ...(enum_type_id !== undefined ? ["--enum-type-id", enum_type_id] : []),
4912
4891
  ...(port ? ["--port", String(port)] : []),
4913
4892
  ];
4914
4893
  return callEditorAction(project_root, args);
@@ -4986,7 +4965,7 @@ server.tool("update_text_style", "Update an existing theme typography token (tex
4986
4965
  "line-through; text_transform is none|uppercase|lowercase|capitalize; text_overflow is clip|ellipsis; white_space " +
4987
4966
  "is normal|pre|pre-wrap|pre-line|wrap|nowrap|collapse; line_break is auto|loose|normal|strict|anywhere; word_break " +
4988
4967
  "is normal|break-all|keep-all; text_wrap_style is auto|balance|pretty|stable. Set `is_default: true` to make " +
4989
- 'this text style the theme\'s default/global style (the editor\'s right-click "Make Default Style" / "Global ' +
4968
+ "this text style the theme's default/global style (the editor's right-click \"Make Default Style\" / \"Global " +
4990
4969
  "Stil Yap\") — any other default in the group is cleared; `is_default: false` removes the flag. A text style's " +
4991
4970
  "current default state is readable from `list_theme_globals` (each typography entry's `isDefault`). Requires " +
4992
4971
  "`ikas theme dev` running with the editor connected.", {
@@ -5018,11 +4997,7 @@ server.tool("update_text_style", "Update an existing theme typography token (tex
5018
4997
  id,
5019
4998
  ...(name !== undefined ? ["--name", name] : []),
5020
4999
  ...textStyleFontArgs(input),
5021
- ...(is_default === true
5022
- ? ["--default"]
5023
- : is_default === false
5024
- ? ["--no-default"]
5025
- : []),
5000
+ ...(is_default === true ? ["--default"] : is_default === false ? ["--no-default"] : []),
5026
5001
  ...(port ? ["--port", String(port)] : []),
5027
5002
  ];
5028
5003
  return callEditorAction(project_root, args);