@plitzi/sdk-mcp 0.37.9 → 0.37.10

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 (52) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +2 -2
  3. package/dist/modules/mcp/apps/render/view/streamProgress.ts +1 -3
  4. package/dist/modules/mcp/helpers/agentPrompt.js +7 -5
  5. package/dist/modules/mcp/helpers/guide.js +122 -5
  6. package/dist/modules/mcp/helpers/uris.d.ts +1 -0
  7. package/dist/modules/mcp/helpers/uris.js +2 -1
  8. package/dist/modules/mcp/resources/explain.d.ts +19 -0
  9. package/dist/modules/mcp/resources/explain.js +22 -0
  10. package/dist/modules/mcp/resources/explain.test.d.ts +1 -0
  11. package/dist/modules/mcp/resources/primer.js +6 -1
  12. package/dist/modules/mcp/resources/register.js +10 -0
  13. package/dist/modules/mcp/resources/schema.js +2 -1
  14. package/dist/modules/mcp/server.js +1 -1
  15. package/dist/modules/mcp/tests/flags.test.d.ts +1 -0
  16. package/dist/modules/mcp/tools/apply/dispatch.js +4 -0
  17. package/dist/modules/mcp/tools/apply/index.d.ts +16 -0
  18. package/dist/modules/mcp/tools/apply/index.js +5 -3
  19. package/dist/modules/mcp/tools/operations/index.d.ts +64 -0
  20. package/dist/modules/mcp/tools/operations/index.js +2 -0
  21. package/dist/modules/mcp/tools/operations/schema/elements/patchElement.d.ts +2 -0
  22. package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +9 -2
  23. package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +3 -0
  24. package/dist/modules/mcp/tools/operations/schema/flags/deleteFlag.d.ts +9 -0
  25. package/dist/modules/mcp/tools/operations/schema/flags/deleteFlag.js +21 -0
  26. package/dist/modules/mcp/tools/operations/schema/flags/upsertFlag.d.ts +15 -0
  27. package/dist/modules/mcp/tools/operations/schema/flags/upsertFlag.js +38 -0
  28. package/dist/modules/mcp/tools/operations/schema/index.d.ts +2 -0
  29. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +18 -0
  30. package/dist/modules/mcp/tools/operations/schema/operations.js +4 -0
  31. package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.d.ts +1 -0
  32. package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +8 -2
  33. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +1 -0
  34. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +28 -2
  35. package/dist/modules/mcp/tools/operations/schema/shared.d.ts +8 -0
  36. package/dist/modules/mcp/tools/operations/schema/shared.js +10 -2
  37. package/dist/modules/mcp/tools/operations/schema/translator.js +8 -0
  38. package/dist/modules/mcp/tools/operations/schema/write.js +4 -1
  39. package/dist/modules/mcp/tools/operations/schemaIds.js +3 -1
  40. package/dist/modules/mcp/tools/preview.d.ts +16 -0
  41. package/dist/modules/mcp/tools/render.d.ts +16 -0
  42. package/dist/modules/mcp/tools/screenshot.d.ts +16 -0
  43. package/dist/modules/mcp/tools/shared/draftBatch.d.ts +2 -0
  44. package/dist/modules/mcp/tools/shared/draftBatch.js +3 -1
  45. package/dist/modules/mcp/tools/shared/lintDraft.js +16 -4
  46. package/dist/modules/mcp/tools/shared/newSuggestions.d.ts +7 -0
  47. package/dist/modules/mcp/tools/shared/newSuggestions.js +20 -0
  48. package/dist/modules/mcp/tools/validate.d.ts +16 -0
  49. package/dist/modules/mcp/tools/validate.js +8 -6
  50. package/dist/modules/mcp/types/aiSchema.d.ts +6 -1
  51. package/dist/modules/mcp/types/toolTypes.d.ts +4 -0
  52. package/package.json +9 -9
