@penpair/core 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +16 -1
- package/dist/adapters.js +62 -19
- package/dist/approvals.d.ts +32 -0
- package/dist/approvals.js +213 -0
- package/dist/atomic-output.d.ts +5 -0
- package/dist/atomic-output.js +33 -0
- package/dist/audit-rules.d.ts +60 -0
- package/dist/audit-rules.js +443 -0
- package/dist/backup-note.d.ts +4 -0
- package/dist/backup-note.js +49 -0
- package/dist/baseline-coverage.d.ts +25 -0
- package/dist/baseline-coverage.js +248 -0
- package/dist/baselines-config.d.ts +25 -0
- package/dist/baselines-config.js +150 -11
- package/dist/baselines.d.ts +37 -8
- package/dist/baselines.js +362 -59
- package/dist/cli-arguments.d.ts +7 -0
- package/dist/cli-arguments.js +47 -0
- package/dist/cli-main.d.ts +13 -1
- package/dist/cli-main.js +2107 -247
- package/dist/client.d.ts +30 -6
- package/dist/client.js +160 -35
- package/dist/command-result.d.ts +159 -0
- package/dist/command-result.js +468 -0
- package/dist/coordination.d.ts +15 -0
- package/dist/coordination.js +184 -0
- package/dist/credentials.d.ts +21 -0
- package/dist/credentials.js +105 -0
- package/dist/deep-check.js +57 -9
- package/dist/design-spec.d.ts +55 -0
- package/dist/design-spec.js +275 -0
- package/dist/endpoint.d.ts +8 -1
- package/dist/endpoint.js +133 -21
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +8 -4
- package/dist/export.d.ts +18 -0
- package/dist/export.js +113 -10
- package/dist/file-archive.d.ts +16 -0
- package/dist/file-archive.js +58 -0
- package/dist/file-guard.d.ts +8 -0
- package/dist/file-guard.js +58 -0
- package/dist/formats.d.ts +4 -0
- package/dist/formats.js +229 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.js +34 -1
- package/dist/inspection.d.ts +106 -0
- package/dist/inspection.js +657 -0
- package/dist/instance/config-schema.d.ts +422 -0
- package/dist/instance/config-schema.js +435 -0
- package/dist/instance/config.d.ts +29 -0
- package/dist/instance/config.js +191 -29
- package/dist/instance/connect.d.ts +10 -1
- package/dist/instance/connect.js +178 -30
- package/dist/instance/connection.d.ts +39 -4
- package/dist/instance/connection.js +198 -14
- 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 +28 -8
- package/dist/journal.d.ts +87 -0
- package/dist/journal.js +423 -0
- package/dist/kit-smoke.d.ts +31 -0
- package/dist/kit-smoke.js +145 -0
- package/dist/kit-source.d.ts +2 -0
- package/dist/kit-source.js +279 -0
- package/dist/kit.d.ts +25 -0
- package/dist/kit.js +158 -0
- package/dist/layout-policy.d.ts +2 -0
- package/dist/layout-policy.js +86 -0
- package/dist/layout.d.ts +21 -0
- package/dist/layout.js +36 -0
- package/dist/live-smoke.d.ts +8 -0
- package/dist/live-smoke.js +23 -0
- package/dist/local-profile.js +2 -1
- package/dist/mcp-server.d.ts +12 -0
- package/dist/mcp-server.js +415 -0
- package/dist/native-api.d.ts +15 -0
- package/dist/native-api.js +78 -0
- package/dist/onboard.d.ts +10 -0
- package/dist/onboard.js +41 -0
- package/dist/operation.d.ts +22 -3
- package/dist/operation.js +341 -31
- package/dist/page-session.d.ts +23 -0
- package/dist/page-session.js +142 -0
- package/dist/png.d.ts +5 -0
- package/dist/png.js +22 -0
- package/dist/portable-tokens.d.ts +22 -0
- package/dist/portable-tokens.js +331 -0
- package/dist/probe.d.ts +6 -2
- package/dist/probe.js +64 -11
- package/dist/project-input.d.ts +6 -0
- package/dist/project-input.js +65 -0
- package/dist/project-snippets.js +29 -12
- package/dist/project-status.d.ts +24 -0
- package/dist/project-status.js +188 -0
- package/dist/registry.d.ts +2 -1
- package/dist/registry.js +224 -53
- package/dist/run-file.d.ts +26 -0
- package/dist/run-file.js +199 -0
- package/dist/shots.d.ts +43 -0
- package/dist/shots.js +172 -0
- package/dist/skill-instructions.d.ts +13 -0
- package/dist/skill-instructions.js +144 -0
- package/dist/skills.d.ts +7 -0
- package/dist/skills.js +163 -46
- package/dist/snippet.d.ts +3 -2
- package/dist/snippet.js +33 -9
- package/dist/snippets/archive.d.ts +9 -0
- package/dist/snippets/archive.js +10 -4
- package/dist/snippets/read.d.ts +1 -1
- package/dist/snippets/read.js +78 -41
- package/dist/structure-snapshot.d.ts +85 -0
- package/dist/structure-snapshot.js +525 -0
- package/dist/token-edit.d.ts +53 -0
- package/dist/token-edit.js +352 -0
- package/dist/token-projection.d.ts +56 -0
- package/dist/token-projection.js +286 -0
- package/dist/tokens-diff.d.ts +19 -6
- package/dist/tokens-diff.js +182 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/penpair-design/SKILL.md +20 -45
- package/skills/penpair-design/references/penpot-plugin-gotchas.md +58 -2
- package/skills/penpair-design/references/project-facts.md +3 -1
- package/skills/penpair-design/references/write-protocol.md +15 -3
- package/skills/penpair-handoff/SKILL.md +16 -0
- package/skills/penpair-handoff/references/comparison-companion.md +13 -0
- package/skills/penpair-handoff/references/handoff-flow.md +13 -0
- package/skills/penpair-recipes/SKILL.md +21 -0
- package/skills/penpair-recipes/references/build-flow.md +11 -0
- package/skills/penpair-setup/SKILL.md +8 -2
- 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
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
import { type Endpoint } from "./endpoint.js";
|
|
2
|
-
import { type PenpairErrorCode } from "./errors.js";
|
|
2
|
+
import { PenpairError, type PenpairErrorCode } from "./errors.js";
|
|
3
|
+
export interface ExecutionMetrics {
|
|
4
|
+
requestSnippetBytes: number;
|
|
5
|
+
responsePayloadBytes: number;
|
|
6
|
+
}
|
|
3
7
|
export interface ToolOutcome {
|
|
8
|
+
metrics?: ExecutionMetrics;
|
|
4
9
|
text: string;
|
|
10
|
+
structuredContent?: unknown;
|
|
5
11
|
isError: boolean;
|
|
6
12
|
}
|
|
7
13
|
/** The only Penpot surface penpair needs: one tool call at a time on an already connected editor. */
|
|
14
|
+
export interface ToolCallOptions {
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
8
17
|
export interface PenpotToolClient {
|
|
9
|
-
callTool(name: string, args: Record<string, unknown
|
|
18
|
+
callTool(name: string, args: Record<string, unknown>, options?: ToolCallOptions): Promise<ToolOutcome>;
|
|
10
19
|
/** The names of the tools the server offers. A hosted Penpot may offer fewer than a local one. */
|
|
11
20
|
listTools?(): Promise<string[]>;
|
|
12
21
|
close(): Promise<void>;
|
|
13
22
|
}
|
|
14
23
|
/** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
|
|
15
24
|
export declare const maxSnippetBytes: number;
|
|
16
|
-
export declare function connect(endpoint: Endpoint
|
|
25
|
+
export declare function connect(endpoint: Endpoint, options?: {
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): Promise<PenpotToolClient>;
|
|
17
29
|
/** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
|
|
18
30
|
export declare function classifyFailure(text: string): PenpairErrorCode;
|
|
19
31
|
export interface ExecuteOptions {
|
|
32
|
+
/** Per-request budget; adapters/guards must forward it. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
/** Only known read-only executions may be retried. Unclassified source is treated as a possible write. */
|
|
35
|
+
effect?: "read" | "write" | "unknown";
|
|
20
36
|
/** How many times to wait and retry a suspended tab. */
|
|
21
37
|
retries?: number;
|
|
22
38
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
@@ -25,13 +41,21 @@ export interface ExecuteOptions {
|
|
|
25
41
|
redact?: (text: string) => string;
|
|
26
42
|
}
|
|
27
43
|
export interface ExecuteResult {
|
|
44
|
+
metrics?: ExecutionMetrics;
|
|
45
|
+
runId: string;
|
|
46
|
+
outcome: "ran";
|
|
28
47
|
/** What the snippet returned, parsed when it was JSON. */
|
|
29
48
|
result: unknown;
|
|
30
49
|
/** What the snippet printed. */
|
|
31
50
|
log: string;
|
|
32
51
|
}
|
|
52
|
+
export declare function executionEnvelope(outcome: ToolOutcome): Pick<ExecuteResult, "result" | "log"> | undefined;
|
|
33
53
|
/**
|
|
34
|
-
*
|
|
35
|
-
* already have run; the caller reads the page before repeating a write.
|
|
54
|
+
* Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
|
|
36
55
|
*/
|
|
37
|
-
export declare function executeCode(client: PenpotToolClient, code: string, { retries, 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>;
|
|
57
|
+
export declare class UncertainExecutionError extends PenpairError {
|
|
58
|
+
readonly runId: string;
|
|
59
|
+
readonly outcome = "uncertain";
|
|
60
|
+
constructor(runId: string, message: string);
|
|
61
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -1,29 +1,81 @@
|
|
|
1
|
+
import { commandSignal, commandSleep } from "./command-result.js";
|
|
1
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import {
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
5
|
import { redactEndpoint } from "./endpoint.js";
|
|
4
6
|
import { PenpairError } from "./errors.js";
|
|
5
7
|
/** Penpot's proxy refuses request bodies of about 110 KB; snippets stay under this. */
|
|
6
8
|
export const maxSnippetBytes = 90 * 1024;
|
|
7
|
-
export async function connect(endpoint) {
|
|
8
|
-
|
|
9
|
+
export async function connect(endpoint, options = {}) {
|
|
10
|
+
commandSignal()?.throwIfAborted();
|
|
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
|
+
});
|
|
9
25
|
const transport = new StreamableHTTPClientTransport(new URL(endpoint.url));
|
|
10
26
|
try {
|
|
11
|
-
await client.connect(transport
|
|
27
|
+
await client.connect(transport, {
|
|
28
|
+
signal,
|
|
29
|
+
timeout: options.timeoutMs ?? 120_000,
|
|
30
|
+
});
|
|
12
31
|
}
|
|
13
32
|
catch (error) {
|
|
33
|
+
await client.close().catch(() => undefined);
|
|
14
34
|
throw transportFailure(error, endpoint);
|
|
15
35
|
}
|
|
16
36
|
return {
|
|
17
|
-
async callTool(name, args) {
|
|
37
|
+
async callTool(name, args, options) {
|
|
18
38
|
try {
|
|
19
|
-
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
|
+
});
|
|
20
51
|
const content = Array.isArray(result.content) ? result.content : [];
|
|
21
52
|
const text = content
|
|
22
|
-
.flatMap((item) => typeof item === "object" &&
|
|
23
|
-
|
|
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
|
+
]
|
|
24
61
|
: [])
|
|
25
62
|
.join("\n");
|
|
26
|
-
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
|
+
};
|
|
27
79
|
}
|
|
28
80
|
catch (error) {
|
|
29
81
|
throw transportFailure(error, endpoint);
|
|
@@ -31,7 +83,10 @@ export async function connect(endpoint) {
|
|
|
31
83
|
},
|
|
32
84
|
async listTools() {
|
|
33
85
|
try {
|
|
34
|
-
return (await client.listTools(
|
|
86
|
+
return (await client.listTools(undefined, {
|
|
87
|
+
signal: commandSignal(),
|
|
88
|
+
timeout: 120_000,
|
|
89
|
+
})).tools.map((tool) => tool.name);
|
|
35
90
|
}
|
|
36
91
|
catch (error) {
|
|
37
92
|
throw transportFailure(error, endpoint);
|
|
@@ -43,58 +98,96 @@ export async function connect(endpoint) {
|
|
|
43
98
|
};
|
|
44
99
|
}
|
|
45
100
|
function transportFailure(error, endpoint) {
|
|
101
|
+
if (commandSignal()?.aborted) {
|
|
102
|
+
return commandSignal().reason;
|
|
103
|
+
}
|
|
46
104
|
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
47
105
|
const message = redactEndpoint(raw, endpoint);
|
|
48
|
-
if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403))
|
|
106
|
+
if (error instanceof StreamableHTTPError && (error.code === 401 || error.code === 403)) {
|
|
49
107
|
return new PenpairError("auth-rejected", message);
|
|
108
|
+
}
|
|
50
109
|
const code = classifyFailure(message);
|
|
51
110
|
return new PenpairError(code === "tool-failed" ? "mcp-unavailable" : code, message);
|
|
52
111
|
}
|
|
53
112
|
/** Maps Penpot MCP failure text to a distinct code so each failure gets its own remedy. */
|
|
54
113
|
export function classifyFailure(text) {
|
|
55
|
-
if (/suspended|no heartbeat/i.test(text))
|
|
114
|
+
if (/suspended|no heartbeat/i.test(text)) {
|
|
56
115
|
return "plugin-suspended";
|
|
57
|
-
|
|
116
|
+
}
|
|
117
|
+
if (/not connected|no (?:penpot )?plugin|no client|plugin (?:is )?(?:not|disconnected)|connect the plugin/i.test(text)) {
|
|
58
118
|
return "plugin-disconnected";
|
|
59
|
-
|
|
119
|
+
}
|
|
120
|
+
if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text)) {
|
|
60
121
|
return "auth-rejected";
|
|
61
|
-
|
|
122
|
+
}
|
|
123
|
+
if (/ECONNREFUSED|ENOTFOUND|fetch failed|EHOSTUNREACH|socket hang up|ECONNRESET|timed? ?out/i.test(text)) {
|
|
62
124
|
return "mcp-unavailable";
|
|
125
|
+
}
|
|
63
126
|
return "tool-failed";
|
|
64
127
|
}
|
|
65
|
-
const defaultSleep =
|
|
66
|
-
function
|
|
67
|
-
let envelope;
|
|
68
|
-
|
|
69
|
-
|
|
128
|
+
const defaultSleep = commandSleep;
|
|
129
|
+
export function executionEnvelope(outcome) {
|
|
130
|
+
let envelope = outcome.structuredContent;
|
|
131
|
+
if (envelope === undefined) {
|
|
132
|
+
try {
|
|
133
|
+
envelope = JSON.parse(outcome.text);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
70
138
|
}
|
|
71
|
-
|
|
139
|
+
if (typeof envelope !== "object" ||
|
|
140
|
+
envelope === null ||
|
|
141
|
+
Array.isArray(envelope) ||
|
|
142
|
+
!("result" in envelope) ||
|
|
143
|
+
("log" in envelope && typeof envelope.log !== "string")) {
|
|
72
144
|
return undefined;
|
|
73
145
|
}
|
|
74
|
-
if (typeof envelope !== "object" || envelope === null || !("result" in envelope))
|
|
75
|
-
return undefined;
|
|
76
146
|
const log = "log" in envelope && typeof envelope.log === "string" ? envelope.log : "";
|
|
77
147
|
const raw = envelope.result;
|
|
78
|
-
if (typeof raw !== "string")
|
|
79
|
-
return {
|
|
148
|
+
if (typeof raw !== "string") {
|
|
149
|
+
return {
|
|
150
|
+
result: raw,
|
|
151
|
+
log,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
80
154
|
try {
|
|
81
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
result: JSON.parse(raw),
|
|
157
|
+
log,
|
|
158
|
+
};
|
|
82
159
|
}
|
|
83
160
|
catch {
|
|
84
|
-
return {
|
|
161
|
+
return {
|
|
162
|
+
result: raw,
|
|
163
|
+
log,
|
|
164
|
+
};
|
|
85
165
|
}
|
|
86
166
|
}
|
|
87
167
|
/**
|
|
88
|
-
*
|
|
89
|
-
* already have run; the caller reads the page before repeating a write.
|
|
168
|
+
* Retries only declared reads. A lost response to any possible write leaves an uncertain outcome requiring independent inspection.
|
|
90
169
|
*/
|
|
91
|
-
export async function executeCode(client, code, { retries = 6, sleep = defaultSleep, onWait, redact = (text) => text } = {}) {
|
|
92
|
-
|
|
170
|
+
export async function executeCode(client, code, { timeoutMs = 120_000, effect = "unknown", retries: requestedRetries = 6, sleep = defaultSleep, onWait, redact = (text) => text, } = {}) {
|
|
171
|
+
commandSignal()?.throwIfAborted();
|
|
172
|
+
if (Buffer.byteLength(code, "utf8") > maxSnippetBytes) {
|
|
93
173
|
throw new PenpairError("snippet-too-large", `The snippet is ${Buffer.byteLength(code, "utf8")} bytes`);
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000) {
|
|
176
|
+
throw new Error("Execution timeout must be from 1 to 120000 ms");
|
|
177
|
+
}
|
|
178
|
+
const retries = effect === "read" ? requestedRetries : 0;
|
|
179
|
+
const runId = randomUUID();
|
|
180
|
+
const uncertain = (message) => new UncertainExecutionError(runId, redact(message));
|
|
94
181
|
for (let attempt = 0;; attempt++) {
|
|
95
182
|
let outcome;
|
|
96
183
|
try {
|
|
97
|
-
|
|
184
|
+
commandSignal()?.throwIfAborted();
|
|
185
|
+
outcome = await client.callTool("execute_code", {
|
|
186
|
+
code,
|
|
187
|
+
}, {
|
|
188
|
+
timeoutMs,
|
|
189
|
+
});
|
|
190
|
+
commandSignal()?.throwIfAborted();
|
|
98
191
|
}
|
|
99
192
|
catch (error) {
|
|
100
193
|
if (error instanceof PenpairError && error.code === "plugin-suspended" && attempt < retries) {
|
|
@@ -102,18 +195,50 @@ export async function executeCode(client, code, { retries = 6, sleep = defaultSl
|
|
|
102
195
|
await sleep(5_000 * (attempt + 1));
|
|
103
196
|
continue;
|
|
104
197
|
}
|
|
198
|
+
if (effect !== "read" &&
|
|
199
|
+
(!(error instanceof PenpairError) ||
|
|
200
|
+
![
|
|
201
|
+
"file-mismatch",
|
|
202
|
+
"snippet-too-large",
|
|
203
|
+
"auth-rejected",
|
|
204
|
+
].includes(error.code))) {
|
|
205
|
+
throw uncertain(error instanceof Error ? error.message : String(error));
|
|
206
|
+
}
|
|
105
207
|
throw error;
|
|
106
208
|
}
|
|
107
|
-
const unpacked =
|
|
209
|
+
const unpacked = executionEnvelope(outcome);
|
|
108
210
|
const failed = outcome.isError || (!unpacked && /^Tool execution failed:/i.test(outcome.text.trim()));
|
|
109
|
-
if (!failed && unpacked)
|
|
110
|
-
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
|
+
}
|
|
111
225
|
const code_ = classifyFailure(outcome.text);
|
|
112
226
|
if (code_ === "plugin-suspended" && attempt < retries) {
|
|
113
227
|
onWait?.(`Penpot tab is suspended; bring it to the foreground (retry ${attempt + 1}/${retries})`);
|
|
114
228
|
await sleep(5_000 * (attempt + 1));
|
|
115
229
|
continue;
|
|
116
230
|
}
|
|
231
|
+
if (effect !== "read" && code_ !== "auth-rejected") {
|
|
232
|
+
throw uncertain(outcome.text.slice(0, 500));
|
|
233
|
+
}
|
|
117
234
|
throw new PenpairError(code_, redact(outcome.text).slice(0, 500));
|
|
118
235
|
}
|
|
119
236
|
}
|
|
237
|
+
export class UncertainExecutionError extends PenpairError {
|
|
238
|
+
runId;
|
|
239
|
+
outcome = "uncertain";
|
|
240
|
+
constructor(runId, message) {
|
|
241
|
+
super("write-uncertain", `Run ${runId} has an uncertain outcome: ${message}`);
|
|
242
|
+
this.runId = runId;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface CommandFailure {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
remedy: string;
|
|
5
|
+
runId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CommandResult {
|
|
8
|
+
schemaVersion: 1;
|
|
9
|
+
redacted?: boolean;
|
|
10
|
+
command: string;
|
|
11
|
+
outcome: "success" | "drift" | "failed" | "incomplete" | "uncertain" | "cancelled" | "deadline";
|
|
12
|
+
phase: string;
|
|
13
|
+
exitCode: number;
|
|
14
|
+
target: {
|
|
15
|
+
fileId?: string;
|
|
16
|
+
pageId?: string;
|
|
17
|
+
} | null;
|
|
18
|
+
runId: string | null;
|
|
19
|
+
data: unknown;
|
|
20
|
+
messages: string[];
|
|
21
|
+
warnings: string[];
|
|
22
|
+
errors: CommandFailure[];
|
|
23
|
+
completeness: {
|
|
24
|
+
complete: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
};
|
|
27
|
+
timings: {
|
|
28
|
+
startedAt: string;
|
|
29
|
+
durationMs: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface CommandOutput {
|
|
33
|
+
log(...values: unknown[]): void;
|
|
34
|
+
error(...values: unknown[]): void;
|
|
35
|
+
result(value: unknown): void;
|
|
36
|
+
}
|
|
37
|
+
export declare const commandOutput: CommandOutput;
|
|
38
|
+
export declare function commandRedactor(redact: (value: string) => string): void;
|
|
39
|
+
export declare function commandFailure(error: unknown): void;
|
|
40
|
+
export declare function withCommandSignal<T>(signal: AbortSignal, work: () => Promise<T>): Promise<T>;
|
|
41
|
+
/** Bounded explicit restoration work may finish after the main command is cancelled. */
|
|
42
|
+
export declare function withCleanupDeadline<T>(work: () => Promise<T>, milliseconds?: number): Promise<T>;
|
|
43
|
+
export declare function commandSignal(): AbortSignal | undefined;
|
|
44
|
+
export declare function commandSleep(milliseconds: number): Promise<void>;
|
|
45
|
+
export declare class CommandInterruptedError extends Error {
|
|
46
|
+
readonly code: "cancelled" | "deadline";
|
|
47
|
+
constructor(code: "cancelled" | "deadline");
|
|
48
|
+
}
|
|
49
|
+
/** Shared service boundary for CLI and future tool surfaces; no stdout or global console interception. */
|
|
50
|
+
export declare function collectCommand(command: string, work: () => Promise<number>, options?: {
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<CommandResult>;
|
|
53
|
+
/** Outer transport contract. Command-specific data remains owned by the command service. */
|
|
54
|
+
export declare const commandResultSchema: {
|
|
55
|
+
readonly type: "object";
|
|
56
|
+
readonly additionalProperties: false;
|
|
57
|
+
readonly required: readonly ["schemaVersion", "command", "outcome", "phase", "exitCode", "target", "runId", "data", "messages", "warnings", "errors", "completeness", "timings"];
|
|
58
|
+
readonly properties: {
|
|
59
|
+
readonly schemaVersion: {
|
|
60
|
+
readonly const: 1;
|
|
61
|
+
};
|
|
62
|
+
readonly redacted: {
|
|
63
|
+
readonly type: "boolean";
|
|
64
|
+
};
|
|
65
|
+
readonly command: {
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
};
|
|
68
|
+
readonly outcome: {
|
|
69
|
+
readonly enum: readonly ["success", "drift", "failed", "incomplete", "uncertain", "cancelled", "deadline"];
|
|
70
|
+
};
|
|
71
|
+
readonly phase: {
|
|
72
|
+
readonly type: "string";
|
|
73
|
+
};
|
|
74
|
+
readonly exitCode: {
|
|
75
|
+
readonly enum: readonly [0, 1, 2, 3, 4, 124, 130];
|
|
76
|
+
};
|
|
77
|
+
readonly target: {
|
|
78
|
+
readonly anyOf: readonly [{
|
|
79
|
+
readonly type: "null";
|
|
80
|
+
}, {
|
|
81
|
+
readonly type: "object";
|
|
82
|
+
readonly properties: {
|
|
83
|
+
readonly fileId: {
|
|
84
|
+
readonly type: "string";
|
|
85
|
+
};
|
|
86
|
+
readonly pageId: {
|
|
87
|
+
readonly type: "string";
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
readonly additionalProperties: false;
|
|
91
|
+
}];
|
|
92
|
+
};
|
|
93
|
+
readonly runId: {
|
|
94
|
+
readonly type: readonly ["string", "null"];
|
|
95
|
+
};
|
|
96
|
+
readonly data: {};
|
|
97
|
+
readonly messages: {
|
|
98
|
+
readonly type: "array";
|
|
99
|
+
readonly items: {
|
|
100
|
+
readonly type: "string";
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
readonly warnings: {
|
|
104
|
+
readonly type: "array";
|
|
105
|
+
readonly items: {
|
|
106
|
+
readonly type: "string";
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
readonly errors: {
|
|
110
|
+
readonly type: "array";
|
|
111
|
+
readonly items: {
|
|
112
|
+
readonly type: "object";
|
|
113
|
+
readonly required: readonly ["code", "message", "remedy"];
|
|
114
|
+
readonly properties: {
|
|
115
|
+
readonly code: {
|
|
116
|
+
readonly type: "string";
|
|
117
|
+
};
|
|
118
|
+
readonly message: {
|
|
119
|
+
readonly type: "string";
|
|
120
|
+
};
|
|
121
|
+
readonly remedy: {
|
|
122
|
+
readonly type: "string";
|
|
123
|
+
};
|
|
124
|
+
readonly runId: {
|
|
125
|
+
readonly type: "string";
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
readonly additionalProperties: false;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
readonly completeness: {
|
|
132
|
+
readonly type: "object";
|
|
133
|
+
readonly required: readonly ["complete"];
|
|
134
|
+
readonly properties: {
|
|
135
|
+
readonly complete: {
|
|
136
|
+
readonly type: "boolean";
|
|
137
|
+
};
|
|
138
|
+
readonly reason: {
|
|
139
|
+
readonly type: "string";
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
readonly additionalProperties: false;
|
|
143
|
+
};
|
|
144
|
+
readonly timings: {
|
|
145
|
+
readonly type: "object";
|
|
146
|
+
readonly required: readonly ["startedAt", "durationMs"];
|
|
147
|
+
readonly properties: {
|
|
148
|
+
readonly startedAt: {
|
|
149
|
+
readonly type: "string";
|
|
150
|
+
};
|
|
151
|
+
readonly durationMs: {
|
|
152
|
+
readonly type: "number";
|
|
153
|
+
readonly minimum: 0;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
readonly additionalProperties: false;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|