@plitzi/sdk-mcp 0.37.2 → 0.37.4

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 (94) hide show
  1. package/CHANGELOG.md +1182 -0
  2. package/README.md +6 -2
  3. package/dist/createServer.js +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -1
  6. package/dist/modules/mcp/accessibilityOutline.d.ts +38 -0
  7. package/dist/modules/mcp/accessibilityOutline.js +75 -0
  8. package/dist/modules/mcp/accessibilityOutline.test.d.ts +1 -0
  9. package/dist/modules/mcp/catalogs/index.d.ts +2 -2
  10. package/dist/modules/mcp/catalogs/index.js +2 -2
  11. package/dist/modules/mcp/catalogs/observed.js +2 -2
  12. package/dist/modules/mcp/catalogs/pluginCatalog.d.ts +6 -3
  13. package/dist/modules/mcp/catalogs/pluginCatalog.js +12 -7
  14. package/dist/modules/mcp/helpers/agentPrompt.js +22 -5
  15. package/dist/modules/mcp/helpers/guide.d.ts +2 -2
  16. package/dist/modules/mcp/helpers/guide.js +212 -48
  17. package/dist/modules/mcp/helpers/interactions.d.ts +2 -1
  18. package/dist/modules/mcp/helpers/interactions.js +2 -0
  19. package/dist/modules/mcp/helpers/space.d.ts +6 -4
  20. package/dist/modules/mcp/helpers/space.js +7 -20
  21. package/dist/modules/mcp/helpers/uris.d.ts +3 -1
  22. package/dist/modules/mcp/helpers/uris.js +4 -1
  23. package/dist/modules/mcp/index.d.ts +2 -0
  24. package/dist/modules/mcp/localScreenshotClient.d.ts +11 -0
  25. package/dist/modules/mcp/localScreenshotClient.js +24 -9
  26. package/dist/modules/mcp/proxy/guard.d.ts +6 -0
  27. package/dist/modules/mcp/proxy/guard.js +8 -12
  28. package/dist/modules/mcp/resources/functions.d.ts +8 -0
  29. package/dist/modules/mcp/resources/functions.js +35 -0
  30. package/dist/modules/mcp/resources/register.d.ts +2 -1
  31. package/dist/modules/mcp/resources/register.js +49 -2
  32. package/dist/modules/mcp/resources/renderGuide.js +3 -2
  33. package/dist/modules/mcp/resources/router.js +3 -1
  34. package/dist/modules/mcp/screenshotClient.js +12 -4
  35. package/dist/modules/mcp/server.js +20 -10
  36. package/dist/modules/mcp/tests/guide.test.d.ts +1 -0
  37. package/dist/modules/mcp/tools/apply/dispatch.js +9 -1
  38. package/dist/modules/mcp/tools/apply/index.d.ts +34 -0
  39. package/dist/modules/mcp/tools/apply/index.js +35 -70
  40. package/dist/modules/mcp/tools/index.js +4 -2
  41. package/dist/modules/mcp/tools/operations/connectors/manifest.d.ts +0 -1
  42. package/dist/modules/mcp/tools/operations/functions/functions.test.d.ts +1 -0
  43. package/dist/modules/mcp/tools/operations/functions/index.d.ts +31 -0
  44. package/dist/modules/mcp/tools/operations/functions/index.js +48 -0
  45. package/dist/modules/mcp/tools/operations/index.d.ts +122 -2
  46. package/dist/modules/mcp/tools/operations/index.js +4 -3
  47. package/dist/modules/mcp/tools/operations/schema/elements/deleteElement.js +5 -6
  48. package/dist/modules/mcp/tools/operations/schema/elements/moveElement.js +3 -11
  49. package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +6 -9
  50. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.d.ts +5 -0
  51. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +3 -1
  52. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +5 -0
  53. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +27 -0
  54. package/dist/modules/mcp/tools/operations/schema/pages/deleteLayout.js +6 -5
  55. package/dist/modules/mcp/tools/operations/schema/pages/deletePage.js +6 -5
  56. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +17 -0
  57. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +34 -5
  58. package/dist/modules/mcp/tools/operations/schema/shared.d.ts +6 -0
  59. package/dist/modules/mcp/tools/operations/schema/shared.js +5 -0
  60. package/dist/modules/mcp/tools/operations/schema/translator.js +3 -2
  61. package/dist/modules/mcp/tools/operations/schema/write.d.ts +10 -5
  62. package/dist/modules/mcp/tools/operations/schema/write.js +16 -16
  63. package/dist/modules/mcp/tools/preview.d.ts +27 -0
  64. package/dist/modules/mcp/tools/render.d.ts +27 -0
  65. package/dist/modules/mcp/tools/render.js +7 -43
  66. package/dist/modules/mcp/tools/screenshot.d.ts +39 -0
  67. package/dist/modules/mcp/tools/screenshot.js +40 -5
  68. package/dist/modules/mcp/tools/shared/draftBatch.d.ts +25 -0
  69. package/dist/modules/mcp/tools/shared/draftBatch.js +58 -0
  70. package/dist/modules/mcp/tools/shared/draftBatch.test.d.ts +1 -0
  71. package/dist/modules/mcp/tools/shared/lintDraft.d.ts +30 -0
  72. package/dist/modules/mcp/tools/shared/lintDraft.js +142 -0
  73. package/dist/modules/mcp/tools/shared/lintDraft.test.d.ts +1 -0
  74. package/dist/modules/mcp/tools/shared/tool.d.ts +4 -0
  75. package/dist/modules/mcp/tools/shared/validator/bindings.d.ts +0 -4
  76. package/dist/modules/mcp/tools/shared/validator/bindings.js +1 -60
  77. package/dist/modules/mcp/tools/shared/validator/elements.js +3 -15
  78. package/dist/modules/mcp/tools/shared/validator/index.d.ts +1 -4
  79. package/dist/modules/mcp/tools/shared/validator/index.js +25 -20
  80. package/dist/modules/mcp/tools/shared/validator/interactions.js +3 -58
  81. package/dist/modules/mcp/tools/tryFunction.d.ts +18 -0
  82. package/dist/modules/mcp/tools/tryFunction.js +43 -0
  83. package/dist/modules/mcp/tools/validate.d.ts +34 -0
  84. package/dist/modules/mcp/tools/validate.js +9 -26
  85. package/dist/modules/mcp/types/aiSchema.d.ts +8 -1
  86. package/dist/modules/mcp/types/previewTypes.d.ts +6 -0
  87. package/dist/modules/mcp/types/screenshotTypes.d.ts +16 -0
  88. package/dist/modules/mcp/types/toolTypes.d.ts +5 -1
  89. package/dist/preview/createPreview.js +5 -4
  90. package/dist/stages/preview.js +15 -1
  91. package/package.json +11 -10
  92. package/skills/plitzi-render/SKILL.md +1 -2
  93. package/dist/modules/mcp/tools/shared/validator/audit.d.ts +0 -4
  94. package/dist/modules/mcp/tools/shared/validator/audit.js +0 -127
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  The AI surface for Plitzi spaces: an [MCP](https://modelcontextprotocol.io) server that lets an agent read and
4
4
  edit a space, the tool engine behind it, the widget renderer, and the draft-preview endpoint.
5
5
 
6
+ Connecting an agent and what it can do once connected: [AI agents and the MCP server](../../docs/en/mcp.md).
7
+
6
8
  It is a sibling of [`@plitzi/sdk-server`](../server/README.md), not a layer on top of it. That package serves
7
9
  pages; this one serves agents. They share only the HTTP kernel, which this package imports from
8
10
  `@plitzi/sdk-server/kernel` — a narrow entry that carries the dispatcher and the transports and nothing else, so
@@ -69,6 +71,7 @@ The server is stateless: it resolves the space per request and reads and writes
69
71
  | `getStyle(spaceId, env)` | yes | The full style document, including `platform`/`mode`. |
70
72
  | `saveSchema(spaceId, env, schema)` | for writes | Persist a mutated schema. Without it, `plitzi_apply` reports `persisted: false`. |
71
73
  | `saveStyle(spaceId, env, style)` | for writes | Persist a mutated style document. |
74
+ | `getChanges(spaceId, env, query)` | no | The space's change history, for the `plitzi://changes` resources. Writes arrive with an `SSRWriteContext` (`userId`, one `batch` per tool call) so a consumer can record them. |
72
75
  | `getOfflineData(spaceId, env, rev)` | for preview | Read side of draft-preview. Only the preview endpoint calls it. |
73
76
 
74
77
  Schema and style are read as **separate documents** on purpose: `getOfflineData` is SSR-shaped and strips
@@ -87,6 +90,7 @@ resolves with `canWrite: false` reads everything and is refused at every write t
87
90
  | `plitzi_validate` | read | Dry-run a batch of operations and report what would fail |
88
91
  | `plitzi_apply` | write | Apply a batch of operations and persist |
89
92
  | `plitzi_preview` | read | Render a draft to HTML through an SSR server |
93
+ | `plitzi_screenshot` | read | Render a draft to a PNG (desktop, mobile or both) through the screenshot service |
90
94
  | `plitzi_render` | read | Render a self-contained UI widget, offline, with no space |
91
95
 
92
96
  Reads follow a filesystem model: list cheap, read one item in detail on demand. Agents are told never to
@@ -159,8 +163,8 @@ API exactly as a consumer would.
159
163
 
160
164
  ## Examples
161
165
 
162
- Runnable setups live in [`examples/`](../../examples) — [03-ai/01](../../examples/03-ai/01-mcp-server) is a dedicated MCP
163
- server, [03-ai/02](../../examples/03-ai/02-ssr-preview) is the combined topology with draft preview. Each starts with
166
+ Runnable setups live in [`examples/`](../../examples) — [self-hosting/06](../../examples/self-hosting/06-mcp-server) is a dedicated
167
+ MCP server, [self-hosting/07](../../examples/self-hosting/07-ssr-preview) is the combined topology with draft preview. Each starts with
164
168
  `yarn start`.
165
169
 
166
170
  ## Entry points
@@ -24,7 +24,7 @@ var createServer = (config, options = {}) => {
24
24
  config,
25
25
  port
26
26
  });
27
- return makeHandler("MCP", buildContext, stages, config.compression);
27
+ return makeHandler("MCP", buildContext, stages, { compression: config.compression });
28
28
  };
