@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/mcp-server.js
CHANGED
|
@@ -2,101 +2,414 @@ import { realpath } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import { runCommand } from "./cli-main.js";
|
|
7
7
|
import { loadConfig } from "./instance/config.js";
|
|
8
8
|
import { resolveProjectOutput } from "./project-input.js";
|
|
9
9
|
import { collectCommand } from "./command-result.js";
|
|
10
10
|
import { version } from "./version.js";
|
|
11
|
-
const string = {
|
|
12
|
-
|
|
11
|
+
const string = {
|
|
12
|
+
type: "string",
|
|
13
|
+
minLength: 1,
|
|
14
|
+
maxLength: 1000,
|
|
15
|
+
};
|
|
16
|
+
const integer = (max) => ({
|
|
17
|
+
type: "integer",
|
|
18
|
+
minimum: 0,
|
|
19
|
+
maximum: max,
|
|
20
|
+
});
|
|
13
21
|
const definitions = [
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
{
|
|
23
|
+
name: "pages",
|
|
24
|
+
description: "List pages in the pinned project file",
|
|
25
|
+
properties: {},
|
|
26
|
+
required: [],
|
|
27
|
+
command: () => [
|
|
28
|
+
"pages",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "tree",
|
|
33
|
+
description: "Bounded tree preview; omitted nodes are explicit",
|
|
34
|
+
properties: {
|
|
35
|
+
page: string,
|
|
36
|
+
depth: integer(100),
|
|
37
|
+
},
|
|
38
|
+
required: [
|
|
39
|
+
"page",
|
|
40
|
+
],
|
|
41
|
+
command: (a) => [
|
|
42
|
+
"tree",
|
|
43
|
+
"--page",
|
|
44
|
+
String(a.page),
|
|
45
|
+
"--depth",
|
|
46
|
+
String(a.depth ?? 5),
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "audit",
|
|
51
|
+
description: "Inspect layout/token findings with documented heuristic limits",
|
|
52
|
+
properties: {
|
|
53
|
+
page: string,
|
|
54
|
+
},
|
|
55
|
+
required: [
|
|
56
|
+
"page",
|
|
57
|
+
],
|
|
58
|
+
command: (a) => [
|
|
59
|
+
"audit",
|
|
60
|
+
"--page",
|
|
61
|
+
String(a.page),
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "inspect",
|
|
66
|
+
description: "One bounded structured inspection chunk; use returned cursor for continuation",
|
|
67
|
+
properties: {
|
|
68
|
+
page: string,
|
|
69
|
+
id: string,
|
|
70
|
+
cursor: string,
|
|
71
|
+
},
|
|
72
|
+
required: [
|
|
73
|
+
"page",
|
|
74
|
+
"id",
|
|
75
|
+
],
|
|
76
|
+
command: (a) => [
|
|
77
|
+
"inspect",
|
|
78
|
+
String(a.id),
|
|
79
|
+
"--page",
|
|
80
|
+
String(a.page),
|
|
81
|
+
...(a.cursor
|
|
82
|
+
? [
|
|
83
|
+
"--cursor",
|
|
84
|
+
String(a.cursor),
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "tokens_dump",
|
|
91
|
+
description: "Bounded token capture; completeness is explicit",
|
|
92
|
+
properties: {},
|
|
93
|
+
required: [],
|
|
94
|
+
command: () => [
|
|
95
|
+
"tokens",
|
|
96
|
+
"dump",
|
|
97
|
+
"--max-total-bytes",
|
|
98
|
+
"131072",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "run_status",
|
|
103
|
+
description: "Inspect an explicit local recovery journal",
|
|
104
|
+
properties: {
|
|
105
|
+
id: string,
|
|
106
|
+
},
|
|
107
|
+
required: [
|
|
108
|
+
"id",
|
|
109
|
+
],
|
|
110
|
+
command: (a) => [
|
|
111
|
+
"runs",
|
|
112
|
+
String(a.id),
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "baselines_status",
|
|
117
|
+
description: "Offline coverage/evidence status; no capture or approval",
|
|
118
|
+
properties: {},
|
|
119
|
+
required: [],
|
|
120
|
+
command: () => [
|
|
121
|
+
"baselines",
|
|
122
|
+
"coverage",
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "project_status",
|
|
127
|
+
description: "Project health with explicit unknowns; no adapter imports",
|
|
128
|
+
properties: {},
|
|
129
|
+
required: [],
|
|
130
|
+
command: () => [
|
|
131
|
+
"status",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "doctor",
|
|
136
|
+
description: "Project connection/file identity diagnostics",
|
|
137
|
+
properties: {},
|
|
138
|
+
required: [],
|
|
139
|
+
command: () => [
|
|
140
|
+
"doctor",
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "design_spec",
|
|
145
|
+
description: "Derived handoff for an exact frame ID; unknown semantics remain explicit",
|
|
146
|
+
properties: {
|
|
147
|
+
page: string,
|
|
148
|
+
id: string,
|
|
149
|
+
},
|
|
150
|
+
required: [
|
|
151
|
+
"page",
|
|
152
|
+
"id",
|
|
153
|
+
],
|
|
154
|
+
command: (a) => [
|
|
155
|
+
"spec",
|
|
156
|
+
"--page",
|
|
157
|
+
String(a.page),
|
|
158
|
+
"--frame",
|
|
159
|
+
String(a.id),
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "export",
|
|
164
|
+
description: "Capture stable PNG to a relative path inside the project; creates a local file",
|
|
165
|
+
properties: {
|
|
166
|
+
page: string,
|
|
167
|
+
id: string,
|
|
168
|
+
out: string,
|
|
169
|
+
},
|
|
170
|
+
required: [
|
|
171
|
+
"page",
|
|
172
|
+
"id",
|
|
173
|
+
"out",
|
|
174
|
+
],
|
|
175
|
+
command: (a) => [
|
|
176
|
+
"export",
|
|
177
|
+
String(a.id),
|
|
178
|
+
String(a.out),
|
|
179
|
+
"--page",
|
|
180
|
+
String(a.page),
|
|
181
|
+
],
|
|
182
|
+
localWrite: true,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "run_registered",
|
|
186
|
+
description: "Execute a trusted registered operation: inspect by default, explicit apply and recovery for writes. Not a sandbox.",
|
|
187
|
+
properties: {
|
|
188
|
+
name: string,
|
|
189
|
+
params: {
|
|
190
|
+
type: "object",
|
|
191
|
+
additionalProperties: {
|
|
192
|
+
type: "string",
|
|
193
|
+
maxLength: 4000,
|
|
194
|
+
},
|
|
195
|
+
maxProperties: 100,
|
|
196
|
+
},
|
|
197
|
+
apply: {
|
|
198
|
+
type: "boolean",
|
|
199
|
+
default: false,
|
|
200
|
+
},
|
|
201
|
+
resume: string,
|
|
202
|
+
checkpoint: {
|
|
203
|
+
type: "boolean",
|
|
204
|
+
default: false,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
required: [
|
|
208
|
+
"name",
|
|
209
|
+
],
|
|
210
|
+
write: true,
|
|
211
|
+
command: (a) => [
|
|
212
|
+
"run",
|
|
213
|
+
String(a.name),
|
|
214
|
+
...Object.entries(a.params ?? {}).flatMap(([k, v]) => [
|
|
215
|
+
"--param",
|
|
216
|
+
`${k}=${v}`,
|
|
217
|
+
]),
|
|
218
|
+
...(a.apply
|
|
219
|
+
? [
|
|
220
|
+
"--apply",
|
|
221
|
+
]
|
|
222
|
+
: []),
|
|
223
|
+
...(a.resume
|
|
224
|
+
? [
|
|
225
|
+
"--resume",
|
|
226
|
+
String(a.resume),
|
|
227
|
+
]
|
|
228
|
+
: []),
|
|
229
|
+
...(a.checkpoint
|
|
230
|
+
? [
|
|
231
|
+
"--checkpoint",
|
|
232
|
+
]
|
|
233
|
+
: []),
|
|
234
|
+
],
|
|
235
|
+
},
|
|
26
236
|
];
|
|
27
237
|
function validate(def, raw) {
|
|
28
|
-
if (raw === undefined)
|
|
238
|
+
if (raw === undefined) {
|
|
29
239
|
raw = {};
|
|
30
|
-
|
|
240
|
+
}
|
|
241
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
31
242
|
throw new Error("Tool arguments must be an object");
|
|
243
|
+
}
|
|
32
244
|
const a = raw;
|
|
33
|
-
for (const key of def.required)
|
|
34
|
-
if (!(key in a))
|
|
245
|
+
for (const key of def.required) {
|
|
246
|
+
if (!(key in a)) {
|
|
35
247
|
throw new Error(`Missing ${key}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
36
250
|
for (const [key, value] of Object.entries(a)) {
|
|
37
251
|
const schema = def.properties[key];
|
|
38
|
-
if (!schema)
|
|
252
|
+
if (!schema) {
|
|
39
253
|
throw new Error(`Unexpected tool argument ${key}`);
|
|
40
|
-
|
|
254
|
+
}
|
|
255
|
+
if (schema.type === "string" &&
|
|
256
|
+
(typeof value !== "string" ||
|
|
257
|
+
!value ||
|
|
258
|
+
value.startsWith("-") ||
|
|
259
|
+
value.length > (schema.maxLength ?? 1000) ||
|
|
260
|
+
/[\x00-\x1f]/.test(value))) {
|
|
41
261
|
throw new Error(`Invalid ${key}`);
|
|
42
|
-
|
|
262
|
+
}
|
|
263
|
+
if (schema.type === "boolean" && typeof value !== "boolean") {
|
|
43
264
|
throw new Error(`Invalid ${key}`);
|
|
44
|
-
|
|
265
|
+
}
|
|
266
|
+
if (schema.type === "integer" &&
|
|
267
|
+
(!Number.isInteger(value) || Number(value) < 0 || Number(value) > schema.maximum)) {
|
|
45
268
|
throw new Error(`Invalid ${key}`);
|
|
46
|
-
|
|
269
|
+
}
|
|
270
|
+
if (key === "params" &&
|
|
271
|
+
(!value ||
|
|
272
|
+
typeof value !== "object" ||
|
|
273
|
+
Array.isArray(value) ||
|
|
274
|
+
Object.keys(value).length > 100 ||
|
|
275
|
+
Object.entries(value).some(([k, v]) => !/^[a-zA-Z][\w-]{0,100}$/.test(k) || typeof v !== "string" || v.length > 4000))) {
|
|
47
276
|
throw new Error("Invalid registered parameters");
|
|
277
|
+
}
|
|
48
278
|
}
|
|
49
|
-
if ((a.resume || a.checkpoint) && !a.apply)
|
|
279
|
+
if ((a.resume || a.checkpoint) && !a.apply) {
|
|
50
280
|
throw new Error("Explicit resume requires apply");
|
|
281
|
+
}
|
|
51
282
|
return a;
|
|
52
283
|
}
|
|
53
284
|
/** Long-lived server with an immutable explicit root and no process.chdir or raw-code tool. */
|
|
54
285
|
export async function createMcpServer(options) {
|
|
55
|
-
if (!path.isAbsolute(options.root))
|
|
286
|
+
if (!path.isAbsolute(options.root)) {
|
|
56
287
|
throw new Error("MCP project root must be absolute");
|
|
57
|
-
|
|
58
|
-
|
|
288
|
+
}
|
|
289
|
+
const root = await realpath(options.root);
|
|
290
|
+
const config = await loadConfig(root);
|
|
291
|
+
if (!config.file?.id) {
|
|
59
292
|
throw new Error("MCP server requires a configured owner-confirmed file pin");
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
293
|
+
}
|
|
294
|
+
const pinned = {
|
|
295
|
+
instance: config.instance,
|
|
296
|
+
fileId: config.file.id,
|
|
297
|
+
profile: JSON.stringify("profile" in config ? config.profile : null),
|
|
298
|
+
};
|
|
299
|
+
const timeout = options.timeoutMs ?? 60000;
|
|
300
|
+
const limit = options.maxOutputBytes ?? 256 * 1024;
|
|
301
|
+
if (!Number.isInteger(timeout) ||
|
|
302
|
+
timeout < 1 ||
|
|
303
|
+
timeout > 300000 ||
|
|
304
|
+
!Number.isInteger(limit) ||
|
|
305
|
+
limit < 1024 ||
|
|
306
|
+
limit > 1024 * 1024) {
|
|
63
307
|
throw new Error("Invalid MCP budgets");
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
308
|
+
}
|
|
309
|
+
const available = definitions.filter((d) => !d.write || options.allowWrites);
|
|
310
|
+
const server = new Server({
|
|
311
|
+
name: "penpair",
|
|
312
|
+
version,
|
|
313
|
+
}, {
|
|
314
|
+
capabilities: {
|
|
315
|
+
tools: {},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const tools = available.map((d) => ({
|
|
319
|
+
name: d.name,
|
|
320
|
+
description: d.description,
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: d.properties,
|
|
324
|
+
required: d.required,
|
|
325
|
+
additionalProperties: false,
|
|
326
|
+
},
|
|
327
|
+
annotations: {
|
|
328
|
+
readOnlyHint: !d.write && !d.localWrite,
|
|
329
|
+
destructiveHint: Boolean(d.write || d.localWrite),
|
|
330
|
+
idempotentHint: !d.write && !d.localWrite,
|
|
331
|
+
openWorldHint: true,
|
|
332
|
+
},
|
|
333
|
+
}));
|
|
334
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
335
|
+
tools,
|
|
336
|
+
}));
|
|
68
337
|
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
69
338
|
const controller = new AbortController();
|
|
70
339
|
const abort = () => controller.abort(extra.signal.reason);
|
|
71
|
-
extra.signal.addEventListener("abort", abort, {
|
|
72
|
-
|
|
340
|
+
extra.signal.addEventListener("abort", abort, {
|
|
341
|
+
once: true,
|
|
342
|
+
});
|
|
343
|
+
if (extra.signal.aborted) {
|
|
73
344
|
abort();
|
|
74
|
-
|
|
345
|
+
}
|
|
346
|
+
const timer = setTimeout(() => controller.abort(Object.assign(new Error("MCP command deadline exceeded"), {
|
|
347
|
+
code: "deadline",
|
|
348
|
+
})), timeout);
|
|
75
349
|
let result;
|
|
76
350
|
try {
|
|
77
|
-
const def = available.find(d => d.name === request.params.name);
|
|
78
|
-
if (!def)
|
|
351
|
+
const def = available.find((d) => d.name === request.params.name);
|
|
352
|
+
if (!def) {
|
|
79
353
|
throw new Error("Unknown or disabled tool");
|
|
354
|
+
}
|
|
80
355
|
const args = validate(def, request.params.arguments);
|
|
81
356
|
const current = await loadConfig(root);
|
|
82
|
-
if (current.instance !== pinned.instance ||
|
|
357
|
+
if (current.instance !== pinned.instance ||
|
|
358
|
+
current.file?.id !== pinned.fileId ||
|
|
359
|
+
JSON.stringify("profile" in current ? current.profile : null) !== pinned.profile) {
|
|
83
360
|
throw new Error("Pinned project target changed; restart and explicitly review server configuration");
|
|
84
|
-
|
|
361
|
+
}
|
|
362
|
+
if (def.localWrite) {
|
|
85
363
|
args.out = await resolveProjectOutput(root, String(args.out));
|
|
86
|
-
|
|
364
|
+
}
|
|
365
|
+
result = await runCommand(def.command(args), {
|
|
366
|
+
...options.cli,
|
|
367
|
+
root,
|
|
368
|
+
projectEndpointOnly: true,
|
|
369
|
+
pinnedConfig: config,
|
|
370
|
+
strictFileGuard: true,
|
|
371
|
+
signal: controller.signal,
|
|
372
|
+
});
|
|
87
373
|
}
|
|
88
374
|
catch (error) {
|
|
89
|
-
result = await collectCommand(request.params.name, async () => {
|
|
375
|
+
result = await collectCommand(request.params.name, async () => {
|
|
376
|
+
throw error;
|
|
377
|
+
});
|
|
90
378
|
}
|
|
91
379
|
finally {
|
|
92
380
|
clearTimeout(timer);
|
|
93
381
|
extra.signal.removeEventListener("abort", abort);
|
|
94
382
|
}
|
|
95
383
|
if (Buffer.byteLength(JSON.stringify(result), "utf8") > limit) {
|
|
96
|
-
result = {
|
|
384
|
+
result = {
|
|
385
|
+
...result,
|
|
386
|
+
outcome: result.outcome === "uncertain" ? "uncertain" : "incomplete",
|
|
387
|
+
exitCode: result.outcome === "uncertain" ? 4 : 3,
|
|
388
|
+
data: null,
|
|
389
|
+
messages: [],
|
|
390
|
+
warnings: [],
|
|
391
|
+
errors: [],
|
|
392
|
+
completeness: {
|
|
393
|
+
complete: false,
|
|
394
|
+
reason: "MCP response byte budget exceeded; use a narrower inspection or the CLI artifact path",
|
|
395
|
+
},
|
|
396
|
+
};
|
|
97
397
|
}
|
|
98
|
-
return {
|
|
398
|
+
return {
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: "text",
|
|
402
|
+
text: JSON.stringify(result),
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
structuredContent: result,
|
|
406
|
+
isError: result.exitCode !== 0,
|
|
407
|
+
};
|
|
99
408
|
});
|
|
100
409
|
return server;
|
|
101
410
|
}
|
|
102
|
-
export async function serveMcp(options) {
|
|
411
|
+
export async function serveMcp(options) {
|
|
412
|
+
const server = await createMcpServer(options);
|
|
413
|
+
await server.connect(new StdioServerTransport());
|
|
414
|
+
return server;
|
|
415
|
+
}
|
package/dist/native-api.js
CHANGED
|
@@ -6,28 +6,54 @@ if (typeof penpot.currentFile.validate !== "function") return { ok: true, suppor
|
|
|
6
6
|
const issues = penpot.currentFile.validate();
|
|
7
7
|
if (!Array.isArray(issues)) return { ok: false, message: "file.validate returned no issue list" };
|
|
8
8
|
return { ok: true, supported: true, valid: issues.length === 0, issues };`, {}, "read"));
|
|
9
|
-
if (typeof result.supported !== "boolean")
|
|
9
|
+
if (typeof result.supported !== "boolean") {
|
|
10
10
|
throw new Error("Integrity check returned no capability result");
|
|
11
|
-
|
|
11
|
+
}
|
|
12
|
+
if (result.supported &&
|
|
13
|
+
(!Array.isArray(result.issues) ||
|
|
14
|
+
typeof result.valid !== "boolean" ||
|
|
15
|
+
result.valid !== (result.issues.length === 0))) {
|
|
12
16
|
throw new Error("Integrity check returned incomplete or contradictory observations");
|
|
13
|
-
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
supported: result.supported,
|
|
20
|
+
...(result.supported
|
|
21
|
+
? {
|
|
22
|
+
valid: result.valid === true,
|
|
23
|
+
issues: result.issues,
|
|
24
|
+
}
|
|
25
|
+
: {}),
|
|
26
|
+
};
|
|
14
27
|
}
|
|
15
28
|
/** Version history is a recovery aid, never a substitute for a restored independent backup. */
|
|
16
29
|
export async function createCheckpoint(client, checkpoint, options = {}) {
|
|
17
|
-
if (!checkpoint.label.trim() ||
|
|
30
|
+
if (!checkpoint.label.trim() ||
|
|
31
|
+
checkpoint.label.length > 120 ||
|
|
32
|
+
!/^[a-zA-Z0-9-]{1,100}$/.test(checkpoint.runId)) {
|
|
18
33
|
throw new Error("A checkpoint requires a short label and a stable alphanumeric/hyphen run ID");
|
|
34
|
+
}
|
|
19
35
|
const capability = await runSnippet(client, snippet("checkpoint-capability", `return { ok: true, fileId: penpot.currentFile.id, supported: typeof penpot.currentFile.saveVersion === "function" && typeof penpot.currentFile.findVersions === "function" };`, {}, "read"));
|
|
20
|
-
if (capability.ok !== true ||
|
|
36
|
+
if (capability.ok !== true ||
|
|
37
|
+
typeof capability.supported !== "boolean" ||
|
|
38
|
+
typeof capability.fileId !== "string") {
|
|
21
39
|
throw new Error("Checkpoint capability returned no target/presence observations");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
}
|
|
41
|
+
if (capability.supported !== true) {
|
|
42
|
+
return {
|
|
43
|
+
supported: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const params = {
|
|
47
|
+
label: `penpair/${checkpoint.runId}/${checkpoint.label}`,
|
|
48
|
+
fileId: capability.fileId,
|
|
49
|
+
};
|
|
25
50
|
const guard = 'if (penpot.currentFile.id !== CFG.fileId) return { ok: false, message: "checkpoint file changed" };';
|
|
26
51
|
const listing = `${guard}
|
|
27
52
|
const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
|
|
28
53
|
return { ok: true, fileId: penpot.currentFile.id, versions: versions.map((version) => ({ label: version.label, createdAt: String(version.createdAt) })) };`;
|
|
29
54
|
const report = await runOperation(client, {
|
|
30
|
-
name: "checkpoint",
|
|
55
|
+
name: "checkpoint",
|
|
56
|
+
page: () => undefined,
|
|
31
57
|
inspect: () => snippet("checkpoint-inspect", listing, params, "read"),
|
|
32
58
|
apply: () => snippet("checkpoint-apply", `${guard}
|
|
33
59
|
const versions = (await penpot.currentFile.findVersions()).filter((version) => version.label === CFG.label);
|
|
@@ -36,7 +62,17 @@ if (versions.length === 1) return { ok: true, existed: true };
|
|
|
36
62
|
await penpot.currentFile.saveVersion(CFG.label);
|
|
37
63
|
return { ok: true, created: true };`, params, "write"),
|
|
38
64
|
readback: () => snippet("checkpoint-readback", listing, params, "read"),
|
|
39
|
-
verify: ({ readback }) => readback.fileId === params.fileId &&
|
|
65
|
+
verify: ({ readback }) => readback.fileId === params.fileId &&
|
|
66
|
+
Array.isArray(readback.versions) &&
|
|
67
|
+
readback.versions.length === 1 &&
|
|
68
|
+
readback.versions[0]?.label === params.label
|
|
69
|
+
? []
|
|
70
|
+
: [
|
|
71
|
+
"Checkpoint was not independently found once in the intended file",
|
|
72
|
+
],
|
|
40
73
|
}, undefined, options);
|
|
41
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
supported: true,
|
|
76
|
+
report,
|
|
77
|
+
};
|
|
42
78
|
}
|
package/dist/onboard.js
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import { isExternal } from "./instance/config.js";
|
|
2
2
|
export function onboardingChecklist(config) {
|
|
3
|
-
const origin = isExternal(config)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const origin = isExternal(config)
|
|
4
|
+
? config.profile.origin
|
|
5
|
+
: `http://localhost:${config.ports.frontend}`;
|
|
6
|
+
const fileUrl = config.file
|
|
7
|
+
? `${origin}/#/workspace?file-id=${encodeURIComponent(config.file.id)}`
|
|
8
|
+
: null;
|
|
9
|
+
return {
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
project: config.instance,
|
|
12
|
+
origin,
|
|
13
|
+
file: config.file ?? null,
|
|
14
|
+
fileUrl,
|
|
15
|
+
steps: [
|
|
16
|
+
"Clone the project and install its locked dependencies with the repository's package manager.",
|
|
7
17
|
`Ask the owner to invite your own Penpot account to the shared file at ${origin}.`,
|
|
8
18
|
"Create your own personal MCP key. Keep it out of chat, commits and command arguments.",
|
|
9
19
|
"Run the project's penpair connect command; enter your own key in its hidden prompt.",
|
|
10
|
-
config.file
|
|
20
|
+
config.file
|
|
21
|
+
? `Open ${config.file.name || config.file.id} (${config.file.id}). Use the owner's full editor link if the direct file link needs team context.`
|
|
22
|
+
: "Ask the owner to pin the shared file with penpair connect --pin-file and commit the pin.",
|
|
11
23
|
"Open the Penpot MCP plugin and Connect. Keep the editor tab open and awake.",
|
|
12
24
|
"Run penpair doctor --json, then a read-only penpair pages before proposing writes.",
|
|
13
|
-
"Coordinate a write window with colleagues. Inspect the plan, apply once, verify independently and review the export."
|
|
14
|
-
|
|
25
|
+
"Coordinate a write window with colleagues. Inspect the plan, apply once, verify independently and review the export.",
|
|
26
|
+
],
|
|
27
|
+
warnings: [
|
|
28
|
+
...(!isExternal(config)
|
|
29
|
+
? [
|
|
30
|
+
"localhost is this computer only. A colleague needs access to a shared reachable deployment; do not share generated instance secrets.",
|
|
31
|
+
]
|
|
32
|
+
: []),
|
|
33
|
+
...(!config.file
|
|
34
|
+
? [
|
|
35
|
+
"No shared file ID is pinned yet.",
|
|
36
|
+
]
|
|
37
|
+
: []),
|
|
38
|
+
"Writer markers are advisory and do not block human editing or guarantee exclusion across machines.",
|
|
39
|
+
],
|
|
40
|
+
};
|
|
15
41
|
}
|
package/dist/operation.d.ts
CHANGED
|
@@ -53,8 +53,8 @@ export interface OperationReport {
|
|
|
53
53
|
problems: string[];
|
|
54
54
|
verified: boolean;
|
|
55
55
|
}
|
|
56
|
-
/** Runs
|
|
57
|
-
export declare function runOnPage(client: PenpotToolClient, s: Snippet, page: string | undefined, { settleMs, sleep, onStep, ...execute }?: OperationOptions): Promise<SnippetResult>;
|
|
56
|
+
/** Runs once on the expected page; page transitions require independently observed IDs. */
|
|
57
|
+
export declare function runOnPage(client: PenpotToolClient, s: Snippet, page: string | undefined, { settleMs: _settleMs, sleep, onStep, ...execute }?: OperationOptions): Promise<SnippetResult>;
|
|
58
58
|
export declare function runOperation<P>(client: PenpotToolClient, operation: Operation<P>, params: P, options?: OperationOptions): Promise<OperationReport>;
|
|
59
59
|
/** Nests the frames that are not part of the live work into one archive board. Frames keep their names, ids and component links. */
|
|
60
60
|
export declare const archiveOperation: Operation<ArchiveParams>;
|