@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
@@ -1,132 +1,329 @@
1
- const types = ["color", "dimension", "number", "fontFamily", "fontWeight", "typography"];
1
+ const types = [
2
+ "color",
3
+ "dimension",
4
+ "number",
5
+ "fontFamily",
6
+ "fontWeight",
7
+ "typography",
8
+ ];
2
9
  const isObject = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
3
10
  const aliasPath = (v) => typeof v === "string" && /^\{[^{}]+\}$/.test(v) ? v.slice(1, -1) : undefined;
4
- function fail(message) { throw new Error("Portable token subset: " + message); }
11
+ function fail(message) {
12
+ throw new Error("Portable token subset: " + message);
13
+ }
5
14
  export function importDtcg(raw) {
6
- if (!isObject(raw) || JSON.stringify(raw).length > 8 * 1024 * 1024)
15
+ if (!isObject(raw) || JSON.stringify(raw).length > 8 * 1024 * 1024) {
7
16
  fail("input must be a bounded JSON object");
17
+ }
8
18
  let groupCount = 0;
9
- const tokens = [], groups = Object.create(null);
19
+ const tokens = [];
20
+ const groups = Object.create(null);
10
21
  const visit = (node, path, inherited) => {
11
- if (path.length > 32 || tokens.length > 10000)
22
+ if (path.length > 32 || tokens.length > 10000) {
12
23
  fail("depth/token budget exceeded");
13
- for (const key of Object.keys(node))
14
- if (key.startsWith("$") && !["$value", "$type", "$description", "$extensions"].includes(key))
24
+ }
25
+ for (const key of Object.keys(node)) {
26
+ if (key.startsWith("$") &&
27
+ ![
28
+ "$value",
29
+ "$type",
30
+ "$description",
31
+ "$extensions",
32
+ ].includes(key)) {
15
33
  fail(`unsupported metadata/reference ${key}`);
34
+ }
35
+ }
16
36
  const type = typeof node.$type === "string" ? node.$type : inherited;
17
- if (node.$type !== undefined && (typeof node.$type !== "string" || !types.includes(node.$type)))
37
+ if (node.$type !== undefined &&
38
+ (typeof node.$type !== "string" || !types.includes(node.$type))) {
18
39
  fail("type must be a string");
19
- if (node.$description !== undefined && typeof node.$description !== "string")
40
+ }
41
+ if (node.$description !== undefined && typeof node.$description !== "string") {
20
42
  fail("description must be a string");
21
- if (node.$extensions !== undefined && !isObject(node.$extensions))
43
+ }
44
+ if (node.$extensions !== undefined && !isObject(node.$extensions)) {
22
45
  fail("extensions must be an object");
46
+ }
23
47
  if ("$value" in node) {
24
- if (!path.length || !type || !types.includes(type) || Object.keys(node).some(k => !k.startsWith("$")))
48
+ if (!path.length ||
49
+ !type ||
50
+ !types.includes(type) ||
51
+ Object.keys(node).some((k) => !k.startsWith("$"))) {
25
52
  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) } : {}) });
53
+ }
54
+ tokens.push({
55
+ path,
56
+ type,
57
+ value: structuredClone(node.$value),
58
+ ...(node.$description !== undefined
59
+ ? {
60
+ description: node.$description,
61
+ }
62
+ : {}),
63
+ ...(node.$extensions !== undefined
64
+ ? {
65
+ extensions: structuredClone(node.$extensions),
66
+ }
67
+ : {}),
68
+ });
27
69
  return;
28
70
  }
29
- if (++groupCount > 10000)
71
+ if (++groupCount > 10000) {
30
72
  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) } : {}) };
