@penpair/core 0.1.3 → 0.3.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 (149) hide show
  1. package/dist/adapters.d.ts +16 -1
  2. package/dist/adapters.js +62 -19
  3. package/dist/approvals.d.ts +32 -0
  4. package/dist/approvals.js +213 -0
  5. package/dist/atomic-output.d.ts +5 -0
  6. package/dist/atomic-output.js +33 -0
  7. package/dist/audit-rules.d.ts +60 -0
  8. package/dist/audit-rules.js +443 -0
  9. package/dist/backup-note.d.ts +4 -0
  10. package/dist/backup-note.js +49 -0
  11. package/dist/baseline-coverage.d.ts +25 -0
  12. package/dist/baseline-coverage.js +248 -0
  13. package/dist/baselines-config.d.ts +25 -0
  14. package/dist/baselines-config.js +150 -11
  15. package/dist/baselines.d.ts +37 -8
  16. package/dist/baselines.js +362 -59
  17. package/dist/cli-arguments.d.ts +7 -0
  18. package/dist/cli-arguments.js +47 -0
  19. package/dist/cli-main.d.ts +13 -1
  20. package/dist/cli-main.js +2107 -247
  21. package/dist/client.d.ts +30 -6
  22. package/dist/client.js +160 -35
  23. package/dist/command-result.d.ts +159 -0
  24. package/dist/command-result.js +468 -0
  25. package/dist/coordination.d.ts +15 -0
  26. package/dist/coordination.js +184 -0
  27. package/dist/credentials.d.ts +21 -0
  28. package/dist/credentials.js +105 -0
  29. package/dist/deep-check.js +57 -9
  30. package/dist/design-spec.d.ts +55 -0
  31. package/dist/design-spec.js +275 -0
  32. package/dist/endpoint.d.ts +8 -1
  33. package/dist/endpoint.js +133 -21
  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 +113 -10
  38. package/dist/file-archive.d.ts +16 -0
  39. package/dist/file-archive.js +58 -0
  40. package/dist/file-guard.d.ts +8 -0
  41. package/dist/file-guard.js +58 -0
  42. package/dist/formats.d.ts +4 -0
  43. package/dist/formats.js +229 -0
  44. package/dist/index.d.ts +34 -1
  45. package/dist/index.js +34 -1
  46. package/dist/inspection.d.ts +106 -0
  47. package/dist/inspection.js +657 -0
  48. package/dist/instance/config-schema.d.ts +422 -0
  49. package/dist/instance/config-schema.js +435 -0
  50. package/dist/instance/config.d.ts +29 -0
  51. package/dist/instance/config.js +191 -29
  52. package/dist/instance/connect.d.ts +10 -1
  53. package/dist/instance/connect.js +178 -30
  54. package/dist/instance/connection.d.ts +39 -4
  55. package/dist/instance/connection.js +198 -14
  56. package/dist/instance/gitignore.js +5 -2
  57. package/dist/instance/init.js +32 -7
  58. package/dist/instance/runner.js +32 -10
  59. package/dist/instance/setup.js +46 -13
  60. package/dist/instance/tty.js +28 -8
  61. package/dist/journal.d.ts +87 -0
  62. package/dist/journal.js +423 -0
  63. package/dist/kit-smoke.d.ts +31 -0
  64. package/dist/kit-smoke.js +145 -0
  65. package/dist/kit-source.d.ts +2 -0
  66. package/dist/kit-source.js +279 -0
  67. package/dist/kit.d.ts +25 -0
  68. package/dist/kit.js +158 -0
  69. package/dist/layout-policy.d.ts +2 -0
  70. package/dist/layout-policy.js +86 -0
  71. package/dist/layout.d.ts +21 -0
  72. package/dist/layout.js +36 -0
  73. package/dist/live-smoke.d.ts +8 -0
  74. package/dist/live-smoke.js +23 -0
  75. package/dist/local-profile.js +2 -1
  76. package/dist/mcp-server.d.ts +12 -0
  77. package/dist/mcp-server.js +415 -0
  78. package/dist/native-api.d.ts +15 -0
  79. package/dist/native-api.js +78 -0
  80. package/dist/onboard.d.ts +10 -0
  81. package/dist/onboard.js +41 -0
  82. package/dist/operation.d.ts +22 -3
  83. package/dist/operation.js +341 -31
  84. package/dist/page-session.d.ts +23 -0
  85. package/dist/page-session.js +142 -0
  86. package/dist/png.d.ts +5 -0
  87. package/dist/png.js +22 -0
  88. package/dist/portable-tokens.d.ts +22 -0
  89. package/dist/portable-tokens.js +331 -0
  90. package/dist/probe.d.ts +6 -2
  91. package/dist/probe.js +64 -11
  92. package/dist/project-input.d.ts +6 -0
  93. package/dist/project-input.js +65 -0
  94. package/dist/project-snippets.js +29 -12
  95. package/dist/project-status.d.ts +24 -0
  96. package/dist/project-status.js +188 -0
  97. package/dist/registry.d.ts +2 -1
  98. package/dist/registry.js +224 -53
  99. package/dist/run-file.d.ts +26 -0
  100. package/dist/run-file.js +199 -0
  101. package/dist/shots.d.ts +43 -0
  102. package/dist/shots.js +172 -0
  103. package/dist/skill-instructions.d.ts +13 -0
  104. package/dist/skill-instructions.js +144 -0
  105. package/dist/skills.d.ts +7 -0
  106. package/dist/skills.js +163 -46
  107. package/dist/snippet.d.ts +3 -2
  108. package/dist/snippet.js +33 -9
  109. package/dist/snippets/archive.d.ts +9 -0
  110. package/dist/snippets/archive.js +10 -4
  111. package/dist/snippets/read.d.ts +1 -1
  112. package/dist/snippets/read.js +78 -41
  113. package/dist/structure-snapshot.d.ts +85 -0
  114. package/dist/structure-snapshot.js +525 -0
  115. package/dist/token-edit.d.ts +53 -0
  116. package/dist/token-edit.js +352 -0
  117. package/dist/token-projection.d.ts +56 -0
  118. package/dist/token-projection.js +286 -0
  119. package/dist/tokens-diff.d.ts +19 -6
  120. package/dist/tokens-diff.js +182 -18
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/package.json +1 -1
  124. package/skills/penpair-design/SKILL.md +20 -45
  125. package/skills/penpair-design/references/penpot-plugin-gotchas.md +58 -2
  126. package/skills/penpair-design/references/project-facts.md +3 -1
  127. package/skills/penpair-design/references/write-protocol.md +15 -3
  128. package/skills/penpair-handoff/SKILL.md +16 -0
  129. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  130. package/skills/penpair-handoff/references/handoff-flow.md +13 -0
  131. package/skills/penpair-recipes/SKILL.md +21 -0
  132. package/skills/penpair-recipes/references/build-flow.md +11 -0
  133. package/skills/penpair-setup/SKILL.md +8 -2
  134. package/dist/cli.d.ts +0 -2
  135. package/dist/cli.js +0 -3
  136. package/dist/docker/lifecycle.d.ts +0 -96
  137. package/dist/docker/lifecycle.js +0 -273
  138. package/dist/docker/secrets.d.ts +0 -11
  139. package/dist/docker/secrets.js +0 -38
  140. package/dist/docker/template.d.ts +0 -8
  141. package/dist/docker/template.js +0 -158
  142. package/dist/docker-profile.d.ts +0 -114
  143. package/dist/docker-profile.js +0 -18
  144. package/dist/instance/lifecycle.d.ts +0 -129
  145. package/dist/instance/lifecycle.js +0 -336
  146. package/dist/instance/secrets.d.ts +0 -11
  147. package/dist/instance/secrets.js +0 -38
  148. package/dist/instance/template.d.ts +0 -8
  149. package/dist/instance/template.js +0 -158
