@penpair/core 0.2.0 → 0.3.1

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 (125) hide show
  1. package/dist/adapters.js +58 -11
  2. package/dist/approvals.js +143 -29
  3. package/dist/atomic-output.js +9 -4
  4. package/dist/audit-rules.js +345 -79
  5. package/dist/backup-note.js +29 -8
  6. package/dist/baseline-coverage.js +169 -35
  7. package/dist/baselines-config.js +119 -29
  8. package/dist/baselines.js +280 -57
  9. package/dist/cli-arguments.js +18 -6
  10. package/dist/cli-main.js +1645 -392
  11. package/dist/client.d.ts +5 -2
  12. package/dist/client.js +127 -28
  13. package/dist/command-result.js +341 -52
  14. package/dist/coordination.js +91 -29
  15. package/dist/credentials.js +57 -13
  16. package/dist/deep-check.js +56 -12
  17. package/dist/design-spec.js +189 -49
  18. package/dist/endpoint.js +102 -34
  19. package/dist/export.js +86 -18
  20. package/dist/file-archive.js +33 -7
  21. package/dist/file-guard.js +29 -11
  22. package/dist/formats.js +215 -14
  23. package/dist/index.d.ts +8 -7
  24. package/dist/index.js +8 -7
  25. package/dist/inspection.d.ts +1 -1
  26. package/dist/inspection.js +378 -65
  27. package/dist/instance/config-schema.js +400 -41
  28. package/dist/instance/config.js +101 -30
  29. package/dist/instance/connect.js +151 -44
  30. package/dist/instance/connection.d.ts +26 -5
  31. package/dist/instance/connection.js +194 -21
  32. package/dist/instance/gitignore.js +5 -2
  33. package/dist/instance/init.js +32 -7
  34. package/dist/instance/runner.js +32 -10
  35. package/dist/instance/setup.js +46 -13
  36. package/dist/instance/tty.js +26 -8
  37. package/dist/journal.d.ts +10 -0
  38. package/dist/journal.js +212 -49
  39. package/dist/kit-smoke.js +82 -20
  40. package/dist/kit-source.js +10 -40
  41. package/dist/kit.js +102 -34
  42. package/dist/layout-policy.d.ts +2 -0
  43. package/dist/layout-policy.js +86 -0
  44. package/dist/layout.d.ts +7 -1
  45. package/dist/layout.js +27 -70
  46. package/dist/live-smoke.js +13 -4
  47. package/dist/local-profile.js +2 -1
  48. package/dist/mcp-server.js +360 -47
  49. package/dist/native-api.js +47 -11
  50. package/dist/onboard.js +33 -7
  51. package/dist/operation.d.ts +2 -2
  52. package/dist/operation.js +279 -69
  53. package/dist/page-session.d.ts +23 -0
  54. package/dist/page-session.js +142 -0
  55. package/dist/png.js +18 -3
  56. package/dist/portable-tokens.js +254 -57
  57. package/dist/probe.d.ts +1 -1
  58. package/dist/probe.js +48 -11
  59. package/dist/project-input.js +24 -10
  60. package/dist/project-snippets.js +18 -6
  61. package/dist/project-status.js +133 -24
  62. package/dist/registry.js +217 -60
  63. package/dist/run-file.js +129 -30
  64. package/dist/shots.js +108 -26
  65. package/dist/skill-check-entry.d.ts +17 -0
  66. package/dist/skill-check-entry.js +89 -0
  67. package/dist/skill-check-instructions.d.ts +7 -0
  68. package/dist/skill-check-instructions.js +39 -0
  69. package/dist/skill-check.d.ts +15 -0
  70. package/dist/skill-check.js +36 -0
  71. package/dist/skill-file.d.ts +2 -0
  72. package/dist/skill-file.js +31 -0
  73. package/dist/skill-instructions.d.ts +4 -0
  74. package/dist/skill-instructions.js +97 -22
  75. package/dist/skill-metadata.d.ts +8 -0
  76. package/dist/skill-metadata.js +52 -0
  77. package/dist/skill-routing.d.ts +2 -0
  78. package/dist/skill-routing.js +36 -0
  79. package/dist/skills.js +131 -41
  80. package/dist/snippet.js +27 -7
  81. package/dist/snippets/read.js +8 -2
  82. package/dist/structure-snapshot.d.ts +12 -0
  83. package/dist/structure-snapshot.js +439 -85
  84. package/dist/token-edit.js +219 -64
  85. package/dist/token-projection.d.ts +56 -0
  86. package/dist/token-projection.js +286 -0
  87. package/dist/tokens-diff.js +125 -32
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +3 -2
  91. package/skills/design-judgment/SKILL.md +1 -1
  92. package/skills/design-judgment/agents/openai.yaml +6 -0
  93. package/skills/penpair-design/SKILL.md +14 -1
  94. package/skills/penpair-design/agents/openai.yaml +6 -0
  95. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  96. package/skills/penpair-design/references/project-facts.md +2 -0
  97. package/skills/penpair-handoff/SKILL.md +14 -1
  98. package/skills/penpair-handoff/agents/openai.yaml +6 -0
  99. package/skills/penpair-handoff/references/comparison-companion.md +16 -0
  100. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  101. package/skills/penpair-recipes/SKILL.md +13 -1
  102. package/skills/penpair-recipes/agents/openai.yaml +6 -0
  103. package/skills/penpair-recipes/assets/visual-checkpoint.md +48 -0
  104. package/skills/penpair-recipes/references/visual-checkpoint.md +37 -0
  105. package/skills/penpair-setup/SKILL.md +9 -3
  106. package/skills/penpair-setup/agents/openai.yaml +6 -0
  107. package/skills/penpair-verify/SKILL.md +49 -0
  108. package/skills/penpair-verify/agents/openai.yaml +6 -0
  109. package/skills/penpair-verify/references/browser-evidence.md +33 -0
  110. package/dist/cli.d.ts +0 -2
  111. package/dist/cli.js +0 -3
  112. package/dist/docker/lifecycle.d.ts +0 -96
  113. package/dist/docker/lifecycle.js +0 -273
  114. package/dist/docker/secrets.d.ts +0 -11
  115. package/dist/docker/secrets.js +0 -38
  116. package/dist/docker/template.d.ts +0 -8
  117. package/dist/docker/template.js +0 -158
  118. package/dist/docker-profile.d.ts +0 -114
  119. package/dist/docker-profile.js +0 -18
  120. package/dist/instance/lifecycle.d.ts +0 -129
  121. package/dist/instance/lifecycle.js +0 -336
  122. package/dist/instance/secrets.d.ts +0 -11
  123. package/dist/instance/secrets.js +0 -38
  124. package/dist/instance/template.d.ts +0 -8
  125. package/dist/instance/template.js +0 -158
