@penpair/core 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/adapters.d.ts +16 -1
  2. package/dist/adapters.js +8 -12
  3. package/dist/approvals.d.ts +32 -0
  4. package/dist/approvals.js +99 -0
  5. package/dist/atomic-output.d.ts +5 -0
  6. package/dist/atomic-output.js +28 -0
  7. package/dist/audit-rules.d.ts +60 -0
  8. package/dist/audit-rules.js +177 -0
  9. package/dist/backup-note.d.ts +4 -0
  10. package/dist/backup-note.js +28 -0
  11. package/dist/baseline-coverage.d.ts +25 -0
  12. package/dist/baseline-coverage.js +114 -0
  13. package/dist/baselines-config.d.ts +25 -0
  14. package/dist/baselines-config.js +52 -3
  15. package/dist/baselines.d.ts +37 -8
  16. package/dist/baselines.js +132 -52
  17. package/dist/cli-arguments.d.ts +7 -0
  18. package/dist/cli-arguments.js +35 -0
  19. package/dist/cli-main.d.ts +13 -1
  20. package/dist/cli-main.js +770 -139
  21. package/dist/client.d.ts +26 -5
  22. package/dist/client.js +46 -20
  23. package/dist/command-result.d.ts +159 -0
  24. package/dist/command-result.js +179 -0
  25. package/dist/coordination.d.ts +15 -0
  26. package/dist/coordination.js +122 -0
  27. package/dist/credentials.d.ts +21 -0
  28. package/dist/credentials.js +61 -0
  29. package/dist/deep-check.js +9 -5
  30. package/dist/design-spec.d.ts +55 -0
  31. package/dist/design-spec.js +135 -0
  32. package/dist/endpoint.d.ts +8 -1
  33. package/dist/endpoint.js +48 -4
  34. package/dist/errors.d.ts +2 -1
  35. package/dist/errors.js +8 -4
  36. package/dist/export.d.ts +18 -0
  37. package/dist/export.js +42 -7
  38. package/dist/file-archive.d.ts +16 -0
  39. package/dist/file-archive.js +32 -0
  40. package/dist/file-guard.d.ts +8 -0
  41. package/dist/file-guard.js +40 -0
  42. package/dist/formats.d.ts +4 -0
  43. package/dist/formats.js +28 -0
  44. package/dist/index.d.ts +33 -1
  45. package/dist/index.js +33 -1
  46. package/dist/inspection.d.ts +106 -0
  47. package/dist/inspection.js +344 -0
  48. package/dist/instance/config-schema.d.ts +422 -0
  49. package/dist/instance/config-schema.js +76 -0
  50. package/dist/instance/config.d.ts +29 -0
  51. package/dist/instance/config.js +102 -11
  52. package/dist/instance/connect.d.ts +10 -1
  53. package/dist/instance/connect.js +53 -12
  54. package/dist/instance/connection.d.ts +14 -0
  55. package/dist/instance/connection.js +13 -2
  56. package/dist/instance/init.js +1 -1
  57. package/dist/instance/setup.js +1 -1
  58. package/dist/instance/tty.js +3 -1
  59. package/dist/journal.d.ts +77 -0
  60. package/dist/journal.js +260 -0
  61. package/dist/kit-smoke.d.ts +31 -0
  62. package/dist/kit-smoke.js +83 -0
  63. package/dist/kit-source.d.ts +2 -0
  64. package/dist/kit-source.js +309 -0
  65. package/dist/kit.d.ts +25 -0
  66. package/dist/kit.js +90 -0
  67. package/dist/layout.d.ts +15 -0
  68. package/dist/layout.js +79 -0
  69. package/dist/live-smoke.d.ts +8 -0
  70. package/dist/live-smoke.js +14 -0
  71. package/dist/mcp-server.d.ts +12 -0
  72. package/dist/mcp-server.js +102 -0
  73. package/dist/native-api.d.ts +15 -0
  74. package/dist/native-api.js +42 -0
  75. package/dist/onboard.d.ts +10 -0
  76. package/dist/onboard.js +15 -0
  77. package/dist/operation.d.ts +20 -1
  78. package/dist/operation.js +113 -13
  79. package/dist/png.d.ts +5 -0
  80. package/dist/png.js +7 -0
  81. package/dist/portable-tokens.d.ts +22 -0
  82. package/dist/portable-tokens.js +134 -0
  83. package/dist/probe.d.ts +6 -2
  84. package/dist/probe.js +19 -3
  85. package/dist/project-input.d.ts +6 -0
  86. package/dist/project-input.js +51 -0
  87. package/dist/project-snippets.js +12 -7
  88. package/dist/project-status.d.ts +24 -0
  89. package/dist/project-status.js +79 -0
  90. package/dist/registry.d.ts +2 -1
  91. package/dist/registry.js +17 -3
  92. package/dist/run-file.d.ts +26 -0
  93. package/dist/run-file.js +100 -0
  94. package/dist/shots.d.ts +43 -0
  95. package/dist/shots.js +90 -0
  96. package/dist/skill-instructions.d.ts +13 -0
  97. package/dist/skill-instructions.js +75 -0
  98. package/dist/skills.d.ts +11 -0
  99. package/dist/skills.js +71 -33
  100. package/dist/snippet.d.ts +3 -2
  101. package/dist/snippet.js +12 -8
  102. package/dist/snippets/archive.d.ts +9 -0
  103. package/dist/snippets/archive.js +10 -4
  104. package/dist/snippets/read.d.ts +1 -1
  105. package/dist/snippets/read.js +71 -40
  106. package/dist/structure-snapshot.d.ts +73 -0
  107. package/dist/structure-snapshot.js +171 -0
  108. package/dist/token-edit.d.ts +53 -0
  109. package/dist/token-edit.js +197 -0
  110. package/dist/tokens-diff.d.ts +19 -6
  111. package/dist/tokens-diff.js +89 -18
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +1 -1
  115. package/skills/penpair-design/SKILL.md +12 -40
  116. package/skills/penpair-design/references/penpot-plugin-gotchas.md +56 -0
  117. package/skills/penpair-design/references/project-facts.md +1 -1
  118. package/skills/penpair-design/references/write-protocol.md +15 -3
  119. package/skills/penpair-handoff/SKILL.md +14 -0
  120. package/skills/penpair-handoff/references/handoff-flow.md +11 -0
  121. package/skills/penpair-recipes/SKILL.md +16 -0
  122. package/skills/penpair-recipes/references/build-flow.md +11 -0
  123. package/skills/penpair-setup/SKILL.md +6 -2