73
+ }
74
+ groups[path.join(".")] = {
75
+ ...(node.$type
76
+ ? {
77
+ type: String(node.$type),
78
+ }
79
+ : {}),
80
+ ...(node.$description !== undefined
81
+ ? {
82
+ description: node.$description,
83
+ }
84
+ : {}),
85
+ ...(node.$extensions !== undefined
86
+ ? {
87
+ extensions: structuredClone(node.$extensions),
88
+ }
89
+ : {}),
90
+ };
32
91
  for (const [key, value] of Object.entries(node)) {
33
- if (key.startsWith("$"))
92
+ if (key.startsWith("$")) {
34
93
  continue;
35
- if (!key || /[.{}\x00-\x1f]/.test(key) || !isObject(value))
94
+ }
95
+ if (!key || /[.{}\x00-\x1f]/.test(key) || !isObject(value)) {
36
96
  fail("invalid token/group path");
37
- visit(value, [...path, key], type);
97
+ }
98
+ visit(value, [
99
+ ...path,
100
+ key,
101
+ ], type);
38
102
  }
39
103
  };
40
104
  visit(raw, []);
41
- if (!tokens.length || tokens.length > 10000)
105
+ if (!tokens.length || tokens.length > 10000) {
42
106
  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: [] };
107
+ }
108
+ const portable = {
109
+ schemaVersion: 1,
110
+ kind: "portable-tokens",
111
+ format: "dtcg-2025.10-subset",
112
+ tokens: tokens.sort((a, b) => a.path.join(".").localeCompare(b.path.join("."))),
113
+ groups,
114
+ losses: [],
115
+ };
44
116
  validatePortableValues(portable);
45
117
  return portable;
46
118
  }
47
119
  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)
120
+ const byPath = new Map(document.tokens.map((t) => [
121
+ t.path.join("."),
122
+ t,
123
+ ]));
124
+ const resolved = new Map();
125
+ if (byPath.size !== document.tokens.length) {
50
126
  fail("duplicate token path");
127
+ }
51
128
  const resolveToken = (name, expected, chain) => {
52
129
  const token = byPath.get(name);
53
- if (!token || token.type !== expected)
130
+ if (!token || token.type !== expected) {
54
131
  fail(`missing/type-mismatched alias ${name}`);
55
- if (chain.includes(name))
56
- fail("alias cycle: " + [...chain, name].join(" -> "));
57
- if (chain.length > 64)
132
+ }
133
+ if (chain.includes(name)) {
134
+ fail("alias cycle: " +
135
+ [
136
+ ...chain,
137
+ name,
138
+ ].join(" -> "));
139
+ }
140
+ if (chain.length > 64) {
58
141
  fail("alias depth exceeds 64");
59
- if (resolved.has(name))
142
+ }
143
+ if (resolved.has(name)) {
60
144
  return resolved.get(name);
61
- const value = validateValue(token.type, token.value, [...chain, name]);
145
+ }
146
+ const value = validateValue(token.type, token.value, [
147
+ ...chain,
148
+ name,
149
+ ]);
62
150
  resolved.set(name, value);
63
151
  return value;
64
152
  };
65
153
  const validateValue = (type, value, chain) => {
66
154
  const ref = aliasPath(value);
67
- if (ref !== undefined)
155
+ if (ref !== undefined) {
68
156
  return resolveToken(ref, type, chain);
69
- if (isObject(value) && "$ref" in value)
157
+ }
158
+ if (isObject(value) && "$ref" in value) {
70
159
  fail("JSON Pointer property references are outside this subset");
71
- if (type === "number" && (typeof value !== "number" || !Number.isFinite(value)))
160
+ }
161
+ if (type === "number" && (typeof value !== "number" || !Number.isFinite(value))) {
72
162
  fail("number must be finite");
73
- if (type === "fontWeight" && (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 1000))
163
+ }
164
+ if (type === "fontWeight" &&
165
+ (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 1000)) {
74
166
  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)))
167
+ }
168
+ if (type === "fontFamily" &&
169
+ !((typeof value === "string" && value.length > 0) ||
170
+ (Array.isArray(value) &&
171
+ value.length > 0 &&
172
+ value.length <= 100 &&
173
+ value.every((v) => typeof v === "string" && v.length > 0)))) {
76
174
  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))))
175
+ }
176
+ if (type === "dimension" &&
177
+ (!isObject(value) ||
178
+ Object.keys(value).some((k) => ![
179
+ "value",
180
+ "unit",
181
+ ].includes(k)) ||
182
+ typeof value.value !== "number" ||
183
+ !Number.isFinite(value.value) ||
184
+ ![
185
+ "px",
186
+ "rem",
187
+ ].includes(String(value.unit)))) {
78
188
  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))))