@@ -2,101 +2,414 @@ import { realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { runCommand } from "./cli-main.js";
7
7
  import { loadConfig } from "./instance/config.js";
8
8
  import { resolveProjectOutput } from "./project-input.js";
9
9
  import { collectCommand } from "./command-result.js";
10
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 });
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
+ });
13
21
  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"] : [])] },
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
+ },
26
236
  ];
27
237
  function validate(def, raw) {
28
- if (raw === undefined)
238
+ if (raw === undefined) {
29
239
  raw = {};
30
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
240
+ }
241
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
31
242
  throw new Error("Tool arguments must be an object");
243
+ }
32
244
  const a = raw;
33
- for (const key of def.required)
34
- if (!(key in a))
245
+ for (const key of def.required) {
246
+ if (!(key in a)) {
35
247
  throw new Error(`Missing ${key}`);
248
+ }
249
+ }
36
250
  for (const [key, value] of Object.entries(a)) {
37
251
  const schema = def.properties[key];
38
- if (!schema)
252
+ if (!schema) {
39
253
  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)))
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))) {
41
261
  throw new Error(`Invalid ${key}`);
42
- if (schema.type === "boolean" && typeof value !== "boolean")
262
+ }
263
+ if (schema.type === "boolean" && typeof value !== "boolean") {
43
264
  throw new Error(`Invalid ${key}`);
44
- if (schema.type === "integer" && (!Number.isInteger(value) || Number(value) < 0 || Number(value) > schema.maximum))
265
+ }
266
+ if (schema.type === "integer" &&
267
+ (!Number.isInteger(value) || Number(value) < 0 || Number(value) > schema.maximum)) {
45
268
  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)))
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))) {
47
276
  throw new Error("Invalid registered parameters");
277
+ }
48
278
  }