@@ -0,0 +1,102 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import { runCommand } from "./cli-main.js";
7
+ import { loadConfig } from "./instance/config.js";
8
+ import { resolveProjectOutput } from "./project-input.js";
9
+ import { collectCommand } from "./command-result.js";
10
+ import { version } from "./version.js";
11
+ const string = { type: "string", minLength: 1, maxLength: 1000 };
12
+ const integer = (max) => ({ type: "integer", minimum: 0, maximum: max });
13
+ const definitions = [
14
+ { name: "pages", description: "List pages in the pinned project file", properties: {}, required: [], command: () => ["pages"] },
15
+ { name: "tree", description: "Bounded tree preview; omitted nodes are explicit", properties: { page: string, depth: integer(100) }, required: ["page"], command: a => ["tree", "--page", String(a.page), "--depth", String(a.depth ?? 5)] },
16
+ { name: "audit", description: "Inspect layout/token findings with documented heuristic limits", properties: { page: string }, required: ["page"], command: a => ["audit", "--page", String(a.page)] },
17
+ { name: "inspect", description: "One bounded structured inspection chunk; use returned cursor for continuation", properties: { page: string, id: string, cursor: string }, required: ["page", "id"], command: a => ["inspect", String(a.id), "--page", String(a.page), ...(a.cursor ? ["--cursor", String(a.cursor)] : [])] },
18
+ { name: "tokens_dump", description: "Bounded token capture; completeness is explicit", properties: {}, required: [], command: () => ["tokens", "dump", "--max-total-bytes", "131072"] },
19
+ { name: "run_status", description: "Inspect an explicit local recovery journal", properties: { id: string }, required: ["id"], command: a => ["runs", String(a.id)] },
20
+ { name: "baselines_status", description: "Offline coverage/evidence status; no capture or approval", properties: {}, required: [], command: () => ["baselines", "coverage"] },
21
+ { name: "project_status", description: "Project health with explicit unknowns; no adapter imports", properties: {}, required: [], command: () => ["status"] },
22
+ { name: "doctor", description: "Project connection/file identity diagnostics", properties: {}, required: [], command: () => ["doctor"] },
23
+ { name: "design_spec", description: "Derived handoff for an exact frame ID; unknown semantics remain explicit", properties: { page: string, id: string }, required: ["page", "id"], command: a => ["spec", "--page", String(a.page), "--frame", String(a.id)] },
24
+ { name: "export", description: "Capture stable PNG to a relative path inside the project; creates a local file", properties: { page: string, id: string, out: string }, required: ["page", "id", "out"], command: a => ["export", String(a.id), String(a.out), "--page", String(a.page)], localWrite: true },
25
+ { name: "run_registered", description: "Execute a trusted registered operation: inspect by default, explicit apply and recovery for writes. Not a sandbox.", properties: { name: string, params: { type: "object", additionalProperties: { type: "string", maxLength: 4000 }, maxProperties: 100 }, apply: { type: "boolean", default: false }, resume: string, checkpoint: { type: "boolean", default: false } }, required: ["name"], write: true, command: a => ["run", String(a.name), ...Object.entries(a.params ?? {}).flatMap(([k, v]) => ["--param", `${k}=${v}`]), ...(a.apply ? ["--apply"] : []), ...(a.resume ? ["--resume", String(a.resume)] : []), ...(a.checkpoint ? ["--checkpoint"] : [])] },
26
+ ];
27
+ function validate(def, raw) {
28
+ if (raw === undefined)
29
+ raw = {};
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
31
+ throw new Error("Tool arguments must be an object");
32
+ const a = raw;
33
+ for (const key of def.required)
34
+ if (!(key in a))
35
+ throw new Error(`Missing ${key}`);
36
+ for (const [key, value] of Object.entries(a)) {
37
+ const schema = def.properties[key];
38
+ if (!schema)
39
+ throw new Error(`Unexpected tool argument ${key}`);
40
+ if (schema.type === "string" && (typeof value !== "string" || !value || value.startsWith("-") || value.length > (schema.maxLength ?? 1000) || /[\x00-\x1f]/.test(value)))
41
+ throw new Error(`Invalid ${key}`);
42
+ if (schema.type === "boolean" && typeof value !== "boolean")
43
+ throw new Error(`Invalid ${key}`);
44
+ if (schema.type === "integer" && (!Number.isInteger(value) || Number(value) < 0 || Number(value) > schema.maximum))
45
+ throw new Error(`Invalid ${key}`);
46
+ if (key === "params" && (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).length > 100 || Object.entries(value).some(([k, v]) => !/^[a-zA-Z][\w-]{0,100}$/.test(k) || typeof v !== "string" || v.length > 4000)))
47
+ throw new Error("Invalid registered parameters");
48
+ }
49
+ if ((a.resume || a.checkpoint) && !a.apply)
50
+ throw new Error("Explicit resume requires apply");
51
+ return a;
52
+ }
53
+ /** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
54
+ export async function createMcpServer(options) {
55
+ if (!path.isAbsolute(options.root))
56
+ throw new Error("MCP project root must be absolute");
57
+ const root = await realpath(options.root), config = await loadConfig(root);
58
+ if (!config.file?.id)
59
+ throw new Error("MCP server requires a configured owner-confirmed file pin");
60
+ const pinned = { instance: config.instance, fileId: config.file.id, profile: JSON.stringify("profile" in config ? config.profile : null) };
61
+ const timeout = options.timeoutMs ?? 60000, limit = options.maxOutputBytes ?? 256 * 1024;
62
+ if (!Number.isInteger(timeout) || timeout < 1 || timeout > 300000 || !Number.isInteger(limit) || limit < 1024 || limit > 1024 * 1024)
63
+ throw new Error("Invalid MCP budgets");
64
+ const available = definitions.filter(d => !d.write || options.allowWrites);
65
+ const server = new Server({ name: "penpair", version }, { capabilities: { tools: {} } });
66
+ const tools = available.map(d => ({ name: d.name, description: d.description, inputSchema: { type: "object", properties: d.properties, required: d.required, additionalProperties: false }, annotations: { readOnlyHint: !d.write && !d.localWrite, destructiveHint: Boolean(d.write || d.localWrite), idempotentHint: !d.write && !d.localWrite, openWorldHint: true } }));
67
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
68
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
69
+ const controller = new AbortController();
70
+ const abort = () => controller.abort(extra.signal.reason);
71
+ extra.signal.addEventListener("abort", abort, { once: true });
72
+ if (extra.signal.aborted)
73
+ abort();
74
+ const timer = setTimeout(() => controller.abort(Object.assign(new Error("MCP command deadline exceeded"), { code: "deadline" })), timeout);
75
+ let result;
76
+ try {
77
+ const def = available.find(d => d.name === request.params.name);
78
+ if (!def)
79
+ throw new Error("Unknown or disabled tool");
80
+ const args = validate(def, request.params.arguments);
81
+ const current = await loadConfig(root);
82
+ if (current.instance !== pinned.instance || current.file?.id !== pinned.fileId || JSON.stringify("profile" in current ? current.profile : null) !== pinned.profile)
83
+ throw new Error("Pinned project target changed; restart and explicitly review server configuration");
84
+ if (def.localWrite)
85
+ args.out = await resolveProjectOutput(root, String(args.out));
86
+ result = await runCommand(def.command(args), { ...options.cli, root, projectEndpointOnly: true, pinnedConfig: config, strictFileGuard: true, signal: controller.signal });
87
+ }
88
+ catch (error) {
89
+ result = await collectCommand(request.params.name, async () => { throw error; });
90
+ }
91
+ finally {
92
+ clearTimeout(timer);
93
+ extra.signal.removeEventListener("abort", abort);
94
+ }
95
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > limit) {
96
+ result = { ...result, outcome: result.outcome === "uncertain" ? "uncertain" : "incomplete", exitCode: result.outcome === "uncertain" ? 4 : 3, data: null, messages: [], warnings: [], errors: [], completeness: { complete: false, reason: "MCP response byte budget exceeded; use a narrower inspection or the CLI artifact path" } };
97
+ }
98
+ return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result, isError: result.exitCode !== 0 };
99
+ });
100
+ return server;
101
+ }
102
+ export async function serveMcp(options) { const server = await createMcpServer(options); await server.connect(new StdioServerTransport()); return server; }
@@ -0,0 +1,15 @@
1
+ import type { PenpotToolClient } from "./client.js";
2
+ import { type OperationReport, type OperationOptions } from "./operation.js";
3
+ export declare function validateFileIntegrity(client: PenpotToolClient): Promise<{
4
+ supported: boolean;
5
+ valid?: boolean;
6
+ issues?: unknown[];
7
+ }>;
8
+ /** Version history is a recovery aid, never a substitute for a restored independent backup. */
9
+ export declare function createCheckpoint(client: PenpotToolClient, checkpoint: {
10
+ label: string;
11
+ runId: string;
12
+ }, options?: OperationOptions): Promise<{
13
+ supported: boolean;
14
+ report?: OperationReport;
15
+ }>;
@@ -0,0 +1,42 @@
1
+ import { runOperation } from "./operation.js";
2
+ import { runSnippet, snippet } from "./snippet.js";
3
+ export async function validateFileIntegrity(client) {
4
+ const result = await runSnippet(client, snippet("file-integrity", `
5
+ if (typeof penpot.currentFile.validate !== "function") return { ok: true, supported: false };
6
+ const issues = penpot.currentFile.validate();
7
+ if (!Array.isArray(issues)) return { ok: false, message: "file.validate returned no issue list" };
8
+ return { ok: true, supported: true, valid: issues.length === 0, issues };`, {}, "read"));
9
+ if (typeof result.supported !== "boolean")
10
+ throw new Error("Integrity check returned no capability result");
11
+ if (result.supported && (!Array.isArray(result.issues) || typeof result.valid !== "boolean" || result.valid !== (result.issues.length === 0)))
12
+ throw new Error("Integrity check returned incomplete or contradictory observations");
13
+ return { supported: result.supported, ...(result.supported ? { valid: result.valid === true, issues: result.issues } : {}) };
14
+ }
15
+ /** Version history is a recovery aid, never a substitute for a restored independent backup. */
16
+ export async function createCheckpoint(client, checkpoint, options = {}) {
17
+ if (!checkpoint.label.trim() || checkpoint.label.length > 120 || !/^[a-zA-Z0-9-]{1,100}$/.test(checkpoint.runId))
18
+ throw new Error("A checkpoint requires a short label and a stable alphanumeric/hyphen run ID");
19
+ const capability = await runSnippet(client, snippet("checkpoint-capability", `return { ok: true, fileId: penpot.currentFile.id, supported: typeof penpot.currentFile.saveVersion === "function" && typeof penpot.currentFile.findVersions === "function" };`, {}, "read"));
20
+ if (capability.ok !== true || typeof capability.supported !== "boolean" || typeof capability.fileId !== "string")
21
+ throw new Error("Checkpoint capability returned no target/presence observations");
22
+ if (capability.supported !== true)
23
+ return { supported: false };
24
+ const params = { label: `penpair/${checkpoint.runId}/${checkpoint.label}`, fileId: capability.fileId };
25
+ const guard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok: false, message: "checkpoint file changed" };';
26
+ const listing = `${guard}
27
+ const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
28
+ return { ok: true, fileId: penpot.currentFile.id, versions: versions.map((version) => ({ label: version.label, createdAt: String(version.createdAt) })) };`;
29
+ const report = await runOperation(client, {
30
+ name: "checkpoint", page: () => undefined,
31
+ inspect: () => snippet("checkpoint-inspect", listing, params, "read"),
32
+ apply: () => snippet("checkpoint-apply", `${guard}
33
+ const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
34
+ if (versions.length > 1) return { ok: false, message: "checkpoint label is ambiguous" };
35
+ if (versions.length === 1) return { ok: true, existed: true };
36
+ await penpot.currentFile.saveVersion(CFG.label);
37
+ return { ok: true, created: true };`, params, "write"),
38
+ readback: () => snippet("checkpoint-readback", listing, params, "read"),
39
+ verify: ({ readback }) => readback.fileId === params.fileId && Array.isArray(readback.versions) && readback.versions.length === 1 && readback.versions[0]?.label === params.label ? [] : ["Checkpoint was not independently found once in the intended file"],
40
+ }, undefined, options);
41
+ return { supported: true, report };
42
+ }
@@ -0,0 +1,10 @@
1
+ import { type ProjectConfig } from "./instance/config.js";
2
+ export declare function onboardingChecklist(config: ProjectConfig): {
3
+ schemaVersion: 1;
4
+ project: string;
5
+ origin: string;
6
+ file: ProjectConfig["file"] | null;
7
+ fileUrl: string | null;
8
+ steps: string[];
9
+ warnings: string[];
10
+ };
@@ -0,0 +1,15 @@
1
+ import { isExternal } from "./instance/config.js";
2
+ export function onboardingChecklist(config) {
3
+ const origin = isExternal(config) ? config.profile.origin : `http://localhost:${config.ports.frontend}`;
4
+ const fileUrl = config.file ? `${origin}/#/workspace?file-id=${encodeURIComponent(config.file.id)}` : null;
5
+ return { schemaVersion: 1, project: config.instance, origin, file: config.file ?? null, fileUrl,
6
+ steps: ["Clone the project and install its locked dependencies with the repository's package manager.",
7
+ `Ask the owner to invite your own Penpot account to the shared file at ${origin}.`,
8
+ "Create your own personal MCP key. Keep it out of chat, commits and command arguments.",
9
+ "Run the project's penpair connect command; enter your own key in its hidden prompt.",
10
+ config.file ? `Open ${config.file.name || config.file.id} (${config.file.id}). Use the owner's full editor link if the direct file link needs team context.` : "Ask the owner to pin the shared file with penpair connect --pin-file and commit the pin.",
11
+ "Open the Penpot MCP plugin and Connect. Keep the editor tab open and awake.",
12
+ "Run penpair doctor --json, then a read-only penpair pages before proposing writes.",
13
+ "Coordinate a write window with colleagues. Inspect the plan, apply once, verify independently and review the export."],
14
+ warnings: [...(!isExternal(config) ? ["localhost is this computer only. A colleague needs access to a shared reachable deployment; do not share generated instance secrets."] : []), ...(!config.file ? ["No shared file ID is pinned yet."] : []), "Writer markers are advisory and do not block human editing or guarantee exclusion across machines."] };
15
+ }
@@ -1,6 +1,8 @@
1
+ import { type LayoutOptions, type LayoutResult } from "./layout.js";
1
2
  import type { ExecuteOptions, PenpotToolClient } from "./client.js";
