@penpair/core 0.2.0 → 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.
- package/dist/adapters.js +58 -11
- package/dist/approvals.js +143 -29
- package/dist/atomic-output.js +9 -4
- package/dist/audit-rules.js +345 -79
- package/dist/backup-note.js +29 -8
- package/dist/baseline-coverage.js +169 -35
- package/dist/baselines-config.js +119 -29
- package/dist/baselines.js +280 -57
- package/dist/cli-arguments.js +18 -6
- package/dist/cli-main.js +1621 -392
- package/dist/client.d.ts +5 -2
- package/dist/client.js +127 -28
- package/dist/command-result.js +341 -52
- package/dist/coordination.js +91 -29
- package/dist/credentials.js +57 -13
- package/dist/deep-check.js +56 -12
- package/dist/design-spec.js +189 -49
- package/dist/endpoint.js +102 -34
- package/dist/export.js +86 -18
- package/dist/file-archive.js +33 -7
- package/dist/file-guard.js +29 -11
- package/dist/formats.js +215 -14
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/inspection.d.ts +1 -1
- package/dist/inspection.js +378 -65
- package/dist/instance/config-schema.js +400 -41
- package/dist/instance/config.js +101 -30
- package/dist/instance/connect.js +151 -44
- package/dist/instance/connection.d.ts +26 -5
- package/dist/instance/connection.js +194 -21
- package/dist/instance/gitignore.js +5 -2
- package/dist/instance/init.js +32 -7
- package/dist/instance/runner.js +32 -10
- package/dist/instance/setup.js +46 -13
- package/dist/instance/tty.js +26 -8
- package/dist/journal.d.ts +10 -0
- package/dist/journal.js +212 -49
- package/dist/kit-smoke.js +82 -20
- package/dist/kit-source.js +10 -40
- package/dist/kit.js +102 -34
- package/dist/layout-policy.d.ts +2 -0
- package/dist/layout-policy.js +86 -0
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +27 -70
- package/dist/live-smoke.js +13 -4
- package/dist/local-profile.js +2 -1
- package/dist/mcp-server.js +360 -47
- package/dist/native-api.js +47 -11
- package/dist/onboard.js +33 -7
- package/dist/operation.d.ts +2 -2
- package/dist/operation.js +279 -69
- package/dist/page-session.d.ts +23 -0
- package/dist/page-session.js +142 -0
- package/dist/png.js +18 -3
- package/dist/portable-tokens.js +254 -57
- package/dist/probe.d.ts +1 -1
- package/dist/probe.js +48 -11
- package/dist/project-input.js +24 -10
- package/dist/project-snippets.js +18 -6
- package/dist/project-status.js +133 -24
- package/dist/registry.js +217 -60
- package/dist/run-file.js +129 -30
- package/dist/shots.js +108 -26
- package/dist/skill-instructions.js +91 -22
- package/dist/skills.js +129 -39
- package/dist/snippet.js +27 -7
- package/dist/snippets/read.js +8 -2
- package/dist/structure-snapshot.d.ts +12 -0
- package/dist/structure-snapshot.js +439 -85
- package/dist/token-edit.js +219 -64
- package/dist/token-projection.d.ts +56 -0
- package/dist/token-projection.js +286 -0
- package/dist/tokens-diff.js +125 -32
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/penpair-design/SKILL.md +3 -0
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
- package/skills/penpair-design/references/project-facts.md +2 -0
- package/skills/penpair-handoff/SKILL.md +2 -0
- package/skills/penpair-handoff/references/comparison-companion.md +13 -0
- package/skills/penpair-handoff/references/handoff-flow.md +2 -0
- package/skills/penpair-recipes/SKILL.md +5 -0
- package/skills/penpair-setup/SKILL.md +3 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -3
- package/dist/docker/lifecycle.d.ts +0 -96
- package/dist/docker/lifecycle.js +0 -273
- package/dist/docker/secrets.d.ts +0 -11
- package/dist/docker/secrets.js +0 -38
- package/dist/docker/template.d.ts +0 -8
- package/dist/docker/template.js +0 -158
- package/dist/docker-profile.d.ts +0 -114
- package/dist/docker-profile.js +0 -18
- package/dist/instance/lifecycle.d.ts +0 -129
- package/dist/instance/lifecycle.js +0 -336
- package/dist/instance/secrets.d.ts +0 -11
- package/dist/instance/secrets.js +0 -38
- package/dist/instance/template.d.ts +0 -8
- package/dist/instance/template.js +0 -158
package/dist/client.d.ts
CHANGED
|
@@ -22,7 +22,10 @@ export interface PenpotToolClient {
|
|
|
22
22
|
}
|
|
23
23
|
/** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
|
|
24
24
|
export declare const maxSnippetBytes: number;
|
|
25
|
-
export declare function connect(endpoint: Endpoint
|
|
25
|
+
export declare function connect(endpoint: Endpoint, options?: {
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): Promise<PenpotToolClient>;
|
|
26
29
|
/** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
|
|
27
30
|
export declare function classifyFailure(text: string): PenpairErrorCode;
|
|
28
31
|
export interface ExecuteOptions {
|
|
@@ -50,7 +53,7 @@ export declare function executionEnvelope(outcome: ToolOutcome): Pick<ExecuteRes
|
|
|
50
53
|
/**
|
|
51
54
|
* Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
|
|
52
55
|
*/
|
|
53
|
-
export declare function executeCode(client: PenpotToolClient, code: string, { timeoutMs, effect, retries: requestedRetries, sleep, onWait, redact }?: ExecuteOptions): Promise<ExecuteResult>;
|
|
56
|
+
export declare function executeCode(client: PenpotToolClient, code: string, { timeoutMs, effect, retries: requestedRetries, sleep, onWait, redact, }?: ExecuteOptions): Promise<ExecuteResult>;
|
|
54
57
|
export declare class UncertainExecutionError extends PenpairError {
|
|
55
58
|
readonly runId: string;
|
|
56
59
|
readonly outcome = "uncertain";
|
package/dist/client.js
CHANGED
|
@@ -1,32 +1,81 @@
|
|
|
1
1
|
import { commandSignal, commandSleep } from "./command-result.js";
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
5
|
import { redactEndpoint } from "./endpoint.js";
|
|
6
6
|
import { PenpairError } from "./errors.js";
|
|
7
7
|
/** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
|
|
8
8
|
export const maxSnippetBytes = 90 * 1024;
|
|
9
|
-
export async function connect(endpoint) {
|
|
9
|
+
export async function connect(endpoint, options = {}) {
|
|
10
10
|
commandSignal()?.throwIfAborted();
|
|
11
|
-
const
|
|
11
|
+
const signal = options.signal
|
|
12
|
+
? AbortSignal.any([
|
|
13
|
+
options.signal,
|
|
14
|
+
...(commandSignal()
|
|
15
|
+
? [
|
|
16
|
+
commandSignal(),
|
|
17
|
+
]
|
|
18
|
+
: []),
|
|
19
|
+
])
|
|
20
|
+
: commandSignal();
|
|
21
|
+
const client = new Client({
|
|
22
|
+
name: "penpair",
|
|
23
|
+
version: "0",
|
|
24
|
+
});
|
|
12
25
|
const transport = new StreamableHTTPClientTransport(new URL(endpoint.url));
|
|
13
26
|
try {
|
|
14
|
-
await client.connect(transport, {
|
|
27
|
+
await client.connect(transport, {
|
|
28
|
+
signal,
|
|
29
|
+
timeout: options.timeoutMs ?? 120_000,
|
|
30
|
+
});
|
|
15
31
|
}
|
|
16
32
|
catch (error) {
|
|
33
|
+
await client.close().catch(() => undefined);
|
|
17
34
|
throw transportFailure(error, endpoint);
|
|
18
35
|
}
|
|
19
36
|
return {
|
|
20
37
|
async callTool(name, args, options) {
|
|
21
38
|
try {
|
|
22
|
-
const result = await client.callTool({
|
|
39
|
+
const result = await client.callTool({
|
|
40
|
+
name,
|
|
41
|
+
arguments: args,
|
|
42
|
+
}, undefined, {
|
|
43
|
+
timeout: options?.timeoutMs ?? 120_000,
|
|
44
|
+
signal: signal && commandSignal()
|
|
45
|
+
? AbortSignal.any([
|
|
46
|
+
signal,
|
|
47
|
+
commandSignal(),
|
|
48
|
+
])
|
|
49
|
+
: (signal ?? commandSignal()),
|
|
50
|
+
});
|
|
23
51
|
const content = Array.isArray(result.content) ? result.content : [];
|
|
24
52
|
const text = content
|
|
25
|
-
.flatMap((item) => typeof item === "object" &&
|
|
26
|
-
|
|
53
|
+
.flatMap((item) => typeof item === "object" &&
|
|
54
|
+
item !== null &&
|
|
55
|
+
"type" in item &&
|
|
56
|
+
item.type === "text" &&
|
|
57
|
+
"text" in item
|
|
58
|
+
? [
|
|
59
|
+
String(item.text),
|
|
60
|
+
]
|
|
27
61
|
: [])
|
|
28
62
|
.join("\n");
|
|
29
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
text,
|
|
65
|
+
metrics: {
|
|
66
|
+
requestSnippetBytes: typeof args.code === "string" ? Buffer.byteLength(args.code, "utf8") : 0,
|
|
67
|
+
responsePayloadBytes: Buffer.byteLength(text, "utf8") +
|
|
68
|
+
(result.structuredContent === undefined
|
|
69
|
+
? 0
|
|
70
|
+
: Buffer.byteLength(JSON.stringify(result.structuredContent), "utf8")),
|
|
71
|
+
},
|
|
72
|
+
...(result.structuredContent !== undefined
|
|
73
|
+
? {
|
|
74
|
+
structuredContent: result.structuredContent,
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
isError: result.isError === true,
|
|
78
|
+
};
|
|
30
79
|
}
|
|
31
80
|
catch (error) {
|
|
32
81
|
throw transportFailure(error, endpoint);
|
|
@@ -34,7 +83,10 @@ export async function connect(endpoint) {
|
|
|
34
83
|
},
|
|
35
84
|
async listTools() {
|
|
36
85
|
try {
|
|
37
|
-
return (await client.listTools(undefined, {
|
|
86
|
+
return (await client.listTools(undefined, {
|
|
87
|
+
signal: commandSignal(),
|
|
88
|
+
timeout: 120_000,
|
|
89
|
+
})).tools.map((tool) => tool.name);
|
|
38
90
|
}
|
|
39
91
|
catch (error) {
|
|
40
92
|
throw transportFailure(error, endpoint);
|
|
@@ -46,25 +98,31 @@ export async function connect(endpoint) {
|
|
|
46
98
|
};
|
|
47
99
|
}
|
|
48
100
|
function transportFailure(error, endpoint) {
|
|
49
|
-
if (commandSignal()?.aborted)
|
|
101
|
+
if (commandSignal()?.aborted) {
|
|
50
102
|
return commandSignal().reason;
|
|
103
|
+
}
|
|
51
104
|
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
52
105
|
const message = redactEndpoint(raw, endpoint);
|
|
53
|
-
if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403))
|
|
106
|
+
if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
54
107
|
return new PenpairError("auth-rejected", message);
|
|
108
|
+
}
|
|
55
109
|
const code = classifyFailure(message);
|
|
56
110
|
return new PenpairError(code === "tool-failed" ? "mcp-unavailable" : code, message);
|
|
57
111
|
}
|
|
58
112
|
/** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
|
|
59
113
|
export function classifyFailure(text) {
|
|
60
|
-
if (/suspended|no heartbeat/i.test(text))
|
|
114
|
+
if (/suspended|no heartbeat/i.test(text)) {
|
|
61
115
|
return "plugin-suspended";
|
|
62
|
-
|
|
116
|
+
}
|
|
117
|
+
if (/not connected|no (?:penpot )?plugin|no client|plugin (?:is )?(?:not|disconnected)|connect the plugin/i.test(text)) {
|
|
63
118
|
return "plugin-disconnected";
|
|
64
|
-
|
|
119
|
+
}
|
|
120
|
+
if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text)) {
|
|
65
121
|
return "auth-rejected";
|
|
66
|
-
|
|
122
|
+
}
|
|
123
|
+
if (/ECONNREFUSED|ENOTFOUND|fetch failed|EHOSTUNREACH|socket hang up|ECONNRESET|timed? ?out/i.test(text)) {
|
|
67
124
|
return "mcp-unavailable";
|
|
125
|
+
}
|
|
68
126
|
return "tool-failed";
|
|
69
127
|
}
|
|
70
128
|
const defaultSleep = commandSleep;
|
|
@@ -78,28 +136,45 @@ export function executionEnvelope(outcome) {
|
|
|
78
136
|
return undefined;
|
|
79
137
|
}
|
|
80
138
|
}
|
|
81
|
-
if (typeof envelope !== "object" ||
|
|
139
|
+
if (typeof envelope !== "object" ||
|
|
140
|
+
envelope === null ||
|
|
141
|
+
Array.isArray(envelope) ||
|
|
142
|
+
!("result" in envelope) ||
|
|
143
|
+
("log" in envelope && typeof envelope.log !== "string")) {
|
|
82
144
|
return undefined;
|
|
145
|
+
}
|
|
83
146
|
const log = "log" in envelope && typeof envelope.log === "string" ? envelope.log : "";
|
|
84
147
|
const raw = envelope.result;
|
|
85
|
-
if (typeof raw !== "string")
|
|
86
|
-
return {
|
|
148
|
+
if (typeof raw !== "string") {
|
|
149
|
+
return {
|
|
150
|
+
result: raw,
|
|
151
|
+
log,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
87
154
|
try {
|
|
88
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
result: JSON.parse(raw),
|
|
157
|
+
log,
|
|
158
|
+
};
|
|
89
159
|
}
|
|
90
160
|
catch {
|
|
91
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
result: raw,
|
|
163
|
+
log,
|
|
164
|
+
};
|
|
92
165
|
}
|
|
93
166
|
}
|
|
94
167
|
/**
|
|
95
168
|
* Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
|
|
96
169
|
*/
|
|
97
|
-
export async function executeCode(client, code, { timeoutMs = 120_000, effect = "unknown", retries: requestedRetries = 6, sleep = defaultSleep, onWait, redact = (text) => text } = {}) {
|
|
170
|
+
export async function executeCode(client, code, { timeoutMs = 120_000, effect = "unknown", retries: requestedRetries = 6, sleep = defaultSleep, onWait, redact = (text) => text, } = {}) {
|
|
98
171
|
commandSignal()?.throwIfAborted();
|
|
99
|
-
if (Buffer.byteLength(code, "utf8") > maxSnippetBytes)
|
|
172
|
+
if (Buffer.byteLength(code, "utf8") > maxSnippetBytes) {
|
|
100
173
|
throw new PenpairError("snippet-too-large", `The snippet is ${Buffer.byteLength(code, "utf8")} bytes`);
|
|
101
|
-
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
|
|
102
176
|
throw new Error("Execution timeout must be from 1 to 120000 ms");
|
|
177
|
+
}
|
|
103
178
|
const retries = effect === "read" ? requestedRetries : 0;
|
|
104
179
|
const runId = randomUUID();
|
|
105
180
|
const uncertain = (message) => new UncertainExecutionError(runId, redact(message));
|
|
@@ -107,7 +182,11 @@ export async function executeCode(client, code, { timeoutMs = 120_000, effect =
|
|
|
107
182
|
let outcome;
|
|
108
183
|
try {
|
|
109
184
|
commandSignal()?.throwIfAborted();
|
|
110
|
-
outcome = await client.callTool("execute_code", {
|
|
185
|
+
outcome = await client.callTool("execute_code", {
|
|
186
|
+
code,
|
|
187
|
+
}, {
|
|
188
|
+
timeoutMs,
|
|
189
|
+
});
|
|
111
190
|
commandSignal()?.throwIfAborted();
|
|
112
191
|
}
|
|
113
192
|
catch (error) {
|
|
@@ -116,22 +195,42 @@ export async function executeCode(client, code, { timeoutMs = 120_000, effect =
|
|
|
116
195
|
await sleep(5_000 * (attempt + 1));
|
|
117
196
|
continue;
|
|
118
197
|
}
|
|
119
|
-
if (effect !== "read" &&
|
|
198
|
+
if (effect !== "read" &&
|
|
199
|
+
(!(error instanceof PenpairError) ||
|
|
200
|
+
![
|
|
201
|
+
"file-mismatch",
|
|
202
|
+
"snippet-too-large",
|
|
203
|
+
"auth-rejected",
|
|
204
|
+
].includes(error.code))) {
|
|
120
205
|
throw uncertain(error instanceof Error ? error.message : String(error));
|
|
206
|
+
}
|
|
121
207
|
throw error;
|
|
122
208
|
}
|
|
123
209
|
const unpacked = executionEnvelope(outcome);
|
|
124
210
|
const failed = outcome.isError || (!unpacked && /^Tool execution failed:/i.test(outcome.text.trim()));
|
|
125
|
-
if (!failed && unpacked)
|
|
126
|
-
return {
|
|
211
|
+
if (!failed && unpacked) {
|
|
212
|
+
return {
|
|
213
|
+
...unpacked,
|
|
214
|
+
runId,
|
|
215
|
+
outcome: "ran",
|
|
216
|
+
metrics: outcome.metrics ?? {
|
|
217
|
+
requestSnippetBytes: Buffer.byteLength(code, "utf8"),
|
|
218
|
+
responsePayloadBytes: Buffer.byteLength(outcome.text, "utf8") +
|
|
219
|
+
(outcome.structuredContent === undefined
|
|
220
|
+
? 0
|
|
221
|
+
: Buffer.byteLength(JSON.stringify(outcome.structuredContent), "utf8")),
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
127
225
|
const code_ = classifyFailure(outcome.text);
|
|
128
226
|
if (code_ === "plugin-suspended" && attempt < retries) {
|
|
129
227
|
onWait?.(`Penpot tab is suspended; bring it to the foreground (retry ${attempt + 1}/${retries})`);
|
|
130
228
|
await sleep(5_000 * (attempt + 1));
|
|
131
229
|
continue;
|
|
132
230
|
}
|
|
133
|
-
if (effect !== "read" && code_ !== "auth-rejected")
|
|
231
|
+
if (effect !== "read" && code_ !== "auth-rejected") {
|
|
134
232
|
throw uncertain(outcome.text.slice(0, 500));
|
|
233
|
+
}
|
|
135
234
|
throw new PenpairError(code_, redact(outcome.text).slice(0, 500));
|
|
136
235
|
}
|
|
137
236
|
}
|