189
+ }
190
+ if (type === "color" &&
191
+ (!isObject(value) ||
192
+ Object.keys(value).some((k) => ![
193
+ "colorSpace",
194
+ "components",
195
+ "alpha",
196
+ "hex",
197
+ ].includes(k)) ||
198
+ ![
199
+ "srgb",
200
+ "display-p3",
201
+ ].includes(String(value.colorSpace)) ||
202
+ !Array.isArray(value.components) ||
203
+ value.components.length !== 3 ||
204
+ value.components.some((c) => typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) ||
205
+ (value.alpha !== undefined &&
206
+ (typeof value.alpha !== "number" ||
207
+ !Number.isFinite(value.alpha) ||
208
+ value.alpha < 0 ||
209
+ value.alpha > 1)) ||
210
+ (value.hex !== undefined &&
211
+ (typeof value.hex !== "string" || !/^#[0-9a-f]{6}$/i.test(value.hex))))) {
80
212
  fail("color supports finite srgb/display-p3 components in 0..1 and optional alpha/hex");
213
+ }
81
214
  if (type === "typography") {
82
- if (!isObject(value) || Object.keys(value).some(k => !["fontFamily", "fontSize", "fontWeight", "letterSpacing", "lineHeight"].includes(k)))
215
+ if (!isObject(value) ||
216
+ Object.keys(value).some((k) => ![
217
+ "fontFamily",
218
+ "fontSize",
219
+ "fontWeight",
220
+ "letterSpacing",
221
+ "lineHeight",
222
+ ].includes(k))) {
83
223
  fail("typography property outside subset");
84
- const fields = { fontFamily: "fontFamily", fontSize: "dimension", fontWeight: "fontWeight", letterSpacing: "dimension", lineHeight: "number" };
224
+ }
225
+ const fields = {
226
+ fontFamily: "fontFamily",
227
+ fontSize: "dimension",
228
+ fontWeight: "fontWeight",
229
+ letterSpacing: "dimension",
230
+ lineHeight: "number",
231
+ };
85
232
  const result = {};
86
- for (const [key, expected] of Object.entries(fields))
233
+ for (const [key, expected] of Object.entries(fields)) {
87
234
  result[key] = validateValue(expected, value[key], chain);
88
- if (Number(result.lineHeight) <= 0)
235
+ }
236
+ if (Number(result.lineHeight) <= 0) {
89
237
  fail("lineHeight must be positive");
238
+ }
90
239
  return result;
91
240
  }
92
- if (!types.includes(type))
241
+ if (!types.includes(type)) {
93
242
  fail("unsupported type " + type);
243
+ }
94
244
  return value;
95
245
  };
96
- for (const token of document.tokens)
246
+ for (const token of document.tokens) {
97
247
  resolveToken(token.path.join("."), token.type, []);
248
+ }
98
249
  }
99
250
  export function exportDtcg(raw) {
100
251
  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)
252
+ if (!v ||
253
+ JSON.stringify(v).length > 8 * 1024 * 1024 ||
254
+ v.schemaVersion !== 1 ||
255
+ v.kind !== "portable-tokens" ||
256
+ v.format !== "dtcg-2025.10-subset" ||
257
+ !Array.isArray(v.tokens) ||
258
+ !v.tokens.length ||
259
+ v.tokens.length > 10000 ||
260
+ !isObject(v.groups) ||
261
+ !Array.isArray(v.losses) ||
262
+ v.losses.length) {
102
263
  fail("unsupported/incomplete portable schema; use import-tokens on a supported document");
264
+ }
103
265
  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; };