2
3
  import { type Snippet, type SnippetResult } from "./snippet.js";
3
4
  import { type ArchiveParams } from "./snippets/archive.js";
5
+ import { type JournalOptions } from "./journal.js";
4
6
  /**
5
7
  * A Penpot change done the safe way: look first, change once, read back independently. `verify` compares the three
6
8
  * results and returns what is wrong; an empty list means the change is confirmed.
@@ -10,15 +12,24 @@ export interface Operation<P> {
10
12
  /** The page the operation works on, so the runner can open it. */
11
13
  page(params: P): string | undefined;
12
14
  inspect(params: P): Snippet;
13
- apply(params: P): Snippet;
15
+ apply(params: P, inspected?: SnippetResult): Snippet;
14
16
  readback(params: P): Snippet;
15
17
  verify(results: {
16
18
  inspect: SnippetResult;
17
19
  apply: SnippetResult;
18
20
  readback: SnippetResult;
19
21
  }): string[];
22
+ /** Explicit reconciliation of a partial write; absent it, only unchanged inputs or independently verified final state may resume. */
23
+ reconcile?(results: {
24
+ before: SnippetResult;
25
+ current: SnippetResult;
26
+ readback: SnippetResult;
27
+ }): "complete" | "pending" | "conflict";
20
28
  }