49
- if ((a.resume || a.checkpoint) && !a.apply)
279
+ if ((a.resume || a.checkpoint) && !a.apply) {
50
280
  throw new Error("Explicit resume requires apply");
281
+ }
51
282
  return a;
52
283
  }
53
284
  /** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
54
285
  export async function createMcpServer(options) {
55
- if (!path.isAbsolute(options.root))
286
+ if (!path.isAbsolute(options.root)) {
56
287
  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)
288
+ }
289
+ const root = await realpath(options.root);
290
+ const config = await loadConfig(root);
291
+ if (!config.file?.id) {
59
292
  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)
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) {
63
307
  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 }));
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
+ }));
68
337
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
69
338
  const controller = new AbortController();
70
339
  const abort = () => controller.abort(extra.signal.reason);
71
- extra.signal.addEventListener("abort", abort, { once: true });
72
- if (extra.signal.aborted)
340
+ extra.signal.addEventListener("abort", abort, {
341
+ once: true,
342
+ });
343
+ if (extra.signal.aborted) {
73
344
  abort();
74
- const timer = setTimeout(() => controller.abort(Object.assign(new Error("MCP command deadline exceeded"), { code: "deadline" })), timeout);
345
+ }
346
+ const timer = setTimeout(() => controller.abort(Object.assign(new Error("MCP command deadline exceeded"), {
347
+ code: "deadline",
348
+ })), timeout);
75
349
  let result;
76
350
  try {
77
- const def = available.find(d => d.name === request.params.name);
78
- if (!def)
351
+ const def = available.find((d) => d.name === request.params.name);
352
+ if (!def) {
79
353
  throw new Error("Unknown or disabled tool");
354
+ }
80
355
  const args = validate(def, request.params.arguments);
81
356
  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)
357
+ if (current.instance !== pinned.instance ||
358
+ current.file?.id !== pinned.fileId ||
359
+ JSON.stringify("profile" in current ? current.profile : null) !== pinned.profile) {
83
360
  throw new Error("Pinned project target changed; restart and explicitly review server configuration");
84
- if (def.localWrite)
361
+ }
362
+ if (def.localWrite) {
85
363
  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 });
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
+ });
87
373
  }
88
374
  catch (error) {
89
- result = await collectCommand(request.params.name, async () => { throw error; });
375
+ result = await collectCommand(request.params.name, async () => {
376
+ throw error;
377
+ });
90
378
  }
91
379
  finally {
92
380
  clearTimeout(timer);
93
381
  extra.signal.removeEventListener("abort", abort);
94
382
  }
95
383
  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" } };
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
+ };
97
397
  }
98
- return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result, isError: result.exitCode !== 0 };
398
+ return {
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: JSON.stringify(result),
403
+ },
404
+ ],
405
+ structuredContent: result,
406
+ isError: result.exitCode !== 0,
407
+ };
99
408
  });
100
409
  return server;
101
410
  }
102
- export async function serveMcp(options) { const server = await createMcpServer(options); await server.connect(new StdioServerTransport()); return server; }
411
+ export async function serveMcp(options) {
412
+ const server = await createMcpServer(options);
413
+ await server.connect(new StdioServerTransport());
414
+ return server;
415
+ }
@@ -6,28 +6,54 @@ if (typeof penpot.currentFile.validate !== "function") return { ok: true, suppor
6
6
  const issues = penpot.currentFile.validate();
7
7
  if (!Array.isArray(issues)) return { ok: false, message: "file.validate returned no issue list" };
8
8
  return { ok: true, supported: true, valid: issues.length === 0, issues };`, {}, "read"));
9
- if (typeof result.supported !== "boolean")
9
+ if (typeof result.supported !== "boolean") {
10
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)))
11
+ }
12
+ if (result.supported &&
13
+ (!Array.isArray(result.issues) ||
14
+ typeof result.valid !== "boolean" ||
15
+ result.valid !== (result.issues.length === 0))) {
12
16
  throw new Error("Integrity check returned incomplete or contradictory observations");
13
- return { supported: result.supported, ...(result.supported ? { valid: result.valid === true, issues: result.issues } : {}) };
17
+ }
18
+ return {
19
+ supported: result.supported,
20
+ ...(result.supported
21
+ ? {
22
+ valid: result.valid === true,
23
+ issues: result.issues,
24
+ }
25
+ : {}),
26
+ };
14
27
  }
15
28
  /** Version history is a recovery aid, never a substitute for a restored independent backup. */