29
29
  return createHttpServer(config, makeHandlerForPort, {
30
30
  label: "MCP",
package/dist/index.d.ts CHANGED
@@ -11,9 +11,9 @@ export { previewStage } from './stages/preview';
11
11
  export { createWidgetProxyStage } from './stages/proxy';
12
12
  export { createPreview } from './preview/createPreview';
13
13
  export { AIEngine, toolResponseOk, toolResponseErr, zodToJsonSchema, getAllowedModes, bindTools, isToolActive, resolveToolHandler, isCallToolResult, toolResponseFromResult } from './modules/ai';
14
- export { createMcpServer, handleMcp, serveMcp, readMcpBody, buildAgentGuide, createHttpPreviewClient, createHttpScreenshotClient, createLocalScreenshotClient, resolveLocalBrowser } from './modules/mcp';
14
+ export { createMcpServer, handleMcp, serveMcp, readMcpBody, buildAgentGuide, createHttpPreviewClient, createHttpScreenshotClient, createLocalScreenshotClient, resolveLocalBrowser, outlineOfSnapshot, outlineOfTree, unnamedControls } from './modules/mcp';
15
15
  export { apply, search, read, validate, applyShape, searchShape, readShape, validateShape, operation, tools } from './modules/mcp/tools';
16
- export type { McpServerContext, McpRequestOptions, PreviewClient, PreviewRequestBody, PreviewResult, HttpPreviewClientConfig, ScreenshotClient, ScreenshotImage, ScreenshotResult, Viewport, HttpScreenshotClientConfig } from './modules/mcp';
16
+ export type { McpServerContext, McpRequestOptions, PreviewClient, PreviewRequestBody, PreviewResult, HttpPreviewClientConfig, ScreenshotClient, ScreenshotImage, ScreenshotResult, AccessibilityNode, AccessibilityOutline, CaptureView, UnnamedControl, Viewport, HttpScreenshotClientConfig } from './modules/mcp';
17
17
  export type { ApplyInput, SearchInput, SearchResponse, SearchHit, ReadInput, ReadResponse, ReadHit, ValidateInput, WriteResponse, WriteElement, Persisters, Operation, ToolDef, ToolContext } from './modules/mcp/tools';
18
18
  export type { Space } from './modules/mcp/helpers';
19
19
  export type { Env } from './modules/mcp/types';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { buildAgentGuide } from "./modules/mcp/helpers/agentPrompt.js";
2
2
  import { operation } from "./modules/mcp/tools/operations/index.js";
3
3
  import { apply, applyShape } from "./modules/mcp/tools/apply/index.js";
4
4
  import { read, readShape } from "./modules/mcp/tools/read.js";
5
+ import { outlineOfSnapshot, outlineOfTree, unnamedControls } from "./modules/mcp/accessibilityOutline.js";
5
6
  import { search, searchShape } from "./modules/mcp/tools/search.js";
6
7
  import { validate, validateShape } from "./modules/mcp/tools/validate.js";
7
8
  import { tools } from "./modules/mcp/tools/index.js";
@@ -19,4 +20,4 @@ import { createServer } from "./createServer.js";
19
20
  import AIEngine from "./modules/ai/AIEngine.js";
20
21
  import { createLocalScreenshotClient, resolveLocalBrowser } from "./modules/mcp/localScreenshotClient.js";
21
22
  import { createOAuthGuardStage, createOAuthStage } from "@plitzi/sdk-server/oauth";
22
- export { AIEngine, apply, applyShape, bindTools, buildAgentGuide, buildMCPPipeline, createHttpPreviewClient, createHttpScreenshotClient, createLocalScreenshotClient, createMcpOnlyStage, createMcpServer, createMcpStage, createOAuthGuardStage, createOAuthStage, createPreview, createServer, createWidgetProxyStage, getAllowedModes, handleMcp, isCallToolResult, isToolActive, mcpExtensions, operation, previewStage, read, readMcpBody, readShape, resolveLocalBrowser, resolveToolHandler, search, searchShape, serveMcp, toolResponseErr, toolResponseFromResult, toolResponseOk, tools, validate, validateShape, zodToJsonSchema };
23
+ export { AIEngine, apply, applyShape, bindTools, buildAgentGuide, buildMCPPipeline, createHttpPreviewClient, createHttpScreenshotClient, createLocalScreenshotClient, createMcpOnlyStage, createMcpServer, createMcpStage, createOAuthGuardStage, createOAuthStage, createPreview, createServer, createWidgetProxyStage, getAllowedModes, handleMcp, isCallToolResult, isToolActive, mcpExtensions, operation, outlineOfSnapshot, outlineOfTree, previewStage, read, readMcpBody, readShape, resolveLocalBrowser, resolveToolHandler, search, searchShape, serveMcp, toolResponseErr, toolResponseFromResult, toolResponseOk, tools, unnamedControls, validate, validateShape, zodToJsonSchema };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A page's accessibility tree as text an agent can read: the roles and names a screen reader announces, and the ones a
3
+ * browser agent (Claude in Chrome) finds the page's controls by.
4
+ *
5
+ * Two browsers answer it in two shapes. Playwright writes this outline itself (`locator.ariaSnapshot()`); Puppeteer —
6
+ * what the cluster's screenshot service runs — hands back a tree of nodes, which `outlineOfTree` writes in the same
7
+ * form. Whatever reads an outline (`unnamedControls`) reads either.
8
+ */
9
+ /** The slice of Puppeteer's `SerializedAXNode` an outline is written from. */
10
+ export type AccessibilityNode = {
11
+ role?: string;
12
+ name?: string;
13
+ value?: string | number;
14
+ level?: number;
15
+ checked?: boolean | 'mixed';
16
+ pressed?: boolean | 'mixed';
17
+ expanded?: boolean;
18
+ selected?: boolean;
19
+ disabled?: boolean;
20
+ invalid?: string;
21
+ children?: AccessibilityNode[];
22
+ };
23
+ /** Past this an outline is cut, and says so: a page is read to find something in it, not to be held whole. */
24
+ export declare const OUTLINE_LIMIT = 24000;
25
+ /** Puppeteer's tree, written as the outline Playwright writes. */
26
+ export declare const outlineOfTree: (root: AccessibilityNode | null | undefined) => string;
27
+ /** Playwright's own outline, held to the same limit. */
28
+ export declare const outlineOfSnapshot: (snapshot: string) => string;
29
+ export type UnnamedControl = {
30
+ role: string;
31
+ line: number;
32
+ };
33
+ /**
34
+ * The controls and pictures in an outline that have no name: what a screen reader announces as "button" and nothing
35
+ * else, and what a browser agent cannot tell apart. Each by its role and its line in the outline, so the place it sits
36
+ * in is there to read around it.
37
+ */
38
+ export declare const unnamedControls: (outline: string) => UnnamedControl[];
@@ -0,0 +1,75 @@
1
+ //#region src/modules/mcp/accessibilityOutline.ts
2
+ /** Past this an outline is cut, and says so: a page is read to find something in it, not to be held whole. */
3
+ var OUTLINE_LIMIT = 24e3;
4
+ /** Nodes that are only structure: their children are written in their place. */
5
+ var TRANSPARENT = /* @__PURE__ */ new Set([
6
+ "generic",
7
+ "none",
8
+ "presentation",
9
+ "RootWebArea",
10
+ "WebArea",
11
+ "LineBreak",
12
+ "InlineTextBox",
13
+ "LabelText"
14
+ ]);
15
+ /** Nodes left out with everything under them: a list's bullets and numbers are the list's look, not its words. */
16
+ var DROPPED = /* @__PURE__ */ new Set(["ListMarker"]);
17
+ var TEXT_ROLES = /* @__PURE__ */ new Set(["StaticText", "text"]);
18
+ var quote = (text) => JSON.stringify(text.replace(/\s+/g, " ").trim());
19
+ var statesOf = (node) => {
20
+ return [
21
+ node.level !== void 0 ? `level=${node.level}` : "",
22
+ node.checked === "mixed" ? "checked=mixed" : node.checked ? "checked" : "",
23
+ node.pressed === "mixed" ? "pressed=mixed" : node.pressed ? "pressed" : "",
24
+ node.expanded === void 0 ? "" : `expanded=${String(node.expanded)}`,
25
+ node.selected ? "selected" : "",
26
+ node.disabled ? "disabled" : "",
27
+ node.invalid && node.invalid !== "false" ? "invalid" : ""
28
+ ].filter(Boolean).map((state) => ` [${state}]`).join("");
29
+ };
30
+ var linesOf = (node, depth) => {
31
+ const role = node.role ?? "";
32
+ const name = node.name?.trim() ?? "";
33
+ const children = node.children ?? [];
34
+ if (DROPPED.has(role)) return [];
35
+ if (TEXT_ROLES.has(role)) return name ? [`${" ".repeat(depth)}- text: ${quote(name)}`] : [];
36
+ if (TRANSPARENT.has(role) && !name) return children.flatMap((child) => linesOf(child, depth));
37
+ const value = node.value === void 0 || node.value === "" ? "" : `: ${quote(String(node.value))}`;
38
+ return [`${" ".repeat(depth)}- ${role}${name ? ` ${quote(name)}` : ""}${statesOf(node)}${value}`, ...children.flatMap((child) => linesOf(child, depth + 1)).filter((line) => line.trim() !== `- text: ${quote(name)}`)];
39
+ };
40
+ var limited = (outline) => outline.length > 24e3 ? `${outline.slice(0, OUTLINE_LIMIT)}\n… cut at ${OUTLINE_LIMIT} characters of ${outline.length}: capture one page at a time, or read the part you need.` : outline;
41
+ /** Puppeteer's tree, written as the outline Playwright writes. */
42
+ var outlineOfTree = (root) => root ? limited(linesOf(root, 0).join("\n")) : "";
43
+ /** Playwright's own outline, held to the same limit. */
44
+ var outlineOfSnapshot = (snapshot) => limited(snapshot.trim());
45
+ var UNNAMED = new RegExp(`^\\s*- (${[
46
+ "button",
47
+ "link",
48
+ "textbox",
49
+ "searchbox",
50
+ "combobox",
51
+ "listbox",
52
+ "checkbox",
53
+ "radio",
54
+ "switch",
55
+ "slider",
56
+ "spinbutton",
57
+ "tab",
58
+ "menuitem",
59
+ "img",
60
+ "image"
61
+ ].join("|")})(?=\\s*(?:\\[|:|$))`);
62
+ /**
63
+ * The controls and pictures in an outline that have no name: what a screen reader announces as "button" and nothing
64
+ * else, and what a browser agent cannot tell apart. Each by its role and its line in the outline, so the place it sits
65
+ * in is there to read around it.
66
+ */
67
+ var unnamedControls = (outline) => outline.split("\n").flatMap((text, index) => {
68
+ const match = UNNAMED.exec(text);
69
+ return match ? [{
70
+ role: match[1] === "image" ? "img" : match[1],
71
+ line: index + 1
72
+ }] : [];
73
+ });
74
+ //#endregion
75
+ export { OUTLINE_LIMIT, outlineOfSnapshot, outlineOfTree, unnamedControls };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- export { BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS, BUILTIN_TRANSFORMERS, BUILTIN_UTILITIES, applyBuiltinCallback, applyElementCallback, applyUtility, cssProperties, cssShorthands, elementCatalog, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getTransformer, getUtility, hiddenParams, invalidParams, isCssProperty, missingRequiredParams, reconcileParams, shorthandLonghands, suggestCssProperty, suggestTransformer, transformerCatalog } from '@plitzi/sdk-authoring';
2
- export type { BuiltinElementCallback, BuiltinGlobalCallback, BuiltinParam, BuiltinParamType, BuiltinTransformer, BuiltinUtility, CssPatch, ElementSemantics, InvalidParam, ParamSpec, TransformerInfo } from '@plitzi/sdk-authoring';
1
+ export { BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS, BUILTIN_UTILITIES, applyBuiltinCallback, applyElementCallback, applyUtility, cssProperties, cssShorthands, elementCatalog, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getUtility, hiddenParams, isCssProperty, reconcileParams, suggestCssProperty, transformerCatalog } from '@plitzi/sdk-authoring';
2
+ export type { BuiltinParam, CssPatch, ElementSemantics, ParamSpec, TransformerInfo } from '@plitzi/sdk-authoring';
3
3
  export * from './observed';
4
4
  export * from './registry';
@@ -1,4 +1,4 @@
1
1
  import { buildDataSourceCatalog, buildInteractionCatalog, observedDataSources, observedInteractionActions } from "./observed.js";
2
2
  import { buildTypeRegistry } from "./registry.js";
3
- import { BUILTIN_ELEMENT_CALLBACKS as BUILTIN_ELEMENT_CALLBACKS$1, BUILTIN_GLOBAL_CALLBACKS as BUILTIN_GLOBAL_CALLBACKS$1, BUILTIN_UTILITIES as BUILTIN_UTILITIES$1, applyBuiltinCallback, applyElementCallback, applyUtility, cssProperties as cssProperties$1, cssShorthands, elementCatalog as elementCatalog$1, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getTransformer, getUtility, hiddenParams, invalidParams, isCssProperty, missingRequiredParams, reconcileParams, suggestCssProperty, suggestTransformer } from "@plitzi/sdk-authoring";
4
- export { BUILTIN_ELEMENT_CALLBACKS$1 as BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS$1 as BUILTIN_GLOBAL_CALLBACKS, BUILTIN_UTILITIES$1 as BUILTIN_UTILITIES, applyBuiltinCallback, applyElementCallback, applyUtility, buildDataSourceCatalog, buildInteractionCatalog, buildTypeRegistry, cssProperties$1 as cssProperties, cssShorthands, elementCatalog$1 as elementCatalog, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getTransformer, getUtility, hiddenParams, invalidParams, isCssProperty, missingRequiredParams, observedDataSources, observedInteractionActions, reconcileParams, suggestCssProperty, suggestTransformer };
3
+ import { BUILTIN_ELEMENT_CALLBACKS as BUILTIN_ELEMENT_CALLBACKS$1, BUILTIN_GLOBAL_CALLBACKS as BUILTIN_GLOBAL_CALLBACKS$1, BUILTIN_UTILITIES as BUILTIN_UTILITIES$1, applyBuiltinCallback, applyElementCallback, applyUtility, cssProperties as cssProperties$1, cssShorthands, elementCatalog as elementCatalog$1, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getUtility, hiddenParams, isCssProperty, reconcileParams, suggestCssProperty } from "@plitzi/sdk-authoring";
4
+ export { BUILTIN_ELEMENT_CALLBACKS$1 as BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS$1 as BUILTIN_GLOBAL_CALLBACKS, BUILTIN_UTILITIES$1 as BUILTIN_UTILITIES, applyBuiltinCallback, applyElementCallback, applyUtility, buildDataSourceCatalog, buildInteractionCatalog, buildTypeRegistry, cssProperties$1 as cssProperties, cssShorthands, elementCatalog$1 as elementCatalog, elementTypeNames, expandShorthand, expandShorthandPatch, getElementCallback, getGlobalCallback, getUtility, hiddenParams, isCssProperty, observedDataSources, observedInteractionActions, reconcileParams, suggestCssProperty };
@@ -1,4 +1,4 @@
1
- import { BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS, BUILTIN_UTILITIES, transformerCatalog } from "@plitzi/sdk-authoring";
1
+ import { BUILTIN_ELEMENT_CALLBACKS, BUILTIN_GLOBAL_CALLBACKS, BUILTIN_UTILITIES, GLOBAL_SOURCES, transformerCatalog } from "@plitzi/sdk-authoring";
2
2
  //#region src/modules/mcp/catalogs/observed.ts
3
3
  var INTERACTION_NOTE = "Interaction actions observed on this space, grouped by node type. A flow is a trigger followed by ordered callbacks/utilities; write one with upsertInteractionFlow. Names not listed may still be valid plugin actions.";
4
4
  var GLOBAL_CALLBACKS_NOTE = "Built-in global callbacks always available in every space. A globalCallback is registered on its SOURCE MODULE, so its node `elementId` is the listed `source` (e.g. \"space\" for addNotification) — NOT the element that hosts the flow. Omit `elementId` and the MCP sets the right source and fills the param defaults for you. Use ONLY the params listed under each callback (with the exact spelling shown) — for addNotification the visible text goes in `content`; there is no title/message/type param. When strictParams is true, any other key is rejected.";
@@ -33,7 +33,7 @@ var builtinUtilities = () => Object.entries(BUILTIN_UTILITIES).map(([action, { t
33
33
  params: toParamInfo(params)
34
34
  }));
35
35
  var DATA_SOURCE_NOTE = "Data-source paths and binding targets observed on this space. Bind a source to an element field with upsertBinding (category attributes|style|initialState). Paths not listed may still be valid. A binding may post-process its value through `transformers` (see below) and be gated by a `when` QueryBuilder guard.";
36
- var DATA_SOURCE_SCOPE_NOTE = "SCOPE: an element source named `<type>_<id>` (e.g. `apiContainer_products`, `list_food-list`) is provided by that element to its DESCENDANTS ONLY — the provider wraps its subtree in the source’s scope, so only elements INSIDE the provider can bind to it. Binding a sibling or unrelated element to it is schema-valid but broken at runtime (the source is not in scope), and validate/apply REJECT it as an error. To consume `apiContainer_x.data`, the bound element must live under that apiContainer. Module sources (no `<type>_<id>` head — e.g. state/space/navigation/auth/collection) are global and bindable anywhere.";
36
+ var DATA_SOURCE_SCOPE_NOTE = `SCOPE: an element source named \`<type>_<id>\` (e.g. \`apiContainer_products\`, \`list_food-list\`) is provided by that element to its DESCENDANTS ONLY — the provider wraps its subtree in the source’s scope, so only elements INSIDE the provider can bind to it. Binding a sibling or unrelated element to it is schema-valid but broken at runtime (the source is not in scope), and validate/apply REJECT it as an error. To consume \`apiContainer_x.data\`, the bound element must live under that apiContainer. The global sources (no \`<type>_<id>\` head — ${GLOBAL_SOURCES.join(", ")}) are bindable anywhere.`;
37
37
  var TRANSFORMERS_NOTE = "Built-in transformers that post-process a binding value before it reaches the field: `source → t₁ → t₂ → field`. Set them on a binding as `transformers: [{ action, params }]` (params are strings). The runtime resolves each by its `action` alone, so an UNKNOWN action is silently skipped (the value passes through unchanged) — use the exact action names below. Common formatting is `twigTemplate` (the value is the {{source}} token, not {{value}}).";
38
38
  var buildInteractionCatalog = (schema) => {
39
39
  const actions = {};
@@ -8,9 +8,12 @@ export declare const stringDeclarations: (block: Record<string, unknown> | undef
8
8
  /**
9
9
  * Builds the plugin half of a component catalog from a space's installed plugins, keyed by schema `type`.
10
10
  *
11
- * A plugin whose manifest cannot be read yields `{ custom: true }` with no metadata rather than failing the whole
12
- * catalog — the agent then sees the type exists but learns nothing more about it, which is strictly better than
13
- * being told the space has no plugins at all.
11
+ * One entry per element a plugin DECLARES (its manifest's `pluginSchema`), not per plugin: a package installed under
12
+ * one name registers every element it ships — a canvas and the cards beside it — and each is a type of its own.
13
+ *
14
+ * A plugin whose manifest cannot be read yields `{ custom: true }` under its own name, with no metadata, rather than
15
+ * failing the whole catalog — the agent then sees the type exists but learns nothing more about it, which is strictly
16
+ * better than being told the space has no plugins at all.
14
17
  */
15
18
  export declare const pluginCatalog: (plugins: Record<string, {
16
19
  resource: string;
@@ -51,7 +51,7 @@ var entryFromManifest = (type, manifest) => {
51
51
  const elementDef = pluginSchema?.definition;
52
52
  const label = strOrUndefined(elementDef?.label) ?? strOrUndefined(manifest.definition.name);
53
53
  const description = strOrUndefined(elementDef?.description);
54
- const bindingsAllowed = pluginSchema?.defaultStyle.bindingsAllowed;
54
+ const bindingsAllowed = (pluginSchema?.defaultStyle)?.bindingsAllowed;
55
55
  const attributesTargets = pathsOf(bindingsAllowed?.attributes);
56
56
  const initialStateTargets = pathsOf(bindingsAllowed?.initialState);
57
57
  const pluginBase = (pluginSchema?.defaultStyle)?.style?.base?.default;
@@ -73,18 +73,23 @@ var entryFromManifest = (type, manifest) => {
73
73
  /**
74
74
  * Builds the plugin half of a component catalog from a space's installed plugins, keyed by schema `type`.
75
75
  *
76
- * A plugin whose manifest cannot be read yields `{ custom: true }` with no metadata rather than failing the whole
77
- * catalog — the agent then sees the type exists but learns nothing more about it, which is strictly better than
78
- * being told the space has no plugins at all.
76
+ * One entry per element a plugin DECLARES (its manifest's `pluginSchema`), not per plugin: a package installed under
77
+ * one name registers every element it ships — a canvas and the cards beside it — and each is a type of its own.
78
+ *
79
+ * A plugin whose manifest cannot be read yields `{ custom: true }` under its own name, with no metadata, rather than
80
+ * failing the whole catalog — the agent then sees the type exists but learns nothing more about it, which is strictly
81
+ * better than being told the space has no plugins at all.
79
82
  */
80
83
  var pluginCatalog = async (plugins) => {
81
84
  const catalog = {};
82
85
  if (!plugins) return catalog;
83
- const entries = await Promise.all(Object.entries(plugins).map(async ([type, { resource }]) => {
86
+ const entries = await Promise.all(Object.entries(plugins).map(async ([name, { resource }]) => {
84
87
  const manifest = await fetchManifest(resource);
85
- return [type, manifest ? entryFromManifest(type, manifest) : { custom: true }];
88
+ if (!manifest) return [[name, { custom: true }]];
89
+ const declared = Object.keys(manifest.pluginSchema);
90
+ return (declared.length > 0 ? declared : [name]).map((type) => [type, entryFromManifest(type, manifest)]);
86
91
  }));
87
- for (const [type, entry] of entries) catalog[type] = entry;
92
+ for (const [type, entry] of entries.flat()) catalog[type] = entry;
88
93
  return catalog;
89
94
  };
90
95
  //#endregion
@@ -23,28 +23,43 @@ var TOOLS = [
23
23
  "• plitzi_validate — check a batch of operations without executing; returns teachable errors and warnings.",
24
24
  "• plitzi_apply — persist a batch of operations. Pass dryRun to preview the full diff without writing, and",
25
25
  " expectedResourceVersions to guard against concurrent edits (apply and search hand back the versions you need).",
26
- "• plitzi_preview / plitzi_screenshot — render an element/page already in the schema (HTML / image).",
26
+ "• plitzi_preview / plitzi_screenshot — render a page (HTML / PNG), with unsaved operations applied if you pass them.",
27
+ " plitzi_screenshot view:\"accessibility\" reads it as a screen reader or a browser agent does, listing what has no name.",
28
+ "• plitzi_render — show the user an offline widget; it never touches the space (read plitzi://render/guide).",
27
29
  "Discover → browse resources. Find a name → plitzi_search. Fetch known URIs → plitzi_read. Do not confuse them."
28
30
  ].join("\n");
29
31
  var RESOURCES = [
30
32
  "━━ MCP RESOURCES (read) ━━",
31
- "Read plitzi://primer/{env} FIRST — a cold-start bundle: guide + types + css-properties + page/definition/variable",
32
- "summaries in one call. Then, on demand:",
33
+ "Read plitzi://primer/{env} FIRST — a cold-start bundle: the guide’s quickstart + types + css-properties +",
34
+ "page/definition/variable summaries in one call. Then, on demand:",
33
35
  "• plitzi://guide — full usage reference.",
34
36
  "• plitzi://types — element types observed in this space (props, slots, subTypes, label/description/category).",
35
37
  "• plitzi://css-properties — valid kebab-case CSS property keys.",
36
38
  "• plitzi://schema/{env}/pages , /{ref} — page summaries, then one page as a skeleton tree.",
39
+ "• plitzi://schema/{env}/layouts — shared layout shells (header/sidebar/footer) and the pages inside each.",
37
40
  "• plitzi://folders/{env} — page folders (the sidebar tree).",
38
- "• plitzi://definitions/{env} , plitzi://style-variables/{env} — style classes and design tokens.",
41
+ "• plitzi://definitions/{env} , plitzi://style-variables/{env} , plitzi://fonts/{env} — style classes, design",
42
+ " tokens and the font families the space loads.",
39
43
  "• plitzi://interactions/{env} — interaction actions + built-in globalCallbacks with their source + param schema.",
40
44
  "• plitzi://data-sources/{env} — data-source paths and binding targets (vocabulary for upsertBinding).",
45
+ "• plitzi://connectors/{env} , plitzi://connector-presets — CMS/API connectors (this space’s, and working ones).",
46
+ "• plitzi://actions/{env} , /tasks — server actions, and the tasks this deployment can build them from.",
47
+ "• plitzi://changes/{env} , /{id} — the change history (read-only): who changed what, before and after.",
41
48
  "• plitzi://settings/{env} — space-level settings."
42
49
  ].join("\n");
50
+ var ACCESSIBILITY = [
51
+ "━━ ACCESSIBILITY ━━",
52
+ "Screen readers and browser agents (Claude in Chrome) work a page through its accessibility tree, so build every page",
53
+ "to be usable without sight: a button or link has words (an icon-only button a `title`; a link wrapping a card a",
54
+ "`label`), a field a `label` (`hideLabel: true` keeps it out of sight), an image an `alt` or `decorative: true`.",
55
+ "Clicks go on a button (it holds children) or a link — never on a container, text or image. Toggles bind",
56
+ "`ariaPressed`, openers `ariaExpanded`. Headings step down one level at a time. The linter warns on each of these."
57
+ ].join("\n");
43
58
  var NAVIGATION = [
44
59
  "━━ NAVIGATION & DYNAMIC PAGES ━━",
45
60
  "To move between pages, PREFER the Link element over an interaction — it is a CONTAINER that wraps any children and",
46
61
  "navigates on click. mode \"page\" links to another space page (href = the target page); mode \"internal\" takes a",
47
- "path inside the space and resolves {{token}} templates in it (e.g. /posts/{{postId}}); mode \"external\" is a full",
62
+ "path inside the space and resolves {{token}} templates in it (e.g. posts/{{postId}}); mode \"external\" is a full",
48
63
  "URL. Reach for the navigate globalCallback only when navigation must be one step inside a larger flow.",
49
64
  "A page slug is RELATIVE — never start it with \"/\" (the runtime prepends it). Folder slugs PREPEND to the page",
50
65
  "slug, so a page at slug \"post\" inside folders \"blog\" > \"2024\" resolves to /blog/2024/post.",
@@ -96,6 +111,8 @@ var buildAgentGuide = () => [
96
111
  "",
97
112
  RESOURCES,
98
113
  "",
114
+ ACCESSIBILITY,
115
+ "",
99
116
  NAVIGATION,
100
117
  "",
101
118
  INTERACTIONS()
@@ -1,4 +1,4 @@
1
1
  export declare const serverInstructions: string;
2
2
  export declare const widgetsOnlyInstructions: string;
3
- export declare const guideQuickstart = "# Plitzi AI MCP \u2014 quickstart\nThis is the condensed guide; read `plitzi://guide` for the full reference (every resource, op and example).\n\nA space is **two schemas you edit together in one atomic `plitzi_apply` batch**: the **element schema** (tree of\npages/elements) and the **style schema** (definitions = CSS classes, tokens, theme). To style an element: write a\n**definition** and attach it via the element's `style.base` in the same batch.\n\n**Big spaces:** a primer section may arrive as `{elided:true, bytes, entries, read}` instead of its contents \u2014\nthe space outgrew what one cold-start read can carry. `read` is the resource that serves that exact section whole\n(`plitzi://definitions/{env}`, `plitzi://interactions/{env}`, \u2026). Read the ones your task needs; nothing is\nmissing, it is one round-trip away.\n\n**Workflow:** (1) you already have this primer (guide + types + css + page/style summaries). (2) `plitzi_search`\nwith `include:\"detail\"` to find elements \u2014 each hit carries its `uri`, `stateVersion` and full style, so no\nper-element read. (3) `plitzi_apply` with `dryRun:true` to preview. (4) `plitzi_apply` to persist, passing\n`expectedResourceVersions` (uri \u2192 the stateVersion you read) for every resource you change \u2014 omitting it lets a\nconcurrent edit be lost. Use `patchElement`/`patchDefinition` to change only some props/CSS (upsert replaces all).\n\n**Names & wiring:** an element has ONE name and it is its id (letters, numbers, hyphens and underscores,\n**starting with a letter \u2014 no dots**). It is also the runtime wiring key: a provider's source is\n`<type>_<name>`, and interactions target by it. A dot would split that path; an **underscore is fine** \u2014 the first\n`_` separates the type from the name (element types have none), so `list_food_item` reads unambiguously as type\n`list`, name `food_item`.\n\n**Styling:** CSS keys are **kebab-case** (`background-color`); `var(--token)` for style vars, `{{name}}` for schema\nvars. **Write plain CSS** \u2014 shorthands (`border`, `padding`, `margin`, `gap`, `overflow`, `flex`, `background`,\n`font`, `transition`, `animation`, `grid`, `grid-row`/`grid-column`, `place-*`, `outline`, `columns`,\n`list-style`, `text-decoration`) are expanded to their longhands for you, so `border: 1px solid red` is stored as\n`border-top-color`/`border-top-width`/`border-top-style`/\u2026 That is also how you read them back. Flex layout is\nstill `display: flex` + `flex-direction`/`align-items`/\u2026, not a `flex` value. Mind a type's `defaultStyle`\n(`text` is `display: inline`). Global styles (`button {\u2026}`) and id styles (`#id`) have their own ops.\n\n**Data bindings** (`upsertBinding`, category attributes|style|initialState): connect a `source` to a `to` field.\nA source `<type>_<name>` is scoped to the provider's **DESCENDANTS only** \u2014 bind inside the provider's subtree\n(module sources state/space/navigation/auth/collection are global). `apiContainer.mockData` is builder-only; set a\nreal `query` for production. `transformers: [{action, params}]` post-process the value \u2014 use exact action names\nfrom `plitzi://data-sources`; `twigTemplate` formats it (the value is `{{source}}`, not `{{value}}`). `when` is\na QueryBuilder RuleGroup gating the binding.\n\n**Interactions** (`upsertInteractionFlow`): a `trigger` node first, then callbacks/utilities **in order** (links\ncomputed for you). Node types: `callback` (an element's own callback \u2014 `elementId` is that element), `globalCallback`\n(a source module \u2014 omit `elementId`, the MCP sets it), `utility` (no element). Element `setState`\n(category/key/value/revertOnFinish) \u2260 global `setState` (source `state`, key/type/value). To turn a step off use\n`patchInteractionNode {enabled:false}` \u2014 `deleteInteraction` removes it (destructive; confirm first). Any param\n**value** can be a binding token `{{ source }}` (e.g. notification `content: \"{{ list_<name>.item.name }}\"`).\n\n**CMS / API integrations** (`upsertConnector`): a **connector** is a manifest declaring a provider's base URL,\nendpoints, auth template and filter operators, executed by the **server** \u2014 so integrating Strapi, WordPress,\nContentful or any REST API is configuration, not code. Read `plitzi://connector-presets` (working manifests + every\ntemplate token) and `plitzi://connectors/{env}` (this space's). **You never create or see credentials** \u2014 write the\nmanifest with `{{credential.token}}` and no `connection.credential`; it saves, and the space owner attaches the\nsecret in the builder. To consume one, an `apiContainer` needs **`runtime: \"server\"`** plus `connector`,\n`resource`, optional `endpoint`/`filters`/`limit`/`singleRecord`/`pagination`; it then publishes\n`apiContainer_<name>.records` (or `.record`) `.pageInfo .isEmpty .hasError` to its **descendants**. Full worked\nflow \u2014 list page, detail page, paging, writes \u2014 in `plitzi://guide`.\n\n**Pages & navigation:** `upsertPage` \u2014 always set a **relative** `slug` (no leading `/`; the runtime and folder\nslugs prepend the path). A `:name` segment (`\"posts/:postId\"`) is a route param, readable as `{{name}}` and as the\nsource `navigation.routeParams.name` \u2192 build dynamic pages this way. To move between pages **prefer the `Link`\nelement** (a container: `mode` \"page\"/\"internal\"/\"external\") over a `navigate` interaction.\n\n**Touched resources must be malformation-free.** Editing an element/definition also checks its CURRENT stored content\nand BLOCKS the save on any `Pre-existing malformation in <resource>` error (a broken transformer, malformed node,\ninvalid CSS) \u2014 even parts you did not touch. These are NOT from your change (the message says so); fix them in the\nSAME batch and re-apply (the check runs on the result, so the fix unblocks it). `Pre-existing issue` warnings advise\nbut do not block.\n\nRead `plitzi://guide` before anything above is unclear.\n";
4
- export declare const guideText = "# Plitzi AI MCP \u2014 usage guide\n\nA Plitzi space is **two separate schemas** you edit together:\n- **Element schema** \u2014 the tree of pages and elements (their type, label, props, and which style classes they use).\n- **Style schema** \u2014 reusable **definitions** (CSS classes), design tokens (variables), theme.\n\nThey are stored and persisted independently, but a single `plitzi_apply` batch may touch **both atomically**.\nTo style a specific element you do two things in one batch: write a **definition** (style schema) and **attach**\nit via the element's `style.base` (element schema). Example \u2014 \"rename button X to PEPE and make it red\":\n```json\n{ \"operations\": [\n { \"type\": \"upsertDefinition\", \"ref\": \"btn-x\", \"desktop\": { \"color\": \"red\" } },\n { \"type\": \"upsertElement\", \"pageRef\": \"home\",\n \"element\": { \"ref\": \"X\", \"type\": \"button\", \"label\": \"PEPE\", \"style\": { \"base\": [\"btn-x\"] } } }\n] }\n```\n\nReads are cheap by design \u2014 treat them like a filesystem: **list** to navigate, **read one item** for detail.\nNever download a whole tree you do not need.\n\n## Resources (read)\n- `plitzi://primer/{env}` \u2014 **cold-start bundle**: guide + types + css-properties + page/definition/variable\n **summaries** in one read. Fetch this first instead of the individual resources below. Summaries only \u2014 open a\n page skeleton or element for its tree/detail.\n- `plitzi://guide` \u2014 this guide.\n- `plitzi://types` \u2014 element types **observed in this space** (ground truth): props, slots, subTypes, plus each\n type's `label`, `description` (what it is FOR) and `category`, and a `source` (`builtin` | `plugin` | `unknown`).\n Read the descriptions to pick the right type \u2014 e.g. `apiContainer` fetches backend data into the frontend,\n `link` navigates between pages, `list` repeats a template over a data array. `plugin` types are custom elements.\n- `plitzi://css-properties` \u2014 valid kebab-case CSS property keys.\n- `plitzi://schema/{env}/pages` \u2014 page **summaries** (ref, label, elementCount, folder). No element trees.\n- `plitzi://folders/{env}` \u2014 page **folders** (the sidebar tree): ref, name, slug, parentId. `/{ref}` for one.\n- `plitzi://schema/{env}/pages/{ref}` \u2014 one page as a **skeleton tree**: each node is `ref/type/label` **plus the\n style classes it attaches** (`base`, and `slots` for non-base slots) \u2014 names only, no CSS. So you can map every\n element to its class in a single page read, without opening each element just to learn which class it uses.\n- `plitzi://schema/{env}/pages/{ref}/styles` \u2014 **every style the page uses in one read**: the class definitions its\n elements attach (deduplicated, **with full CSS**) plus the global styles affecting any element type on the page.\n Reach for this to recolor/restyle a whole page \u2014 it needs no shared class-name prefix and no per-element reads.\n- `plitzi://schema/{env}/elements/{ref}` \u2014 one element in **full detail** (props, style, parentRef, childRefs).\n Its `resolvedStyle` inlines the **CSS of every definition** the element attaches (keyed by class ref), so you\n can see and edit its style without a separate definition read. Its `globalStyles` lists the **global element\n selectors** that also affect it (the CSS equivalent of `button { \u2026 }`, keyed by the type they target) \u2014 every\n element of that type inherits them. Edit a global only through the global-style tools (never per element). If the\n element carries a DOM `id` that an id rule matches, its `idStyle` (`#id`) is inlined too.\n- `plitzi://definitions/{env}` \u2014 the **names** of every style definition.\n- `plitzi://definitions/{env}/{ref}` \u2014 one definition's CSS.\n- `plitzi://global-styles/{env}` \u2014 element **types** that have a site-wide global style. `/{componentType}` for one.\n- `plitzi://id-styles/{env}` \u2014 DOM **ids** that have an id rule (`#id`) targeting a single element. `/{targetId}` for one.\n- `plitzi://style-variables/{env}` \u2014 design tokens by category. `/{category}` for one.\n- `plitzi://schema-variables/{env}` \u2014 space-level values referenced in props as `{{name}}`.\n- `plitzi://settings/{env}` \u2014 space-level settings: the global `customCss` and the state/auth (user-provider) config.\n- `plitzi://interactions/{env}` \u2014 interaction **actions** observed in this space (grouped by node type): the\n vocabulary for interaction flows.\n- `plitzi://data-sources/{env}` \u2014 data-source **paths** and binding targets observed in this space: the\n vocabulary for data bindings.\n- `plitzi://connector-presets` \u2014 working starting **manifests** (Strapi, WordPress, Directus, Contentful, plain\n REST) with the credential keys each provider needs, plus every **template token** the connector engine binds.\n Space-independent. Read it before writing a connector by hand.\n- `plitzi://connectors/{env}` \u2014 the **connectors this space has**: for each, its read/write endpoint names, its\n filter operators and its published fields. `/{ref}` opens one manifest in full. See *Connectors* below.\n- `plitzi://actions/{env}` \u2014 the **server actions this space has**: what starts each one, who may run it, and the\n input/output contract a caller is held to. `/{ref}` opens one flow in full. See *Server actions* below.\n\nThe style resources also answer under the `plitzi://schema/{env}/\u2026` root as aliases \u2014 `plitzi://schema/{env}/definitions/{ref}`, `plitzi://schema/{env}/style-variables/{category}`, `plitzi://schema/{env}/schema-variables` \u2014 but prefer the ready-made `uri` from search / a write response over hand-building either form.\n\nData resources return `{ stateVersion, data }`. Keep `stateVersion` for optimistic concurrency.\n\n**Reuse what you already know \u2014 don't re-scan an unchanged page.** A page read (`plitzi://schema/{env}/pages/{ref}`)\nreturns a `stateVersion` that is an **aggregate of the whole page**: it changes if and only if some element on the\npage changed. Its skeleton `tree` also carries a `stateVersion` **per node**, identical to the one a direct element\nread or search hit returns for that element. So when you come back to a page you already inspected:\n1. Re-read just the page skeleton and compare its top-level `stateVersion` to the one you held. **Same \u2192 nothing\n changed since your read; skip re-reading and re-searching the tree** and act on what you already know.\n2. If it differs, diff the per-node `stateVersion`s against the ones you cached and `plitzi_read` **only the nodes\n that changed** \u2014 never re-search or re-read the whole tree.\n\nThis is a **read-time** shortcut, not a safety guarantee. **Other agents/sub-agents may edit the same space\nconcurrently**, so a version you cached can go stale between your read and your write. Never skip the write-time\ncheck below on the strength of a cached hash \u2014 the guarantee that no concurrent edit is lost comes only from\n`expectedResourceVersions` on `plitzi_apply`, which re-validates against the live data at write time.\n\n## Navigating (files analogy)\nPages and containers are folders; elements are files. **Prefer `plitzi_search` (especially with `include: \"detail\"`)\nover reading elements one by one** \u2014 it jumps straight to elements by label/type/attribute and each hit already\ncarries the element's `uri`, `stateVersion`, `pageUri`, `parentRef` and tree `path`, so you can edit it (with\noptimistic concurrency) **without a follow-up read**. `include: \"detail\"` additionally inlines each hit's props/style\n**and** its `resolvedStyle` (the CSS behind its classes) \u2014 so a search-then-edit is the efficient path and a manual\nelement read is the exception. Search also matches **pages** by name/slug (returned under `pages`, each with its uri +\nstateVersion) and returns any **style definitions** whose name matches the query, with full CSS, under `definitions`.\nWhen you do hold several refs to open (e.g. from a skeleton), read them together with `plitzi_read` rather than one at a time.\n\n## Tools (write)\n- `plitzi_validate` \u2014 check a batch, returns teachable errors/warnings. Writes nothing. Also reports **pre-existing\n malformations** in any resource the batch touches (see below).\n- `plitzi_apply` \u2014 validate \u2192 apply \u2192 persist atomically. Rejects the whole batch on any error or conflict. Pass\n `dryRun: true` to apply in memory only and get the same result back (changed versions + full element detail)\n without persisting \u2014 inspect it, then re-run without `dryRun` to commit.\n- `plitzi_search` \u2014 find elements (and pages/definitions) across the space.\n- `plitzi_read` \u2014 read many resource **uris in one batch** (pages, elements, definitions, variables). Pass the\n ready-made uris from search / a write response; each result is `{ uri, stateVersion, data }` or a teachable error,\n so one bad uri never fails the batch. Use it instead of N single reads whenever you already hold several refs.\n\n## Readers: resources vs plitzi_search vs plitzi_read (do not confuse them)\nThree ways to read, each for a different moment \u2014 pick by what you have in hand:\n- **MCP resources** (the `plitzi://\u2026` catalog above) \u2014 the **browsable index**. List them to discover what exists, or\n open one by URI when you are exploring. This is the passive catalog, not a tool.\n- **`plitzi_search`** \u2014 you know *what* you want but not its **ref/uri** (\"the hero button\"). Search finds it by\n label/type/attribute and hands back the uri + stateVersion (and, with `include:\"detail\"`, the full element).\n- **`plitzi_read`** \u2014 you **already hold one or more uris** (from search or a write response) and want their\n content in one batch. It is the tool form of opening resources, for when you have the addresses.\nRule of thumb: **discover \u2192 resources**, **find a name \u2192 plitzi_search**, **fetch known uris \u2192 plitzi_read**. Never\nhand-build a URI to guess your way to an element \u2014 search for it instead.\n\nWrite tools return what **changed** (`{ uri, stateVersion }`) plus counts, and the **full detail of every element\nthey created or updated** \u2014 each with its own `uri` and `stateVersion` (`elements: [...]`) so a follow-up edit of\nthe same element needs **no intermediate read**. Other resources (pages, definitions, variables) still report only\nuri+stateVersion \u2014 re-read them if you need their new content. The operation shapes are in each tool's input\nschema (discriminated by `type`).\n\n## Addressing\nEvery element and page has **one name, and that name is its id**. You choose it, you address it by it, and the\nruntime wires by it \u2014 there is no second key and nothing to translate. Creating an element with\n`\"ref\": \"products-api\"` stores it under exactly that.\n\nThat name is the **wiring key**: a provider registers its data source as `<type>_<name>`, so a `source` you write\nagainst a name resolves to that element at runtime. Rules for a **new** name (both enforced; a violation fails the\nbatch):\n- Charset `[A-Za-z0-9_-]`, **starting with a letter** (e.g. `\"products-api\"`, `\"food_item\"`). A `.` would split\n the `<type>_<name>.<field>` source path and the interaction target lookup, so **no dots**. An **underscore is\n allowed**: the FIRST `_` separates `<type>` from the name and element types are camelCase with none, so\n underscores inside a name are unambiguous (`list_food_item` \u2192 type `list`, name `food_item`).\n- **Unique across the space**; a name that is taken is rejected (address the existing element instead).\n\n**Never invent a name for something that already exists** \u2014 read it. Every read hands you the name back\n(`plitzi_search`, a page skeleton, a write response), and that is the string every op takes.\n\n**Where an element renders** is its `runtime`, settable on `upsertElement`/`patchElement`: `\"shared\"` (the\ndefault, both sides), `\"client\"` (browser only) or `\"server\"` (SSR only). It matters for one thing above all \u2014 an\n`apiContainer` reads through a **connector** only when it is `\"server\"` (see *Connectors*). An element read\nreports it only when it is set.\n\n**Renaming** (`patchElement` with `rename`) moves the one key: the parent's child list, every binding source and\nevery interaction target across the space that named the old one is repointed with it, so nothing comes unwired.\nYou do not have to rewrite them.\n\n## Styling (crosses both schemas)\n- **Mind the type's intrinsic default style.** A type renders with a base CSS *before* any class is attached \u2014 read\n it from `defaultStyle` on the type in `plitzi://types` (the primer includes it). Do not assume `display: block`:\n `text`, for one, defaults to `display: inline`, so margins/width/vertical padding behave differently. If you need\n block/flex layout on such an element, set `display` explicitly in your definition rather than relying on a default.\n- **Images: preserve aspect ratio.** Setting only `width` and `height` (or forcing both) distorts an image. Change\n one dimension and let the other be `auto`, or set `aspect-ratio` with `object-fit: cover`/`contain`, so the\n image scales without stretching.\n- **Font size and line height move together.** When you change `font-size`, set `line-height` in the same edit\n (prefer a unitless ratio like `1.5`, which tracks the font size). Changing one without the other leaves cramped or\n loosely-spaced text \u2014 they are a joint change, not two separate ones.\n- A definition lives in the **style schema**; an element's `style.base` (element schema) is the link that applies\n it. Styling an element = upsertDefinitions + upsertElement with that ref in `style.base`, in one batch.\n- **Repeating siblings**: when a set of siblings shares a shape and differs only in data (a list, cards, rows,\n steps), use `repeatElement` \u2014 the template once with `{{item.field}}` placeholders plus `items`, which\n creates the wrapper and numbers each row's refs (`step-1`, `step-2`\u2026). A list inside each row is the same op:\n the wrapping node carries `repeat: { items: \"{{item.<list>}}\", template: \u2026 }` and its refs number both levels\n (`blk-2-3`). Copy-pasting the subtree N times costs N times the tokens and drifts.\n- CSS keys are **kebab-case** (`background-color`). camelCase is rejected \u2014 read `plitzi://css-properties`.\n- **Write normal CSS \u2014 shorthands are accepted and expanded for you.** `border`, `border-{side}`,\n `border-width`/`-color`/`-style`, `border-radius`, `padding`, `margin`, `inset`, `gap`, `overflow`,\n `flex`, `flex-flow`, `background`, `font`, `transition`, `animation`, `grid`, `grid-template`,\n `grid-area`, `grid-row`, `grid-column`, `place-content`, `place-items`, `place-self`, `outline`,\n `columns`, `list-style`, `text-decoration` \u2014 the full list is in `plitzi://css-properties` under\n `shorthands`. They **persist as longhand keys** (that is what a read gives back), so a breakpoint/state/variant\n can still override one property at a time.\n - `border: 1px solid red` \u2192 `border-top-width: 1px`, `border-top-style: solid`, `border-top-color: red`, \u2026\n for all four sides. A shorthand also RESETS what it omits, so `border: none` clears a width a previous\n definition set.\n - Comma-separated layers are kept per longhand: `transition: opacity 200ms, transform 300ms` \u2192\n `transition-property: opacity, transform` + `transition-duration: 200ms, 300ms`.\n - In a **patch**, a shorthand replaces the longhands it controls (`padding: 8px` overwrites a previous\n `padding-left`), and `\"border\": null` removes all twelve border longhands.\n- **Flex layout is not a `flex` value**: set `display: flex` **plus** `flex-direction`, `align-items`,\n `justify-content` as separate properties.\n- CSS is grouped by breakpoint: `desktop`, `tablet`, `mobile`.\n- Reference a style variable in CSS as `var(--name)`; a schema variable in a prop as `{{name}}`.\n- `element.style.base` is a **list** of definition refs; other slots go under `element.style.slots`.\n- **An element can attach SEVERAL classes at once, and they all apply.** `style.base` holds a list, and each\n non-base slot holds its own \u2014 every attached definition contributes CSS, and they **cascade** (a later class, then a\n global/id rule, overrides an earlier one on the same property). So when a style looks wrong, the culprit may be\n ANY attached class, not the one you just edited: read the element's `resolvedStyle` (it inlines the CSS of every\n class it attaches, keyed by ref) together with its `globalStyles`/`idStyle` and the type's `defaultStyle`, and\n fix the class that actually sets the property \u2014 do not just pile another class on top.\n- **Three kinds of style live in the style schema \u2014 do not confuse them:**\n - **Definitions** = reusable CSS **classes** (`upsertDefinition`/`patchDefinition`/`deleteDefinition`, keyed by a\n class `ref`). Attach one to an element via `style.base` to style **that** element (and anything else that opts in).\n This is the **default** way to style one element. Declaring MORE than one class in a batch? Use\n `upsertDefinitions` \u2014 one op carrying `{ \"<class>\": { desktop: \u2026 }, \u2026 }`, same result as the run of\n `upsertDefinition` it replaces, without repeating the envelope once per class.\n - **Global styles** = the CSS equivalent of a bare element selector like `button { \u2026 }`\n (`upsertGlobalStyle`/`patchGlobalStyle`/`deleteGlobalStyle`, keyed by `componentType`). They style **every**\n element of that type at once. Use these for site-wide intent \u2014 e.g. \"all buttons rounded\":\n `{ \"type\": \"upsertGlobalStyle\", \"componentType\": \"button\", \"desktop\": { \"border-radius\": \"9999px\" } }`.\n - **Id styles** = the CSS equivalent of an id selector like `#hero { \u2026 }`\n (`upsertIdStyle`/`patchIdStyle`/`deleteIdStyle`, keyed by `targetId`). They style the **single** element whose\n DOM `id` attribute equals `targetId` \u2014 so the element must carry that `id` (set it in its props). Prefer a\n **definition** for one element; reach for an id style only when a specific, uniquely-identified node must be\n targeted by id: `{ \"type\": \"upsertIdStyle\", \"targetId\": \"hero\", \"desktop\": { \"min-height\": \"100vh\" } }`.\n - The three share one name space, so an op refuses a name held by another kind (guards against a typo silently\n rewriting every element of a type, or converting a class into an id rule). If refused, you targeted the wrong\n kind \u2014 switch tools or rename.\n\n## Style variants & element state\nA **variant** is a named CSS override on a definition (e.g. a button class with a `primary` variant). It takes two\nsteps across the two schemas:\n- **Declare** the variant CSS on the class (style schema): `upsertDefinition`/`patchDefinition` with\n `variants: { \"primary\": { \"desktop\": { \"background-color\": \"#111\" } } }` (per slot under `slots.<slot>.variants`).\n- **Apply** it to an element (element schema): `initialState.styleVariant` =\n `{ \"<class-ref>\": { \"base\": \"primary\" } }` \u2014 a slot name instead of `base` targets that slot; an array applies\n several. Set it via `upsertElement`/`patchElement`.\nAn element read reports `availableVariants` (which variant each attached class offers) and the element's current\n`initialState`, so you can see a button **has** a `primary` variant and whether it uses it. If the user asks for a\nvariant that does not exist yet, **create it (upsertDefinition variants) and apply it in the same batch**.\n- `initialState.visibility` (boolean) sets whether the element starts shown or hidden.\n\n**Ancestor conditions** \u2014 a class that changes inside an ANCESTOR (`.toolbar .icon`), or while that ancestor is\nhovered or in a variant (`.card:hover .icon`, a collapsed sidebar hiding its labels) is `ancestors` on that class,\nkeyed by a class the ancestor wears: `ancestors: { \"toolbar\": { \"desktop\": { \u2026 } }, \"card\": { \"states\": { \"hover\":\n{ \"desktop\": { \u2026 } } } }, \"sidebar\": { \"variants\": { \"collapsed\": \u2026 } } }`.\nAny ancestor counts, not only the parent; the class's own states and variants win over these. Never customCss for it.\n\n## Data bindings\nConnect a data **source** to an element field. A binding is `{ to, source, transformers?, when?, enabled? }` grouped\nby **category**: `attributes` (a prop), `style` (a style value), `initialState` (an initial-state key).\n- `upsertBinding` adds one, or replaces the binding already feeding the same `to` (or `id`).\n- `patchBinding` edits an existing one (matched by `to`/`id`); `deleteBinding` removes it.\nDiscover valid source paths **and the transformer catalog** in `plitzi://data-sources/{env}`. Example \u2014 feed an API\nlist into a list element:\n`{ \"type\": \"upsertBinding\", \"pageRef\": \"home\", \"ref\": \"myList\", \"category\": \"attributes\",\n \"binding\": { \"to\": \"items\", \"source\": \"apiContainer_x.data\" } }`.\n\n**Source scope \u2014 a source is visible to the provider's DESCENDANTS only.** An element source named\n`<type>_<name>` (e.g. `apiContainer_products`, `list_food-list`) is published by that element into the scope of\nits **subtree**, so **only elements INSIDE the provider can bind to it**. Binding a sibling or an unrelated element\nto it resolves to nothing at runtime. So to consume `apiContainer_products.data`, the bound element must live under\nthat apiContainer; inside a `list`, the repeated `listItem` and its children read the per-row source\n(`list_<name>.item.<field>`). Module sources (no `<type>_<name>` head \u2014 `state`, `space`, `navigation`,\n`auth`, `collection`) are global and bindable anywhere. Binding an element to an element source outside its\nprovider's subtree is schema-valid but **broken at runtime** (the source is not in scope), so\n`plitzi_validate`/`plitzi_apply` treat it as an **error and reject the batch** \u2014 move the element under the\nprovider, or bind a source that is in scope.\n\n**mockData is builder-only.** An `apiContainer`'s `mockData` prop feeds sample data **while editing in the\nbuilder**; the published runtime fetches real data instead. Never rely on mockData as the production source \u2014 give\nthe provider a real source: a **connector** with `runtime: \"server\"` (see *Connectors*, the right answer whenever\nthe API needs a token or the content should be in the HTML), or a browser-side `query`/`method` for a public URL.\n\n**`transformers` \u2014 post-process the value before it reaches the field** (`source \u2192 t\u2081 \u2192 t\u2082 \u2192 field`). An array of\n`{ action, params }`; the runtime runs them in order and resolves each by its `action` alone, so an **unknown\naction is silently skipped** and the raw value passes through. Use the **exact** action names from\n`plitzi://data-sources` (`transformers`). The most common is **`twigTemplate`** to format a value \u2014 the incoming\nvalue is the **`{{source}}`** token (NOT `{{value}}`); `{{sourceTo}}` is the field's previous value. Example \u2014\nshow a number with units:\n`{ \"type\": \"upsertBinding\", \"pageRef\": \"home\", \"ref\": \"food-item-time\", \"category\": \"attributes\",\n \"binding\": { \"to\": \"content\", \"source\": \"list_food-list.item.cookTimeMinutes\",\n \"transformers\": [ { \"action\": \"twigTemplate\", \"params\": { \"template\": \"{{source}} min de cocci\u00F3n\" } } ] } }`.\nOther transformers: `dateConverter` (format a date/timestamp), `capitalize`, `stringToArray` (split on a\nseparator), `arrayMap` (remap the keys of each object in an array), `staticValue`. Transformer `params` values are\nstrings. Each transformer also takes an optional `enabled` flag: set `\"enabled\": false` to keep it in the chain but\nskip it at runtime (defaults to true) \u2014 the value passes through untouched, and a disabled transformer is not\nvalidated.\n\n**`when` \u2014 gate the binding** with a QueryBuilder RuleGroup: the binding only applies when the guard passes against\nthe data source. Shape: `{ \"combinator\": \"and\", \"rules\": [ { \"field\": \"<path>\", \"operator\": \"=\", \"value\": \"x\" } ] }`\n(operators: `=`, `!=`, `<`, `>`, `contains`, `beginsWith`, `empty`, `in`, `between`, \u2026; nest RuleGroups for\nand/or). The guard is validated structurally. Example \u2014 only bind when a flag is set:\n`\"when\": { \"combinator\": \"and\", \"rules\": [ { \"field\": \"status\", \"operator\": \"=\", \"value\": \"published\" } ] }`.\n\n## Interactions\nAn interaction **flow** is a **trigger** (an event like `onClick`, `onPageLoad`) followed by the callbacks/utilities\nit runs, in order. You pass the steps **in order** and the stored beforeNode/afterNode/flowId links are computed for\nyou \u2014 never wire them by hand. Each step also has an `enabled` flag (see disable vs delete below).\n\n**Node types & `elementId`** \u2014 a step names which element (or module) provides the callback it runs. Picking the\n**wrong node type for an action** makes the runtime resolve it against nothing, so the step **silently does nothing**:\n- `trigger` \u2014 the event; belongs to the host element. `elementId` defaults to the host.\n- `callback` \u2014 a callback provided by a **specific element**. `elementId` is that element's name (the flow host by\n default, or another element to act on). Every element\n registers a built-in **`setState`** callback that changes **its own attribute or state**: params\n `category` (`\"attribute\"` \u2014 set a prop like `content`/`disabled` \u2014 or `\"state\"` \u2014 `visibility` or a style\n selector), `key`, `value` (a **scalar** whose type follows the target attribute \u2014 a real boolean `true`/`false`\n for a boolean attribute, a number for a numeric one, otherwise a string), and **`revertOnFinish`**. Set\n `revertOnFinish: true` for a **temporary** change (a \"loading\u2026\" label, disabling a button while it works): it is\n **undone automatically when the flow finishes**, so you do **NOT** add manual restore steps at the end. This element\n `setState` has **no** `type` param (that belongs to the global one below). Beside it every element registers\n **`toggleState`** \u2014 the same write with **no `value`**: it stores the OPPOSITE of what is there\n (`category`, `key`, `revertOnFinish`). That is how expand/collapse is authored \u2014 **one step on one trigger**,\n never two `setState` branches under complementary `when` conditions, which read the state as it was when the\n flow STARTED and so are always one step behind. An element type may also register its own extra callbacks.\n- `globalCallback` \u2014 a callback provided by a **source module**, NOT by any element: `addNotification` (source\n `space`), `setState`/`toggleState`/`clearState` (`state`), `navigate` (`navigation`), `login`/`logout`/\n `refreshDetails` (`auth`), `runServerAction`/`cancelServerAction` (`actions`). Its `elementId` is the\n **source module id**, never the host element \u2014 a node that stored the host element's name here would resolve to\n nothing at runtime. **Omit `elementId`**: the MCP sets the correct source and fills the\n builder's **param defaults** (e.g. `addNotification` gets `autoDismiss:true`, `autoDismissTimeout:5000`,\n `placement:\"top-right\"`, `appeareance:\"success\"`) for any params you leave out. Use **only** the params each\n callback declares (exact spelling) \u2014 for `addNotification` the visible text goes in `content`; there is **no**\n `title`/`message`/`type` param, and any unknown key is dropped. See the full param schema for each callback under\n `globalCallbacks` in `plitzi://interactions/{env}`.\n- **Two `setState`s (and two `toggleState`s) \u2014 do not mix them:** the **element** `setState` (nodeType\n `callback`, on an element, category/key/value/revertOnFinish) changes THAT element's attribute/state and is what\n you want to change a button's label or disabled flag. The **global** `setState` (nodeType `globalCallback`,\n source `state`, key/type/value) writes `runtime.state.<key>`. They share a name but have different node types\n AND different params, and `toggleState` splits the same way: on an element (category/key/revertOnFinish) it flips\n that element's own field; on source `state` (key) it flips `runtime.state.<key>`. Pick the element one when\n nothing outside the element needs to know it is open; pick the state one when something else binds to it.\n- `utility` \u2014 a built-in utility action (no element/source module); nodeType `utility`. Use the **exact** param\n names: `delayTime` waits `time` milliseconds (**not** `delay`), `twigTemplate` (`returnMode`, `template`),\n `webHook` (`url`, `method`, \u2026). See `utilities` in `plitzi://interactions/{env}`.\n\n**A param value can be a data binding.** Any interaction param may hold a `{{ source }}` token instead of a literal \u2014\nit resolves at runtime exactly like a prop binding, using the same source grammar (`<type>_<name>.<path>`, or a\nmodule source like `navigation`/`state`). This is how a step reacts to *the data in context*: inside a `listItem`,\na click on a row can show `addNotification` with `content: \"{{ list_<name>.item.name }}\"` \u2014 the clicked row's field.\nThe value follows the source's type, so a token is valid even where a param expects a boolean/number. Copy/paste of an\nelement repoints these tokens to the copies' names automatically, along with the element's bindings.\n\n**Navigating between pages \u2014 prefer the `Link` element over an interaction.** For a plain \"go to page X\" the right\ntool is a `link` element (a container, see *Pages & folders*), not a `navigate` interaction step. Use the\n`navigate` globalCallback only when the navigation is one step of a larger flow (e.g. submit a form, then go).\n\nTools:\n- `upsertInteractionFlow` \u2014 create or replace one flow. The FIRST node must be a `trigger`. Pass `flowId` (the\n trigger's node id) to replace an existing flow. Example (elementId omitted \u2014 the MCP wires it to `space` and fills\n the notification defaults):\n `{ \"type\": \"upsertInteractionFlow\", \"pageRef\": \"home\", \"ref\": \"cta\", \"nodes\": [\n { \"nodeType\": \"trigger\", \"action\": \"onClick\", \"title\": \"Click\" },\n { \"nodeType\": \"globalCallback\", \"action\": \"addNotification\", \"title\": \"Notify\",\n \"params\": { \"content\": \"Saved!\" } } ] }`.\n- `patchInteractionNode` \u2014 change one step in place (by `nodeId`); `params` merge onto the node.\n\n**Disable vs delete a step \u2014 do not confuse them (three different intents):**\n- **Disable / deactivate / turn off a step** (keep it in the flow, just stop it running): `patchInteractionNode`\n with `{ \"enabled\": false }`. Re-enable with `{ \"enabled\": true }`. This is NOT a deletion \u2014 the step stays.\n- **Remove one step** from a flow: `deleteInteraction` with `nodeId` (its neighbors are re-linked).\n- **Remove the whole flow**: `deleteInteraction` with `flowId` (the trigger node id).\n\nSo \"deactivate the addNotification step\" means `patchInteractionNode { enabled: false }` \u2014 never delete the step, and\nnever delete the flow. `deleteInteraction` is **destructive and not undoable**: only use it when the user asked to\n*remove* something, and **confirm with the user before deleting** a step or a flow.\n\nDiscover valid actions in `plitzi://interactions/{env}`: `actions` = observed, `globalCallbacks` /\n`elementCallbacks` / `utilities` = the built-in vocabularies with their full param schema, so you know the exact\nnode type and valid params per action. An element read lists its flows as ordered nodes (each with its `id` and\n`enabled`), so a follow-up patch/delete needs no extra read.\n\n## Connectors \u2014 CMS and API integrations\nA **connector** is how a space reads real content from Strapi, WordPress, Contentful, Directus or any REST service.\nIt is **a manifest, not code**: you declare the base URL, the endpoints, the auth template and the filter operators,\nand the **server** executes it during the render. Integrating a new CMS never means shipping an adapter \u2014 it means\nwriting one document, which you can do here with `upsertConnector`.\n\nWhy the server and not the browser: the request (and the token behind it) never reaches the visitor, and the content\nis in the HTML search engines see. That is also the one hard rule below \u2014 a provider element that is not\nserver-rendered ignores its connector entirely.\n\n**Read these two first:**\n- `plitzi://connector-presets` \u2014 **working manifests** for Strapi v5, WordPress, Directus, Contentful and a plain\n REST API, each with the `credentialKeys` that provider needs, **plus every template token** the engine binds.\n Start from the preset for the user's provider and edit it; do not invent a shape from memory.\n- `plitzi://connectors/{env}` \u2014 the connectors **this space already has**, each with its read/write endpoint names,\n its filter operators and its published fields. `plitzi://connectors/{env}/{ref}` opens one manifest in full.\n\n### Server actions\nWork a page cannot do in the browser \u2014 charge a card, send mail, read a system only the server reaches. An action\nis a stored flow the SERVER runs; a page names it and never learns what it does. Write one with\n`upsertAction` / `patchAction`, then call it from a page flow with the `runServerAction` step (mode\n`await` to use the result, `detached` to fire and carry on).\n\nFour rules worth knowing before you write:\n\n- **Steps are server tasks.** `plitzi://actions/{env}/tasks` lists what this deployment can run \u2014 a browser step\n (`setState`, `navigate`) has nothing to act on here, and the reverse is true too: a task cannot run in a page\n flow.\n- **The contracts.** `input` is coerced and anything undeclared is DROPPED. The OUTPUT is whatever the final\n `flow.output` step names \u2014 there is no separate list to keep in step with it \u2014 and that step must be LAST,\n since only the last one that runs is answered. Everything else a step produced stays on the server.\n- **Whether it runs lives on its TRIGGERS.** Each trigger step carries `enabled`, and the action is on when any\n way into it is \u2014 there is no switch beside the flow. So \"pause this action\" means disabling its trigger step(s),\n never deleting them.\n- **A step names the credential it uses**, and sees no other. `credentials` on the document says which ones the\n action may ask for at all; the secret itself is never yours to see, exactly as with a connector.\n\n### Credentials are NOT yours to write\nYou never create, see or transmit a secret. Author the manifest with the token in place\n(`\"value\": \"Bearer {{credential.token}}\"`) and **leave `connection.credential` unset** unless the user hands you an\nidentifier. The connector **saves fine that way** \u2014 validation reports it as a *warning*, not an error \u2014 and its\nrequests go unauthenticated until the space owner creates the credential in the builder and attaches it. Say that to\nthe user as the last step of any integration you build, naming the keys the preset lists (e.g. Strapi needs `token`).\n\n### The manifest\nEvery string is a template the server renders per request. `{{resource}}` is the collection the element asked for;\n`{{limit}}`/`{{offset}}`/`{{page}}`/`{{cursor}}` the window; `{{routeParams.x}}`/`{{queryParams.x}}` the\nvisitor's URL; `{{credential.x}}` the secret; `{{field}}`/`{{value}}` inside an operator; `{{id}}`/`{{values}}`\ninside a write. The op splits it in two, exactly as the engine does:\n- **`connection`** \u2014 what applies to every call: `credential`, `auth`, `headers`, `pagination`, `operators`,\n `mediaBaseUrl`, `fields`, `projection`.\n- **`endpoints`** \u2014 the individual calls: `read` (named; **`list` is the one an element addresses when it names\n none**) and `write` (named; **omit for read-only** \u2014 an undeclared write can never be executed).\n\nA read endpoint says where things live in *that* provider's response: `itemsPath` (the array), `totalPath` (the\ncount), `idPath` (the record's id), `valuesPath` (its fields). Strapi's `data` + `meta.pagination.total` and\nContentful's `items` + `fields` are the same manifest with different paths.\n\nTwo traps the validator will catch for you, both silent at runtime otherwise:\n- An **operator template must render a whole `key=value` entry** (`\"eq\": \"filters[{{field}}][$eq]={{value}}\"`). A\n filter naming an operator the manifest does not declare is **dropped** \u2014 the query then returns *unfiltered*\n records, not none.\n- **Paging must appear in the request.** Declaring `pagination: \"page\"` without `{{page}}` in the query makes every\n page resolve to the first one.\n\nExample \u2014 Strapi, from the preset:\n```json\n{ \"type\": \"upsertConnector\", \"ref\": \"strapi-blog\", \"name\": \"Blog CMS\",\n \"baseUrl\": \"https://cms.example.com\",\n \"connection\": {\n \"auth\": { \"in\": \"header\", \"name\": \"Authorization\", \"value\": \"Bearer {{credential.token}}\" },\n \"pagination\": \"offset\",\n \"operators\": { \"eq\": \"filters[{{field}}][$eq]={{value}}\", \"contains\": \"filters[{{field}}][$containsi]={{value}}\" },\n \"mediaBaseUrl\": \"https://cms.example.com\" },\n \"endpoints\": { \"read\": { \"list\": {\n \"path\": \"/api/{{resource}}\",\n \"query\": { \"pagination[start]\": \"{{offset}}\", \"pagination[limit]\": \"{{limit}}\", \"populate\": \"*\" },\n \"itemsPath\": \"data\", \"totalPath\": \"meta.pagination.total\", \"idPath\": \"documentId\" } } } }\n```\n`patchConnector` changes part of one (endpoints merge **by name**; `null` removes one) \u2014 that is how you add a\n`detail` read to a large manifest without resending it. `deleteConnector` is destructive: every element pointing\nat it stops resolving, so confirm first.\n\n### Using a connector on a page\nThe provider element is `apiContainer`, and it needs **both halves**:\n1. `runtime: \"server\"` \u2014 set it on `upsertElement`/`patchElement`. **Without it the connector is ignored** and the\n element falls back to fetching its own `query` URL from the browser. This is the single most common mistake; the\n validator warns when it sees one half without the other.\n2. props: `connector` (the ref), `resource` (the collection \u2192 `{{resource}}`), and optionally `endpoint` (a read\n other than `list`), `filters`, `limit`, `singleRecord`, `pagination`, `pageParam`.\n\nIt then publishes one source **to its descendants only** (like any provider \u2014 bind *inside* its subtree):\n`apiContainer_<name>.records` (an array), `.pageInfo` (`page`, `pageCount`, `total`, `hasNextPage`\u2026),\n`.isEmpty`, `.hasError`, `.errorMessage`, `.isLoading`. With `singleRecord: true` it publishes `.record`\ninstead of `.records` \u2014 that is what a **detail page** uses. Bind an empty-state block's visibility to `.isEmpty`\nand an error block's to `.hasError`; they are ordinary bindings, no special mechanism.\n\n**A list page** \u2014 provider, then a `list` inside it bound to the records, then the row template reading the item:\n```json\n{ \"operations\": [\n { \"type\": \"upsertElement\", \"pageRef\": \"blog\", \"element\": {\n \"ref\": \"posts-api\", \"type\": \"apiContainer\", \"runtime\": \"server\",\n \"props\": { \"connector\": \"strapi-blog\", \"resource\": \"articles\", \"limit\": \"9\", \"pagination\": \"url\" },\n \"children\": [ { \"ref\": \"posts\", \"type\": \"list\", \"children\": [\n { \"ref\": \"post-row\", \"type\": \"listItem\", \"children\": [\n { \"ref\": \"post-title\", \"type\": \"text\" } ] } ] } ] } },\n { \"type\": \"upsertBinding\", \"pageRef\": \"blog\", \"ref\": \"posts\", \"category\": \"attributes\",\n \"binding\": { \"to\": \"items\", \"source\": \"apiContainer_posts-api.records\" } },\n { \"type\": \"upsertBinding\", \"pageRef\": \"blog\", \"ref\": \"post-title\", \"category\": \"attributes\",\n \"binding\": { \"to\": \"content\", \"source\": \"list_posts.item.title\" } } ] }\n```\n\n**A detail page** is the same provider with `singleRecord` and a filter resolved from the URL. Create the page with a\nroute param (`\"slug\": \"blog/:postSlug\"`), then filter on it \u2014 the filter `value` is a template the server resolves:\n```json\n{ \"type\": \"upsertElement\", \"pageRef\": \"post-detail\", \"element\": {\n \"ref\": \"post-api\", \"type\": \"apiContainer\", \"runtime\": \"server\",\n \"props\": { \"connector\": \"strapi-blog\", \"resource\": \"articles\", \"singleRecord\": true,\n \"filters\": [ { \"field\": \"slug\", \"operator\": \"eq\", \"value\": \"{{routeParams.postSlug}}\" } ] } } }\n```\nIts children then bind to `apiContainer_post-api.record.<field>`. A filter whose template resolves to nothing\nreturns **no records** rather than the whole collection \u2014 a URL that addressed one post never renders a different one.\n\n**Paging** is the `pagination` prop: `\"none\"`, `\"url\"` (the page number rides the query string, so pages are\nshareable and indexable \u2014 prefer it) or `\"append\"` (a \"load more\" list). Give each list on a page its own\n`pageParam` so two lists page independently. The provider also offers the `loadMore` and `goToPage` callbacks to\ninteraction flows, and `performQuery` to refetch.\n\n**Writing back** (a form, a \"delete\" button): declare the endpoint under `endpoints.write`, then call the provider's\n**`writeRecord`** callback from an interaction flow \u2014 nodeType `callback`, `elementId` the apiContainer's ref,\nparams `action` (the write endpoint's name) and `recordId`. The write goes through the server, which owns the\ncredential and refuses any action the manifest does not declare. Writes exist **only** on a server-rendered provider.\n\n**Connectors need a server.** A space published without server rendering has nowhere to resolve one: it works in the\nbuilder and renders empty when published. If the user has no SSR deployment, say so rather than leaving them to\ndiscover it \u2014 the alternative is a browser-side `query`, which cannot keep a secret.\n\n## Shared layouts \u2014 the chrome a page does NOT contain\nA page's tree is usually **not the whole page**. The header, the sidebar and the footer normally live in a **layout\ncontainer**: a shell several pages are rendered inside, so the navigation is authored once instead of once per page.\n\nIt is a **root of its own** \u2014 a `layoutContainer` element that no page owns and that is not in the page list \u2014 and\na page points at it with two attributes:\n- `layout` \u2014 the shell's ref. This is the whole of \"which chrome do I render inside\".\n- `layoutContainer` \u2014 the **slot**: the ref of a container INSIDE that shell where this page's own body is placed.\n\nSo a page with `layout: \"main-layout\"` renders `main-layout`'s whole tree, with its own elements dropped into the\none container it named. Two pages naming the same shell share every element of it \u2014 **editing the shell changes\nevery page that uses it**, which is the point and also the thing to say out loud before you touch one.\n\n**Reading them.**\n- `plitzi://schema/{env}/layouts` lists the shells: `ref`, `label`, `usedBy` (the pages), `slots`, `elementCount`.\n- Every page in `plitzi://schema/{env}/pages` carries `layout` and `layoutSlot` when it has them.\n- Read a shell exactly like a page: `plitzi://schema/{env}/pages/{layoutRef}` for its skeleton,\n `.../pages/{layoutRef}/styles` for its classes. `plitzi_search` reports a layout's elements with the layout as\n their `pageRef`.\n\n**Writing them.** Every element/binding/interaction op takes the shell's ref as its `pageRef` \u2014 there is no separate\nvocabulary:\n```json\n{ \"type\": \"upsertElement\", \"pageRef\": \"main-layout\", \"parentRef\": \"sidebar\",\n \"element\": { \"ref\": \"upgrade-card\", \"type\": \"container\", \"children\": [ \u2026 ] } }\n```\n- `upsertLayout` creates a shell (`ref`, `label`, `subType`). Put the chrome in it with `upsertElement`, including\n an empty container to serve as the slot.\n- `upsertPage { layout, layoutContainer }` attaches a page to it; `layout: null` detaches. A `layoutContainer` that\n is not an element of that shell is refused \u2014 stored, it would render the page's body where nobody sees it.\n- `deleteLayout` removes a shell and everything in it, and is **refused while any page still renders inside it**\n (the refusal names them). Destructive: what it deletes is on every page that uses it, so confirm first.\n\n**Where does this element belong?** If the user asks for something \"on every page\" \u2014 a nav item, a banner, a footer\nlink, an upgrade prompt in the sidebar \u2014 it belongs in the LAYOUT, not in the page you happen to have open. Check\nthe page's `layout` first; if the element you are looking for is not in the page's own tree, it is in the shell.\n\n## Pages & folders\n- **Always set a `slug` when creating a page** (`upsertPage`) \u2014 it is the page's URL path and good practice for a\n clean, stable route (e.g. `\"pricing\"` or `\"posts/:postId\"`). Omit it and the page ref is used as the slug,\n and `plitzi_validate`/`plitzi_apply` warn so you remember to set a meaningful one.\n- **A page slug is RELATIVE \u2014 do NOT start it with `/`.** The runtime prepends the leading slash (and any folder\n path) itself, so a leading slash doubles it. Write `\"pricing\"`, not `\"/pricing\"`. (upsertPage strips a leading\n slash for you, but write it relative.)\n- **Dynamic pages \u2014 route params.** A slug segment written `:name` (e.g. `\"posts/:postId\"`) is a **route param**,\n exactly like React Router. On that page it is readable **two ways**: as `{{name}}` inside a prop, and as the\n data-binding source **`navigation.routeParams.name`** (the `navigation` module source, global \u2014 bindable\n anywhere). So a blog is a `\"posts\"` list page plus a `\"posts/:postId\"` detail page whose `apiContainer` query\n binds `navigation.routeParams.postId` to fetch that one post. (`navigation.queryParams.<name>` exposes `?query=`\n string params the same way.)\n- **Navigate with the `Link` element, not an interaction.** `link` is a **container** \u2014 it wraps any children and\n navigates on click, so it is the default way to move between pages. Its `mode`: `\"page\"` links to another space\n page (set `href` to the target page, folder path resolved for you); `\"internal\"` takes a path inside the space and\n resolves `{{token}}` templates in it, so a row's \"view\" link is `mode:\"internal\", href:\"posts/{{postId}}\"`;\n `\"external\"` is a full URL. `target` is `self`/`blank`/`parent`/`top`. Reach for the `navigate` globalCallback\n only when navigation must be **one step inside a larger interaction flow** (e.g. save, then go) \u2014 for a plain link,\n use `link`.\n\nPages can be grouped into **folders** (the sidebar tree). A folder is `{ ref, name, slug, parentId? }`; its `ref`\n**is its id**, and that id is what a page and a nested folder reference.\n- **Folder slugs PREPEND to the page URL \u2014 this is how nested URLs are built.** The full path is each ancestor\n folder's slug plus the page slug, joined by `/`: a page at slug `\"1-1-1\"` inside `folder-1` > `folder-1-1`\n resolves to `/folder-1/folder-1-1/1-1-1`. So a folder slug is part of the route; keep folder slugs relative too.\n- Create/rename/move a folder with `upsertFolder` (the `ref` you pass on create becomes its id \u2014 pick a stable one\n like `\"blog\"`). Nest it under another with `parentId` (a folder ref); `parentId: null` moves it back to the root.\n- Put a page in a folder with `upsertPage`'s `folder` (a folder ref). A page's `folder` is always either **empty\n (root)** or an **existing folder id**: `folder: null` or `folder: \"\"` moves it to the root, and any other value\n must resolve to a folder that already exists or is created earlier in the same batch \u2014 an unknown folder is\n rejected, never stored.\n- `deleteFolder` removes a folder and **promotes its contents up one level** \u2014 its child folders and its pages move\n to its parent (or the root). A folder cannot be nested under itself or one of its descendants.\n- **Disable a page** with `upsertPage`'s `enabled: false`; `enabled: true` re-enables it (defaults to enabled, and\n a page read reports its current `enabled`). Disabling only affects the **published SDK runtime** \u2014 the page stops\n being routable/accessible to end users. It stays fully **editable here**: you can still read it and apply any op to\n a disabled page. This does not delete it \u2014 `deletePage` does.\n\n## Settings\nSpace-level configuration lives in `plitzi://settings/{env}` and is edited with a single **`patchSettings`** op\n(merge \u2014 only the fields you pass change):\n- `customCss` \u2014 **raw global CSS** injected for the whole space. Use it only for genuinely site-wide rules\n (`@keyframes`, `@font-face`, resets). To style an element, write a **definition** and attach it \u2014 never customCss.\n- `keepState` / `stateStorage` \u2014 persist element state across reloads (`localStorage`/`sessionStorage`).\n- **User provider / auth**: `userProvider` \u2014 `basic` for any HTTP+JSON backend, the name of a provider registered\n in the page, or `\"\"` to disable auth \u2014 plus `tokenStorage`, the `loginUrl`/`userUrl`/`refreshUrl`/`logoutUrl`\n endpoints and the `detailsPath`/`tokenPath`/`refreshTokenPath`/`expirationTimePath` mapping that says where the\n values sit in their responses. `sessionHintCookie` is worth setting whenever the backend can: it names a readable\n cookie carrying only expiries, which is what lets a page answer \"nobody is signed in\" without a request.\n Example \u2014 inject a keyframe globally:\n `{ \"type\": \"patchSettings\", \"customCss\": \"@keyframes spin { to { transform: rotate(360deg); } }\" }`.\n\n## Semantics\n- **props are fully replaced** on `upsertElement`: send every prop you want to keep. To change only some props,\n use **`patchElement`** \u2014 it merges `props`/`style` onto the existing element (listed keys change, `null` unsets\n a key, everything else is preserved) and never creates. Combined with `plitzi_search` (which returns the ref +\n stateVersion), a targeted edit is two calls with no read.\n- **definition CSS is fully replaced** on `upsertDefinition`: send every property you want to keep. To change only\n some declarations, use **`patchDefinition`** \u2014 it merges CSS per breakpoint/state/variant/slot onto the existing\n definition (listed keys change, `null` removes a property, everything else is preserved) and never creates.\n Example \u2014 recolor one definition without resending it: `{ \"type\": \"patchDefinition\", \"ref\": \"btn-x\",\n \"desktop\": { \"background-color\": \"#111\" } }`.\n- **Atomic batches**: if any operation fails, `plitzi_apply` persists nothing.\n- **Every resource you touch must be malformation-free \u2014 pre-existing errors block the save.** When your batch edits\n an element (or a definition/global/id style), the validator also checks the resource's **current stored content**\n for malformations \u2014 a broken transformer action, a malformed interaction node, invalid CSS \u2014 even in parts your\n edit does not touch. Such a finding is reported as a `Pre-existing malformation in <resource>: \u2026` **error**, and\n `plitzi_apply` rejects the batch until it is fixed. These are **not caused by your change** (the message says so)\n \u2014 do not be confused; fix them **in the same batch** and re-apply. Because the check runs on the resulting state,\n including the fix in your batch is exactly what unblocks the save. (Advisory issues \u2014 an unobserved source/action\n name that may still be a valid plugin, a binding target a plugin manifest does not list \u2014 come back as\n `Pre-existing issue \u2026` **warnings** and do not block.)\n- **Optimistic concurrency \u2014 read before you write, and prove your read is current.** Editing a resource means you\n read it first, so you hold its `stateVersion`. **Always pass `expectedResourceVersions`** (URI \u2192 the stateVersion\n you read) for every resource your batch changes. If another agent edited it in the meantime, the live version no\n longer matches and apply is **rejected with a conflict** \u2014 nothing persists. Then re-read the reported resources\n (their new content + version) and retry on top of the fresh state. This is exactly how a file editor forces a\n re-read after a stale write: it is what keeps concurrent agents from silently overwriting each other's changes, so\n never omit it \"to save a call\".\n";
3
+ export declare const guideQuickstart: string;
4
+ export declare const guideText: string;