266
+ const group = (path) => {
267
+ let current = root;
268
+ for (const key of path) {
269
+ if (!key || key.startsWith("$") || /[.{}\x00-\x1f]/.test(key)) {
270
+ fail("invalid path");
271
+ }
272
+ const next = current[key];
273
+ if (next !== undefined && (!isObject(next) || "$value" in next)) {
274
+ fail("group/token collision");
275
+ }
276
+ current = (current[key] ??= Object.create(null));
277
+ }
278
+ return current;
279
+ };
112
280
  for (const [key, metadata] of Object.entries(v.groups)) {
113
- if (!isObject(metadata) || Object.keys(metadata).some(k => !["type", "description", "extensions"].includes(k)))
281
+ if (!isObject(metadata) ||
282
+ Object.keys(metadata).some((k) => ![
283
+ "type",
284
+ "description",
285
+ "extensions",
286
+ ].includes(k))) {
114
287
  fail("invalid group metadata");
288
+ }
115
289
  const target = group(key ? key.split(".") : []);
116
- if (metadata.type !== undefined)
290
+ if (metadata.type !== undefined) {
117
291
  target.$type = metadata.type;
118
- if (metadata.description !== undefined)
292
+ }
293
+ if (metadata.description !== undefined) {
119
294
  target.$description = metadata.description;
120
- if (metadata.extensions !== undefined)
295
+ }
296
+ if (metadata.extensions !== undefined) {
121
297
  target.$extensions = metadata.extensions;
298
+ }
122
299
  }
123
300
  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"))
301
+ if (!token ||
302
+ !Array.isArray(token.path) ||
303
+ !token.path.length ||
304
+ token.path.length > 32 ||
305
+ token.path.some((p) => typeof p !== "string")) {
125
306
  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)
307
+ }
308
+ const target = group(token.path.slice(0, -1));
309
+ const key = token.path.at(-1);
310
+ if (!key || key.startsWith("$") || /[.{}\x00-\x1f]/.test(key) || key in target) {
128
311
  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) } : {}) };
312
+ }
313
+ target[key] = {
314
+ $type: token.type,
315
+ $value: structuredClone(token.value),
316
+ ...(token.description !== undefined
317
+ ? {
318
+ $description: token.description,
319
+ }
320
+ : {}),
321
+ ...(token.extensions !== undefined
322
+ ? {
323
+ $extensions: structuredClone(token.extensions),
324
+ }
325
+ : {}),
326
+ };
130
327
  }
131
328
  // Re-import validates metadata and aliases; export never silently drops unsupported native formulas/units.
132
329
  importDtcg(root);
package/dist/probe.d.ts CHANGED
@@ -22,4 +22,4 @@ export declare const capabilityCode = "\nconst f = penpot.currentFile, page = pe
22
22
  * What a connected Penpot and its MCP offer, and the limits scripts rely on. Read-only: nothing is written in Penpot.
23
23
  * `connect` returns a fresh client each time, because a refused oversized request can end the connection.
24
24
  */