16
29
  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))
30
+ if (!checkpoint.label.trim() ||
31
+ checkpoint.label.length > 120 ||
32
+ !/^[a-zA-Z0-9-]{1,100}$/.test(checkpoint.runId)) {
18
33
  throw new Error("A checkpoint requires a short label and a stable alphanumeric/hyphen run ID");
34
+ }
19
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"));
20
- if (capability.ok !== true || typeof capability.supported !== "boolean" || typeof capability.fileId !== "string")
36
+ if (capability.ok !== true ||
37
+ typeof capability.supported !== "boolean" ||
38
+ typeof capability.fileId !== "string") {
21
39
  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 };
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
+ };
25
50
  const guard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok: false, message: "checkpoint file changed" };';
26
51
  const listing = `${guard}
27
52
  const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
28
53
  return { ok: true, fileId: penpot.currentFile.id, versions: versions.map((version) => ({ label: version.label, createdAt: String(version.createdAt) })) };`;
29
54
  const report = await runOperation(client, {
30
- name: "checkpoint", page: () => undefined,
55
+ name: "checkpoint",
56
+ page: () => undefined,
31
57
  inspect: () => snippet("checkpoint-inspect", listing, params, "read"),
32
58
  apply: () => snippet("checkpoint-apply", `${guard}
33
59
  const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
@@ -36,7 +62,17 @@ if (versions.length === 1) return { ok: true, existed: true };
36
62
  await penpot.currentFile.saveVersion(CFG.label);
37
63
  return { ok: true, created: true };`, params, "write"),
38
64
  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"],
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
+ ],
40
73
  }, undefined, options);
41
- return { supported: true, report };
74
+ return {
75
+ supported: true,
76
+ report,
77
+ };
42
78
  }
package/dist/onboard.js CHANGED
@@ -1,15 +1,41 @@
1
1
  import { isExternal } from "./instance/config.js";
2
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.",
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.",
7
17
  `Ask the owner to invite your own Penpot account to the shared file at ${origin}.`,
8
18
  "Create your own personal MCP key. Keep it out of chat, commits and command arguments.",
9
19
  "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.",
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.",
11
23
  "Open the Penpot MCP plugin and Connect. Keep the editor tab open and awake.",
12
24
  "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."] };
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
+ };
15
41
  }
@@ -53,8 +53,8 @@ export interface OperationReport {
53
53
  problems: string[];
54
54
  verified: boolean;
55
55
  }
56
- /** Runs a snippet; if the editor shows another page, opens the right one, waits, and tries once more. */
57
- 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>;
58
58
  export declare function runOperation<P>(client: PenpotToolClient, operation: Operation<P>, params: P, options?: OperationOptions): Promise<OperationReport>;
59
59
  /** Nests the frames that are not part of the live work into one archive board. Frames keep their names, ids and component links. */
60
60
  export declare const archiveOperation: Operation<ArchiveParams>;