@penpair/core 0.4.0 → 0.5.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.
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +1 -0
- package/dist/browser-wake.d.ts +22 -0
- package/dist/browser-wake.js +241 -0
- package/dist/cli-main.js +140 -15
- package/dist/clipboard-payload.d.ts +2 -0
- package/dist/clipboard-payload.js +89 -0
- package/dist/design-commands/context.d.ts +5 -0
- package/dist/design-commands/copy-command.d.ts +3 -0
- package/dist/design-commands/copy-command.js +101 -0
- package/dist/design-commands/release-command.js +6 -1
- package/dist/design-releases/copy-evidence.d.ts +33 -0
- package/dist/design-releases/copy-evidence.js +278 -0
- package/dist/design-releases/copy-plan.d.ts +49 -0
- package/dist/design-releases/copy-plan.js +55 -0
- package/dist/design-releases/copy-snippets.d.ts +11 -0
- package/dist/design-releases/copy-snippets.js +293 -0
- package/dist/design-releases/copy.d.ts +28 -0
- package/dist/design-releases/copy.js +466 -0
- package/dist/design-releases/index.d.ts +1 -0
- package/dist/design-releases/index.js +1 -0
- package/dist/errors.js +1 -1
- package/dist/instance/connection.d.ts +2 -0
- package/dist/instance/connection.js +15 -3
- package/dist/kit-source.js +1 -1
- package/dist/layout-policy.js +21 -11
- package/dist/layout-settling.d.ts +1 -0
- package/dist/layout-settling.js +51 -17
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +19 -3
- package/dist/mcp-server.js +99 -3
- package/dist/native-editor-clipboard.d.ts +17 -0
- package/dist/native-editor-clipboard.js +470 -0
- package/dist/plugin-health.d.ts +13 -0
- package/dist/plugin-health.js +39 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/layout-settling.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Self-contained policy: its JavaScript source also runs inside the Penpot sandbox. */
|
|
2
2
|
export async function settleLayout(observe, options) {
|
|
3
|
-
const { timeoutMs, intervalMs, strategy = "auto", sleep } = options;
|
|
4
|
-
if (!
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const { timeoutMs, intervalMs, strategy = "auto", maxNodes = 10000, sleep } = options;
|
|
4
|
+
if (!Number.isInteger(maxNodes) ||
|
|
5
|
+
maxNodes < 1 ||
|
|
6
|
+
maxNodes > 100000 ||
|
|
7
|
+
![
|
|
8
|
+
"auto",
|
|
9
|
+
"native",
|
|
10
|
+
"geometry",
|
|
11
|
+
].includes(strategy) ||
|
|
9
12
|
!Number.isFinite(timeoutMs) ||
|
|
10
13
|
timeoutMs < 1 ||
|
|
11
14
|
timeoutMs > 30000 ||
|
|
@@ -17,6 +20,9 @@ export async function settleLayout(observe, options) {
|
|
|
17
20
|
const started = Date.now();
|
|
18
21
|
const deadline = started + timeoutMs;
|
|
19
22
|
let samples = 0;
|
|
23
|
+
let nodeCount = 0;
|
|
24
|
+
let rootIds = [];
|
|
25
|
+
let truncated = false;
|
|
20
26
|
let nativeOutcome = "skipped";
|
|
21
27
|
let fallbackReason;
|
|
22
28
|
let targetIdentity;
|
|
@@ -24,8 +30,12 @@ export async function settleLayout(observe, options) {
|
|
|
24
30
|
let matchingSamples = 0;
|
|
25
31
|
const createResult = (stable, method, reason) => ({
|
|
26
32
|
stable,
|
|
27
|
-
method,
|
|
33
|
+
method: truncated ? "partial" : method,
|
|
28
34
|
samples,
|
|
35
|
+
rootIds,
|
|
36
|
+
nodeCount,
|
|
37
|
+
maxNodes,
|
|
38
|
+
truncated,
|
|
29
39
|
strategy,
|
|
30
40
|
nativeOutcome,
|
|
31
41
|
...(fallbackReason
|
|
@@ -34,9 +44,10 @@ export async function settleLayout(observe, options) {
|
|
|
34
44
|
}
|
|
35
45
|
: {}),
|
|
36
46
|
elapsedMs: Date.now() - started,
|
|
37
|
-
...(reason
|
|
47
|
+
...(reason || truncated
|
|
38
48
|
? {
|
|
39
|
-
reason
|
|
49
|
+
reason: reason ??
|
|
50
|
+
"Only the bounded layout sample was observed; independent readback is required",
|
|
40
51
|
}
|
|
41
52
|
: {}),
|
|
42
53
|
});
|
|
@@ -74,20 +85,34 @@ export async function settleLayout(observe, options) {
|
|
|
74
85
|
const nextTargetIdentity = JSON.stringify([
|
|
75
86
|
observation.fileId,
|
|
76
87
|
observation.pageId,
|
|
77
|
-
observation.
|
|
88
|
+
observation.rootIds ?? [
|
|
89
|
+
observation.geometry?.[0]?.[0],
|
|
90
|
+
],
|
|
78
91
|
]);
|
|
79
92
|
if (targetIdentity && targetIdentity !== nextTargetIdentity) {
|
|
80
93
|
throw new Error("Layout target changed between samples");
|
|
81
94
|
}
|
|
82
95
|
targetIdentity = nextTargetIdentity;
|
|
83
|
-
if (observation.
|
|
84
|
-
!Array.isArray(observation.geometry) ||
|
|
96
|
+
if (!Array.isArray(observation.geometry) ||
|
|
85
97
|
!observation.geometry.length ||
|
|
86
|
-
observation.geometry.length >
|
|
98
|
+
observation.geometry.length > maxNodes) {
|
|
87
99
|
throw new Error("Incomplete layout geometry evidence");
|
|
88
100
|
}
|
|
101
|
+
const observedRoots = observation.rootIds ?? [
|
|
102
|
+
observation.geometry[0][0],
|
|
103
|
+
];
|
|
104
|
+
if (!Array.isArray(observedRoots) ||
|
|
105
|
+
!observedRoots.length ||
|
|
106
|
+
observedRoots.some((id) => typeof id !== "string" || !id) ||
|
|
107
|
+
new Set(observedRoots).size !== observedRoots.length) {
|
|
108
|
+
throw new Error("Malformed layout root evidence");
|
|
109
|
+
}
|
|
110
|
+
rootIds = observedRoots;
|
|
111
|
+
nodeCount = observation.geometry.length;
|
|
112
|
+
truncated = observation.truncated;
|
|
89
113
|
const observedShapeIds = new Set();
|
|
90
114
|
const siblingCountByParent = new Map();
|
|
115
|
+
let rootIndex = 0;
|
|
91
116
|
for (const geometryRow of observation.geometry) {
|
|
92
117
|
if (!Array.isArray(geometryRow) ||
|
|
93
118
|
geometryRow.length !== 7 ||
|
|
@@ -102,9 +127,12 @@ export async function settleLayout(observe, options) {
|
|
|
102
127
|
geometryRow[6] < 0) {
|
|
103
128
|
throw new Error("Layout API returned no geometry evidence or malformed hierarchy");
|
|
104
129
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
if (geometryRow[5] === null) {
|
|
131
|
+
if (geometryRow[0] !== rootIds[rootIndex++] || geometryRow[6] !== 0) {
|
|
132
|
+
throw new Error("Malformed layout root hierarchy");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (typeof geometryRow[5] !== "string") {
|
|
108
136
|
throw new Error("Malformed layout root hierarchy");
|
|
109
137
|
}
|
|
110
138
|
if (geometryRow[5] !== null) {
|
|
@@ -116,6 +144,9 @@ export async function settleLayout(observe, options) {
|
|
|
116
144
|
}
|
|
117
145
|
observedShapeIds.add(geometryRow[0]);
|
|
118
146
|
}
|
|
147
|
+
if (!truncated && rootIndex !== rootIds.length) {
|
|
148
|
+
throw new Error("Incomplete layout root evidence");
|
|
149
|
+
}
|
|
119
150
|
if (sampleIndex === 0) {
|
|
120
151
|
nativeOutcome =
|
|
121
152
|
observation.nativeOutcome ||
|
|
@@ -143,7 +174,10 @@ export async function settleLayout(observe, options) {
|
|
|
143
174
|
if (Date.now() >= deadline) {
|
|
144
175
|
return createResult(false, "geometry", "Layout observation exceeded its deadline");
|
|
145
176
|
}
|
|
146
|
-
const currentGeometry = JSON.stringify(
|
|
177
|
+
const currentGeometry = JSON.stringify([
|
|
178
|
+
observation.geometry,
|
|
179
|
+
observation.truncated,
|
|
180
|
+
]);
|
|
147
181
|
matchingSamples = currentGeometry === previousGeometry ? matchingSamples + 1 : 1;
|
|
148
182
|
if (matchingSamples >= 3) {
|
|
149
183
|
return createResult(true, "geometry");
|
package/dist/layout.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { PenpotToolClient } from "./client.js";
|
|
|
2
2
|
export type LayoutStrategy = "auto" | "native" | "geometry";
|
|
3
3
|
export interface LayoutOptions {
|
|
4
4
|
shapeId?: string;
|
|
5
|
+
shapeIds?: string[];
|
|
6
|
+
maxNodes?: number;
|
|
5
7
|
strategy?: LayoutStrategy;
|
|
6
8
|
timeoutMs?: number;
|
|
7
9
|
intervalMs?: number;
|
|
@@ -9,7 +11,11 @@ export interface LayoutOptions {
|
|
|
9
11
|
}
|
|
10
12
|
export interface LayoutResult {
|
|
11
13
|
stable: boolean;
|
|
12
|
-
method: "native" | "geometry" | "unavailable";
|
|
14
|
+
method: "native" | "geometry" | "partial" | "unavailable";
|
|
15
|
+
rootIds?: string[];
|
|
16
|
+
nodeCount?: number;
|
|
17
|
+
maxNodes?: number;
|
|
18
|
+
truncated?: boolean;
|
|
13
19
|
samples: number;
|
|
14
20
|
reason?: string;
|
|
15
21
|
strategy?: LayoutStrategy;
|
package/dist/layout.js
CHANGED
|
@@ -5,10 +5,25 @@ import { layoutPolicySource } from "./layout-policy.js";
|
|
|
5
5
|
import { settleLayout } from "./layout-settling.js";
|
|
6
6
|
/** Layout evidence is independent of render equality and visual approval. Transport failures propagate without fallback. */
|
|
7
7
|
export async function waitForStableLayout(client, options = {}) {
|
|
8
|
-
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, } = options;
|
|
8
|
+
const { timeoutMs = 30000, intervalMs = 200, strategy = "auto", sleep = commandSleep, shapeId, shapeIds, maxNodes = 10000, } = options;
|
|
9
|
+
const roots = shapeIds ??
|
|
10
|
+
(shapeId
|
|
11
|
+
? [
|
|
12
|
+
shapeId,
|
|
13
|
+
]
|
|
14
|
+
: []);
|
|
15
|
+
if (!Array.isArray(roots) ||
|
|
16
|
+
roots.some((id) => typeof id !== "string" || !id.trim()) ||
|
|
17
|
+
new Set(roots).size !== roots.length) {
|
|
18
|
+
throw new Error("Layout roots must be distinct nonempty shape IDs");
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1 || maxNodes > 100000) {
|
|
21
|
+
throw new Error("Layout node budget must be an integer from 1 to 100000");
|
|
22
|
+
}
|
|
9
23
|
try {
|
|
10
|
-
return await settleLayout(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst
|
|
11
|
-
|
|
24
|
+
return await settleLayout(async (nativeMs, remaining) => runSnippet(client, snippet("layout-state", `${layoutPolicySource}\nconst roots = CFG.shapeIds.length ? CFG.shapeIds.map(id => penpot.currentPage.getShapeById(id)) : [penpot.currentPage.root];\nreturn await observeLayout(penpot, roots, CFG);`, {
|
|
25
|
+
shapeIds: roots,
|
|
26
|
+
maxNodes,
|
|
12
27
|
nativeMs,
|
|
13
28
|
}, "read"), {
|
|
14
29
|
retries: 0,
|
|
@@ -17,6 +32,7 @@ export async function waitForStableLayout(client, options = {}) {
|
|
|
17
32
|
timeoutMs,
|
|
18
33
|
intervalMs,
|
|
19
34
|
strategy,
|
|
35
|
+
maxNodes,
|
|
20
36
|
sleep,
|
|
21
37
|
});
|
|
22
38
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -13,9 +13,9 @@ const string = {
|
|
|
13
13
|
minLength: 1,
|
|
14
14
|
maxLength: 1000,
|
|
15
15
|
};
|
|
16
|
-
const integer = (max) => ({
|
|
16
|
+
const integer = (max, min = 0) => ({
|
|
17
17
|
type: "integer",
|
|
18
|
-
minimum:
|
|
18
|
+
minimum: min,
|
|
19
19
|
maximum: max,
|
|
20
20
|
});
|
|
21
21
|
const definitions = [
|
|
@@ -247,6 +247,83 @@ const definitions = [
|
|
|
247
247
|
],
|
|
248
248
|
localWrite: true,
|
|
249
249
|
},
|
|
250
|
+
{
|
|
251
|
+
name: "design_release_copy",
|
|
252
|
+
annotations: {
|
|
253
|
+
readOnlyHint: false,
|
|
254
|
+
destructiveHint: false,
|
|
255
|
+
idempotentHint: false,
|
|
256
|
+
openWorldHint: true,
|
|
257
|
+
},
|
|
258
|
+
description: "Experimental same-file native board copy to release/<version>, preview by default. Requires a configured project, pinned file and connected Penpot plugin; advertised only with --allow-writes. Applying also requires an already enabled loopback browser debugging port, existing clipboard read/write access and focused editor canvas. Does not start a browser or grant permissions. Host-independent CLI/MCP operation; no automatic fallback to the assistant's computer-control tools. The successful native-UI trial used an injected test driver; bundled CDP/Penpot qualification remains pending. Preserves originals, changes focus/clipboard, never blindly replays an uncertain paste and does not approve a release.",
|
|
259
|
+
properties: {
|
|
260
|
+
sourcePage: {
|
|
261
|
+
...string,
|
|
262
|
+
description: "Exact source page name or ID within the project's pinned Penpot file.",
|
|
263
|
+
},
|
|
264
|
+
frameIds: {
|
|
265
|
+
type: "array",
|
|
266
|
+
items: string,
|
|
267
|
+
minItems: 1,
|
|
268
|
+
maxItems: 100,
|
|
269
|
+
uniqueItems: true,
|
|
270
|
+
description: "Observed native board IDs on the source page; selections must not overlap.",
|
|
271
|
+
},
|
|
272
|
+
releaseVersion: {
|
|
273
|
+
...string,
|
|
274
|
+
pattern: "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$",
|
|
275
|
+
description: "Version naming the new or existing destination page: release/<version>.",
|
|
276
|
+
},
|
|
277
|
+
debugPort: {
|
|
278
|
+
...integer(65535, 1),
|
|
279
|
+
description: "Required for apply: an existing loopback CDP port. This does not enable debugging or grant clipboard access.",
|
|
280
|
+
},
|
|
281
|
+
apply: {
|
|
282
|
+
type: "boolean",
|
|
283
|
+
default: false,
|
|
284
|
+
description: "False previews only. True explicitly authorizes native copying and requires debugPort plus existing browser clipboard read/write access.",
|
|
285
|
+
},
|
|
286
|
+
resume: {
|
|
287
|
+
...string,
|
|
288
|
+
description: "Original run ID for explicit reconciliation with apply=true and the same inputs; never blindly repeats a paste.",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
required: [
|
|
292
|
+
"sourcePage",
|
|
293
|
+
"frameIds",
|
|
294
|
+
"releaseVersion",
|
|
295
|
+
],
|
|
296
|
+
write: true,
|
|
297
|
+
command: (a) => [
|
|
298
|
+
"design-release",
|
|
299
|
+
"copy",
|
|
300
|
+
"--source-page",
|
|
301
|
+
String(a.sourcePage),
|
|
302
|
+
...a.frameIds.flatMap((frameId) => [
|
|
303
|
+
"--frame",
|
|
304
|
+
frameId,
|
|
305
|
+
]),
|
|
306
|
+
"--release",
|
|
307
|
+
String(a.releaseVersion),
|
|
308
|
+
...(a.debugPort !== undefined
|
|
309
|
+
? [
|
|
310
|
+
"--debug-port",
|
|
311
|
+
String(a.debugPort),
|
|
312
|
+
]
|
|
313
|
+
: []),
|
|
314
|
+
...(a.apply
|
|
315
|
+
? [
|
|
316
|
+
"--apply",
|
|
317
|
+
]
|
|
318
|
+
: []),
|
|
319
|
+
...(a.resume
|
|
320
|
+
? [
|
|
321
|
+
"--resume",
|
|
322
|
+
String(a.resume),
|
|
323
|
+
]
|
|
324
|
+
: []),
|
|
325
|
+
],
|
|
326
|
+
},
|
|
250
327
|
{
|
|
251
328
|
name: "run_registered",
|
|
252
329
|
annotations: {
|
|
@@ -329,6 +406,7 @@ function validate(def, raw) {
|
|
|
329
406
|
!value ||
|
|
330
407
|
value.startsWith("-") ||
|
|
331
408
|
value.length > (schema.maxLength ?? 1000) ||
|
|
409
|
+
(schema.pattern !== undefined && !new RegExp(schema.pattern).test(value)) ||
|
|
332
410
|
/[\x00-\x1f]/.test(value))) {
|
|
333
411
|
throw new Error(`Invalid ${key}`);
|
|
334
412
|
}
|
|
@@ -336,7 +414,22 @@ function validate(def, raw) {
|
|
|
336
414
|
throw new Error(`Invalid ${key}`);
|
|
337
415
|
}
|
|
338
416
|
if (schema.type === "integer" &&
|
|
339
|
-
(!Number.isInteger(value) ||
|
|
417
|
+
(!Number.isInteger(value) ||
|
|
418
|
+
Number(value) < (schema.minimum ?? 0) ||
|
|
419
|
+
Number(value) > schema.maximum)) {
|
|
420
|
+
throw new Error(`Invalid ${key}`);
|
|
421
|
+
}
|
|
422
|
+
if (schema.type === "array" &&
|
|
423
|
+
(!Array.isArray(value) ||
|
|
424
|
+
value.length < (schema.minItems ?? 0) ||
|
|
425
|
+
value.length > (schema.maxItems ?? 100) ||
|
|
426
|
+
(schema.uniqueItems && new Set(value).size !== value.length) ||
|
|
427
|
+
value.some((item) => schema.items?.type !== "string" ||
|
|
428
|
+
typeof item !== "string" ||
|
|
429
|
+
!item.trim() ||
|
|
430
|
+
item.startsWith("-") ||
|
|
431
|
+
item.length > (schema.items.maxLength ?? 1000) ||
|
|
432
|
+
/[\x00-\x1f]/.test(item)))) {
|
|
340
433
|
throw new Error(`Invalid ${key}`);
|
|
341
434
|
}
|
|
342
435
|
if (key === "params" &&
|
|
@@ -351,6 +444,9 @@ function validate(def, raw) {
|
|
|
351
444
|
if ((a.resume || a.checkpoint) && !a.apply) {
|
|
352
445
|
throw new Error("Explicit resume requires apply");
|
|
353
446
|
}
|
|
447
|
+
if (def.name === "design_release_copy" && a.apply && a.debugPort === undefined) {
|
|
448
|
+
throw new Error("Native copy apply requires debugPort");
|
|
449
|
+
}
|
|
354
450
|
return a;
|
|
355
451
|
}
|
|
356
452
|
/** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BrowserTargetOptions } from "./browser-wake.js";
|
|
2
|
+
export interface NativeClipboard {
|
|
3
|
+
copy(expectedPageId: string, expectedFrameIds: string[]): Promise<void>;
|
|
4
|
+
paste(expectedPageId: string): Promise<void>;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface ClipboardSocket extends EventTarget {
|
|
8
|
+
readyState: number;
|
|
9
|
+
send(data: string): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export interface NativeClipboardOptions extends BrowserTargetOptions {
|
|
13
|
+
/** Tests may substitute the protocol transport; ordinary commands use Node's WebSocket. */
|
|
14
|
+
createSocket?: (url: string) => ClipboardSocket;
|
|
15
|
+
}
|
|
16
|
+
/** Holds a short exclusive clipboard session; closing releases control, never the browser. */
|
|
17
|
+
export declare function connectNativeClipboard(options: NativeClipboardOptions): Promise<NativeClipboard>;
|