@@ -2,7 +2,8 @@ import { ConfigError } from "./instance/config.js";
2
2
  /** The commands that run Penpot on this computer exist only when a package provides a profile for them. */
3
3
  export const needsLocalProfile = "Running Penpot on this computer is not part of this install. It comes with `@penpair/docker`: install it with `npm i -D @penpair/docker` and run `npx penpair …` (or, once, `npx @penpair/docker <command>`). Penpot on the web needs nothing extra.";
4
4
  export function requireLocalProfile(local) {
5
- if (!local)
5
+ if (!local) {
6
6
  throw new ConfigError(needsLocalProfile);
7
+ }
7
8
  return local;
8
9
  }
@@ -0,0 +1,12 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { type CliOptions } from "./cli-main.js";
3
+ export interface McpOptions {
4
+ root: string;
5
+ allowWrites?: boolean;
6
+ cli?: CliOptions;
7
+ timeoutMs?: number;
8
+ maxOutputBytes?: number;
9
+ }
10
+ /** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
11
+ export declare function createMcpServer(options: McpOptions): Promise<Server>;
12
+ export declare function serveMcp(options: McpOptions): Promise<Server>;
@@ -0,0 +1,415 @@
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 = {
12
+ type: "string",
13
+ minLength: 1,
14
+ maxLength: 1000,
15
+ };
16
+ const integer = (max) => ({
17
+ type: "integer",
18
+ minimum: 0,
19
+ maximum: max,
20
+ });
21
+ const definitions = [
22
+ {
23
+ name: "pages",
24
+ description: "List pages in the pinned project file",
25
+ properties: {},
26
+ required: [],
27
+ command: () => [
28
+ "pages",
29
+ ],
30
+ },
31
+ {
32
+ name: "tree",
33
+ description: "Bounded tree preview; omitted nodes are explicit",
34
+ properties: {
35
+ page: string,
36
+ depth: integer(100),
37
+ },
38
+ required: [
39
+ "page",
40
+ ],
41
+ command: (a) => [
42
+ "tree",
43
+ "--page",
44
+ String(a.page),
45
+ "--depth",
46
+ String(a.depth ?? 5),
47
+ ],
48
+ },
49
+ {
50
+ name: "audit",
51
+ description: "Inspect layout/token findings with documented heuristic limits",
52
+ properties: {
53
+ page: string,
54
+ },
55
+ required: [
56
+ "page",
57
+ ],
58
+ command: (a) => [
59
+ "audit",
60
+ "--page",
61
+ String(a.page),
62
+ ],
63
+ },
64
+ {
65
+ name: "inspect",
66
+ description: "One bounded structured inspection chunk; use returned cursor for continuation",
67
+ properties: {
68
+ page: string,
69
+ id: string,
70
+ cursor: string,
71
+ },
72
+ required: [
73
+ "page",
74
+ "id",
75
+ ],
76
+ command: (a) => [
77
+ "inspect",
78
+ String(a.id),
79
+ "--page",
80
+ String(a.page),
81
+ ...(a.cursor
82
+ ? [
83
+ "--cursor",
84
+ String(a.cursor),
85
+ ]
86
+ : []),
87
+ ],
88
+ },
89
+ {
90
+ name: "tokens_dump",
91
+ description: "Bounded token capture; completeness is explicit",
92
+ properties: {},
93
+ required: [],
94
+ command: () => [
95
+ "tokens",
96
+ "dump",
97
+ "--max-total-bytes",
98
+ "131072",
99
+ ],
100
+ },
101
+ {
102
+ name: "run_status",
103
+ description: "Inspect an explicit local recovery journal",
104
+ properties: {
105
+ id: string,
106
+ },
107
+ required: [
108
+ "id",
109
+ ],
110
+ command: (a) => [
111
+ "runs",
112
+ String(a.id),
113
+ ],
114
+ },
115
+ {
116
+ name: "baselines_status",
117
+ description: "Offline coverage/evidence status; no capture or approval",
118
+ properties: {},
119
+ required: [],
120
+ command: () => [
121
+ "baselines",
122
+ "coverage",
123
+ ],
124
+ },
125
+ {
126
+ name: "project_status",
127
+ description: "Project health with explicit unknowns; no adapter imports",
128
+ properties: {},
129
+ required: [],
130
+ command: () => [
131
+ "status",
132
+ ],
133
+ },
134
+ {
135
+ name: "doctor",
136
+ description: "Project connection/file identity diagnostics",
137
+ properties: {},
138
+ required: [],
139
+ command: () => [
140
+ "doctor",
141
+ ],
142
+ },
143
+ {
144
+ name: "design_spec",
145
+ description: "Derived handoff for an exact frame ID; unknown semantics remain explicit",
146
+ properties: {
147
+ page: string,
148
+ id: string,
149
+ },
150
+ required: [
151
+ "page",
152
+ "id",
153
+ ],
154
+ command: (a) => [
155
+ "spec",
156
+ "--page",
157
+ String(a.page),
158
+ "--frame",
159
+ String(a.id),
160
+ ],
161
+ },
162
+ {
163
+ name: "export",
164
+ description: "Capture stable PNG to a relative path inside the project; creates a local file",
165
+ properties: {
166
+ page: string,
167
+ id: string,
168
+ out: string,
169
+ },
170
+ required: [
171
+ "page",
172
+ "id",
173
+ "out",
174
+ ],
175
+ command: (a) => [
176
+ "export",
177
+ String(a.id),
178
+ String(a.out),
179
+ "--page",
180
+ String(a.page),
181
+ ],
182
+ localWrite: true,
183
+ },
184
+ {
185
+ name: "run_registered",
186
+ description: "Execute a trusted registered operation: inspect by default, explicit apply and recovery for writes. Not a sandbox.",
187
+ properties: {
188
+ name: string,
189
+ params: {
190
+ type: "object",
191
+ additionalProperties: {
192
+ type: "string",
193
+ maxLength: 4000,
194
+ },
195
+ maxProperties: 100,
196
+ },
197
+ apply: {
198
+ type: "boolean",
199
+ default: false,
200
+ },
201
+ resume: string,
202
+ checkpoint: {
203
+ type: "boolean",
204
+ default: false,
205
+ },
206
+ },
207
+ required: [
208
+ "name",
209
+ ],
210
+ write: true,
211
+ command: (a) => [
212
+ "run",
213
+ String(a.name),
214
+ ...Object.entries(a.params ?? {}).flatMap(([k, v]) => [
215
+ "--param",
216
+ `${k}=${v}`,
217
+ ]),
218
+ ...(a.apply
219
+ ? [
220
+ "--apply",
221
+ ]
222
+ : []),
223
+ ...(a.resume
224
+ ? [
225
+ "--resume",
226
+ String(a.resume),
227
+ ]
228
+ : []),
229
+ ...(a.checkpoint
230
+ ? [
231
+ "--checkpoint",
232
+ ]
233
+ : []),
234
+ ],
235
+ },
236
+ ];
237
+ function validate(def, raw) {
238
+ if (raw === undefined) {
239
+ raw = {};
240
+ }
241
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
242
+ throw new Error("Tool arguments must be an object");
243
+ }
244
+ const a = raw;
245
+ for (const key of def.required) {
246
+ if (!(key in a)) {
247
+ throw new Error(`Missing ${key}`);
248
+ }
249
+ }
250
+ for (const [key, value] of Object.entries(a)) {
251
+ const schema = def.properties[key];
252
+ if (!schema) {
253
+ throw new Error(`Unexpected tool argument ${key}`);
254
+ }
255
+ if (schema.type === "string" &&
256
+ (typeof value !== "string" ||
257
+ !value ||
258
+ value.startsWith("-") ||
259
+ value.length > (schema.maxLength ?? 1000) ||
260
+ /[\x00-\x1f]/.test(value))) {
261
+ throw new Error(`Invalid ${key}`);
262
+ }
263
+ if (schema.type === "boolean" && typeof value !== "boolean") {
264
+ throw new Error(`Invalid ${key}`);
265
+ }
266
+ if (schema.type === "integer" &&
267
+ (!Number.isInteger(value) || Number(value) < 0 || Number(value) > schema.maximum)) {
268
+ throw new Error(`Invalid ${key}`);
269
+ }
270
+ if (key === "params" &&
271
+ (!value ||
272
+ typeof value !== "object" ||
273
+ Array.isArray(value) ||
274
+ Object.keys(value).length > 100 ||
275
+ Object.entries(value).some(([k, v]) => !/^[a-zA-Z][\w-]{0,100}$/.test(k) || typeof v !== "string" || v.length > 4000))) {
276
+ throw new Error("Invalid registered parameters");
277
+ }
278
+ }
279
+ if ((a.resume || a.checkpoint) && !a.apply) {
280
+ throw new Error("Explicit resume requires apply");
281
+ }
282
+ return a;
283
+ }
284
+ /** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
285
+ export async function createMcpServer(options) {
286
+ if (!path.isAbsolute(options.root)) {
287
+ throw new Error("MCP project root must be absolute");
288
+ }
289
+ const root = await realpath(options.root);
290
+ const config = await loadConfig(root);
291
+ if (!config.file?.id) {
292
+ throw new Error("MCP server requires a configured owner-confirmed file pin");
293
+ }
294
+ const pinned = {
295
+ instance: config.instance,
296
+ fileId: config.file.id,
297
+ profile: JSON.stringify("profile" in config ? config.profile : null),
298
+ };
299
+ const timeout = options.timeoutMs ?? 60000;
300
+ const limit = options.maxOutputBytes ?? 256 * 1024;
301
+ if (!Number.isInteger(timeout) ||
302
+ timeout < 1 ||
303
+ timeout > 300000 ||
304
+ !Number.isInteger(limit) ||
305
+ limit < 1024 ||
306
+ limit > 1024 * 1024) {
307
+ throw new Error("Invalid MCP budgets");
308
+ }
309
+ const available = definitions.filter((d) => !d.write || options.allowWrites);
310
+ const server = new Server({
311
+ name: "penpair",
312
+ version,
313
+ }, {
314
+ capabilities: {
315
+ tools: {},
316
+ },
317
+ });
318
+ const tools = available.map((d) => ({
319
+ name: d.name,
320
+ description: d.description,
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: d.properties,
324
+ required: d.required,
325
+ additionalProperties: false,
326
+ },
327
+ annotations: {
328
+ readOnlyHint: !d.write && !d.localWrite,
329
+ destructiveHint: Boolean(d.write || d.localWrite),
330
+ idempotentHint: !d.write && !d.localWrite,
331
+ openWorldHint: true,
332
+ },
333
+ }));
334
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
335
+ tools,
336
+ }));
337
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
338
+ const controller = new AbortController();
339
+ const abort = () => controller.abort(extra.signal.reason);
340
+ extra.signal.addEventListener("abort", abort, {
341
+ once: true,
342
+ });
343
+ if (extra.signal.aborted) {
344
+ abort();
345
+ }
346
+ const timer = setTimeout(() => controller.abort(Object.assign(new Error("MCP command deadline exceeded"), {
347
+ code: "deadline",
348
+ })), timeout);
349
+ let result;
350
+ try {
351
+ const def = available.find((d) => d.name === request.params.name);
352
+ if (!def) {
353
+ throw new Error("Unknown or disabled tool");
354
+ }
355
+ const args = validate(def, request.params.arguments);
356
+ const current = await loadConfig(root);
357
+ if (current.instance !== pinned.instance ||
358
+ current.file?.id !== pinned.fileId ||
359
+ JSON.stringify("profile" in current ? current.profile : null) !== pinned.profile) {
360
+ throw new Error("Pinned project target changed; restart and explicitly review server configuration");
361
+ }
362
+ if (def.localWrite) {
363
+ args.out = await resolveProjectOutput(root, String(args.out));
364
+ }
365
+ result = await runCommand(def.command(args), {
366
+ ...options.cli,
367
+ root,
368
+ projectEndpointOnly: true,
369
+ pinnedConfig: config,
370
+ strictFileGuard: true,
371
+ signal: controller.signal,
372
+ });
373
+ }
374
+ catch (error) {
375
+ result = await collectCommand(request.params.name, async () => {
376
+ throw error;
377
+ });
378
+ }
379
+ finally {
380
+ clearTimeout(timer);
381
+ extra.signal.removeEventListener("abort", abort);
382
+ }
383
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > limit) {
384
+ result = {
385
+ ...result,
386
+ outcome: result.outcome === "uncertain" ? "uncertain" : "incomplete",
387
+ exitCode: result.outcome === "uncertain" ? 4 : 3,
388
+ data: null,
389
+ messages: [],
390
+ warnings: [],
391
+ errors: [],
392
+ completeness: {
393
+ complete: false,
394
+ reason: "MCP response byte budget exceeded; use a narrower inspection or the CLI artifact path",
395
+ },
396
+ };
397
+ }
398
+ return {
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: JSON.stringify(result),
403
+ },
404
+ ],
405
+ structuredContent: result,
406
+ isError: result.exitCode !== 0,
407
+ };
408
+ });
409
+ return server;
410
+ }
411
+ export async function serveMcp(options) {
412
+ const server = await createMcpServer(options);
413
+ await server.connect(new StdioServerTransport());
414
+ return server;
415
+ }
@@ -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,78 @@
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
+ }
12
+ if (result.supported &&
13
+ (!Array.isArray(result.issues) ||
14
+ typeof result.valid !== "boolean" ||
15
+ result.valid !== (result.issues.length === 0))) {
16
+ throw new Error("Integrity check returned incomplete or contradictory observations");
17
+ }
18
+ return {
19
+ supported: result.supported,
20
+ ...(result.supported
21
+ ? {
22
+ valid: result.valid === true,
23
+ issues: result.issues,
24
+ }
25
+ : {}),
26
+ };
27
+ }
28
+ /** Version history is a recovery aid, never a substitute for a restored independent backup. */
29
+ export async function createCheckpoint(client, checkpoint, options = {}) {
30
+ if (!checkpoint.label.trim() ||
31
+ checkpoint.label.length > 120 ||
32
+ !/^[a-zA-Z0-9-]{1,100}$/.test(checkpoint.runId)) {
33
+ throw new Error("A checkpoint requires a short label and a stable alphanumeric/hyphen run ID");
34
+ }
35
+ 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"));
36
+ if (capability.ok !== true ||
37
+ typeof capability.supported !== "boolean" ||
38
+ typeof capability.fileId !== "string") {
39
+ throw new Error("Checkpoint capability returned no target/presence observations");
40
+ }
41
+ if (capability.supported !== true) {
42
+ return {
43
+ supported: false,
44
+ };
45
+ }
46
+ const params = {
47
+ label: `penpair/${checkpoint.runId}/${checkpoint.label}`,
48
+ fileId: capability.fileId,
49
+ };
50
+ const guard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok: false, message: "checkpoint file changed" };';
51
+ const listing = `${guard}
52
+ const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
53
+ return { ok: true, fileId: penpot.currentFile.id, versions: versions.map((version) => ({ label: version.label, createdAt: String(version.createdAt) })) };`;
54
+ const report = await runOperation(client, {
55
+ name: "checkpoint",
56
+ page: () => undefined,
57
+ inspect: () => snippet("checkpoint-inspect", listing, params, "read"),
58
+ apply: () => snippet("checkpoint-apply", `${guard}
59
+ const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
60
+ if (versions.length > 1) return { ok: false, message: "checkpoint label is ambiguous" };
61
+ if (versions.length === 1) return { ok: true, existed: true };
62
+ await penpot.currentFile.saveVersion(CFG.label);
63
+ return { ok: true, created: true };`, params, "write"),
64
+ readback: () => snippet("checkpoint-readback", listing, params, "read"),
65
+ verify: ({ readback }) => readback.fileId === params.fileId &&
66
+ Array.isArray(readback.versions) &&
67
+ readback.versions.length === 1 &&
68
+ readback.versions[0]?.label === params.label
69
+ ? []
70
+ : [
71
+ "Checkpoint was not independently found once in the intended file",
72
+ ],
73
+ }, undefined, options);
74
+ return {
75
+ supported: true,
76
+ report,
77
+ };
78
+ }
@@ -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,41 @@
1
+ import { isExternal } from "./instance/config.js";
2
+ export function onboardingChecklist(config) {
3
+ const origin = isExternal(config)
4
+ ? config.profile.origin
5
+ : `http://localhost:${config.ports.frontend}`;
6
+ const fileUrl = config.file
7
+ ? `${origin}/#/workspace?file-id=${encodeURIComponent(config.file.id)}`
8
+ : null;
9
+ return {
10
+ schemaVersion: 1,
11
+ project: config.instance,
12
+ origin,
13
+ file: config.file ?? null,
14
+ fileUrl,
15
+ steps: [
16
+ "Clone the project and install its locked dependencies with the repository's package manager.",
17
+ `Ask the owner to invite your own Penpot account to the shared file at ${origin}.`,
18
+ "Create your own personal MCP key. Keep it out of chat, commits and command arguments.",
19
+ "Run the project's penpair connect command; enter your own key in its hidden prompt.",
20
+ config.file
21
+ ? `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.`
22
+ : "Ask the owner to pin the shared file with penpair connect --pin-file and commit the pin.",
23
+ "Open the Penpot MCP plugin and Connect. Keep the editor tab open and awake.",
24
+ "Run penpair doctor --json, then a read-only penpair pages before proposing writes.",
25
+ "Coordinate a write window with colleagues. Inspect the plan, apply once, verify independently and review the export.",
26
+ ],
27
+ warnings: [
28
+ ...(!isExternal(config)
29
+ ? [
30
+ "localhost is this computer only. A colleague needs access to a shared reachable deployment; do not share generated instance secrets.",
31
+ ]
32
+ : []),
33
+ ...(!config.file
34
+ ? [
35
+ "No shared file ID is pinned yet.",
36
+ ]
37
+ : []),
38
+ "Writer markers are advisory and do not block human editing or guarantee exclusion across machines.",
39
+ ],
40
+ };
41
+ }
@@ -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;
@@ -34,8 +53,8 @@ export interface OperationReport {
34
53
  problems: string[];
35
54
  verified: boolean;
36
55
  }
37
- /** Runs a snippet; if the editor shows another page, opens the right one, waits, and tries once more. */
38
- export declare function runOnPage(client: PenpotToolClient, s: Snippet, page: string | undefined, { settleMs, sleep, onStep, ...execute }?: OperationOptions): Promise<SnippetResult>;
56
+ /** Runs once on the expected page; page transitions require independently observed IDs. */
57
+ export declare function runOnPage(client: PenpotToolClient, s: Snippet, page: string | undefined, { settleMs: _settleMs, sleep, onStep, ...execute }?: OperationOptions): Promise<SnippetResult>;
39
58
  export declare function runOperation<P>(client: PenpotToolClient, operation: Operation<P>, params: P, options?: OperationOptions): Promise<OperationReport>;
40
59
  /** Nests the frames that are not part of the live work into one archive board. Frames keep their names, ids and component links. */
41
60
  export declare const archiveOperation: Operation<ArchiveParams>;