@plitzi/sdk-server 0.32.16 → 0.32.18

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 (62) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/helpers/serverLog.js +3 -3
  3. package/dist/modules/mcp/apps/render/view.tsx +107 -3
  4. package/dist/modules/mcp/apps/shared/shell.ejs +19 -2
  5. package/dist/modules/mcp/helpers/guide.js +9 -2
  6. package/dist/modules/mcp/helpers/space.js +1 -1
  7. package/dist/modules/mcp/resources/renderGuide.js +205 -18
  8. package/dist/modules/mcp/server.js +1 -1
  9. package/dist/modules/mcp/tools/apply/dispatch.js +2 -0
  10. package/dist/modules/mcp/tools/apply/index.js +17 -3
  11. package/dist/modules/mcp/tools/operations/index.js +4 -0
  12. package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +1 -1
  13. package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.js +148 -0
  14. package/dist/modules/mcp/tools/operations/schema/interactions/deleteInteraction.js +1 -1
  15. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +1 -1
  16. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +1 -1
  17. package/dist/modules/mcp/tools/operations/schema/operations.js +2 -0
  18. package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +1 -1
  19. package/dist/modules/mcp/tools/operations/schema/shared.js +8 -5
  20. package/dist/modules/mcp/tools/operations/schemaIds.js +54 -0
  21. package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +1 -1
  22. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +1 -1
  23. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +40 -0
  24. package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +1 -1
  25. package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +1 -1
  26. package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +1 -1
  27. package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +1 -1
  28. package/dist/modules/mcp/tools/operations/style/operations.js +2 -0
  29. package/dist/modules/mcp/tools/operations/style/shared.js +1 -1
  30. package/dist/modules/mcp/tools/render.js +70 -8
  31. package/dist/modules/mcp/tools/shared/expandOperations.js +44 -0
  32. package/dist/modules/mcp/tools/shared/validator/audit.js +3 -0
  33. package/dist/modules/mcp/tools/shared/validator/batch.js +13 -4
  34. package/dist/modules/mcp/tools/shared/validator/index.js +15 -0
  35. package/dist/modules/mcp/tools/validate.js +11 -3
  36. package/dist/modules/oauth/metadata.js +11 -4
  37. package/dist/src/helpers/serverLog.d.ts +1 -1
  38. package/dist/src/modules/mcp/apps/render/heldBatch.d.ts +10 -0
  39. package/dist/src/modules/mcp/apps/render/heldBatch.test.d.ts +1 -0
  40. package/dist/src/modules/mcp/e2e/index.d.ts +2 -2
  41. package/dist/src/modules/mcp/e2e/renderingHost.d.ts +16 -1
  42. package/dist/src/modules/mcp/helpers/guide.d.ts +1 -1
  43. package/dist/src/modules/mcp/helpers/space.d.ts +1 -1
  44. package/dist/src/modules/mcp/tests/schemaIds.test.d.ts +1 -0
  45. package/dist/src/modules/mcp/tools/apply/index.d.ts +50 -0
  46. package/dist/src/modules/mcp/tools/operations/index.d.ts +100 -0
  47. package/dist/src/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +55 -0
  48. package/dist/src/modules/mcp/tools/operations/schema/index.d.ts +1 -0
  49. package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +19 -0
  50. package/dist/src/modules/mcp/tools/operations/schema/shared.d.ts +15 -0
  51. package/dist/src/modules/mcp/tools/operations/schemaIds.d.ts +6 -0
  52. package/dist/src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +41 -0
  53. package/dist/src/modules/mcp/tools/operations/style/index.d.ts +1 -0
  54. package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +33 -0
  55. package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +4 -3
  56. package/dist/src/modules/mcp/tools/preview.d.ts +50 -0
  57. package/dist/src/modules/mcp/tools/render.d.ts +421 -363
  58. package/dist/src/modules/mcp/tools/screenshot.d.ts +50 -0
  59. package/dist/src/modules/mcp/tools/shared/expandOperations.d.ts +13 -0
  60. package/dist/src/modules/mcp/tools/validate.d.ts +50 -0
  61. package/dist/src/modules/oauth/metadata.d.ts +11 -4
  62. package/package.json +5 -5