@@ -20,7 +20,8 @@ var TOOLS = [
20
20
  "• plitzi_search — find elements by label/type/attribute; a hit carries the NAME every op takes (include:\"detail\"",
21
21
  " also returns its style + versions).",
22
22
  "• plitzi_read — batch-read one or more resource URIs you already hold (from search or a write response).",
23
- "• plitzi_validate — check a batch of operations without executing; returns teachable errors and warnings.",
23
+ "• plitzi_validate — check a batch of operations without executing; returns teachable errors and warnings, and",
24
+ " suggestions: a shorter way to the same page (a layout, a component, the content of a link) — never blocking.",
24
25
  "• plitzi_apply — persist a batch of operations. Pass dryRun to preview the full diff without writing, and",
25
26
  " expectedResourceVersions to guard against concurrent edits (apply and search hand back the versions you need).",
26
27
  "• plitzi_preview / plitzi_screenshot — render a page (HTML / PNG), with unsaved operations applied if you pass them.",
@@ -57,10 +58,11 @@ var ACCESSIBILITY = [
57
58
  ].join("\n");
58
59
  var NAVIGATION = [
59
60
  "━━ NAVIGATION & DYNAMIC PAGES ━━",
60
- "To move between pages, PREFER the Link element over an interaction — it is a CONTAINER that wraps any children and",
61
- "navigates on click. mode \"page\" links to another space page (href = the target page); mode \"internal\" takes a",
62
- "path inside the space and resolves {{token}} templates in it (e.g. posts/{{postId}}); mode \"external\" is a full",
63
- "URL. Reach for the navigate globalCallback only when navigation must be one step inside a larger flow.",
61
+ "To move between pages, PREFER the Link element over an interaction — its words are its `content`, and it can wrap",
62
+ "any children too. The link to the page being shown is styled by its class's `current` state. mode \"page\" links",
63
+ "to another space page (href = the target page); mode \"internal\" takes a path inside the space and resolves",
64
+ "{{token}} templates in it (e.g. posts/{{postId}}); mode \"external\" is a full URL. Reach for the navigate",
65
+ "globalCallback only when navigation must be one step inside a larger flow.",
64
66
  "A page slug is RELATIVE — never start it with \"/\" (the runtime prepends it). Folder slugs PREPEND to the page",
65
67
  "slug, so a page at slug \"post\" inside folders \"blog\" > \"2024\" resolves to /blog/2024/post.",
66
68
  "A slug segment \":name\" (e.g. \"posts/:postId\") declares a ROUTE PARAM, react-router style. It is readable on that",
@@ -36,6 +36,10 @@ vars. **Write plain CSS** — shorthands (\`border\`, \`padding\`, \`margin\`, \
36
36
  still \`display: flex\` + \`flex-direction\`/\`align-items\`/…, not a \`flex\` value. Mind a type's \`defaultStyle\`
37
37
  (\`text\` is \`display: inline\`). Global styles (\`button {…}\`) and id styles (\`#id\`) have their own ops.
38
38
 
39
+ **Motion:** animate only \`opacity\` and \`transform\` — they stay smooth while the page loads; an animated blur,
40
+ shadow, size, \`top\`/\`left\` or \`background-position\` stutters on every visitor's machine. Good practices in
41
+ \`plitzi://guide\` (Motion).
42
+
39
43
  **Data bindings** (\`upsertBinding\`, category attributes|style|initialState): connect a \`source\` to a \`to\` field.
40
44
  A source \`<type>_<name>\` is scoped to the provider's **DESCENDANTS only** — bind inside the provider's subtree
41
45
  (the global sources ${GLOBALS} are bindable anywhere). \`apiContainer.mockData\` is builder-only; set a
@@ -43,6 +47,14 @@ real \`query\` for production. \`transformers: [{action, params}]\` post-process
43
47
  from \`plitzi://data-sources\`; \`twigTemplate\` formats it (the value is \`{{source}}\`, not \`{{value}}\`). \`when\` is
44
48
  a QueryBuilder RuleGroup gating the binding.
45
49
 
50
+ **Fewer elements, same page:** a \`link\` or \`button\` says its words itself — \`content\`, with an icon beside them
51
+ as \`icon\` (\`iconPlacement\` before/after, its class on the \`icon\` slot) — never a \`text\` or \`fontAwesome\` child;
52
+ a text child also ignores the box's colour and hover. Chrome on several pages is a **layout**; the nav item of the page being shown is marked by the
53
+ \`current\` state of its class, not by a copy of the header per page. Sibling rows of data are one \`repeatElement\`; a
54
+ block repeated with other words is a **component** (\`upsertComponent { fromRef }\`). \`plitzi_validate\` and
55
+ \`plitzi_apply\` answer with **\`suggestions\`** for what the batch could have written shorter, each with the elements
56
+ it saves — never blocking; take them unless the copies are about to diverge.
57
+
46
58
  **Interactions** (\`upsertInteractionFlow\`): a \`trigger\` node first, then callbacks/utilities **in order** (links
47
59
  computed for you). Node types: \`callback\` (an element's own callback — \`elementId\` is that element), \`globalCallback\`
48
60
  (a source module — omit \`elementId\`, the MCP sets it), \`utility\` (no element). Element \`setState\`
@@ -65,7 +77,11 @@ flow — list page, detail page, paging, writes — in \`plitzi://guide\`.
65
77
  **Pages & navigation:** \`upsertPage\` — always set a **relative** \`slug\` (no leading \`/\`; the runtime and folder
66
78
  slugs prepend the path). A \`:name\` segment (\`"posts/:postId"\`) is a route param, readable as \`{{name}}\` and as the
67
79
  source \`navigation.routeParams.name\` → build dynamic pages this way. To move between pages **prefer the \`Link\`
68
- element** (a container: \`mode\` "page"/"internal"/"external") over a \`navigate\` interaction.
80
+ element** (\`mode\` "page"/"internal"/"external"; its words are its \`content\`, and it can wrap any children too) over
81
+ a \`navigate\` interaction.
82
+
83
+ **Feature flags:** what a person switches on (a beta, a rollout) is \`upsertFlag\` plus \`flag: "x"\` / \`"!x"\` on the
84
+ element or page — not \`visible\`. Read anywhere as \`{{ flags.x }}\`.
69
85
 
70
86
  **Accessible by default:** screen readers and browser agents (Claude in Chrome) read the accessibility tree, so every
71
87
  button/link needs words (an icon-only button a \`title\`), every field a \`label\` (\`hideLabel: true\` hides it), every
@@ -76,7 +92,8 @@ level at a time. \`plitzi_screenshot view:"accessibility"\` shows the tree and w
76
92
  the save on any \`Pre-existing malformation in element …\` error (a broken transformer, a malformed step, an attribute
77
93
  it never reads) — even parts you did not touch. These are NOT from your change (the message says so); fix them in the
78
94
  SAME batch and re-apply (the check runs on the result, so the fix unblocks it). \`Pre-existing issue\` warnings advise
79
- but do not block. A broken tree your batch leaves ANYWHERE (an element orphaned by a delete) blocks it too.
95
+ but do not block. A broken tree your batch leaves ANYWHERE (an element orphaned by a delete) blocks it too. Each
96
+ problem leads with its \`[code]\`, and its hint says what to write instead.
80
97
 
81
98
  Read \`plitzi://guide\` before anything above is unclear.
82
99
  `;
@@ -110,6 +127,9 @@ Never download a whole tree you do not need.
110
127
  Read the descriptions to pick the right type — e.g. \`apiContainer\` fetches backend data into the frontend,
111
128
  \`link\` navigates between pages, \`list\` repeats a template over a data array. \`plugin\` types are custom elements.
112
129
  - \`plitzi://css-properties\` — valid kebab-case CSS property keys.
130
+ - \`plitzi://explain/{name}\` — what a name means: an element type's attributes, triggers and callbacks; a step's
131
+ params; what a trigger hands its flow; a problem code's fix. \`plitzi://explain/steps\` (or elements, triggers,
132
+ codes, transformers) lists every one of a kind.
113
133
  - \`plitzi://schema/{env}/pages\` — page **summaries** (ref, label, elementCount, folder). No element trees.
114
134
  - \`plitzi://schema/{env}/layouts\` — the shared **layout shells** (header/sidebar/footer) and the pages rendered
115
135
  inside each. Read one like a page. See *Shared layouts* below.
@@ -135,6 +155,7 @@ Never download a whole tree you do not need.
135
155
  - \`plitzi://style-variables/{env}\` — design tokens by category. \`/{category}\` for one.
136
156
  - \`plitzi://fonts/{env}\` — the font **families the space loads**, with their source and fallback. \`/{family}\` for one.
137
157
  - \`plitzi://schema-variables/{env}\` — space-level values referenced in props as \`{{name}}\`.
158
+ - \`plitzi://flags/{env}\` — the space's **feature flags**, each a default and its rules (see **Feature flags**).
138
159
  - \`plitzi://settings/{env}\` — space-level settings: the global \`customCss\` and the state/auth (user-provider) config.
139
160
  - \`plitzi://interactions/{env}\` — interaction **actions** observed in this space (grouped by node type): the
140
161
  vocabulary for interaction flows.
@@ -188,10 +209,10 @@ When you do hold several refs to open (e.g. from a skeleton), read them together
188
209
 
189
210
  ## Tools
190
211
  - \`plitzi_validate\` — check a batch, returns teachable errors/warnings. Writes nothing. Also reports **pre-existing
191
- malformations** in any resource the batch touches (see below).
212
+ malformations** in any resource the batch touches (see below), and \`suggestions\` (see *Suggestions*).
192
213
  - \`plitzi_apply\` — validate → apply → persist atomically. Rejects the whole batch on any error or conflict. Pass
193
214
  \`dryRun: true\` to apply in memory only and get the same result back (changed versions + full element detail)
194
- without persisting — inspect it, then re-run without \`dryRun\` to commit.
215
+ without persisting — inspect it, then re-run without \`dryRun\` to commit. Its answer carries \`suggestions\` too.
195
216
  - \`plitzi_search\` — find elements (and pages/definitions) across the space.
196
217
  - \`plitzi_read\` — read many resource **uris in one batch** (pages, elements, definitions, variables). Pass the
197
218
  ready-made uris from search / a write response; each result is \`{ uri, stateVersion, data }\` or a teachable error,
@@ -204,6 +225,25 @@ When you do hold several refs to open (e.g. from a skeleton), read them together
204
225
  (\`"both"\` returns both). Both need the SSR render service; where it is missing they answer \`PREVIEW_UNAVAILABLE\`.
205
226
  - \`plitzi_render\` — a different job: show the user a self-contained widget built offline. It never touches the space.
206
227
 
228
+ ## Suggestions — the same page with fewer elements
229
+ \`plitzi_validate\` and \`plitzi_apply\` answer with \`suggestions\`: lines like \`[content-attribute] … (saves 4
230
+ elements)\`, each naming a shorter way to what the batch just wrote. Only what THIS batch opened up is said — what the
231
+ space already had is not repeated on every batch — and none of them blocks anything: the page renders as written.
232
+ - \`repeated-on-pages\` — the same block at the edge of several pages: put it in a **layout** once (a block only some
233
+ pages of a layout carry is a **component** instead). Copies differing only in one link's class are the nav item of the
234
+ page being shown: the \`current\` state marks it (see *Pages & folders*).
235
+ - \`repeated-shape\` — one structure written again with other words: a **component** with props
236
+ (\`upsertComponent { fromRef }\` turns one copy into it), or, for sibling rows of data, one \`repeatElement\`/\`list\`.
237
+ A few cards a person rewords by hand can stay cards.
238
+ - \`content-attribute\` — a \`button\` or \`link\` whose children are only a \`text\` and/or a \`fontAwesome\`: its words
239
+ are its own \`content\` and the icon its \`icon\`. What the text's class adds (\`white-space: nowrap\`) moves to the
240
+ box's class — never the class itself; the icon's class goes in the \`icon\` slot.
241
+ - \`custom-css-class\`, \`custom-css-sdk-default\`, \`custom-css-notifications\` — \`customCss\` that a class's
242
+ \`states\`/\`ancestors\`, the SDK (reduced motion, the theme toggle's icons) or the space's notification look says.
243
+ - \`heavy-animation\` — keyframes that animate a size, a position, a blur, a shadow, or a colour in a loop: they stutter
244
+ while the page loads. Lighter, not shorter — see **Motion — good practices**.
245
+ \`plitzi://explain/<code>\` explains each. Take one unless the copies are about to diverge.
246
+
207
247
  ## Readers: resources vs plitzi_search vs plitzi_read (do not confuse them)
208
248
  Three ways to read, each for a different moment — pick by what you have in hand:
209
249
  - **MCP resources** (the \`plitzi://…\` catalog above) — the **browsable index**. List them to discover what exists, or
@@ -255,6 +295,12 @@ You do not have to rewrite them.
255
295
  - \`deleteElement { pageRef, ref }\` removes an element **and everything under it**. A binding or an interaction
256
296
  elsewhere that named one of them is left pointing at nothing, and the save reports it — delete them in the same
257
297
  batch. Destructive: confirm first.
298
+ - \`flag\` on \`upsertElement\` / \`patchElement\` gates an element on a feature flag (\`"newCheckout"\` or
299
+ \`"!newCheckout"\`) — not a visibility: gated off it is not rendered at all. See **Feature flags**.
300
+ - **A compound element is written with its parts**, as \`children\` of the same \`upsertElement\`: a \`carousel\` holds a
301
+ \`carouselTrack\` (the slides go in it), a \`tabContainer\` a \`tabContainerHeader\` and a \`tabContainerBody\`, a
302
+ \`dropdown\` a \`dropdownPopup\`. Without its part it shows nothing (\`part-missing\`); a part anywhere else breaks the
303
+ page (\`outside-ancestor\`).
258
304
 
259
305
  ## Styling (crosses both schemas)
260
306
  - **Mind the type's intrinsic default style.** A type renders with a base CSS *before* any class is attached — read
@@ -681,6 +727,30 @@ element binds \`grant\` to it. Without one the topic is refused however well its
681
727
  \`expected\` = only if nothing is there yet), ordered lists with \`list.put\` / \`list.range\`, and put
682
728
  \`flow.rateLimit\` first in any public action that writes.
683
729
 
730
+ ## Feature flags
731
+ A flag turns a part of the space on or off without anyone editing it: a feature still being built, a version for
732
+ beta users, the old checkout kept until the new one ships. **Not a visibility** — visibility hides an element that is
733
+ still rendered; a flag decides whether it exists at all.
734
+
735
+ - **Declare** with \`upsertFlag { name, description?, value, rules? }\`. \`value\` is its answer when no rule matches;
736
+ \`rules\` are \`[{ when, value }]\` read top to bottom, the first match deciding. A rule's \`when\` is a rule group over
737
+ \`environment\` (\`main\`, \`development\`, \`staging\`, \`production\`), \`hostname\`, \`routeParams.<name>\`,
738
+ \`queryParams.<name>\` and the visitor: \`user.authenticated\`, \`user.email\`, \`user.username\`, \`user.roles\` (with
739
+ \`contains\`). A group with no rules is skipped, never read as "always". \`deleteFlag { name }\` removes one.
740
+ - **Gate** an element with \`flag\` on \`upsertElement\` / \`patchElement\`: \`"newCheckout"\` exists only while the flag is
741
+ on, \`"!newCheckout"\` only while it is off — put both side by side for a rollout. On \`upsertPage\` a gated-off page
742
+ answers 404. \`null\` on a patch removes the gate. Gated off, it is not rendered and its server data is not
743
+ resolved — but the space's document, gated elements included, still reaches the browser: a flag is not a secret.
744
+ - **Read** it anywhere a source is read: \`{{ flags.newCheckout }}\` in a binding, a \`when\` of a flow step, a computed
745
+ value — and in a server action's steps, where it is resolved on the server.
746
+
747
+ A gate or a read naming a flag the space does not declare is an error (\`flag-undeclared\`, \`flag-unknown\`): an
748
+ undeclared flag is off. A declared flag nothing reads is a warning (\`flag-unused\`) — remove it once the feature
749
+ ships. The draft applies flags at once. Flags are not part of a snapshot: each environment has one set, shared by every
750
+ snapshot it serves — publishing a snapshot sends the draft's with it (and the snapshot keeps a copy, as a fallback), the
751
+ builder's *Publish flags* sends only them, and a rollback keeps them. Above the space, the server rendering it, the SDK embedding it and a tester with the dev tools
752
+ may each override a flag; you only ever write the space's own.
753
+
684
754
  ## Shared layouts — the chrome a page does NOT contain
685
755
  A page's tree is usually **not the whole page**. The header, the sidebar and the footer normally live in a **layout
686
756
  container**: a shell several pages are rendered inside, so the navigation is authored once instead of once per page.
@@ -714,6 +784,9 @@ vocabulary:
714
784
  - \`deleteLayout\` removes a shell and everything in it, and is **refused while any page still renders inside it**
715
785
  (the refusal names them). Destructive: what it deletes is on every page that uses it, so confirm first.
716
786
 
787
+ **Only some pages?** A block that only some of a layout's pages carry (a newsletter band on two of ten) is not
788
+ chrome: make it a **component** and place it on those pages.
789
+
717
790
  **Where does this element belong?** If the user asks for something "on every page" — a nav item, a banner, a footer
718
791
  link, an upgrade prompt in the sidebar — it belongs in the LAYOUT, not in the page you happen to have open. Check
719
792
  the page's \`layout\` first; if the element you are looking for is not in the page's own tree, it is in the shell.
@@ -724,6 +797,10 @@ any page as an **instance**. It is part of the space, so it is published, versio
724
797
  not inside any page: it is a root of its own, like a layout, and **editing it changes every instance** — say so
725
798
  before you touch one.
726
799
 
800
+ It is not a **snippet**. The builder's *Save as snippet* keeps a copy of a block as a file people drop onto pages, and
801
+ each drop is the page's own from then on. No operation here writes one: when someone wants a block that stays the same
802
+ everywhere, that is a component.
803
+
727
804
  It is **closed**. Inside, a binding or a template reads **\`props\`** — what each instance hands in — and the globals
728
805
  (\`state\`, \`auth\`, \`navigation\`, \`theme\`, \`variables\`, \`computed\`), never the page an instance sits on. A
729
806
  component that needs a list row's record gets it as a prop: the INSTANCE binds it (\`item: "{{ list_rows.item }}"\`),
@@ -760,6 +837,8 @@ A required prop left out, a prop the component does not declare, a value of the
760
837
  the component does not have is reported by \`plitzi_validate\` — fix it before applying.
761
838
 
762
839
  ## Pages & folders
840
+ - **A page behind a feature flag**: \`upsertPage { flag: "labs" }\` answers 404 while the flag is off (see **Feature
841
+ flags**); \`flag: null\` removes it.
763
842
  - **Always set a \`slug\` when creating a page** (\`upsertPage\`) — it is the page's URL path and good practice for a
764
843
  clean, stable route (e.g. \`"pricing"\` or \`"posts/:postId"\`). Omit it and the page ref is used as the slug,
765
844
  and \`plitzi_validate\`/\`plitzi_apply\` warn so you remember to set a meaningful one.
@@ -779,6 +858,18 @@ the component does not have is reported by \`plitzi_validate\` — fix it before
779
858
  \`"external"\` is a full URL. \`target\` is \`self\`/\`blank\`/\`parent\`/\`top\`. Reach for the \`navigate\` globalCallback
780
859
  only when navigation must be **one step inside a larger interaction flow** (e.g. save, then go) — for a plain link,
781
860
  use \`link\`.
861
+ - **A link's words are its \`content\`, and an icon beside them its \`icon\`** — \`{ "type": "link", "props": { "mode":
862
+ "page", "href": "pricing", "content": "Pricing", "icon": "fa-solid fa-arrow-right", "iconPlacement": "after" } }\`
863
+ (a \`button\` takes the same two). The icon's class goes in the element's \`icon\` slot (\`style.slots.icon\`). With
864
+ children too, \`contentPlacement\` puts the words \`before\` or \`after\` them. A \`text\` or \`fontAwesome\` inside is
865
+ one element more, and a text keeps a colour of its own instead of the link's (and its hover).
866
+ - **The link to the page being shown marks itself**: it carries \`aria-current="page"\`, and its class's \`current\`
867
+ state says how it looks — \`{ "type": "patchDefinition", "ref": "nav-link", "states": { "current": { "desktop": {
868
+ "color": "var(--primary)" } } } }\`. So a header written once in a layout lights the right item on every page; never
869
+ copy the header per page to style one item.
870
+ - **A section of a page** is an element with an \`anchor\` (its \`id\` in the DOM: lowercase, digits, \`-\`; one per
871
+ page, layouts included; not inside a list row or a component). A link lands on it with \`hash\`:
872
+ \`{ "mode": "page", "href": "home", "hash": "plans" }\` goes to \`/#plans\` and scrolls there, from any page.
782
873
 
783
874
  Pages can be grouped into **folders** (the sidebar tree). A folder is \`{ ref, name, slug, parentId? }\`; its \`ref\`
784
875
  **is its id**, and that id is what a page and a nested folder reference.
@@ -803,6 +894,11 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
803
894
  (merge — only the fields you pass change):
804
895
  - \`customCss\` — **raw global CSS** injected for the whole space. Use it only for genuinely site-wide rules
805
896
  (\`@keyframes\`, \`@font-face\`, resets). To style an element, write a **definition** and attach it — never customCss.
897
+ - \`notifications\` — how the toasts an \`addNotification\` step shows look: colours (\`background\`, \`text\`,
898
+ \`success\`, \`danger\`, \`warning\`, \`info\`) and shape (\`radius\`, \`font\`, \`fontSize\`, \`border\`, \`shadow\`,
899
+ \`padding\`), any CSS value — a token follows the theme: \`{ "type": "patchSettings", "notifications": { "background":
900
+ "var(--card)", "border": "1px solid var(--border)" } }\`. Merged field by field, \`null\` removes one. It is stored as a
901
+ rule inside \`customCss\` but reads and changes apart from it: never write a \`.Toastify__toast\` rule yourself.
806
902
  - \`keepState\` / \`stateStorage\` — keep the runtime state (\`state.<key>\`, what \`setState\` writes) across reloads,
807
903
  in \`localStorage\`/\`sessionStorage\`, filed under whoever is signed in. It is the space's setting — pages do not
808
904
  take one. \`transientState\` lists the top-level keys never kept (a filter, a demo, a panel left open); do not
@@ -818,12 +914,33 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
818
914
  host: a \`link\` with \`mode: "external"\` to \`/auth/sign-in?return=/\` starts it, \`authLogout\` ends it.
819
915
  - \`visitorRoles\` — what each visitor role gives, \`{ "author": ["postPublish"] }\`: the permissions an action's
820
916
  \`access: { mode: "role" }\` and a page's \`can()\` ask for. A visitor holds ONLY the permissions of their roles here,
821
- never their account's. Who holds a role is given by email in the builder's Visitors panel — never in settings.
917
+ never their account's. Who holds a role is given by email in the builder's Settings → Visitors — never in the
918
+ space's \`settings\`.
822
919
  - \`channels\` — the realtime channels, by topic pattern (see **Realtime channels**). Merged pattern by pattern;
823
920
  \`null\` removes one.
824
921
  Example — inject a keyframe globally:
825
922
  \`{ "type": "patchSettings", "customCss": "@keyframes spin { to { transform: rotate(360deg); } }" }\`.
826
923
 
924
+ ## Motion — good practices
925
+ Keyframes go in \`customCss\` (\`patchSettings\`), a definition names them (\`animation\`). Only \`opacity\` and
926
+ \`transform\` (\`translate\`, \`scale\`, \`rotate\`) animate off the main thread: they stay smooth while the page
927
+ hydrates and on a slow phone. Anything else repaints or lays the page out again on every frame, and freezes while the
928
+ page's scripts run.
929
+ - Slide, grow, fade with \`transform\` and \`opacity\` — never \`left\`, \`top\`, \`width\`, \`height\` or
930
+ \`margin\`. Something travelling across a box is \`transform: translate(%)\` on an element as large as the box.
931
+ - No animated blur: no \`filter: blur()\` in an entrance or a loop, no \`backdrop-filter\` over moving content. A soft
932
+ shape is a \`radial-gradient\` fading to transparent.
933
+ - Fake the expensive ones: a growing shadow is a second shadow on a pseudo-element whose \`opacity\` changes; a
934
+ pulsing glow is a static gradient whose \`opacity\` or \`scale\` changes.
935
+ - Decoration that must animate something else (a custom property, \`background-position\`) starts paused and runs
936
+ once the page is hydrated: \`.glow { animation: glow 8s linear infinite paused; } [data-hydrated] .glow {
937
+ animation-play-state: running; }\` in \`customCss\`.
938
+ - Entrances: \`opacity\` + a small \`translateY\`, under ~600ms, staggered by tens of ms.
939
+ - One slow ambient loop per screen at most — not one per card: \`infinite\` runs on every visitor's battery.
940
+ - Name what transitions (\`transition: transform 200ms, opacity 200ms\`), never \`all\`; \`will-change\` only on the
941
+ few elements about to move.
942
+ - Reduced motion is the SDK's: it cuts every animation to an instant for whoever asks — never write that reset.
943
+
827
944
  ## Accessibility — pages people, screen readers and browser agents can all use
828
945
  A page is read three ways: seen, heard through a screen reader, and worked by a **browser agent** (Claude in Chrome)
829
946
  — which finds the controls it can press in the same **accessibility tree** a screen reader walks. A control that
@@ -13,6 +13,7 @@ export declare const elementUri: (env: Env, ref: string) => string;
13
13
  export declare const componentsUri: (env: Env) => string;
14
14
  export declare const schemaVarsUri: (env: Env) => string;
15
15
  export declare const settingsUri: (env: Env) => string;
16
+ export declare const flagsUri: (env: Env) => string;
16
17
  export declare const interactionsUri: (env: Env) => string;
17
18
  export declare const dataSourcesUri: (env: Env) => string;
18
19
  /** Worked manifests + the template vocabulary. Space-independent, hence no env — and a distinct root, so a
@@ -13,6 +13,7 @@ var elementUri = (env, ref) => `plitzi://schema/${env}/elements/${ref}`;
13
13
  var componentsUri = (env) => `plitzi://schema/${env}/components`;
14
14
  var schemaVarsUri = (env) => `plitzi://schema-variables/${env}`;
15
15
  var settingsUri = (env) => `plitzi://settings/${env}`;
16
+ var flagsUri = (env) => `plitzi://flags/${env}`;
16
17
  var interactionsUri = (env) => `plitzi://interactions/${env}`;
17
18
  var dataSourcesUri = (env) => `plitzi://data-sources/${env}`;
18
19
  /** Worked manifests + the template vocabulary. Space-independent, hence no env — and a distinct root, so a
@@ -61,4 +62,4 @@ var itemTemplates = (env) => [
61
62
  * family and extracts its ref in one step, without repeating the prefix string for both the startsWith and slice. */
62
63
  var afterPrefix = (uri, prefix) => uri.startsWith(prefix) ? uri.slice(prefix.length) : void 0;
63
64
  //#endregion
64
- export { actionTasksUri, actionUri, actionsUri, afterPrefix, aliasedRoots, changesUri, componentsUri, connectorPresetsUri, connectorUri, connectorsUri, cssPropertiesUri, dataSourcesUri, defUri, defsUri, elementUri, folderUri, foldersUri, fontUri, fontsUri, functionFileUri, functionsUri, globalUri, globalsUri, guideUri, idUri, idsUri, interactionsUri, itemTemplates, layoutsUri, pageUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarUri, styleVarsUri, typesUri };
65
+ export { actionTasksUri, actionUri, actionsUri, afterPrefix, aliasedRoots, changesUri, componentsUri, connectorPresetsUri, connectorUri, connectorsUri, cssPropertiesUri, dataSourcesUri, defUri, defsUri, elementUri, flagsUri, folderUri, foldersUri, fontUri, fontsUri, functionFileUri, functionsUri, globalUri, globalsUri, guideUri, idUri, idsUri, interactionsUri, itemTemplates, layoutsUri, pageUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarUri, styleVarsUri, typesUri };
@@ -0,0 +1,19 @@
1
+ import { ExplainKind, Explanation } from '@plitzi/sdk-authoring';
2
+ /** `plitzi://explain/{name}`: what a name means when authoring, or every name of a kind (`plitzi://explain/steps`). */
3
+ export declare const EXPLAIN_URI_TEMPLATE = "plitzi://explain/{name}";
4
+ /**
5
+ * The read behind the resource: a kind's plural lists it; any other name is explained, each meaning it has, as data
6
+ * and as the text the CLI prints. A name that is nothing says what there is to ask about instead.
7
+ */
8
+ export type ExplainRead = {
9
+ list: ExplainKind;
10
+ names: {
11
+ name: string;
12
+ summary: string;
13
+ }[];
14
+ } | {
15
+ name: string;
16
+ explanations: Explanation[];
17
+ text: string;
18
+ };
19
+ export declare const explainResource: (uri: string) => ExplainRead;
@@ -0,0 +1,22 @@
1
+ import { EXPLAIN_KINDS, explain, explainKindOf, explainList, explanationText } from "@plitzi/sdk-authoring";
2
+ //#region src/modules/mcp/resources/explain.ts
3
+ /** `plitzi://explain/{name}`: what a name means when authoring, or every name of a kind (`plitzi://explain/steps`). */
4
+ var EXPLAIN_URI_TEMPLATE = "plitzi://explain/{name}";
5
+ var PREFIX = "plitzi://explain/";
6
+ var explainResource = (uri) => {
7
+ const name = decodeURIComponent(uri.startsWith(PREFIX) ? uri.slice(17) : uri);
8
+ const kind = explainKindOf(name);
9
+ if (kind) return {
10
+ list: kind,
11
+ names: explainList(kind)
12
+ };
13
+ const explanations = explain(name);
14
+ if (explanations.length === 0) throw new Error(`"${name}" is no element, step, trigger, code or transformer. List what there is: ${Object.values(EXPLAIN_KINDS).map((plural) => `${PREFIX}${plural}`).join(", ")}.`);
15
+ return {
16
+ name,
17
+ explanations,
18
+ text: explanations.map(explanationText).join("\n\n")
19
+ };
20
+ };
21
+ //#endregion
22
+ export { EXPLAIN_URI_TEMPLATE, explainResource };
@@ -0,0 +1 @@
1
+ export {};
@@ -2,7 +2,7 @@ import { buildDataSourceCatalog, buildInteractionCatalog } from "../catalogs/obs
2
2
  import { buildTypeRegistry } from "../catalogs/registry.js";
3
3
  import { cssProperties, cssShorthands } from "../catalogs/index.js";
4
4
  import { componentSummariesToAI } from "../helpers/components.js";
5
- import { componentsUri, connectorsUri, dataSourcesUri, defsUri, foldersUri, fontsUri, interactionsUri, layoutsUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarsUri, typesUri } from "../helpers/uris.js";
5
+ import { componentsUri, connectorsUri, dataSourcesUri, defsUri, flagsUri, foldersUri, fontsUri, interactionsUri, layoutsUri, pagesUri, primerUri, schemaVarsUri, settingsUri, styleVarsUri, typesUri } from "../helpers/uris.js";
6
6
  import { guideQuickstart } from "../helpers/guide.js";
7
7
  import { envelope } from "./envelope.js";
8
8
  import { connectorSummaries } from "./connectors.js";
@@ -104,6 +104,11 @@ var readPrimerResource = (space, env, uri) => {
104
104
  value: schemaVariablesToAI(space.schema, false),
105
105
  read: schemaVarsUri(env)
106
106
  },
107
+ {
108
+ key: "flags",
109
+ value: Object.keys(space.schema.flags ?? {}),
110
+ read: flagsUri(env)
111
+ },
107
112
  {
108
113
  key: "styleVariables",
109
114
  value: styleVariablesToAI(space.style),
@@ -5,6 +5,7 @@ import { envelope, jsonContents } from "./envelope.js";
5
5
  import { resourceErrorMessage } from "./canonical.js";
6
6
  import { registerRenderResources } from "./renderGuide.js";
7
7
  import { readResource } from "./router.js";
8
+ import { EXPLAIN_URI_TEMPLATE, explainResource } from "./explain.js";
8
9
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
9
10
  //#region src/modules/mcp/resources/register.ts
10
11
  /** A page an agent can read in one go: each change carries whole elements before and after. */
@@ -49,6 +50,10 @@ var registerResources = (server, getSpace, env, log, hasSpace, readChanges) => {
49
50
  properties: cssProperties,
50
51
  shorthands: cssShorthands
51
52
  }))));
