@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.
Files changed (101) 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 +1621 -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-instructions.js +91 -22
  66. package/dist/skills.js +129 -39
  67. package/dist/snippet.js +27 -7
  68. package/dist/snippets/read.js +8 -2
  69. package/dist/structure-snapshot.d.ts +12 -0
  70. package/dist/structure-snapshot.js +439 -85
  71. package/dist/token-edit.js +219 -64
  72. package/dist/token-projection.d.ts +56 -0
  73. package/dist/token-projection.js +286 -0
  74. package/dist/tokens-diff.js +125 -32
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +1 -1
  78. package/skills/penpair-design/SKILL.md +3 -0
  79. package/skills/penpair-design/references/penpot-plugin-gotchas.md +2 -2
  80. package/skills/penpair-design/references/project-facts.md +2 -0
  81. package/skills/penpair-handoff/SKILL.md +2 -0
  82. package/skills/penpair-handoff/references/comparison-companion.md +13 -0
  83. package/skills/penpair-handoff/references/handoff-flow.md +2 -0
  84. package/skills/penpair-recipes/SKILL.md +5 -0
  85. package/skills/penpair-setup/SKILL.md +3 -1
  86. package/dist/cli.d.ts +0 -2
  87. package/dist/cli.js +0 -3
  88. package/dist/docker/lifecycle.d.ts +0 -96
  89. package/dist/docker/lifecycle.js +0 -273
  90. package/dist/docker/secrets.d.ts +0 -11
  91. package/dist/docker/secrets.js +0 -38
  92. package/dist/docker/template.d.ts +0 -8
  93. package/dist/docker/template.js +0 -158
  94. package/dist/docker-profile.d.ts +0 -114
  95. package/dist/docker-profile.js +0 -18
  96. package/dist/instance/lifecycle.d.ts +0 -129
  97. package/dist/instance/lifecycle.js +0 -336
  98. package/dist/instance/secrets.d.ts +0 -11
  99. package/dist/instance/secrets.js +0 -38
  100. package/dist/instance/template.d.ts +0 -8
  101. 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): Promise<PenpotToolClient>;
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 client = new Client({ name: "penpair", version: "0" });
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, { signal: commandSignal(), timeout: 120_000 });
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({ name, arguments: args }, undefined, { timeout: options?.timeoutMs ?? 120_000, signal: commandSignal() });
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" && item !== null && "type" in item && item.type === "text" && "text" in item
26
- ? [String(item.text)]
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 { text, metrics: { requestSnippetBytes: typeof args.code === "string" ? Buffer.byteLength(args.code, "utf8") : 0, responsePayloadBytes: Buffer.byteLength(text, "utf8") + (result.structuredContent === undefined ? 0 : Buffer.byteLength(JSON.stringify(result.structuredContent), "utf8")) }, ...(result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {}), isError: result.isError === true };
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, { signal: commandSignal(), timeout: 120_000 })).tools.map((tool) => tool.name);
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
- if (/not connected|no (?:penpot )?plugin|no client|plugin (?:is )?(?:not|disconnected)|connect the plugin/i.test(text))
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
- if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text))
119
+ }
120
+ if (/401|403|unauthori[sz]ed|forbidden|invalid (?:user)?token/i.test(text)) {
65
121
  return "auth-rejected";
66
- if (/ECONNREFUSED|ENOTFOUND|fetch failed|EHOSTUNREACH|socket hang up|ECONNRESET|timed? ?out/i.test(text))
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" || envelope === null || Array.isArray(envelope) || !("result" in envelope) || "log" in envelope && typeof envelope.log !== "string")
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 { result: raw, log };
148
+ if (typeof raw !== "string") {
149
+ return {
150
+ result: raw,
151
+ log,
152
+ };
153
+ }
87
154
  try {
88
- return { result: JSON.parse(raw), log };
155
+ return {
156
+ result: JSON.parse(raw),
157
+ log,
158
+ };
89
159
  }
90
160
  catch {
91
- return { result: raw, log };
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
- if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000)
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", { code }, { timeoutMs });
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" && (!(error instanceof PenpairError) || !["file-mismatch", "snippet-too-large", "auth-rejected"].includes(error.code)))
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 { ...unpacked, runId, outcome: "ran", metrics: outcome.metrics ?? { requestSnippetBytes: Buffer.byteLength(code, "utf8"), responsePayloadBytes: Buffer.byteLength(outcome.text, "utf8") + (outcome.structuredContent === undefined ? 0 : Buffer.byteLength(JSON.stringify(outcome.structuredContent), "utf8")) } };
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
  }