21
29
  export interface OperationOptions extends ExecuteOptions {
30
+ /** Explicit opt-out is for callers that provide their own settling evidence. */
31
+ layout?: false | LayoutOptions;
32
+ journal?: Omit<JournalOptions, "operation" | "input" | "target">;
22
33
  /** Without it only the read-only inspect runs. */
23
34
  apply?: boolean;
24
35
  /** Wait between the write and the readback: token resolution and layout settle slowly. */
@@ -26,6 +37,14 @@ export interface OperationOptions extends ExecuteOptions {
26
37
  onStep?: (message: string) => void;
27
38
  }
28
39
  export interface OperationReport {
40
+ complete?: boolean;
41
+ redacted?: boolean;
42
+ layout?: LayoutResult;
43
+ journal?: {
44
+ runId: string;
45
+ state: string;
46
+ };
47
+ outcome: "inspected" | "verified" | "failed";
29
48
  operation: string;
30
49
  phase: "inspected" | "applied";
31
50
  inspect: SnippetResult;
package/dist/operation.js CHANGED
@@ -1,7 +1,14 @@
1
+ import { waitForStableLayout } from "./layout.js";
2
+ import { commandSleep } from "./command-result.js";
1
3
  import { runSnippet } from "./snippet.js";
2
4
  import { switchPage } from "./snippets/read.js";
3
5
  import { archiveApply, archiveInspect, archiveReadback } from "./snippets/archive.js";
4
- const defaultSleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
6
+ import { PenpairError } from "./errors.js";
7
+ import { UncertainExecutionError } from "./client.js";
8
+ import { observationHash, readJournalTarget, runJournaledSteps } from "./journal.js";
9
+ import { guardRevision, writeCoordinator } from "./coordination.js";
10
+ import { guardFile } from "./file-guard.js";
11
+ const defaultSleep = commandSleep;
5
12
  /** Runs a snippet; if the editor shows another page, opens the right one, waits, and tries once more. */
6
13
  export async function runOnPage(client, s, page, { settleMs = 3_000, sleep = defaultSleep, onStep, ...execute } = {}) {
7
14
  const first = await runSnippet(client, s, execute);
@@ -16,33 +23,107 @@ export async function runOnPage(client, s, page, { settleMs = 3_000, sleep = def
16
23
  return second;
17
24
  }
18
25
  export async function runOperation(client, operation, params, options = {}) {
19
- const { settleMs = 10_000, sleep = defaultSleep, onStep } = options;
26
+ if (options.settleMs !== undefined && (!Number.isFinite(options.settleMs) || options.settleMs < 0 || options.settleMs > 30000))
27
+ throw new Error("Minimum settle delay must be from 0 to 30000 ms");
28
+ if (options.apply && options.journal)
29
+ return journaledOperation(client, operation, params, options);
30
+ const { settleMs = 0, sleep = defaultSleep, onStep } = options;
20
31
  const page = operation.page(params);
21
32
  onStep?.(`${operation.name}: inspecting`);
22
- const inspect = await runOnPage(client, operation.inspect(params), page, options);
33
+ const inspect = await runOnPage(client, operation.inspect(params), page, { ...options, effect: "read" });
34
+ if (inspect.ok !== true)
35
+ throw new PenpairError("tool-failed", `${operation.name}: inspect did not return an explicit ok result; nothing was applied`);
23
36
  if (!options.apply)
24
- return { operation: operation.name, phase: "inspected", inspect, problems: [], verified: false };
37
+ return { operation: operation.name, outcome: "inspected", phase: "inspected", inspect, problems: [], verified: false };
25
38
  onStep?.(`${operation.name}: writing once`);
26
- const apply = await runOnPage(client, operation.apply(params), page, options);
27
- onStep?.(`${operation.name}: waiting ${Math.round(settleMs / 1000)} s before reading back`);
28
- await sleep(settleMs);
29
- const readback = await runOnPage(client, operation.readback(params), page, options);
30
- const problems = operation.verify({ inspect, apply, readback });
31
- return { operation: operation.name, phase: "applied", inspect, apply, readback, problems, verified: problems.length === 0 };
39
+ const apply = await runOnPage(client, operation.apply(params, inspect), page, { ...options, effect: "write" });
40
+ if (apply.ok !== true)
41
+ throw new UncertainExecutionError(String(apply.execution?.runId ?? "unknown"), `${operation.name}: apply returned no explicit acknowledgment`);
42
+ if (settleMs > 0)
43
+ await sleep(settleMs);
44
+ let layout;
45
+ let readback;
46
+ let problems;
47
+ try {
48
+ layout = await settleOperation(client, options);
49
+ readback = await runOnPage(client, operation.readback(params), page, { ...options, effect: "read" });
50
+ problems = verificationProblems(operation, { inspect, apply, readback });
51
+ }
52
+ catch (error) {
53
+ problems = [`Independent verification failed: ${error instanceof Error ? error.message : String(error)}`];
54
+ }
55
+ return { layout, operation: operation.name, phase: "applied", outcome: problems.length === 0 ? "verified" : "failed", inspect, apply, readback, problems, verified: problems.length === 0 };
56
+ }
57
+ function verificationProblems(operation, results) {
58
+ const { readback } = results;
59
+ const observations = Object.keys(readback).filter((key) => !["ok", "readOnly", "execution", "problems"].includes(key));
60
+ if (Array.isArray(readback.problems) && readback.problems.length > 0 && readback.problems.every((problem) => typeof problem === "string"))
61
+ return readback.problems;
62
+ if (readback.ok !== true || observations.length === 0)
63
+ return ["Readback did not provide explicit observations of the resulting state"];
64
+ const result = operation.verify(results);
65
+ return Array.isArray(result) && result.every((problem) => typeof problem === "string") ? result : ["Verifier must return a list of problems"];
66
+ }
67
+ async function settleOperation(client, options) {
68
+ if (options.layout === false)
69
+ return undefined;
70
+ options.onStep?.("waiting for bounded layout completion");
71
+ const layout = await waitForStableLayout(client, { sleep: options.sleep, ...options.layout });
72
+ if (!layout.stable)
73
+ throw new PenpairError("incomplete-output", `Layout did not settle: ${layout.reason ?? "unknown reason"}`);
74
+ return layout;
75
+ }
76
+ async function journaledOperation(client, operation, params, options) {
77
+ const page = operation.page(params);
78
+ await runOnPage(client, operation.inspect(params), page, { ...options, effect: "read" });
79
+ await settleOperation(client, options);
80
+ const initialTarget = await readJournalTarget(client);
81
+ const guarded = guardFile(client, { id: initialTarget.fileId });
82
+ const observe = async () => {
83
+ const inspection = await runOnPage(guarded, operation.inspect(params), page, { ...options, effect: "read" });
84
+ const layout = await settleOperation(guarded, options);
85
+ const readback = await runOnPage(guarded, operation.readback(params), page, { ...options, effect: "read" });
86
+ if (inspection.ok !== true || readback.ok !== true)
87
+ throw new Error("Operation inspection/readback did not acknowledge its observations");
88
+ return { inspection, readback, layout };
89
+ };
90
+ const verify = (current, before, result) => verificationProblems(operation, { inspect: before.inspection, apply: result ?? { ok: true, reconciled: true }, readback: current.readback }).length === 0;
91
+ const record = await runJournaledSteps([{ key: "apply", label: operation.name,
92
+ signature: [operation.inspect(params).code, operation.apply(params).code, operation.readback(params).code, operation.verify.toString(), operation.reconcile?.toString(), settleOperation.toString(), waitForStableLayout.toString()].join("\n"),
93
+ inspect: observe,
94
+ apply: async (current, target) => {
95
+ const result = await runOnPage(guardRevision(guarded, target), operation.apply(params, current.inspection), page, { ...options, effect: "write" });
96
+ if (result.ok !== true)
97
+ throw new Error("Apply returned no explicit acknowledgment");
98
+ await (options.sleep ?? defaultSleep)(options.settleMs ?? 0);
99
+ return result;
100
+ }, verify,
101
+ reconcile: (before, current, result) => {
102
+ if (verify(current, before, result))
103
+ return "complete";
104
+ if (operation.reconcile)
105
+ return operation.reconcile({ before: before.inspection, current: current.inspection, readback: current.readback });
106
+ return observationHash(before.inspection, true) === observationHash(current.inspection, true) ? "pending" : "conflict";
107
+ },
108
+ }], { ...options.journal, coordinate: writeCoordinator(guarded, { root: options.journal.root, operation: operation.name, forceLock: options.journal.forceLock, warn: options.onStep }), operation: operation.name, input: { params, layout: options.layout ?? {}, minimumWaitMs: options.settleMs ?? 0 }, payload: { params }, target: () => readJournalTarget(guarded), onStep: options.onStep ?? options.journal.onStep, redact: options.redact ?? options.journal.redact });
109
+ const saved = record.steps[0];
110
+ return { layout: saved.after.layout, operation: operation.name, phase: "applied", outcome: record.redacted ? "failed" : "verified", ...(record.redacted ? { redacted: true, complete: false } : {}), inspect: saved.before.inspection, apply: saved.result, readback: saved.after.readback, problems: record.redacted ? ["Journal evidence required credential redaction; completeness cannot be confirmed"] : [], verified: !record.redacted, journal: { runId: record.runId, state: record.state } };
32
111
  }
33
112
  /** Nests the frames that are not part of the live work into one archive board. Frames keep their names, ids and component links. */
34
113
  export const archiveOperation = {
35
114
  name: "archive",
36
115
  page: (params) => params.page,
37
116
  inspect: archiveInspect,
38
- apply: archiveApply,
117
+ apply: (params, inspected) => archiveApply({ ...params, expectedPlan: inspected?.plan, expectedArchiveId: inspected?.archiveId }),
39
118
  readback: archiveReadback,
40
119
  verify({ inspect, apply, readback }) {
41
120
  const problems = [];
121
+ if (!Number.isInteger(inspect.willMove) || Number(inspect.willMove) < 0 || !Array.isArray(inspect.plan) || !Array.isArray(readback.insideIds) || !Array.isArray(readback.loose))
122
+ return ["Archive verification is missing movement counts or the independent loose-frame listing"];
42
123
  const expected = Number(inspect.willMove ?? 0);
43
124
  const archive = readback.archive;
44
- if (expected > 0 && Number(apply.moved ?? 0) !== expected)
45
- problems.push(`moved ${apply.moved} frames, expected ${expected}`);
125
+ if (inspect.plan.length !== expected || inspect.plan.some((shape) => !shape.id || !readback.insideIds.includes(shape.id)))
126
+ problems.push("the inspected frames are not all in the archive");
46
127
  if (expected > 0 && !archive)
47
128
  problems.push("the archive board is not on the page");
48
129
  if (archive && archive.children < expected)
@@ -52,4 +133,23 @@ export const archiveOperation = {
52
133
  problems.push(`${loose.length} frames are still loose on the page: ${loose.slice(0, 3).join(", ")}`);
53
134
  return problems;
54
135
  },
136
+ reconcile({ before, current, readback }) {
137
+ if (!Array.isArray(before.plan) || !Array.isArray(current.plan) || !Array.isArray(readback.insideIds))
138
+ return "conflict";
139
+ if (observationHash(before, true) === observationHash(current, true))
140
+ return "pending";
141
+ const original = before.plan;
142
+ const remaining = current.plan;
143
+ const intended = new Map(original.map((shape) => [shape.id, shape]));
144
+ if (remaining.some((shape) => !intended.has(shape.id) || observationHash(shape) !== observationHash(intended.get(shape.id))))
145
+ return "conflict";
146
+ const accounted = new Set([...remaining.map((shape) => shape.id), ...readback.insideIds]);
147
+ if (original.some((shape) => !accounted.has(shape.id)))
148
+ return "conflict";
149
+ if (remaining.length > 0 && (!before.archiveId || before.archiveId !== current.archiveId))
150
+ return "conflict";
151
+ if (Array.isArray(before.insideIds) && before.insideIds.some((id) => !readback.insideIds.includes(id)))
152
+ return "conflict";
153
+ return remaining.length === 0 ? "complete" : "pending";
154
+ },
55
155
  };
package/dist/png.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Pixel size from a PNG's header (the IHDR chunk), or nothing when the bytes are not a PNG. */
2
+ export declare function pngSize(buffer: Buffer): {
3
+ width: number;
4
+ height: number;
5
+ } | undefined;
package/dist/png.js ADDED
@@ -0,0 +1,7 @@
1
+ /** Pixel size from a PNG's header (the IHDR chunk), or nothing when the bytes are not a PNG. */
2
+ export function pngSize(buffer) {
3
+ const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
4
+ if (buffer.length < 24 || !buffer.subarray(0, 8).equals(signature) || buffer.subarray(12, 16).toString("ascii") !== "IHDR")
5
+ return undefined;
6
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
7
+ }
@@ -0,0 +1,22 @@
1
+ /** Deliberately bounded subset of the published DTCG 2025.10 format; no Penpot arithmetic or native writes. */
2
+ export interface PortableToken {
3
+ path: string[];
4
+ type: string;
5
+ value: unknown;
6
+ description?: string;
7
+ extensions?: unknown;
8
+ }
9
+ export interface PortableTokens {
10
+ schemaVersion: 1;
11
+ kind: "portable-tokens";
12
+ format: "dtcg-2025.10-subset";
13
+ tokens: PortableToken[];
14
+ groups: Record<string, {
15
+ type?: string;
16
+ description?: string;
17
+ extensions?: unknown;
18
+ }>;
19
+ losses: string[];
20
+ }
21
+ export declare function importDtcg(raw: unknown): PortableTokens;
22
+ export declare function exportDtcg(raw: unknown): Record<string, unknown>;
@@ -0,0 +1,134 @@
1
+ const types = ["color", "dimension", "number", "fontFamily", "fontWeight", "typography"];
2
+ const isObject = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
3
+ const aliasPath = (v) => typeof v === "string" && /^\{[^{}]+\}$/.test(v) ? v.slice(1, -1) : undefined;
4
+ function fail(message) { throw new Error("Portable token subset: " + message); }
5
+ export function importDtcg(raw) {
6
+ if (!isObject(raw) || JSON.stringify(raw).length > 8 * 1024 * 1024)
7
+ fail("input must be a bounded JSON object");
8
+ let groupCount = 0;
9
+ const tokens = [], groups = Object.create(null);
10
+ const visit = (node, path, inherited) => {
11
+ if (path.length > 32 || tokens.length > 10000)
12
+ fail("depth/token budget exceeded");
13
+ for (const key of Object.keys(node))
14
+ if (key.startsWith("$") && !["$value", "$type", "$description", "$extensions"].includes(key))
15
+ fail(`unsupported metadata/reference ${key}`);
16
+ const type = typeof node.$type === "string" ? node.$type : inherited;
17
+ if (node.$type !== undefined && (typeof node.$type !== "string" || !types.includes(node.$type)))
18
+ fail("type must be a string");
19
+ if (node.$description !== undefined && typeof node.$description !== "string")
20
+ fail("description must be a string");
21
+ if (node.$extensions !== undefined && !isObject(node.$extensions))
22
+ fail("extensions must be an object");
23
+ if ("$value" in node) {
24
+ if (!path.length || !type || !types.includes(type) || Object.keys(node).some(k => !k.startsWith("$")))
25
+ fail("unsupported token type or mixed token/group");
26
+ tokens.push({ path, type, value: structuredClone(node.$value), ...(node.$description !== undefined ? { description: node.$description } : {}), ...(node.$extensions !== undefined ? { extensions: structuredClone(node.$extensions) } : {}) });
27
+ return;
28
+ }
29
+ if (++groupCount > 10000)
30
+ fail("group budget exceeded");
31
+ groups[path.join(".")] = { ...(node.$type ? { type: String(node.$type) } : {}), ...(node.$description !== undefined ? { description: node.$description } : {}), ...(node.$extensions !== undefined ? { extensions: structuredClone(node.$extensions) } : {}) };
32
+ for (const [key, value] of Object.entries(node)) {
33
+ if (key.startsWith("$"))
34
+ continue;
35
+ if (!key || /[.{}\x00-\x1f]/.test(key) || !isObject(value))
36
+ fail("invalid token/group path");
37
+ visit(value, [...path, key], type);
38
+ }
39
+ };
40
+ visit(raw, []);
41
+ if (!tokens.length || tokens.length > 10000)
42
+ fail("requires 1–10000 tokens");
43
+ const portable = { schemaVersion: 1, kind: "portable-tokens", format: "dtcg-2025.10-subset", tokens: tokens.sort((a, b) => a.path.join(".").localeCompare(b.path.join("."))), groups, losses: [] };
44
+ validatePortableValues(portable);
45
+ return portable;
46
+ }
47
+ function validatePortableValues(document) {
48
+ const byPath = new Map(document.tokens.map(t => [t.path.join("."), t])), resolved = new Map();
49
+ if (byPath.size !== document.tokens.length)
50
+ fail("duplicate token path");
51
+ const resolveToken = (name, expected, chain) => {
52
+ const token = byPath.get(name);
53
+ if (!token || token.type !== expected)
54
+ fail(`missing/type-mismatched alias ${name}`);
55
+ if (chain.includes(name))
56
+ fail("alias cycle: " + [...chain, name].join(" -> "));
57
+ if (chain.length > 64)
58
+ fail("alias depth exceeds 64");
59
+ if (resolved.has(name))
60
+ return resolved.get(name);
61
+ const value = validateValue(token.type, token.value, [...chain, name]);
62
+ resolved.set(name, value);
63
+ return value;
64
+ };
65
+ const validateValue = (type, value, chain) => {
66
+ const ref = aliasPath(value);
67
+ if (ref !== undefined)
68
+ return resolveToken(ref, type, chain);
69
+ if (isObject(value) && "$ref" in value)
70
+ fail("JSON Pointer property references are outside this subset");
71
+ if (type === "number" && (typeof value !== "number" || !Number.isFinite(value)))
72
+ fail("number must be finite");
73
+ if (type === "fontWeight" && (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 1000))
74
+ fail("fontWeight supports numeric 1–1000 only");
75
+ if (type === "fontFamily" && !(typeof value === "string" && value.length > 0 || Array.isArray(value) && value.length > 0 && value.length <= 100 && value.every(v => typeof v === "string" && v.length > 0)))
76
+ fail("fontFamily must be a name or ordered names");
77
+ if (type === "dimension" && (!isObject(value) || Object.keys(value).some(k => !["value", "unit"].includes(k)) || typeof value.value !== "number" || !Number.isFinite(value.value) || !["px", "rem"].includes(String(value.unit))))
78
+ fail("dimension supports explicit finite px/rem values");
79
+ if (type === "color" && (!isObject(value) || Object.keys(value).some(k => !["colorSpace", "components", "alpha", "hex"].includes(k)) || !["srgb", "display-p3"].includes(String(value.colorSpace)) || !Array.isArray(value.components) || value.components.length !== 3 || value.components.some(c => typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) || value.alpha !== undefined && (typeof value.alpha !== "number" || !Number.isFinite(value.alpha) || value.alpha < 0 || value.alpha > 1) || value.hex !== undefined && (typeof value.hex !== "string" || !/^#[0-9a-f]{6}$/i.test(value.hex))))
80
+ fail("color supports finite srgb/display-p3 components in 0..1 and optional alpha/hex");
81
+ if (type === "typography") {
82
+ if (!isObject(value) || Object.keys(value).some(k => !["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"].includes(k)))
83
+ fail("typography property outside subset");
84
+ const fields = { fontFamily: "fontFamily", fontSize: "dimension", fontWeight: "fontWeight", letterSpacing: "dimension", lineHeight: "number" };
85
+ const result = {};
86
+ for (const [key, expected] of Object.entries(fields))
87
+ result[key] = validateValue(expected, value[key], chain);
88
+ if (Number(result.lineHeight) <= 0)
89
+ fail("lineHeight must be positive");
90
+ return result;
91
+ }
92
+ if (!types.includes(type))
93
+ fail("unsupported type " + type);
94
+ return value;
95
+ };
96
+ for (const token of document.tokens)
97
+ resolveToken(token.path.join("."), token.type, []);
98
+ }
99
+ export function exportDtcg(raw) {
100
+ const v = raw;
101
+ if (!v || JSON.stringify(v).length > 8 * 1024 * 1024 || v.schemaVersion !== 1 || v.kind !== "portable-tokens" || v.format !== "dtcg-2025.10-subset" || !Array.isArray(v.tokens) || !v.tokens.length || v.tokens.length > 10000 || !isObject(v.groups) || !Array.isArray(v.losses) || v.losses.length)
102
+ fail("unsupported/incomplete portable schema; use import-tokens on a supported document");
103
+ const root = Object.create(null);
104
+ const group = (path) => { let current = root; for (const key of path) {
105
+ if (!key || key.startsWith("$") || /[.{}\x00-\x1f]/.test(key))
106
+ fail("invalid path");
107
+ const next = current[key];
108
+ if (next !== undefined && (!isObject(next) || "$value" in next))
109
+ fail("group/token collision");
110
+ current = (current[key] ??= Object.create(null));
111
+ } return current; };
112
+ for (const [key, metadata] of Object.entries(v.groups)) {
113
+ if (!isObject(metadata) || Object.keys(metadata).some(k => !["type", "description", "extensions"].includes(k)))
114
+ fail("invalid group metadata");
115
+ const target = group(key ? key.split(".") : []);
116
+ if (metadata.type !== undefined)
117
+ target.$type = metadata.type;
118
+ if (metadata.description !== undefined)
119
+ target.$description = metadata.description;
120
+ if (metadata.extensions !== undefined)
121
+ target.$extensions = metadata.extensions;
122
+ }
123
+ for (const token of v.tokens) {
124
+ if (!token || !Array.isArray(token.path) || !token.path.length || token.path.length > 32 || token.path.some(p => typeof p !== "string"))
125
+ fail("invalid token path");
126
+ const target = group(token.path.slice(0, -1)), key = token.path.at(-1);
127
+ if (!key || key.startsWith("$") || /[.{}\x00-\x1f]/.test(key) || key in target)
128
+ fail("token/group collision");
129
+ target[key] = { $type: token.type, $value: structuredClone(token.value), ...(token.description !== undefined ? { $description: token.description } : {}), ...(token.extensions !== undefined ? { $extensions: structuredClone(token.extensions) } : {}) };
130
+ }
131
+ // Re-import validates metadata and aliases; export never silently drops unsupported native formulas/units.
132
+ importDtcg(root);
133
+ return root;
134
+ }
package/dist/probe.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { type Endpoint } from "./endpoint.js";
2
2
  import { type PenpotToolClient } from "./client.js";
3
+ import { validateFileIntegrity } from "./native-api.js";
3
4
  export interface ProbeOptions {
5
+ integrity?: boolean;
4
6
  /** Request sizes in KB to try; the first refused one marks the limit. */
5
7
  requestSizesKb?: number[];
6
8
  responseSizesKb?: number[];
7
9
  }
8
10
  export interface ProbeReport {
11
+ integrity?: Awaited<ReturnType<typeof validateFileIntegrity>>;
12
+ schemaVersion: 1;
9
13
  endpoint: Endpoint["description"];
10
14
  /** Tool names the server offers; absent when the server cannot list them. */
11
15
  tools?: string[];
@@ -13,9 +17,9 @@ export interface ProbeReport {
13
17
  requestLimit: Record<string, string>;
14
18
  responseLimit: Record<string, string>;
15
19
  }
16
- export declare const capabilityCode = "\nconst f = penpot.currentFile, page = penpot.currentPage, lib = penpot.library.local;\nconst shape = page.root.children.find((c) => c.width < 400 && c.height < 400) || page.root.children[0];\nlet exported = null;\nif (shape) { try { const b = await shape.export({ type: \"png\", scale: 1 }); exported = b.length; } catch (e) { exported = \"error: \" + String(e && e.message || e).slice(0, 80); } }\nreturn {\n file: f ? f.name : null, revn: f ? f.revn : null, penpotApi: typeof penpot.version === \"string\" ? penpot.version : null, pages: f ? f.pages.length : null, currentPage: page ? page.name : null,\n apis: { tokens: typeof lib.tokens, tokenSets: lib.tokens ? lib.tokens.sets.length : null, themes: lib.tokens ? lib.tokens.themes.length : null, components: lib.components.length,\n uploadMediaData: typeof penpot.uploadMediaData, createBoard: typeof penpot.createBoard, openPage: typeof penpot.openPage, fileExport: typeof (f && f.export) },\n shapeApis: shape ? { swapComponent: typeof shape.swapComponent, applyToken: typeof shape.applyToken, flex: typeof shape.addFlexLayout, exportSmallShape: exported } : \"not checked: the page has no shape; open a file with at least one shape to check these\",\n};";
20
+ export declare const capabilityCode = "\nconst f = penpot.currentFile, page = penpot.currentPage, lib = penpot.library.local;\nconst shape = page.root.children.find((c) => c.width < 400 && c.height < 400) || page.root.children[0];\nlet exported = null;\nif (shape) { try { const b = await shape.export({ type: \"png\", scale: 1 }); exported = b.length; } catch (e) { exported = \"error: \" + String(e && e.message || e).slice(0, 80); } }\nreturn {\n schemaVersion: 1, observedAt: new Date().toISOString(), fileId: f ? f.id : null, pageId: page ? page.id : null,\n file: f ? f.name : null, revn: f ? f.revn : null, penpotApi: typeof penpot.version === \"string\" ? penpot.version : null, pages: f ? f.pages.length : null, currentPage: page ? page.name : null,\n apis: { tokens: typeof lib.tokens, tokenSets: lib.tokens ? lib.tokens.sets.length : null, themes: lib.tokens ? lib.tokens.themes.length : null, components: lib.components.length,\n uploadMediaData: typeof penpot.uploadMediaData, createBoard: typeof penpot.createBoard, openPage: typeof penpot.openPage, fileExport: typeof (f && f.export),\n layoutWait: typeof penpot.waitForLayoutUpdate, fileValidate: typeof (f && f.validate), saveVersion: typeof (f && f.saveVersion), findVersions: typeof (f && f.findVersions), generateMarkup: typeof penpot.generateMarkup, generateStyle: typeof penpot.generateStyle, generateFontFaces: typeof penpot.generateFontFaces },\n shapeApis: shape ? { swapComponent: typeof shape.swapComponent, applyToken: typeof shape.applyToken, flex: typeof shape.addFlexLayout, exportSmallShape: exported } : \"not checked: the page has no shape; open a file with at least one shape to check these\",\n};";
17
21
  /**
18
22
  * What a connected Penpot and its MCP offer, and the limits scripts rely on. Read-only: nothing is written in Penpot.
19
23
  * `connect` returns a fresh client each time, because a refused oversized request can end the connection.
20
24
  */
21
- export declare function probe(endpoint: Endpoint, connect: () => Promise<PenpotToolClient>, { requestSizesKb, responseSizesKb }?: ProbeOptions): Promise<ProbeReport>;
25
+ export declare function probe(endpoint: Endpoint, connect: () => Promise<PenpotToolClient>, { requestSizesKb, responseSizesKb, integrity }?: ProbeOptions): Promise<ProbeReport>;