@@ -10,7 +10,7 @@ var patchElementOp = z.object({
10
10
  type: z.literal("patchElement"),
11
11
  pageRef: z.string().describe("Page ref or id"),
12
12
  ref: z.string().describe("Existing element ref or id"),
13
- idRef: z.string().optional().describe("Assign or rename the idRef of this element ([A-Za-z0-9_-], starting with a letter, unique in the space). Give an element one to make it bindable: without an idRef it publishes no data source. Renaming one moves its source name with it every binding and interaction across the space that targeted the old name is repointed for you, so the wiring survives the rename."),
13
+ idRef: z.string().optional().describe("Assign or rename this element idRef ([A-Za-z0-9_-], starting with a letter, unique in the space). Without one an element publishes no data source, so it is not bindable. A rename moves the source name with it: every binding and interaction that targeted the old one is repointed for you."),
14
14
  label: z.string().optional(),
15
15
  subType: z.string().optional(),
16
16
  props: z.record(z.string(), z.unknown()).optional().describe("Merged onto existing props: listed keys change, null unsets a key, others are preserved"),
@@ -0,0 +1,148 @@
1
+ import { elementInput, elementShape, position, styleRefs } from "../shared.js";
2
+ import { z } from "zod";
3
+ //#region src/modules/mcp/tools/operations/schema/elements/repeatElement.ts
4
+ var item = z.record(z.string(), z.unknown());
5
+ var MAX_ITEMS = 100;
6
+ var MAX_ROWS = 500;
7
+ var templateElement = z.lazy(() => z.object({
8
+ ...elementShape,
9
+ children: z.array(templateElement).optional(),
10
+ repeat: z.object({
11
+ items: z.string().describe("The row field holding the sub-list, as \"{{item.<field>}}\" (or just the field path)"),
12
+ template: elementInput.describe("Rendered once per entry of that sub-list (a plain element tree)")
13
+ }).optional().describe("Makes THIS element the wrapper of a nested list — a list inside each row")
14
+ }));
15
+ var repeatElementOp = z.object({
16
+ type: z.literal("repeatElement"),
17
+ pageRef: z.string().describe("Page ref or id"),
18
+ ref: z.string().describe("Ref of the WRAPPER element this creates; the rows become its children"),
19
+ elementType: z.string().optional().describe("Type of the wrapper; defaults to container"),
20
+ label: z.string().optional(),
21
+ style: styleRefs.optional().describe("Classes for the wrapper — this is where the row/grid layout goes"),
22
+ parentRef: z.string().optional().describe("Anchor ref/id; defaults to page root"),
23
+ position: position.optional(),
24
+ template: templateElement.describe("The subtree ONE row renders, with {{item.field}} placeholders"),
25
+ items: z.array(item).min(1).max(MAX_ITEMS).describe("One object per row; its fields fill the {{item.field}} placeholders of the template")
26
+ }).describe("Build a LIST from one template plus its `items` data, instead of repeating near-identical elements. Creates a wrapper whose children are the template rendered once per row. `{{item.<field>}}` is replaced by that row field — alone as the whole value it keeps the field type, mixed with text it interpolates. Every ref gets the row number appended (\"day\" → \"day-1\"), which is how you address a row later. A template node may carry `repeat: { items: \"{{item.<list>}}\", template: … }` to nest a list inside each row: it becomes the sub-list wrapper, refs number both levels (\"step-2-3\"), and `{{item.…}}` there reads the SUB-row. Other {{…}} names are untouched, so schema variables still work.");
27
+ var PLACEHOLDER = /\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}/g;
28
+ var WHOLE_PLACEHOLDER = /^\{\{\s*item\.([A-Za-z0-9_.-]+)\s*\}\}$/;
29
+ var lookup = (row, path) => path.split(".").reduce((value, key) => {
30
+ if (value === null || typeof value !== "object") return;
31
+ return value[key];
32
+ }, row);
33
+ /** One string of the template, with its placeholders resolved. A placeholder that IS the whole string yields the
34
+ * field's own value (a number stays a number, an array stays an array); mixed text interpolates. */
35
+ var fillString = (text, row) => {
36
+ const whole = WHOLE_PLACEHOLDER.exec(text);
37
+ if (whole) {
38
+ const value = lookup(row, whole[1]);
39
+ return value === void 0 ? {
40
+ value: text,
41
+ missing: whole[1]
42
+ } : { value };
43
+ }
44
+ let missing;
45
+ const value = text.replace(PLACEHOLDER, (match, path) => {
46
+ const found = lookup(row, path);
47
+ if (found === void 0) {
48
+ missing ??= path;
49
+ return match;
50
+ }
51
+ if (typeof found === "string") return found;
52
+ return typeof found === "number" || typeof found === "boolean" ? found.toString() : JSON.stringify(found);
53
+ });
54
+ return missing === void 0 ? { value } : {
55
+ value,
56
+ missing
57
+ };
58
+ };
59
+ var fillValue = (value, row, missing) => {
60
+ if (typeof value === "string") {
61
+ const filled = fillString(value, row);
62
+ if (filled.missing) missing.add(filled.missing);
63
+ return filled.value;
64
+ }
65
+ if (Array.isArray(value)) return value.map((entry) => fillValue(entry, row, missing));
66
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, fillValue(entry, row, missing)]));
67
+ return value;
68
+ };
69
+ var listOf = (row, spec) => {
70
+ const whole = WHOLE_PLACEHOLDER.exec(spec);
71
+ const value = lookup(row, whole ? whole[1] : spec);
72
+ if (!Array.isArray(value)) return;
73
+ return value.map((entry) => entry !== null && typeof entry === "object" ? entry : { value: entry });
74
+ };
75
+ /** One template node against one row. `suffix` is the row numbering accumulated from the OUTSIDE in, so a nested
76
+ * row reads "step-2-3" (row 2, sub-row 3) rather than the other way round. */
77
+ var expandNode = (node, row, suffix, state) => {
78
+ const { children, repeat, ...own } = node;
79
+ const filled = fillValue(own, row, state.missing);
80
+ const element = {
81
+ ...filled,
82
+ ref: `${filled.ref}${suffix}`
83
+ };
84
+ if (repeat) {
85
+ const rows = listOf(row, repeat.items);
86
+ if (!rows) {
87
+ state.notAList.add(repeat.items);
88
+ return element;
89
+ }
90
+ const capped = rows.slice(0, MAX_ITEMS);
91
+ state.rows += capped.length;
92
+ element.children = capped.map((subRow, index) => expandNode(repeat.template, subRow, `${suffix}-${index + 1}`, state));
93
+ return element;
94
+ }
95
+ if (children) element.children = children.map((child) => expandNode(child, row, suffix, state));
96
+ return element;
97
+ };
98
+ var rowError = (index, message, hint) => ({
99
+ path: `items[${index}]`,
100
+ message,
101
+ hint
102
+ });
103
+ var keysOf = (row) => Object.keys(row).join(", ") || "(nothing)";
104
+ /** Expand one repeat into the single upsertElement it stands for: the wrapper, with the template rendered once per
105
+ * row as its children. Returns the errors instead of throwing, so a bad row reads like every other op error. */
106
+ var expandRepeat = (op) => {
107
+ const errors = [];
108
+ const children = [];
109
+ let total = op.items.length;
110
+ for (const [index, row] of op.items.entries()) {
111
+ const state = {
112
+ missing: /* @__PURE__ */ new Set(),
113
+ notAList: /* @__PURE__ */ new Set(),
114
+ rows: 0
115
+ };
116
+ const expanded = expandNode(op.template, row, `-${index + 1}`, state);
117
+ total += state.rows;
118
+ if (state.missing.size > 0) {
119
+ const fields = [...state.missing];
120
+ errors.push(rowError(index, `Row ${index + 1} has no ${fields.map((field) => `"${field}"`).join(", ")}`, `The template reads it as {{item.${fields[0]}}}. This row carries: ${keysOf(row)}`));
121
+ continue;
122
+ }
123
+ if (state.notAList.size > 0) {
124
+ const [spec] = [...state.notAList];
125
+ errors.push(rowError(index, `Row ${index + 1} has no list at ${spec}`, `A nested repeat needs an ARRAY in that field. This row carries: ${keysOf(row)}`));
126
+ continue;
127
+ }
128
+ children.push(expanded);
129
+ }
130
+ if (errors.length > 0) return { errors };
131
+ if (total > MAX_ROWS) return { errors: [{
132
+ path: "items",
133
+ message: `This repeat expands to ${total} rows (max ${MAX_ROWS})`,
134
+ hint: "Shorten the lists, or split the widget into several repeats."
135
+ }] };
136
+ return {
137
+ element: {
138
+ ref: op.ref,
139
+ type: op.elementType ?? "container",
140
+ ...op.label === void 0 ? {} : { label: op.label },
141
+ ...op.style === void 0 ? {} : { style: op.style },
142
+ children
143
+ },
144
+ errors
145
+ };
146
+ };
147
+ //#endregion
148
+ export { expandRepeat, repeatElementOp };
@@ -9,7 +9,7 @@ var deleteInteractionOp = z.object({
9
9
  ref: z.string().describe("Element ref or id"),
10
10
  flowId: z.string().optional().describe("Remove the whole flow with this id (the trigger node id)"),
11
11
  nodeId: z.string().optional().describe("Remove a single step; its neighbors are re-linked. Deleting a trigger removes its flow")
12
- }).describe("DESTRUCTIVE, not undoable — remove an interaction flow (by flowId) or a single step (by nodeId). Provide exactly one. To merely turn a step OFF without removing it, use patchInteractionNode { enabled: false } instead. Confirm with the user before deleting.");
12
+ }).describe("DESTRUCTIVE, not undoable — remove a flow (by flowId) or a single step (by nodeId); provide exactly one. To turn a step OFF instead, patchInteractionNode { enabled: false }. Confirm with the user first.");
13
13
  var removeFlow = (interactions, flowId) => {
14
14
  let deleted = 0;
15
15
  for (const [nodeId, node] of Object.entries(interactions)) if ((node.flowId || node.id) === flowId) {
@@ -18,7 +18,7 @@ var patchInteractionNodeOp = z.object({
18
18
  when,
19
19
  elementId,
20
20
  preview
21
- }).describe("Partially update ONE step of an interaction flow (found by nodeId): only the fields you pass change; the flow order/links are untouched. This is how you DISABLE/deactivate a step without removing it: pass { enabled: false } (re-enable with { enabled: true }) — the step stays in the flow. Use deleteInteraction only to actually remove a step or a flow. Fails if the node does not exist.");
21
+ }).describe("Partially update ONE step of a flow (found by nodeId): only the fields you pass change, the order/links are untouched. This is how you DISABLE a step without removing it { enabled: false } keeps it in the flow. deleteInteraction is for actually removing one. Fails if the node does not exist.");
22
22
  var patchInteractionNode = (space, env, op) => {
23
23
  const found = resolveElement(space, env, op.pageRef, op.ref);
24
24
  if ("error" in found) return found.error;
@@ -11,7 +11,7 @@ var upsertInteractionFlowOp = z.object({
11
11
  ref: z.string().describe("Element ref or id"),
12
12
  flowId: z.string().optional().describe("Existing flow to replace (the trigger node id). Omit to create a new flow"),
13
13
  nodes: z.array(interactionNode).min(1).describe("Ordered steps of the flow: the FIRST must be a trigger, the rest run after it in order")
14
- }).describe("Create or replace one interaction flow on an element from an ordered list of steps. The stored beforeNode/afterNode/flowId links are computed for you — pass the steps in execution order. For a globalCallback (addNotification, setState, navigate, auth*, *CollectionRecord) OMIT elementId: it is registered on a source module, not the host element, and the MCP sets the right source and fills the default param values for you. To edit a single step in place use patchInteractionNode.");
14
+ }).describe("Create or replace one interaction flow on an element from an ordered list of steps pass them in execution order, the beforeNode/afterNode/flowId links are computed for you. For a globalCallback OMIT elementId: it is registered on a source module, not the host element, and the source and default params are filled in for you. To edit a single step in place use patchInteractionNode.");
15
15
  var upsertInteractionFlow = (space, env, op) => {
16
16
  const found = resolveElement(space, env, op.pageRef, op.ref);
17
17
  if ("error" in found) return found.error;
@@ -4,6 +4,7 @@ import { upsertBindingOp } from "./bindings/upsertBinding.js";
4
4
  import { deleteElementOp } from "./elements/deleteElement.js";
5
5
  import { moveElementOp } from "./elements/moveElement.js";
6
6
  import { patchElementOp } from "./elements/patchElement.js";
7
+ import { repeatElementOp } from "./elements/repeatElement.js";
7
8
  import { upsertElementOp } from "./elements/upsertElement.js";
8
9
  import { deleteFolderOp } from "./folders/deleteFolder.js";
9
10
  import { upsertFolderOp } from "./folders/upsertFolder.js";
@@ -18,6 +19,7 @@ import { upsertVariableOp } from "./variables/upsertVariable.js";
18
19
  //#region src/modules/mcp/tools/operations/schema/operations.ts
19
20
  var elementOps = {
20
21
  upsertElement: upsertElementOp,
22
+ repeatElement: repeatElementOp,
21
23
  patchElement: patchElementOp,
22
24
  deleteElement: deleteElementOp,
23
25
  moveElement: moveElementOp,
@@ -8,7 +8,7 @@ var upsertPageOp = z.object({
8
8
  type: z.literal("upsertPage"),
9
9
  ref: z.string().describe("Page id/slug to update, or a new id you choose to create one. On a new page it is stored as its idRef: starts with a letter, then letters, numbers, hyphens and underscores (\"pricing\"), unique across the space."),
10
10
  label: z.string().optional(),
11
- slug: z.string().optional().describe("The page URL path, RELATIVE — do NOT start it with \"/\" (the runtime and any parent folder slugs prepend the path; a leading \"/\" is stripped). Good practice: always set one on create for a clean, stable route, e.g. \"pricing\" or \"posts/:postId\" (a \":name\" segment is a route param, readable as {{name}} and as the source navigation.routeParams.name). Omitted on create → the page ref is used as the slug."),
11
+ slug: z.string().optional().describe("The page URL path, RELATIVE — never start it with \"/\" (parent folder slugs prepend it; a leading \"/\" is stripped). Set one on create for a stable route: \"pricing\", \"posts/:postId\". A \":name\" segment is a route param, readable as {{name}} and as navigation.routeParams.name. Omitted → the page ref is used."),
12
12
  folder: z.string().nullable().optional().describe("Ref of an existing folder to place this page in; \"\" or null moves it to the root. Unknown → error"),
13
13
  default: z.boolean().optional(),
14
14
  enabled: z.boolean().optional().describe("false disables the page in the published SDK runtime (not routable/accessible to end users); it stays editable here. Defaults to true")
@@ -60,14 +60,17 @@ var initialStateInput = z.object({
60
60
  styleVariant: styleVariantInput.optional().describe("Variant each attached class uses: { className: { base|slot: variantName | [names] } }"),
61
61
  visibility: z.boolean().optional().describe("Initial visibility of the element")
62
62
  });
63
- var elementInput = z.lazy(() => z.object({
64
- ref: z.string().describe("Semantic id you choose, or an existing element ref/id. On a new element this is stored as its idRef and becomes the key everything else addresses it by its data source is named `<type>_<ref>`, and an interaction targets it by this ref. Start with a letter, then letters, numbers, hyphens and underscores (\"hero-cta\" or \"food_item\"): a dot would break the `<type>_<idRef>.<field>` grammar (an underscore is fine — the first `_` separates the type from the idRef). Must be unique across the space."),
63
+ var elementShape = {
64
+ ref: z.string().describe("Semantic id you choose, or an existing element ref/id. On a new element it becomes the idRef everything addresses it by: its data source is `<type>_<ref>`, and interactions target it by this ref. Letters, digits, `-` and `_`, starting with a letter a dot would break the `<type>_<idRef>.<field>` grammar. Unique across the space."),
65
65
  type: z.string().describe("Type from plitzi://types"),
66
66
  label: z.string().optional(),
67
67
  subType: z.string().optional(),
68
68
  props: z.record(z.string(), z.unknown()).optional().describe("Full replacement on update"),
69
69
  style: styleRefs.optional().describe("Definition refs per slot; style the element by attaching a definition"),
70
- initialState: initialStateInput.optional().describe("Applied style variant(s) and initial visibility (see plitzi://guide styling)"),
70
+ initialState: initialStateInput.optional().describe("Applied style variant(s) and initial visibility (see plitzi://guide styling)")
71
+ };
72
+ var elementInput = z.lazy(() => z.object({
73
+ ...elementShape,
71
74
  children: z.array(elementInput).optional()
72
75
  }));
73
76
  var position = z.enum([
@@ -112,8 +115,8 @@ var interactionNode = z.object({
112
115
  params: z.record(z.string(), z.unknown()).optional(),
113
116
  enabled: z.boolean().optional(),
114
117
  when: ruleGroup.optional().describe("QueryBuilder RuleGroup gating this step (validated structurally)"),
115
- elementId: z.string().optional().describe("Element whose callback this step invokes; defaults to this element. Give its ref or its raw id — a raw id is normalised to the element idRef the runtime looks callbacks up by, and a target without an idRef is given one automatically."),
118
+ elementId: z.string().optional().describe("Element whose callback this step invokes; defaults to this element. Its ref or raw id — a raw id is normalised to the idRef the runtime looks callbacks up by, and a target without one is given it."),
116
119
  preview: z.record(z.string(), z.unknown()).optional()
117
120
  });
118
121
  //#endregion
119
- export { bindingCategory, bindingInput, elementInput, initialStateInput, interactionNode, interactionNodeType, position, ruleGroup, scalar, styleRefs, styleVariantInput };
122
+ export { bindingCategory, bindingInput, elementInput, elementShape, initialStateInput, interactionNode, interactionNodeType, position, ruleGroup, scalar, styleRefs, styleVariantInput };
@@ -0,0 +1,54 @@
1
+ import { bindingInput, elementInput, initialStateInput, interactionNode, position, ruleGroup, styleRefs, styleVariantInput } from "./schema/shared.js";
2
+ import { cssMap, cssPatchMap, definitionSlot, definitionSlotPatch, displayModeCss, displayModeCssPatch, themeValue } from "./style/shared.js";
3
+ import { z } from "zod";
4
+ //#region src/modules/mcp/tools/operations/schemaIds.ts
5
+ /** The op union is the tool input of plitzi_apply, plitzi_validate, plitzi_render AND plitzi_preview, and every
6
+ * host reads it as JSON Schema on tools/list — four copies of the same 30 operations in the model's context, on
7
+ * EVERY request of every conversation the server is connected to. It measured ~25k tokens PER TOOL (~100k in
8
+ * total), which dwarfs anything a widget payload ever costs.
9
+ *
10
+ * Most of that weight is one subschema pasted over and over: an element tree appears in upsertElement,
11
+ * patchElement and both repeat templates; a breakpoint CSS block in every style op; a rule group in every binding
12
+ * and interaction step. Zod emits a named `definitions` entry (and `$ref`s to it) for any schema carrying an `id`
13
+ * in the global registry, so registering the shared ones here collapses those copies — without touching the ops,
14
+ * the tools, or the MCP SDK, whose own converter honours the registry (it is given no options otherwise).
15
+ *
16
+ * An id only pays when the schema is REUSED: giving one to a subschema that appears once makes the listing bigger,
17
+ * because the `definitions` entry and the ref cost more than the single copy they replace (measured, and reverted).
18
+ *
19
+ * The ids are the names the MODEL reads in the refs, so they are written for it: `Element`, not `ElementInput`.
20
+ * This runs once, at module load of the op vocabulary, so every conversion path benefits — the MCP tools list and
21
+ * the co-worker's own tool-schema converter alike. */
22
+ var SHARED_SCHEMAS = [
23
+ [elementInput, "Element"],
24
+ [ruleGroup, "RuleGroup"],
25
+ [styleRefs, "StyleRefs"],
26
+ [initialStateInput, "InitialState"],
27
+ [styleVariantInput, "StyleVariant"],
28
+ [interactionNode, "InteractionNode"],
29
+ [bindingInput, "Binding"],
30
+ [position, "Position"],
31
+ [cssMap, "Css"],
32
+ [cssPatchMap, "CssPatch"],
33
+ [definitionSlot, "StyleSlot"],
34
+ [definitionSlotPatch, "StyleSlotPatch"],
35
+ [displayModeCss, "BreakpointCss"],
36
+ [displayModeCssPatch, "BreakpointCssPatch"],
37
+ [themeValue, "ThemeValue"]
38
+ ];
39
+ var claimSchemaId = (schema, id) => {
40
+ const taken = z.globalRegistry._idmap.get(id);
41
+ if (taken !== void 0 && taken !== schema) {
42
+ z.globalRegistry.add(schema, { id: `Plitzi${id}` });
43
+ return;
44
+ }
45
+ z.globalRegistry.add(schema, { id });
46
+ };
47
+ /** Idempotent: the module can be imported more than once in a test run, and a schema that already carries its id
48
+ * is left alone (re-adding the same one is harmless but pointless). Runs at module load, never per request — the
49
+ * MCP builds a server per request and this must not grow with them (pinned in the statelessness test). */
50
+ var registerSharedSchemaIds = () => {
51
+ for (const [schema, id] of SHARED_SCHEMAS) if (z.globalRegistry.get(schema)?.id === void 0) claimSchemaId(schema, id);
52
+ };
53
+ //#endregion
54
+ export { claimSchemaId, registerSharedSchemaIds };
@@ -1,8 +1,8 @@
1
1
  import { defUri, defsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { definitionToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/definitions/patchDefinition.ts
8
8
  var patchDefinitionOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { defUri, defsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/definitions/upsertDefinition.ts
7
7
  var upsertDefinitionOp = z.object({
@@ -0,0 +1,40 @@
1
+ import { defUri, defsUri } from "../../../../helpers/uris.js";
2
+ import { empty } from "../../../../helpers/opResult.js";
3
+ import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
+ import { z } from "zod";
6
+ //#region src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.ts
7
+ var definitionBody = z.object(upsertCssShape);
8
+ var upsertDefinitionsOp = z.object({
9
+ type: z.literal("upsertDefinitions"),
10
+ definitions: z.record(z.string(), definitionBody).describe("Every class keyed by the class name you choose (kebab-case); the value is that class CSS")
11
+ }).describe("Create or fully replace SEVERAL style classes in ONE operation — identical to a run of upsertDefinition, minus the repeated envelope, so prefer it whenever a batch declares more than one class. Apply each by attaching its name to an element via style.base. Use patchDefinition to change only some CSS of one class.");
12
+ /** Writes each entry exactly as {@link upsertDefinition} would. Entries are independent: one bad class reports its
13
+ * own error under `definitions.<ref>` and the rest still write — nothing is persisted while the batch carries
14
+ * errors, so a partial write cannot escape. */
15
+ var upsertDefinitions = (space, env, op) => {
16
+ const result = {
17
+ ...empty(),
18
+ staleResources: [defsUri(env)]
19
+ };
20
+ const errors = [];
21
+ for (const [ref, { slots, ...base }] of Object.entries(op.definitions)) {
22
+ const guard = guardKind(space.style, ref, "class");
23
+ if (guard?.errors) {
24
+ errors.push(...guard.errors.map((error) => ({
25
+ ...error,
26
+ path: `definitions.${ref}`
27
+ })));
28
+ continue;
29
+ }
30
+ writeStyleItem(space.style, ref, base, slots, "class", void 0);
31
+ result.updated += 1;
32
+ result.staleResources.push(defUri(env, ref));
33
+ }
34
+ return errors.length > 0 ? {
35
+ ...result,
36
+ errors
37
+ } : result;
38
+ };
39
+ //#endregion
40
+ export { upsertDefinitions, upsertDefinitionsOp };
@@ -1,8 +1,8 @@
1
1
  import { globalUri, globalsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { globalStyleToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.ts
8
8
  var patchGlobalStyleOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { globalUri, globalsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.ts
7
7
  var upsertGlobalStyleOp = z.object({
@@ -1,8 +1,8 @@
1
1
  import { idUri, idsUri } from "../../../../helpers/uris.js";
2
2
  import { empty, fail } from "../../../../helpers/opResult.js";
3
3
  import { idStyleToAI } from "../translator.js";
4
- import { guardKind, mergePatch, writeStyleItem } from "../write.js";
5
4
  import { patchCssShape } from "../shared.js";
5
+ import { guardKind, mergePatch, writeStyleItem } from "../write.js";
6
6
  import { z } from "zod";
7
7
  //#region src/modules/mcp/tools/operations/style/idStyles/patchIdStyle.ts
8
8
  var patchIdStyleOp = z.object({
@@ -1,7 +1,7 @@
1
1
  import { idUri, idsUri } from "../../../../helpers/uris.js";
2
2
  import { empty } from "../../../../helpers/opResult.js";
3
- import { guardKind, writeStyleItem } from "../write.js";
4
3
  import { upsertCssShape } from "../shared.js";
4
+ import { guardKind, writeStyleItem } from "../write.js";
5
5
  import { z } from "zod";
6
6
  //#region src/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.ts
7
7
  var upsertIdStyleOp = z.object({
@@ -1,6 +1,7 @@
1
1
  import { deleteDefinitionOp } from "./definitions/deleteDefinition.js";
2
2
  import { patchDefinitionOp } from "./definitions/patchDefinition.js";
3
3
  import { upsertDefinitionOp } from "./definitions/upsertDefinition.js";
4
+ import { upsertDefinitionsOp } from "./definitions/upsertDefinitions.js";
4
5
  import { deleteGlobalStyleOp } from "./globalStyles/deleteGlobalStyle.js";
5
6
  import { patchGlobalStyleOp } from "./globalStyles/patchGlobalStyle.js";
6
7
  import { upsertGlobalStyleOp } from "./globalStyles/upsertGlobalStyle.js";
@@ -12,6 +13,7 @@ import { upsertStyleVariableOp } from "./variables/upsertStyleVariable.js";
12
13
  //#region src/modules/mcp/tools/operations/style/operations.ts
13
14
  var styleOps = {
14
15
  upsertDefinition: upsertDefinitionOp,
16
+ upsertDefinitions: upsertDefinitionsOp,
15
17
  patchDefinition: patchDefinitionOp,
16
18
  deleteDefinition: deleteDefinitionOp,
17
19
  upsertGlobalStyle: upsertGlobalStyleOp,
@@ -56,4 +56,4 @@ var patchCssShape = {
56
56
  slots: z.record(z.string(), definitionSlotPatch).optional()
57
57
  };
58
58
  //#endregion
59
- export { displayModeCss, displayModeCssPatch, patchCssShape, styleCategory, themeValue, upsertCssShape };
59
+ export { cssMap, cssPatchMap, definitionSlot, definitionSlotPatch, displayModeCss, displayModeCssPatch, patchCssShape, styleCategory, themeValue, upsertCssShape };
@@ -2,9 +2,12 @@ import { RENDER_APP_URI } from "../apps/render/index.js";
2
2
  import { emptySpace } from "../helpers/space.js";
3
3
  import { operations } from "./operations/index.js";
4
4
  import { applyOperations } from "./apply/dispatch.js";
5
+ import { expandOperations } from "./shared/expandOperations.js";
5
6
  import { defineTool } from "./shared/tool.js";
6
7
  import { validateOperations } from "./shared/validator/index.js";
7
8
  import { auditResources } from "./shared/validator/audit.js";
9
+ import { randomUUID } from "node:crypto";
10
+ import { z } from "zod";
8
11
  import { validateSchema } from "@plitzi/sdk-schema/helpers/schemaValidator";
9
12
  import { generateCache } from "@plitzi/sdk-style/StyleHelper";
10
13
  //#region src/modules/mcp/tools/render.ts
@@ -32,16 +35,26 @@ var seedSpace = () => {
32
35
  return space;
33
36
  };
34
37
  var noWarnings = (warnings) => warnings.length > 0 ? warnings : void 0;
35
- var renderShape = { operations };
38
+ var renderShape = {
39
+ operations,
40
+ patch: z.boolean().optional().describe("Set true to CHANGE a widget you already rendered instead of rebuilding it: send its `renderId` and ONLY the operations that differ (patchDefinition, patchElement, deleteElement, a new repeatElement…). The widget merges them into the batch it was built from and reports back what it applied. The refs it already has (card-1, blk-2-3) are the ones to address. If that widget cannot be recovered — a surface that renders none, a host that keeps no storage — the answer says so and you re-send the whole batch."),
41
+ renderId: z.string().optional().describe("Handle returned by a previous render. Required with patch:true; it names the widget being changed.")
42
+ };
36
43
  var render = (input) => {
37
44
  const space = seedSpace();
38
- const validation = validateOperations(space, input.operations);
45
+ const expansion = expandOperations(input.operations);
46
+ if (expansion.errors.length > 0) return {
47
+ rendered: false,
48
+ errors: expansion.errors
49
+ };
50
+ const ops = expansion.operations;
51
+ const validation = validateOperations(space, ops);
39
52
  if (!validation.valid) return {
40
53
  rendered: false,
41
54
  errors: validation.errors,
42
55
  warnings: noWarnings(validation.warnings)
43
56
  };
44
- const outcome = applyOperations(space, "main", input.operations);
57
+ const outcome = applyOperations(space, "main", ops);
45
58
  if (outcome.errors.length > 0) return {
46
59
  rendered: false,
47
60
  errors: outcome.errors,
@@ -57,7 +70,7 @@ var render = (input) => {
57
70
  })),
58
71
  warnings: noWarnings(validation.warnings)
59
72
  };
60
- const audit = auditResources(space, input.operations);
73
+ const audit = auditResources(space, ops);
61
74
  const warnings = [...validation.warnings, ...audit.warnings];
62
75
  if (audit.errors.length > 0) return {
63
76
  rendered: false,
@@ -67,6 +80,7 @@ var render = (input) => {
67
80
  space.style.cache = generateCache(space.style);
68
81
  return {
69
82
  rendered: true,
83
+ operations: ops,
70
84
  rootRef: HOST_PAGE_REF,
71
85
  elementCount: Object.keys(space.schema.flat).length - 1,
72
86
  offlineData: {
@@ -76,7 +90,53 @@ var render = (input) => {
76
90
  warnings: noWarnings(warnings)
77
91
  };
78
92
  };
79
- var toRenderResult = (res) => {
93
+ /** A patch is not rendered here — it CANNOT be: this server keeps nothing between calls (no session, no store,
94
+ * any replica answers any request), so the previous widget only exists on the host side. The delta therefore
95
+ * travels as a courier result: the model pays for the delta alone, the view recovers the batch that widget was
96
+ * built from (by renderId — see apps/render/heldBatch.ts), merges, and re-calls this same tool with the whole
97
+ * thing over the host bridge, never through the model's context, then reports back with ui/update-model-context.
98
+ *
99
+ * The renderId is the explicit handle the MCP RC asks for in place of implicit session state: the model carries
100
+ * it, so a patch names the widget it means and any replica can serve it.
101
+ *
102
+ * Validation is not skipped, only deferred: the re-call carries the full batch, so refs, integrity and the audit
103
+ * all run exactly as they do on a first render, and their errors reach the model through that report. */
104
+ var toPatchResult = (ops, renderId) => {
105
+ if (ops.length === 0) return { content: [{
106
+ type: "text",
107
+ text: JSON.stringify({
108
+ patch: false,
109
+ error: "A patch carries no operations",
110
+ hint: "Send the operations that differ from the widget on screen, or drop `patch` to render a new one."
111
+ })
112
+ }] };
113
+ if (renderId === void 0) return { content: [{
114
+ type: "text",
115
+ text: JSON.stringify({
116
+ patch: false,
117
+ error: "A patch needs the renderId of the widget it changes",
118
+ hint: "Pass the renderId the render answered with, or drop `patch` to render a new widget."
119
+ })
120
+ }] };
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: JSON.stringify({
125
+ patch: true,
126
+ renderId,
127
+ operations: ops.length,
128
+ note: "Handed to the widget; it will report what it applied. If nothing reports back, it could not be recovered — re-send the full batch without patch."
129
+ })
130
+ }],
131
+ structuredContent: {
132
+ patch: true,
133
+ renderId,
134
+ operations: ops
135
+ }
136
+ };
137
+ };
138
+ var newRenderId = () => `r${randomUUID().slice(0, 8)}`;
139
+ var toRenderResult = (res, renderId) => {
80
140
  if (!res.rendered) return { content: [{
81
141
  type: "text",
82
142
  text: JSON.stringify({
@@ -87,6 +147,7 @@ var toRenderResult = (res) => {
87
147
  }] };
88
148
  const summary = {
89
149
  rendered: true,
150
+ renderId,
90
151
  rootRef: res.rootRef,
91
152
  elementCount: res.elementCount,
92
153
  warnings: res.warnings
@@ -98,19 +159,20 @@ var toRenderResult = (res) => {
98
159
  }],
99
160
  structuredContent: {
100
161
  ...summary,
101
- offlineData: res.offlineData
162
+ offlineData: res.offlineData,
163
+ operations: res.operations
102
164
  }
103
165
  };
104
166
  };
105
167
  var renderTool = defineTool({
106
168
  name: "plitzi_render",
107
169
  title: "Render widget",
108
- description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n2. STYLE — declare reusable classes with upsertDefinition { ref, desktop:{ …CSS props in kebab-case… } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid.\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nReturns a compact summary (the widget is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
170
+ description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look (not per property), and never hand-draw a scene in a data: URI — it costs more than the whole widget; use an https image, a flat colour or a gradient.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Watch the SDK defaults: every container has min-width/min-height 50px (set them to \"0\" for rails, dividers, dots and any flex child that must shrink), and heading/paragraph keep the margins the browser gives them (zero them, space with the parent gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nITERATING — to change a widget you already rendered, do NOT rebuild it: call again with patch:true, the `renderId` that render answered with, and ONLY the operations that differ (patchDefinition, patchElement, deleteElement…). The widget merges them and reports back what it applied; address rows by the refs you already know. Patch ONLY to modify that widget: a different subject or a different kind of widget is a fresh render, without patch — a patch is merged into the previous batch, so patching a new idea leaves you with both.\nReturns a compact summary including the renderId (the widget itself is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
109
171
  inputShape: renderShape,
110
172
  access: "read",
111
173
  spaceless: true,
112
174
  ui: { resourceUri: RENDER_APP_URI },
113
- run: (input) => toRenderResult(render(input))
175
+ run: (input) => input.patch === true ? toPatchResult(input.operations, input.renderId) : toRenderResult(render(input), input.renderId ?? newRenderId())
114
176
  });
115
177
  //#endregion
116
178
  export { render, renderShape, renderTool };