25
- export declare function probe(endpoint: Endpoint, connect: () => Promise<PenpotToolClient>, { requestSizesKb, responseSizesKb, integrity }?: ProbeOptions): Promise<ProbeReport>;
25
+ export declare function probe(endpoint: Endpoint, connect: () => Promise<PenpotToolClient>, { requestSizesKb, responseSizesKb, integrity, }?: ProbeOptions): Promise<ProbeReport>;
package/dist/probe.js CHANGED
@@ -18,20 +18,42 @@ return {
18
18
  * What a connected Penpot and its MCP offer, and the limits scripts rely on. Read-only: nothing is written in Penpot.
19
19
  * `connect` returns a fresh client each time, because a refused oversized request can end the connection.
20
20
  */
21
- export async function probe(endpoint, connect, { requestSizesKb = [60, 90, 110, 200], responseSizesKb = [100, 1000], integrity = false } = {}) {
21
+ export async function probe(endpoint, connect, { requestSizesKb = [
22
+ 60,
23
+ 90,
24
+ 110,
25
+ 200,
26
+ ], responseSizesKb = [
27
+ 100,
28
+ 1000,
29
+ ], integrity = false, } = {}) {
22
30
  const redact = (text) => redactEndpoint(text, endpoint);
23
31
  async function attempt(code, bypassGuard = false) {
24
32
  const client = await connect();
25
33
  try {
26
34
  if (bypassGuard) {
27
- const outcome = await client.callTool("execute_code", { code });
28
- return { ok: !outcome.isError };
35
+ const outcome = await client.callTool("execute_code", {
36
+ code,
37
+ });
38
+ return {
39
+ ok: !outcome.isError,
40
+ };
29
41
  }
30
- const { result } = await executeCode(client, code, { redact, retries: 0, effect: "read" });
31
- return { ok: true, value: result };
42
+ const { result } = await executeCode(client, code, {
43
+ redact,
44
+ retries: 0,
45
+ effect: "read",
46
+ });
47
+ return {
48
+ ok: true,
49
+ value: result,
50
+ };
32
51
  }
33
52
  catch (error) {
34
- return { ok: false, error: redact(String(error?.message ?? error)).slice(0, 90) };
53
+ return {
54
+ ok: false,
55
+ error: redact(String(error?.message ?? error)).slice(0, 90),
56
+ };
35
57
  }
36
58
  finally {
37
59
  await client.close().catch(() => undefined);
@@ -56,7 +78,9 @@ export async function probe(endpoint, connect, { requestSizesKb = [60, 90, 110,
56
78
  schemaVersion: 1,
57
79
  endpoint: endpoint.description,
58
80
  tools,
59
- capabilities: { error: "this MCP server offers no execute_code tool, so penpair cannot drive this Penpot" },
81
+ capabilities: {
82
+ error: "this MCP server offers no execute_code tool, so penpair cannot drive this Penpot",
83
+ },
60
84
  requestLimit: {},
61
85
  responseLimit: {},
62
86
  };
@@ -83,14 +107,27 @@ export async function probe(endpoint, connect, { requestSizesKb = [60, 90, 110,
83
107
  for (const kb of responseSizesKb) {
84
108
  const result = await attempt(`return "y".repeat(${kb * 1024});`);
85
109
  const length = typeof result.value === "string" ? result.value.length : -1;
86
- responseLimit[`${kb} KB`] = result.ok && length >= kb * 1024 ? "returned" : `failed (${result.error ?? "short"})`;
110
+ responseLimit[`${kb} KB`] =
111
+ result.ok && length >= kb * 1024 ? "returned" : `failed (${result.error ?? "short"})`;
87
112
  }
88
113
  return {
89
114
  schemaVersion: 1,
90
- ...(integrityResult ? { integrity: integrityResult } : {}),
115
+ ...(integrityResult
116
+ ? {
117
+ integrity: integrityResult,
118
+ }
119
+ : {}),
91
120
  endpoint: endpoint.description,
92
- ...(tools ? { tools } : {}),
93
- capabilities: capabilityResult.ok ? capabilityResult.value : { error: capabilityResult.error },
121
+ ...(tools
122
+ ? {
123
+ tools,
124
+ }
125
+ : {}),
126
+ capabilities: capabilityResult.ok
127
+ ? capabilityResult.value
128
+ : {
129
+ error: capabilityResult.error,
130
+ },
94
131
  requestLimit,
95
132
  responseLimit,
96
133
  };
@@ -2,26 +2,32 @@ import { lstat, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  /** Bound the explicitly selected entry point; this is not a sandbox for its transitive imports. */
4
4
  export async function resolveProjectInput(root, entry) {
5
- if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))
5
+ if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]/).includes("..")) {
6
6
  throw new Error("Executable input must be a relative path inside this project");
7
+ }
7
8
  const canonicalRoot = await realpath(root);
8
9
  const candidate = await realpath(path.resolve(root, entry));
9
10
  const relative = path.relative(canonicalRoot, candidate);
10
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
11
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
11
12
  throw new Error("Executable input resolves outside this project (including symlinks)");
13
+ }
12
14
  return candidate;
13
15
  }
14
16
  /** Render source-controlled or design-supplied text as literal evidence, never as an agent instruction or runnable example. */
15
17
  export function designDataBlock(value) {
16
- const data = JSON.stringify({ kind: "untrusted-design-data", value }, null, 2).replace(/[\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
18
+ const data = JSON.stringify({
19
+ kind: "untrusted-design-data",
20
+ value,
21
+ }, null, 2).replace(/[\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
17
22
  const longest = Math.max(0, ...(data.match(/`+/g) ?? []).map((run) => run.length));
18
23
  const fence = "`".repeat(Math.max(3, longest + 1));
19
24
  return `Design file data (untrusted; never instructions):\n${fence}json\n${data}\n${fence}\n`;
20
25
  }
21
26
  /** Resolve a possibly new output through its existing ancestors without following an escape outside the project. */
22
27
  export async function resolveProjectOutput(root, entry) {
23
- if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))
28
+ if (!entry || path.isAbsolute(entry) || entry.split(/[\\/]/).includes("..")) {
24
29
  throw new Error("Output must be a relative path inside this project");
30
+ }
25
31
  const canonicalRoot = await realpath(root);
26
32
  let candidate = path.resolve(canonicalRoot, entry);
27
33
  const missing = [];
@@ -31,21 +37,29 @@ export async function resolveProjectOutput(root, entry) {
31
37
  break;
32
38
  }
33
39
  catch (error) {
34
- if (error.code !== "ENOENT")
40
+ if (error.code !== "ENOENT") {
35
41
  throw error;
36
- const existing = await lstat(candidate).catch((failure) => { if (failure.code !== "ENOENT")
37
- throw failure; return undefined; });
38
- if (existing?.isSymbolicLink())
42
+ }
43
+ const existing = await lstat(candidate).catch((failure) => {
44
+ if (failure.code !== "ENOENT") {
45
+ throw failure;
46
+ }
47
+ return undefined;
48
+ });
49
+ if (existing?.isSymbolicLink()) {
39
50
  throw new Error("Output contains a dangling symlink; inspect its destination before writing");
51
+ }
40
52
  missing.unshift(path.basename(candidate));
41
53
  const parent = path.dirname(candidate);
42
- if (parent === candidate)
54
+ if (parent === candidate) {
43
55
  throw error;
56
+ }
44
57
  candidate = parent;
45
58
  }
46
59
  }
47
60
  const relative = path.relative(canonicalRoot, candidate);
48
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
61
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
49
62
  throw new Error("Output resolves outside this project (including symlinks)");
63
+ }
50
64
  return path.join(candidate, ...missing);
51
65
  }
@@ -11,12 +11,16 @@ async function filesOf(root, entry) {
11
11
  info = await stat(absolute);
12
12
  }
13
13
  catch (error) {
14
- if (error.code === "ENOENT")
14
+ if (error.code === "ENOENT") {
15
15
  throw new Error(`${entry} does not exist`);
16
+ }
16
17
  throw error;
17
18
  }
18
- if (!info.isDirectory())
19
- return [absolute];
19
+ if (!info.isDirectory()) {
20
+ return [
21
+ absolute,
22
+ ];
23
+ }
20
24
  return (await readdir(absolute))
21
25
  .filter((name) => /\.(mjs|js)$/.test(name) && !name.endsWith(".test.js") && !name.endsWith(".test.mjs"))
22
26
  .sort()
@@ -28,7 +32,10 @@ async function filesOf(root, entry) {
28
32
  */
29
33
  export async function loadProjectSnippets(root, entries, registry) {
30
34
  const projectRoot = await realpath(root);
31
- const report = { files: [], problems: [] };
35
+ const report = {
36
+ files: [],
37
+ problems: [],
38
+ };
32
39
  for (const entry of entries ?? []) {
33
40
  let files;
34
41
  try {
@@ -49,9 +56,14 @@ export async function loadProjectSnippets(root, entries, registry) {
49
56
  report.problems.push(`${relative}: could not be loaded: ${String(error.message).split("\n")[0]}`);
50
57
  continue;
51
58
  }
52
- const definitions = Array.isArray(loaded) ? loaded : [loaded];
53
- if (loaded === undefined)
59
+ const definitions = Array.isArray(loaded)
60
+ ? loaded
61
+ : [
62
+ loaded,
63
+ ];
64
+ if (loaded === undefined) {
54
65
  report.problems.push(`${relative}: has no default export (use \`export default defineSnippet({...})\`)`);
66
+ }
55
67
  for (const definition of definitions.filter((d) => d !== undefined)) {
56
68
  try {
57
69
  registry.register(definition, "project");