53
+ server.registerResource("Explain", new ResourceTemplate(EXPLAIN_URI_TEMPLATE, { list: void 0 }), {
54
+ description: "What a name means when authoring: an element (container), a step (navigate), a trigger (onScroll), a problem code (class-and-css) or a transformer — or every one of a kind: elements, steps, triggers, codes, transformers",
55
+ mimeType: "application/json"
56
+ }, (uri) => emitStatic(uri.href, () => jsonContents(uri.href, envelope(explainResource(uri.href)))));
52
57
  registerRenderResources(server, log);
53
58
  if (!hasSpace) return;
54
59
  const fixed = [
@@ -107,6 +112,11 @@ var registerResources = (server, getSpace, env, log, hasSpace, readChanges) => {
107
112
  `plitzi://schema-variables/${env}`,
108
113
  "Space-level values referenced via {{name}}"
109
114
  ],
115
+ [
116
+ "Feature flags",
117
+ `plitzi://flags/${env}`,
118
+ "The feature flags the space declares, each a default and its rules: read as {{ flags.<name> }}, named by the `flag` of an element or a page"
119
+ ],
110
120
  [
111
121
  "Settings",
112
122
  `plitzi://settings/${env}`,
@@ -1,7 +1,7 @@
1
1
  import { buildDataSourceCatalog, buildInteractionCatalog } from "../catalogs/observed.js";
2
2
  import { componentSummariesToAI, componentView, findComponentByRef } from "../helpers/components.js";
3
3
  import { findElementByRef, findRootByRef } from "../helpers/space.js";
4
- import { afterPrefix, componentsUri, dataSourcesUri, elementUri, folderUri, foldersUri, interactionsUri, layoutsUri, pageUri, pagesUri, schemaVarsUri, settingsUri } from "../helpers/uris.js";
4
+ import { afterPrefix, componentsUri, dataSourcesUri, elementUri, flagsUri, folderUri, foldersUri, interactionsUri, layoutsUri, pageUri, pagesUri, schemaVarsUri, settingsUri } from "../helpers/uris.js";
5
5
  import { envelope } from "./envelope.js";
6
6
  import { elementView, folderRefToAI, foldersToAI, layoutSummariesToAI, pageSkeletonToAI, pageStylesToAI, pageSummariesToAI, schemaVariablesToAI, settingsToAI } from "../tools/operations/schema/translator.js";
7
7
  import { treeOf } from "@plitzi/sdk-schema/helpers/components";
@@ -42,6 +42,7 @@ var readSchemaResource = (space, env, uri) => {
42
42
  };
43
43
  }
44
44
  if (uri === schemaVarsUri(env)) return envelope(schemaVariablesToAI(space.schema));
45
+ if (uri === flagsUri(env)) return envelope(space.schema.flags ?? {});
45
46
  if (uri === settingsUri(env)) return envelope(settingsToAI(space.schema));
46
47
  if (uri === interactionsUri(env)) return envelope(buildInteractionCatalog(space.schema));
47
48
  if (uri === dataSourcesUri(env)) return envelope(buildDataSourceCatalog(space.schema));
@@ -69,7 +69,7 @@ var createMcpServer = async ({ adapters, getGrant, preview, screenshot, logger,
69
69
  const getSpace = () => spacePromise ??= loadSpace();
70
70
  const server = new McpServer({
71
71
  name: "plitzi-mcp",
72
- version: "0.37.9"
72
+ version: "0.37.10"
73
73
  }, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
74
74
  const { getChanges } = adapters;
75
75
  registerResources(server, getSpace, MCP_ENV, log, hasSpace, getChanges ? (query) => getChanges(requireSpaceId(), MCP_ENV, query) : void 0);
@@ -0,0 +1 @@
1
+ export {};
@@ -17,6 +17,8 @@ import { deleteElement } from "../operations/schema/elements/deleteElement.js";
17
17
  import { moveElement } from "../operations/schema/elements/moveElement.js";
18
18
  import { patchElement } from "../operations/schema/elements/patchElement.js";
19
19
  import { upsertElement } from "../operations/schema/elements/upsertElement.js";
20
+ import { deleteFlag } from "../operations/schema/flags/deleteFlag.js";
21
+ import { upsertFlag } from "../operations/schema/flags/upsertFlag.js";
20
22
  import { deleteFolder } from "../operations/schema/folders/deleteFolder.js";
21
23
  import { upsertFolder } from "../operations/schema/folders/upsertFolder.js";
22
24
  import { deleteInteraction } from "../operations/schema/interactions/deleteInteraction.js";
@@ -78,6 +80,8 @@ var executeOp = (space, env, op) => {
78
80
  case "deleteFolder": return deleteFolder(space, env, op);
79
81
  case "upsertVariable": return upsertVariable(space, env, op);
80
82
  case "deleteVariable": return deleteVariable(space, env, op);
83
+ case "upsertFlag": return upsertFlag(space, env, op);
84
+ case "deleteFlag": return deleteFlag(space, env, op);
81
85
  case "upsertBinding": return upsertBinding(space, env, op);
82
86
  case "patchBinding": return patchBinding(space, env, op);
83
87
  case "deleteBinding": return deleteBinding(space, env, op);
@@ -54,6 +54,8 @@ export declare const applyShape: {
54
54
  client: "client";
55
55
  shared: "shared";
56
56
  }>>;
57
+ flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
+ anchor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
57
59
  }, z.core.$strip>, z.ZodObject<{
58
60
  type: z.ZodLiteral<"deleteElement">;
59
61
  pageRef: z.ZodString;
@@ -78,6 +80,7 @@ export declare const applyShape: {
78
80
  layout: z.ZodOptional<z.ZodNullable<z.ZodString>>;
79
81
  layoutContainer: z.ZodOptional<z.ZodNullable<z.ZodString>>;
80
82
  enabled: z.ZodOptional<z.ZodBoolean>;
83
+ flag: z.ZodOptional<z.ZodNullable<z.ZodString>>;
81
84
  }, z.core.$strip>, z.ZodObject<{
82
85
  type: z.ZodLiteral<"deletePage">;
83
86
  ref: z.ZodString;
@@ -122,6 +125,18 @@ export declare const applyShape: {
122
125
  }, z.core.$strip>, z.ZodObject<{
123
126
  type: z.ZodLiteral<"deleteVariable">;
124
127
  name: z.ZodString;
128
+ }, z.core.$strip>, z.ZodObject<{
129
+ type: z.ZodLiteral<"upsertFlag">;
130
+ name: z.ZodString;
131
+ description: z.ZodOptional<z.ZodString>;
132
+ value: z.ZodBoolean;
133
+ rules: z.ZodOptional<z.ZodArray<z.ZodObject<{
134
+ when: z.ZodType<import('@plitzi/sdk-shared').RuleGroup, unknown, z.core.$ZodTypeInternals<import('@plitzi/sdk-shared').RuleGroup, unknown>>;
135
+ value: z.ZodBoolean;
136
+ }, z.core.$strip>>>;
137
+ }, z.core.$strip>, z.ZodObject<{
138
+ type: z.ZodLiteral<"deleteFlag">;
139
+ name: z.ZodString;
125
140
  }, z.core.$strip>, z.ZodObject<{
126
141
  type: z.ZodLiteral<"upsertBinding">;
127
142
  pageRef: z.ZodString;
@@ -225,6 +240,7 @@ export declare const applyShape: {
225
240
  }, z.core.$strip>, z.ZodObject<{
226
241
  type: z.ZodLiteral<"patchSettings">;
227
242
  customCss: z.ZodOptional<z.ZodString>;
243
+ notifications: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodString>>>;
228
244
  keepState: z.ZodOptional<z.ZodBoolean>;
229
245
  stateStorage: z.ZodOptional<z.ZodEnum<{
230
246
  localStorage: "localStorage";
@@ -53,7 +53,7 @@ var apply = async (input, space, persisters) => {
53
53
  errors: result.errors,
54
54
  warnings: noWarnings(result.warnings)
55
55
  };
56
- const { draft, outcome, warnings } = result;
56
+ const { draft, outcome, warnings, suggestions } = result;
57
57
  if (input.dryRun) return {
58
58
  applied: false,
59
59
  dryRun: true,
@@ -64,7 +64,8 @@ var apply = async (input, space, persisters) => {
64
64
  },
65
65
  changed: changedResources(draft, env, outcome.staleResources),
66
66
  elements: resolvedElements(draft, env, outcome.elementRefs),
67
- warnings: noWarnings(warnings)
67
+ warnings: noWarnings(warnings),
68
+ suggestions: noWarnings(suggestions)
68
69
  };
69
70
  let persisted = true;
70
71
  if (outcome.changedFunctions && draft.functions) {
@@ -118,7 +119,8 @@ var apply = async (input, space, persisters) => {
118
119
  },
119
120
  changed: changedResources(draft, env, outcome.staleResources),
120
121
  elements: resolvedElements(draft, env, outcome.elementRefs),
121
- warnings: noWarnings(warnings)
122
+ warnings: noWarnings(warnings),
123
+ suggestions: noWarnings(suggestions)
122
124
  };
123
125
  };
124
126
  var applyTool = defineTool({