paper-mono 0.62.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/CHANGELOG.md +21 -0
- package/DEPENDENCIES.json +143 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/SAFETY.md +27 -0
- package/THIRD_PARTY_NOTICES.md +244 -0
- package/bin/chunks/chunk-BGKLQ5Y6.js +55 -0
- package/bin/chunks/chunk-I4P43IZS.js +171 -0
- package/bin/chunks/chunk-JCO37BXY.js +31 -0
- package/bin/chunks/chunk-UHVY2TIH.js +3264 -0
- package/bin/chunks/chunk-ZX4GFXSY.js +37 -0
- package/bin/chunks/doctor-7DE4ASQO.js +355 -0
- package/bin/chunks/main-NQIGPQXK.js +23401 -0
- package/bin/chunks/owner-commands-MWXW2KMM.js +403 -0
- package/bin/chunks/tool-contract-ZWFZSYL4.js +32 -0
- package/bin/paper.js +258 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/docs/cli.md +122 -0
- package/docs/paper-mcp.md +33 -0
- package/docs/runtime.md +54 -0
- package/package.json +79 -0
- package/tool-walk/fixtures.json +584 -0
- package/tool-walk/mono-safety-card.json +920 -0
- package/tool-walk/safety-fixtures.json +329 -0
- package/tools.contract.json +1748 -0
|
@@ -0,0 +1,3264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAgentDir,
|
|
3
|
+
getPackageDir
|
|
4
|
+
} from "./chunk-BGKLQ5Y6.js";
|
|
5
|
+
|
|
6
|
+
// src/tool-contract.ts
|
|
7
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
8
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
9
|
+
|
|
10
|
+
// ../../node_modules/@creative-int/mono/dist/tool-contract.js
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
var MONO_TOOLS_CONTRACT_VERSION = "mono-tools.v1";
|
|
16
|
+
var MONO_TOOL_SAFETY_MODES = [
|
|
17
|
+
"mock",
|
|
18
|
+
"dry_run",
|
|
19
|
+
"sandbox",
|
|
20
|
+
"confirm-gate"
|
|
21
|
+
];
|
|
22
|
+
function validateMonoToolContract(value) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
if (!isRecord(value)) {
|
|
25
|
+
return { valid: false, errors: ["contract must be a JSON object"] };
|
|
26
|
+
}
|
|
27
|
+
if (value.schemaVersion !== MONO_TOOLS_CONTRACT_VERSION) {
|
|
28
|
+
errors.push(`schemaVersion must be ${MONO_TOOLS_CONTRACT_VERSION}`);
|
|
29
|
+
}
|
|
30
|
+
requireNonEmptyString(value, "specialist", errors);
|
|
31
|
+
requireNonEmptyString(value, "ownerCommand", errors);
|
|
32
|
+
requireNonEmptyString(value, "agentHome", errors);
|
|
33
|
+
if (!Array.isArray(value.tools)) {
|
|
34
|
+
errors.push("tools must be an array");
|
|
35
|
+
} else {
|
|
36
|
+
value.tools.forEach((tool, index) => {
|
|
37
|
+
validateToolContractEntry(tool, `tools[${index}]`, errors);
|
|
38
|
+
});
|
|
39
|
+
const ids = value.tools.filter(isRecord).map((tool) => tool.id).filter((id) => typeof id === "string");
|
|
40
|
+
const duplicates = duplicateValues(ids);
|
|
41
|
+
if (duplicates.length > 0) {
|
|
42
|
+
errors.push(`tools contains duplicate ids: ${duplicates.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (value.domain !== void 0) {
|
|
46
|
+
if (!isRecord(value.domain)) {
|
|
47
|
+
errors.push("domain must be an object when provided");
|
|
48
|
+
} else {
|
|
49
|
+
if (typeof value.domain.comment !== "string" || !value.domain.comment.startsWith("REQ:")) {
|
|
50
|
+
errors.push("domain.comment must start with REQ:");
|
|
51
|
+
}
|
|
52
|
+
if (!isStringArray(value.domain.toolIds)) {
|
|
53
|
+
errors.push("domain.toolIds must be an array of strings");
|
|
54
|
+
} else if (Array.isArray(value.tools)) {
|
|
55
|
+
const ids = new Set(value.tools.filter(isRecord).map((tool) => tool.id).filter((id) => typeof id === "string"));
|
|
56
|
+
const unknown = value.domain.toolIds.filter((id) => !ids.has(id));
|
|
57
|
+
if (unknown.length > 0) {
|
|
58
|
+
errors.push(`domain.toolIds references undeclared tools: ${unknown.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { valid: errors.length === 0, errors };
|
|
64
|
+
}
|
|
65
|
+
function assertMonoToolContract(value) {
|
|
66
|
+
const validation = validateMonoToolContract(value);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
throw new Error(["Invalid mono tool contract.", ...validation.errors].join("\n"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parseMonoToolContract(source) {
|
|
72
|
+
let value;
|
|
73
|
+
try {
|
|
74
|
+
value = JSON.parse(source);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Invalid mono tool contract JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
assertMonoToolContract(value);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function hashMonoToolContractSource(source) {
|
|
82
|
+
return `sha256:${createHash("sha256").update(source).digest("hex")}`;
|
|
83
|
+
}
|
|
84
|
+
function loadMonoToolContract(filePath) {
|
|
85
|
+
const contractPath = resolve(filePath);
|
|
86
|
+
const source = readFileSync(contractPath, "utf8");
|
|
87
|
+
return {
|
|
88
|
+
contract: parseMonoToolContract(source),
|
|
89
|
+
contractPath,
|
|
90
|
+
source,
|
|
91
|
+
hash: hashMonoToolContractSource(source)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function renderMonoToolPromptInventory(contract) {
|
|
95
|
+
assertMonoToolContract(contract);
|
|
96
|
+
return contract.tools.map((tool) => {
|
|
97
|
+
const fallback = tool.cliFallback ? `<cli_fallback>${escapeXml(tool.cliFallback)}</cli_fallback>` : "<cli_fallback>none; call the registered tool directly</cli_fallback>";
|
|
98
|
+
return [
|
|
99
|
+
`<tool id="${escapeXml(tool.id)}" safety="${tool.safety}">`,
|
|
100
|
+
` <description>${escapeXml(tool.description)}</description>`,
|
|
101
|
+
` ${fallback}`,
|
|
102
|
+
...tool.guidelines.map((guideline) => ` <guideline>${escapeXml(guideline)}</guideline>`),
|
|
103
|
+
"</tool>"
|
|
104
|
+
].join("\n");
|
|
105
|
+
}).join("\n ");
|
|
106
|
+
}
|
|
107
|
+
function extractMonoPromptToolIds(prompt) {
|
|
108
|
+
return [...prompt.matchAll(/<tool id="([^"]+)"\s+safety=/g)].map((match) => match[1] ?? "");
|
|
109
|
+
}
|
|
110
|
+
function assertMonoToolContractHandshake(input) {
|
|
111
|
+
const contractToolIds = input.contract.tools.map((tool) => tool.id);
|
|
112
|
+
const registeredToolIds = [...input.registeredToolIds];
|
|
113
|
+
const customDefinitionToolIds = input.customDefinitionToolIds ? [...input.customDefinitionToolIds] : void 0;
|
|
114
|
+
const promptToolIds = input.promptToolIds ? [...input.promptToolIds] : extractMonoPromptToolIds(input.prompt ?? "");
|
|
115
|
+
const activeToolIds = input.activeToolIds ? [...input.activeToolIds] : void 0;
|
|
116
|
+
const agentHome = resolveMonoAgentHome(input.contract, input.agentHome, input.cwd);
|
|
117
|
+
const comparisons = [
|
|
118
|
+
["registered", registeredToolIds],
|
|
119
|
+
["prompt", promptToolIds]
|
|
120
|
+
];
|
|
121
|
+
if (customDefinitionToolIds) {
|
|
122
|
+
comparisons.push(["custom-definitions", customDefinitionToolIds]);
|
|
123
|
+
}
|
|
124
|
+
if (activeToolIds)
|
|
125
|
+
comparisons.push(["active-session", activeToolIds]);
|
|
126
|
+
const failures = comparisons.flatMap(([label, ids]) => setDiff(contractToolIds, ids, label));
|
|
127
|
+
const duplicateFailures = [
|
|
128
|
+
...duplicateDiff(contractToolIds, "contract"),
|
|
129
|
+
...duplicateDiff(registeredToolIds, "registered"),
|
|
130
|
+
...duplicateDiff(promptToolIds, "prompt"),
|
|
131
|
+
...customDefinitionToolIds ? duplicateDiff(customDefinitionToolIds, "custom-definitions") : [],
|
|
132
|
+
...activeToolIds ? duplicateDiff(activeToolIds, "active-session") : []
|
|
133
|
+
];
|
|
134
|
+
if (failures.length > 0 || duplicateFailures.length > 0) {
|
|
135
|
+
throw new Error([
|
|
136
|
+
`${titleCase(input.contract.specialist)} tool-contract handshake failed.`,
|
|
137
|
+
`contractHash: ${input.contractHash}`,
|
|
138
|
+
`agentHome: ${agentHome}`,
|
|
139
|
+
...failures,
|
|
140
|
+
...duplicateFailures
|
|
141
|
+
].join("\n"));
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
contractHash: input.contractHash,
|
|
145
|
+
agentHome,
|
|
146
|
+
contractToolIds,
|
|
147
|
+
registeredToolIds,
|
|
148
|
+
...customDefinitionToolIds ? { customDefinitionToolIds } : {},
|
|
149
|
+
promptToolIds,
|
|
150
|
+
...activeToolIds ? { activeToolIds } : {}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function resolveMonoAgentHome(contract, requested, cwd = process.cwd()) {
|
|
154
|
+
const canonical = resolvePath(contract.agentHome, cwd);
|
|
155
|
+
if (!requested)
|
|
156
|
+
return canonical;
|
|
157
|
+
const resolved = resolvePath(requested, cwd);
|
|
158
|
+
if (resolved !== canonical) {
|
|
159
|
+
throw new Error([
|
|
160
|
+
`${titleCase(contract.specialist)} agent-home handshake failed.`,
|
|
161
|
+
`contract: ${canonical}`,
|
|
162
|
+
`requested: ${resolved}`,
|
|
163
|
+
"The owner command, direct runtime, and workflow entrypoints must use one agent home."
|
|
164
|
+
].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
return canonical;
|
|
167
|
+
}
|
|
168
|
+
function resolveMonoContractRef(contractPath, reference) {
|
|
169
|
+
const [relativePath] = reference.split("#", 1);
|
|
170
|
+
if (!relativePath) {
|
|
171
|
+
throw new Error(`Invalid mono contract reference: ${reference}`);
|
|
172
|
+
}
|
|
173
|
+
return resolve(dirname(contractPath), relativePath);
|
|
174
|
+
}
|
|
175
|
+
function assertMonoContractFixturesExist(loaded) {
|
|
176
|
+
const missing = loaded.contract.tools.flatMap((tool) => [...tool.cases, ...tool.safetyContract?.fixtureRefs ?? []].map((reference) => resolveMonoContractRef(loaded.contractPath, reference)).filter((filePath) => !existsSync(filePath)).map((filePath) => `${tool.id}: ${filePath}`));
|
|
177
|
+
if (missing.length > 0) {
|
|
178
|
+
throw new Error(["Mono tool-contract fixture references are missing.", ...missing].join("\n"));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function validateToolContractEntry(value, path, errors) {
|
|
182
|
+
if (!isRecord(value)) {
|
|
183
|
+
errors.push(`${path} must be an object`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const key of [
|
|
187
|
+
"id",
|
|
188
|
+
"label",
|
|
189
|
+
"description",
|
|
190
|
+
"handlerRef",
|
|
191
|
+
"inputSchemaRef",
|
|
192
|
+
"doctorProbe"
|
|
193
|
+
]) {
|
|
194
|
+
requireNonEmptyString(value, key, errors, path);
|
|
195
|
+
}
|
|
196
|
+
if (!Number.isInteger(value.version) || Number(value.version) < 1) {
|
|
197
|
+
errors.push(`${path}.version must be a positive integer`);
|
|
198
|
+
}
|
|
199
|
+
if (value.sourcePack !== null && typeof value.sourcePack !== "string") {
|
|
200
|
+
errors.push(`${path}.sourcePack must be a string or null`);
|
|
201
|
+
}
|
|
202
|
+
if (value.safety !== "read" && value.safety !== "mutate") {
|
|
203
|
+
errors.push(`${path}.safety must be read or mutate`);
|
|
204
|
+
}
|
|
205
|
+
if (value.safetyContract !== void 0) {
|
|
206
|
+
validateToolSafetyContract(value.safetyContract, value.output, path, errors);
|
|
207
|
+
}
|
|
208
|
+
for (const key of ["demandRefs", "cases", "guidelines"]) {
|
|
209
|
+
if (!isStringArray(value[key]) || value[key].length === 0) {
|
|
210
|
+
errors.push(`${path}.${key} must be a non-empty array of strings`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (value.cliFallback !== null && typeof value.cliFallback !== "string") {
|
|
214
|
+
errors.push(`${path}.cliFallback must be a string or null`);
|
|
215
|
+
}
|
|
216
|
+
if (!isRecord(value.requires)) {
|
|
217
|
+
errors.push(`${path}.requires must be an object`);
|
|
218
|
+
} else {
|
|
219
|
+
for (const key of ["allOf", "anyOf", "cli", "endpoint"]) {
|
|
220
|
+
if (!isStringArray(value.requires[key])) {
|
|
221
|
+
errors.push(`${path}.requires.${key} must be an array of strings`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.requires.optional !== "boolean") {
|
|
225
|
+
errors.push(`${path}.requires.optional must be boolean`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!isRecord(value.output)) {
|
|
229
|
+
errors.push(`${path}.output must be an object`);
|
|
230
|
+
} else {
|
|
231
|
+
if (value.output.envelope !== "mono-tool-result.v1") {
|
|
232
|
+
errors.push(`${path}.output.envelope must be mono-tool-result.v1`);
|
|
233
|
+
}
|
|
234
|
+
for (const key of ["maxBytes", "maxItems"]) {
|
|
235
|
+
if (!Number.isFinite(value.output[key]) || Number(value.output[key]) <= 0) {
|
|
236
|
+
errors.push(`${path}.output.${key} must be a positive number`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function validateToolSafetyContract(value, output, path, errors) {
|
|
242
|
+
if (!isRecord(value)) {
|
|
243
|
+
errors.push(`${path}.safetyContract must be an object`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!isStringArray(value.modes) || value.modes.length === 0) {
|
|
247
|
+
errors.push(`${path}.safetyContract.modes must be a non-empty array of strings`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const modes = value.modes;
|
|
251
|
+
const invalidModes = modes.filter((mode) => !MONO_TOOL_SAFETY_MODES.includes(mode));
|
|
252
|
+
if (invalidModes.length > 0) {
|
|
253
|
+
errors.push(`${path}.safetyContract.modes contains invalid modes: ${invalidModes.join(", ")}`);
|
|
254
|
+
}
|
|
255
|
+
const duplicateModes = duplicateValues(modes);
|
|
256
|
+
if (duplicateModes.length > 0) {
|
|
257
|
+
errors.push(`${path}.safetyContract.modes contains duplicates: ${duplicateModes.join(", ")}`);
|
|
258
|
+
}
|
|
259
|
+
if (!isRecord(value.guarantees)) {
|
|
260
|
+
errors.push(`${path}.safetyContract.guarantees must be an object`);
|
|
261
|
+
} else {
|
|
262
|
+
for (const mode of modes) {
|
|
263
|
+
validateToolSafetyGuarantee(value.guarantees[mode], mode, `${path}.safetyContract.guarantees.${mode}`, errors);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!isStringArray(value.fixtureRefs) || value.fixtureRefs.length === 0) {
|
|
267
|
+
errors.push(`${path}.safetyContract.fixtureRefs must be a non-empty array of strings`);
|
|
268
|
+
}
|
|
269
|
+
if (!isRecord(value.outputBudget)) {
|
|
270
|
+
errors.push(`${path}.safetyContract.outputBudget must be an object`);
|
|
271
|
+
} else {
|
|
272
|
+
for (const key of ["maxBytes", "maxLines"]) {
|
|
273
|
+
if (!Number.isInteger(value.outputBudget[key]) || Number(value.outputBudget[key]) <= 0) {
|
|
274
|
+
errors.push(`${path}.safetyContract.outputBudget.${key} must be a positive integer`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (isRecord(output) && Number.isFinite(output.maxBytes) && Number.isFinite(value.outputBudget.maxBytes) && Number(value.outputBudget.maxBytes) > Number(output.maxBytes)) {
|
|
278
|
+
errors.push(`${path}.safetyContract.outputBudget.maxBytes must not exceed output.maxBytes`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function validateToolSafetyGuarantee(value, mode, path, errors) {
|
|
283
|
+
if (!isRecord(value)) {
|
|
284
|
+
errors.push(`${path} must be an object`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (value.networkSpend !== "forbidden" && value.networkSpend !== "declared") {
|
|
288
|
+
errors.push(`${path}.networkSpend must be forbidden or declared`);
|
|
289
|
+
}
|
|
290
|
+
if (value.filesystemWrites !== "none" && value.filesystemWrites !== "declared-roots") {
|
|
291
|
+
errors.push(`${path}.filesystemWrites must be none or declared-roots`);
|
|
292
|
+
}
|
|
293
|
+
if (!isStringArray(value.filesystemRoots)) {
|
|
294
|
+
errors.push(`${path}.filesystemRoots must be an array of strings`);
|
|
295
|
+
} else if (value.filesystemWrites === "declared-roots" && value.filesystemRoots.length === 0) {
|
|
296
|
+
errors.push(`${path}.filesystemRoots must declare at least one root`);
|
|
297
|
+
} else if (value.filesystemWrites === "none" && value.filesystemRoots.length > 0) {
|
|
298
|
+
errors.push(`${path}.filesystemRoots must be empty when writes are none`);
|
|
299
|
+
}
|
|
300
|
+
if (value.externalMutations !== "forbidden" && value.externalMutations !== "confirm-gated") {
|
|
301
|
+
errors.push(`${path}.externalMutations must be forbidden or confirm-gated`);
|
|
302
|
+
}
|
|
303
|
+
if (typeof value.requiresConfirmation !== "boolean") {
|
|
304
|
+
errors.push(`${path}.requiresConfirmation must be boolean`);
|
|
305
|
+
}
|
|
306
|
+
if (mode === "confirm-gate" && (value.externalMutations !== "confirm-gated" || value.requiresConfirmation !== true)) {
|
|
307
|
+
errors.push(`${path} must require confirmation and declare confirm-gated mutations`);
|
|
308
|
+
}
|
|
309
|
+
if ((mode === "mock" || mode === "dry_run" || mode === "sandbox") && value.networkSpend !== "forbidden") {
|
|
310
|
+
errors.push(`${path}.networkSpend must be forbidden for ${mode}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function requireNonEmptyString(value, key, errors, path = "contract") {
|
|
314
|
+
if (typeof value[key] !== "string" || value[key].trim() === "") {
|
|
315
|
+
errors.push(`${path}.${key} must be a non-empty string`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function isRecord(value) {
|
|
319
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
320
|
+
}
|
|
321
|
+
function isStringArray(value) {
|
|
322
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
323
|
+
}
|
|
324
|
+
function resolvePath(value, cwd) {
|
|
325
|
+
if (value === "~")
|
|
326
|
+
return homedir();
|
|
327
|
+
if (value.startsWith("~/"))
|
|
328
|
+
return join(homedir(), value.slice(2));
|
|
329
|
+
if (isAbsolute(value))
|
|
330
|
+
return resolve(value);
|
|
331
|
+
return resolve(cwd, value);
|
|
332
|
+
}
|
|
333
|
+
function setDiff(contractIds, candidateIds, label) {
|
|
334
|
+
const contract = new Set(contractIds);
|
|
335
|
+
const candidate = new Set(candidateIds);
|
|
336
|
+
const missing = contractIds.filter((id) => !candidate.has(id));
|
|
337
|
+
const extra = candidateIds.filter((id) => !contract.has(id));
|
|
338
|
+
return [
|
|
339
|
+
...missing.length > 0 ? [`${label} missing: ${missing.join(", ")}`] : [],
|
|
340
|
+
...extra.length > 0 ? [`${label} extra: ${extra.join(", ")}`] : []
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
function duplicateDiff(values, label) {
|
|
344
|
+
const duplicates = duplicateValues(values);
|
|
345
|
+
return duplicates.length > 0 ? [`${label} duplicates: ${duplicates.join(", ")}`] : [];
|
|
346
|
+
}
|
|
347
|
+
function duplicateValues(values) {
|
|
348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
349
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
350
|
+
for (const value of values) {
|
|
351
|
+
if (seen.has(value))
|
|
352
|
+
duplicates.add(value);
|
|
353
|
+
seen.add(value);
|
|
354
|
+
}
|
|
355
|
+
return [...duplicates];
|
|
356
|
+
}
|
|
357
|
+
function titleCase(value) {
|
|
358
|
+
return value.split(/[-_\s]+/).filter(Boolean).map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`).join(" ");
|
|
359
|
+
}
|
|
360
|
+
function escapeXml(value) {
|
|
361
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/core/tools/paper-mcp.ts
|
|
365
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
|
|
366
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2 } from "node:path";
|
|
367
|
+
import { Type } from "@sinclair/typebox";
|
|
368
|
+
var PAPER_MCP_URL = "http://127.0.0.1:29979/mcp";
|
|
369
|
+
var TIMEOUT_MS = 3e4;
|
|
370
|
+
var PAPER_DESKTOP_TOOL_NAMES = {
|
|
371
|
+
paper_get_basic_info: "get_basic_info",
|
|
372
|
+
paper_get_selection: "get_selection",
|
|
373
|
+
paper_get_node_info: "get_node_info",
|
|
374
|
+
paper_get_children: "get_children",
|
|
375
|
+
paper_get_tree_summary: "get_tree_summary",
|
|
376
|
+
paper_get_screenshot: "get_screenshot",
|
|
377
|
+
paper_get_jsx: "get_jsx",
|
|
378
|
+
paper_get_computed_styles: "get_computed_styles",
|
|
379
|
+
paper_get_fill_image: "get_fill_image",
|
|
380
|
+
paper_find_nodes: "find_nodes",
|
|
381
|
+
paper_get_font_family_info: "get_font_family_info",
|
|
382
|
+
paper_get_guide: "get_guide",
|
|
383
|
+
paper_find_placement: "find_placement",
|
|
384
|
+
paper_get_tokens: "get_tokens",
|
|
385
|
+
paper_create_tokens: "create_tokens",
|
|
386
|
+
paper_set_tokens: "set_tokens",
|
|
387
|
+
paper_create_artboard: "create_artboard",
|
|
388
|
+
paper_write_html: "write_html",
|
|
389
|
+
paper_set_text_content: "set_text_content",
|
|
390
|
+
paper_rename_nodes: "rename_nodes",
|
|
391
|
+
paper_duplicate_nodes: "duplicate_nodes",
|
|
392
|
+
paper_update_styles: "update_styles",
|
|
393
|
+
paper_delete_nodes: "delete_nodes",
|
|
394
|
+
paper_move_nodes: "move_nodes",
|
|
395
|
+
paper_finish_working_on_nodes: "finish_working_on_nodes",
|
|
396
|
+
paper_create_page: "create_page",
|
|
397
|
+
paper_open_page: "open_file"
|
|
398
|
+
};
|
|
399
|
+
function resolvePaperDesktopToolName(toolName) {
|
|
400
|
+
return PAPER_DESKTOP_TOOL_NAMES[toolName] ?? toolName;
|
|
401
|
+
}
|
|
402
|
+
var requestId = 1;
|
|
403
|
+
var PaperPageOperationError = class extends Error {
|
|
404
|
+
constructor(operation, code, message, diagnostic) {
|
|
405
|
+
super(message);
|
|
406
|
+
this.operation = operation;
|
|
407
|
+
this.code = code;
|
|
408
|
+
this.diagnostic = diagnostic;
|
|
409
|
+
this.name = "PaperPageOperationError";
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
async function callPaperMcp(toolName, args, signal, timeoutMs = TIMEOUT_MS) {
|
|
413
|
+
const id = requestId++;
|
|
414
|
+
const desktopToolName = resolvePaperDesktopToolName(toolName);
|
|
415
|
+
const body = JSON.stringify({
|
|
416
|
+
jsonrpc: "2.0",
|
|
417
|
+
method: "tools/call",
|
|
418
|
+
params: { name: desktopToolName, arguments: args },
|
|
419
|
+
id
|
|
420
|
+
});
|
|
421
|
+
let endpointReached = false;
|
|
422
|
+
try {
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
425
|
+
if (signal) {
|
|
426
|
+
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
427
|
+
}
|
|
428
|
+
let resp;
|
|
429
|
+
try {
|
|
430
|
+
resp = await fetch(PAPER_MCP_URL, {
|
|
431
|
+
method: "POST",
|
|
432
|
+
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
433
|
+
body,
|
|
434
|
+
signal: controller.signal
|
|
435
|
+
});
|
|
436
|
+
endpointReached = true;
|
|
437
|
+
} finally {
|
|
438
|
+
clearTimeout(timeout);
|
|
439
|
+
}
|
|
440
|
+
if (!resp.ok) {
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
error: `Paper MCP returned ${resp.status} ${resp.statusText}`,
|
|
444
|
+
failure: "contract",
|
|
445
|
+
desktopToolName
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const text = await resp.text();
|
|
449
|
+
let json;
|
|
450
|
+
const dataMatch = text.match(/^data: (.+)$/m);
|
|
451
|
+
if (dataMatch) {
|
|
452
|
+
json = JSON.parse(dataMatch[1]);
|
|
453
|
+
} else {
|
|
454
|
+
json = JSON.parse(text);
|
|
455
|
+
}
|
|
456
|
+
if (json.error) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
error: `Paper MCP error calling ${desktopToolName}: ${json.error.message}`,
|
|
460
|
+
failure: "contract",
|
|
461
|
+
desktopToolName
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const providerError = getMcpProviderError(json.result);
|
|
465
|
+
if (providerError) {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
error: `Paper MCP tool ${desktopToolName} failed: ${providerError}`,
|
|
469
|
+
failure: "contract",
|
|
470
|
+
desktopToolName
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return { ok: true, result: json.result, desktopToolName };
|
|
474
|
+
} catch (err) {
|
|
475
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
476
|
+
if (!endpointReached) {
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
error: "Paper Desktop is not running. Open a file in Paper Desktop to start the MCP server (http://127.0.0.1:29979/mcp).",
|
|
480
|
+
failure: "unreachable",
|
|
481
|
+
desktopToolName
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
error: `Paper MCP contract error calling ${desktopToolName}: ${message}`,
|
|
487
|
+
failure: "contract",
|
|
488
|
+
desktopToolName
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
var PAPER_HIGH_VOLUME_RESPONSE_LIMITS = {
|
|
493
|
+
maxTextUtf8Bytes: 160 * 1024,
|
|
494
|
+
maxDecodedImageBytes: 6 * 1024 * 1024
|
|
495
|
+
};
|
|
496
|
+
function formatMcpResult(result) {
|
|
497
|
+
if (!result || typeof result !== "object") {
|
|
498
|
+
return [{ type: "text", text: JSON.stringify(result, null, 2) }];
|
|
499
|
+
}
|
|
500
|
+
const r = result;
|
|
501
|
+
const content = r.content;
|
|
502
|
+
if (!Array.isArray(content)) {
|
|
503
|
+
return [{ type: "text", text: JSON.stringify(result, null, 2) }];
|
|
504
|
+
}
|
|
505
|
+
const parts = [];
|
|
506
|
+
for (const item of content) {
|
|
507
|
+
if (typeof item !== "object" || item === null) continue;
|
|
508
|
+
const entry = item;
|
|
509
|
+
if (entry.type === "text" && typeof entry.text === "string") {
|
|
510
|
+
parts.push({ type: "text", text: entry.text });
|
|
511
|
+
} else if (entry.type === "image" && typeof entry.data === "string") {
|
|
512
|
+
parts.push({
|
|
513
|
+
type: "image",
|
|
514
|
+
data: entry.data,
|
|
515
|
+
mimeType: typeof entry.mimeType === "string" ? entry.mimeType : "image/png"
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (parts.length === 0) {
|
|
520
|
+
return [{ type: "text", text: JSON.stringify(result, null, 2) }];
|
|
521
|
+
}
|
|
522
|
+
return parts;
|
|
523
|
+
}
|
|
524
|
+
function decodedBase64ByteLength(data) {
|
|
525
|
+
if (data.length === 0) return 0;
|
|
526
|
+
const paddingBytes = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
527
|
+
return Math.max(0, Math.floor(data.length * 3 / 4) - paddingBytes);
|
|
528
|
+
}
|
|
529
|
+
function formatMcpResultWithinBudget(def, result) {
|
|
530
|
+
const parts = formatMcpResult(result);
|
|
531
|
+
const budget = def.responseBudget;
|
|
532
|
+
if (!budget) return parts;
|
|
533
|
+
const observedTextBytes = parts.reduce(
|
|
534
|
+
(total, part) => total + (part.type === "text" ? Buffer.byteLength(part.text, "utf8") : 0),
|
|
535
|
+
0
|
|
536
|
+
);
|
|
537
|
+
const observedImageBytes = parts.reduce(
|
|
538
|
+
(total, part) => total + (part.type === "image" ? decodedBase64ByteLength(part.data) : 0),
|
|
539
|
+
0
|
|
540
|
+
);
|
|
541
|
+
if (observedTextBytes <= budget.maxTextUtf8Bytes && observedImageBytes <= budget.maxDecodedImageBytes) {
|
|
542
|
+
return parts;
|
|
543
|
+
}
|
|
544
|
+
const retained = [];
|
|
545
|
+
let retainedTextBytes = 0;
|
|
546
|
+
let retainedImageBytes = 0;
|
|
547
|
+
let suppressedTextParts = 0;
|
|
548
|
+
let suppressedImageParts = 0;
|
|
549
|
+
for (const part of parts) {
|
|
550
|
+
if (part.type === "text") {
|
|
551
|
+
const bytes2 = Buffer.byteLength(part.text, "utf8");
|
|
552
|
+
if (retainedTextBytes + bytes2 > budget.maxTextUtf8Bytes) {
|
|
553
|
+
suppressedTextParts += 1;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
retainedTextBytes += bytes2;
|
|
557
|
+
retained.push(part);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const bytes = decodedBase64ByteLength(part.data);
|
|
561
|
+
if (retainedImageBytes + bytes > budget.maxDecodedImageBytes) {
|
|
562
|
+
suppressedImageParts += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
retainedImageBytes += bytes;
|
|
566
|
+
retained.push(part);
|
|
567
|
+
}
|
|
568
|
+
const receiptSuffix = `Nothing was truncated or returned partially. ${budget.retryGuidance}`;
|
|
569
|
+
if (suppressedTextParts > 0) {
|
|
570
|
+
retained.push({
|
|
571
|
+
type: "text",
|
|
572
|
+
text: `Paper suppressed ${suppressedTextParts} over-budget text content part${suppressedTextParts === 1 ? "" : "s"} from ${def.name} (observed ${observedTextBytes} UTF-8 bytes; configured limit ${budget.maxTextUtf8Bytes} bytes). ${receiptSuffix}`
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (suppressedImageParts > 0) {
|
|
576
|
+
retained.push({
|
|
577
|
+
type: "text",
|
|
578
|
+
text: `Paper suppressed ${suppressedImageParts} over-budget image content part${suppressedImageParts === 1 ? "" : "s"} from ${def.name} (observed ${observedImageBytes} decoded bytes; configured limit ${budget.maxDecodedImageBytes} bytes). ${receiptSuffix}`
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return retained;
|
|
582
|
+
}
|
|
583
|
+
function parseMcpTextContent(result) {
|
|
584
|
+
const parts = formatMcpResult(result);
|
|
585
|
+
const textParts = parts.filter((p) => p.type === "text").map((p) => p.text);
|
|
586
|
+
return textParts.length > 0 ? textParts.join("\n") : void 0;
|
|
587
|
+
}
|
|
588
|
+
var PaperMaterializationError = class extends Error {
|
|
589
|
+
constructor(stage, message, artboardId) {
|
|
590
|
+
super(`Paper materialization stopped at ${stage}: ${message}`);
|
|
591
|
+
this.stage = stage;
|
|
592
|
+
this.artboardId = artboardId;
|
|
593
|
+
this.code = "PAPER_MATERIALIZATION_FAILED";
|
|
594
|
+
this.name = "PaperMaterializationError";
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
var MATERIALIZATION_STEPS = [
|
|
598
|
+
"paper_create_artboard",
|
|
599
|
+
"paper_write_html",
|
|
600
|
+
"paper_get_screenshot",
|
|
601
|
+
"paper_finish_working_on_nodes"
|
|
602
|
+
];
|
|
603
|
+
function unwrapMcpTextJson(result) {
|
|
604
|
+
const text = parseMcpTextContent(result);
|
|
605
|
+
if (!text) return result;
|
|
606
|
+
try {
|
|
607
|
+
return JSON.parse(text);
|
|
608
|
+
} catch {
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function findFirstStringByKeys(value, keys) {
|
|
613
|
+
const queue = [unwrapMcpTextJson(value)];
|
|
614
|
+
let visited = 0;
|
|
615
|
+
while (queue.length > 0 && visited < 500) {
|
|
616
|
+
visited += 1;
|
|
617
|
+
const current = queue.shift();
|
|
618
|
+
if (Array.isArray(current)) {
|
|
619
|
+
queue.push(...current);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (!current || typeof current !== "object") continue;
|
|
623
|
+
for (const [key, entry] of Object.entries(current)) {
|
|
624
|
+
if (keys.has(key) && typeof entry === "string" && entry.trim()) return entry.trim();
|
|
625
|
+
if (entry && typeof entry === "object") queue.push(entry);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
function findFirstNumberByKey(value, key) {
|
|
631
|
+
const queue = [unwrapMcpTextJson(value)];
|
|
632
|
+
let visited = 0;
|
|
633
|
+
while (queue.length > 0 && visited < 500) {
|
|
634
|
+
visited += 1;
|
|
635
|
+
const current = queue.shift();
|
|
636
|
+
if (Array.isArray(current)) {
|
|
637
|
+
queue.push(...current);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (!current || typeof current !== "object") continue;
|
|
641
|
+
for (const [entryKey, entry] of Object.entries(current)) {
|
|
642
|
+
if (entryKey === key && typeof entry === "number" && Number.isFinite(entry)) return entry;
|
|
643
|
+
if (entry && typeof entry === "object") queue.push(entry);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
function normalizeMaterializationInput(input) {
|
|
649
|
+
const name = input.name.trim();
|
|
650
|
+
const html = input.html.trim();
|
|
651
|
+
if (!name) throw new TypeError("materializeDirection requires a non-empty artboard name.");
|
|
652
|
+
if (!Number.isFinite(input.width) || input.width < 1) {
|
|
653
|
+
throw new TypeError("materializeDirection requires a positive finite width.");
|
|
654
|
+
}
|
|
655
|
+
if (!Number.isFinite(input.height) || input.height < 1) {
|
|
656
|
+
throw new TypeError("materializeDirection requires a positive finite height.");
|
|
657
|
+
}
|
|
658
|
+
if (!html) throw new TypeError("materializeDirection requires non-empty literal inline-CSS HTML.");
|
|
659
|
+
if (/\bclass\s*=/i.test(html) || /var\s*\(/i.test(html) || /<script\b/i.test(html)) {
|
|
660
|
+
throw new TypeError(
|
|
661
|
+
"materializeDirection accepts literal inline-CSS HTML only (no class attributes, CSS variables, or scripts)."
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
name,
|
|
666
|
+
width: input.width,
|
|
667
|
+
height: input.height,
|
|
668
|
+
html,
|
|
669
|
+
confirm: input.confirm === true,
|
|
670
|
+
dryRun: input.dryRun === true
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
async function callMaterializationStage(callTool, stage, toolName, args, artboardId) {
|
|
674
|
+
try {
|
|
675
|
+
return await callTool(toolName, args);
|
|
676
|
+
} catch (error) {
|
|
677
|
+
throw new PaperMaterializationError(stage, error instanceof Error ? error.message : String(error), artboardId);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function buildMaterializationReceipt(payload) {
|
|
681
|
+
const materialized = payload.status === "materialized";
|
|
682
|
+
const evidence = materialized ? [
|
|
683
|
+
{
|
|
684
|
+
kind: "paper-artboard",
|
|
685
|
+
locator: `paper://node/${payload.artboardId}`,
|
|
686
|
+
accessClass: "private-corpus"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
kind: "paper-screenshot",
|
|
690
|
+
locator: `paper://node/${payload.artboardId}/screenshot`,
|
|
691
|
+
accessClass: "private-corpus"
|
|
692
|
+
}
|
|
693
|
+
] : [
|
|
694
|
+
{
|
|
695
|
+
kind: "paper-materialization-preview",
|
|
696
|
+
locator: `paper://materialize-direction/${encodeURIComponent(payload.name)}`,
|
|
697
|
+
accessClass: "private"
|
|
698
|
+
}
|
|
699
|
+
];
|
|
700
|
+
return {
|
|
701
|
+
workflowId: "paper.materializeDirection.v1",
|
|
702
|
+
summary: materialized ? `Created and screenshot-verified adjacent artboard \u201C${payload.name}\u201D.` : `Previewed an adjacent ${payload.width}\xD7${payload.height} artboard named \u201C${payload.name}\u201D; no Paper mutation ran.`,
|
|
703
|
+
evidence,
|
|
704
|
+
sourceNotes: [
|
|
705
|
+
"Paper Desktop remains the source of truth for canvas state.",
|
|
706
|
+
"The operation uses literal inline-CSS HTML and inserts children only into the newly created artboard."
|
|
707
|
+
],
|
|
708
|
+
riskNotes: [
|
|
709
|
+
"A live write requires confirm=true and dryRun!=true.",
|
|
710
|
+
"The operation never deletes, replaces, renames, or updates source artboards."
|
|
711
|
+
],
|
|
712
|
+
nextActions: materialized ? ["Inspect the returned screenshot before treating the direction as accepted."] : ["Review the preview and rerun with confirm=true to create the adjacent artboard."],
|
|
713
|
+
payload: { ...payload, steps: MATERIALIZATION_STEPS, sourceArtboardsMutated: false }
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
async function materializeDirection(input, io) {
|
|
717
|
+
const normalized = normalizeMaterializationInput(input);
|
|
718
|
+
if (!normalized.confirm || normalized.dryRun) {
|
|
719
|
+
return {
|
|
720
|
+
receipt: buildMaterializationReceipt({
|
|
721
|
+
status: "dry-run",
|
|
722
|
+
artboardId: null,
|
|
723
|
+
name: normalized.name,
|
|
724
|
+
width: normalized.width,
|
|
725
|
+
height: normalized.height,
|
|
726
|
+
placement: { x: null, y: null },
|
|
727
|
+
verifiedBy: null
|
|
728
|
+
}),
|
|
729
|
+
screenshot: []
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const createResult = await callMaterializationStage(io.callTool, "create-artboard", "paper_create_artboard", {
|
|
733
|
+
name: normalized.name,
|
|
734
|
+
styles: { width: `${normalized.width}px`, height: `${normalized.height}px` }
|
|
735
|
+
});
|
|
736
|
+
const artboardId = findFirstStringByKeys(createResult, /* @__PURE__ */ new Set(["nodeId", "artboardId", "id"]));
|
|
737
|
+
if (!artboardId) {
|
|
738
|
+
throw new PaperMaterializationError(
|
|
739
|
+
"create-artboard",
|
|
740
|
+
"Paper created the artboard but returned no node ID, so writing and verification stopped safely."
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
await callMaterializationStage(
|
|
744
|
+
io.callTool,
|
|
745
|
+
"write-html",
|
|
746
|
+
"paper_write_html",
|
|
747
|
+
{ targetNodeId: artboardId, html: normalized.html, mode: "insert-children" },
|
|
748
|
+
artboardId
|
|
749
|
+
);
|
|
750
|
+
const screenshotResult = await callMaterializationStage(
|
|
751
|
+
io.callTool,
|
|
752
|
+
"screenshot",
|
|
753
|
+
"paper_get_screenshot",
|
|
754
|
+
{ nodeId: artboardId, scale: 1 },
|
|
755
|
+
artboardId
|
|
756
|
+
);
|
|
757
|
+
await callMaterializationStage(
|
|
758
|
+
io.callTool,
|
|
759
|
+
"finish-working",
|
|
760
|
+
"paper_finish_working_on_nodes",
|
|
761
|
+
{ nodeIds: [artboardId] },
|
|
762
|
+
artboardId
|
|
763
|
+
);
|
|
764
|
+
return {
|
|
765
|
+
receipt: buildMaterializationReceipt({
|
|
766
|
+
status: "materialized",
|
|
767
|
+
artboardId,
|
|
768
|
+
name: normalized.name,
|
|
769
|
+
width: normalized.width,
|
|
770
|
+
height: normalized.height,
|
|
771
|
+
placement: {
|
|
772
|
+
x: findFirstNumberByKey(createResult, "worldX") ?? findFirstNumberByKey(createResult, "x") ?? null,
|
|
773
|
+
y: findFirstNumberByKey(createResult, "worldY") ?? findFirstNumberByKey(createResult, "y") ?? null
|
|
774
|
+
},
|
|
775
|
+
verifiedBy: "paper_get_screenshot"
|
|
776
|
+
}),
|
|
777
|
+
screenshot: formatMcpResult(screenshotResult).filter((part) => part.type === "image").map(({ data, mimeType }) => ({ data, mimeType }))
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function collectUniqueValues(styles, property) {
|
|
781
|
+
const values = /* @__PURE__ */ new Set();
|
|
782
|
+
for (const nodeStyles of Object.values(styles)) {
|
|
783
|
+
if (typeof nodeStyles !== "object" || nodeStyles === null) continue;
|
|
784
|
+
const ns = nodeStyles;
|
|
785
|
+
const value = ns[property];
|
|
786
|
+
if (typeof value === "string" && value.trim()) {
|
|
787
|
+
values.add(value);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return Array.from(values);
|
|
791
|
+
}
|
|
792
|
+
function extractColorTokens(styles) {
|
|
793
|
+
const colorProps = ["color", "backgroundColor", "borderColor", "fill"];
|
|
794
|
+
const buckets = {};
|
|
795
|
+
for (const prop of colorProps) {
|
|
796
|
+
buckets[prop] = collectUniqueValues(styles, prop);
|
|
797
|
+
}
|
|
798
|
+
return buckets;
|
|
799
|
+
}
|
|
800
|
+
var PAPER_TREE_DEFAULT_DEPTH = 4;
|
|
801
|
+
var PAPER_TREE_MIN_DEPTH = 1;
|
|
802
|
+
var PAPER_TREE_MAX_DEPTH = 8;
|
|
803
|
+
var PAPER_COMPUTED_STYLES_MAX_NODE_IDS = 50;
|
|
804
|
+
function highVolumeReadBudget(retryGuidance) {
|
|
805
|
+
return { ...PAPER_HIGH_VOLUME_RESPONSE_LIMITS, retryGuidance };
|
|
806
|
+
}
|
|
807
|
+
var optionalFileIdSchema = Type.Optional(
|
|
808
|
+
Type.String({
|
|
809
|
+
minLength: 1,
|
|
810
|
+
description: "Explicit Paper file ID. Pass this whenever multiple files may be open; omitting it follows this MCP session's most recently opened file."
|
|
811
|
+
})
|
|
812
|
+
);
|
|
813
|
+
var requiredFileIdSchema = Type.String({
|
|
814
|
+
minLength: 1,
|
|
815
|
+
description: "Explicit Paper file ID. Live Paper mutations refuse to infer the frontmost file."
|
|
816
|
+
});
|
|
817
|
+
function optionalFileId(value) {
|
|
818
|
+
if (value === void 0) return void 0;
|
|
819
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
820
|
+
throw new TypeError("Paper fileId must be a non-empty string when provided.");
|
|
821
|
+
}
|
|
822
|
+
return value.trim();
|
|
823
|
+
}
|
|
824
|
+
function boundedTreeSummaryDepth(value) {
|
|
825
|
+
if (value === void 0) return PAPER_TREE_DEFAULT_DEPTH;
|
|
826
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < PAPER_TREE_MIN_DEPTH || value > PAPER_TREE_MAX_DEPTH) {
|
|
827
|
+
throw new TypeError(
|
|
828
|
+
`paper_get_tree_summary depth must be an integer from ${PAPER_TREE_MIN_DEPTH} through ${PAPER_TREE_MAX_DEPTH}; the default is ${PAPER_TREE_DEFAULT_DEPTH}.`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return value;
|
|
832
|
+
}
|
|
833
|
+
function boundedComputedStyleNodeIds(value) {
|
|
834
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > PAPER_COMPUTED_STYLES_MAX_NODE_IDS || value.some((id) => typeof id !== "string" || id.length === 0)) {
|
|
835
|
+
throw new TypeError(
|
|
836
|
+
`paper_get_computed_styles accepts between 1 and ${PAPER_COMPUTED_STYLES_MAX_NODE_IDS} node IDs per call. Retry with a smaller targeted batch.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
function providerArgsForTool(def, args) {
|
|
842
|
+
const { _fixture: _ignoredFixture, dryRun: _ignoredDryRun, fileId: rawFileId, ...toolArgs } = args;
|
|
843
|
+
const mappedArgs = def.mapArgs ? def.mapArgs(toolArgs) : toolArgs;
|
|
844
|
+
const fileId = optionalFileId(rawFileId);
|
|
845
|
+
return fileId && def.fileIdPassthrough !== false ? { ...mappedArgs, fileId } : mappedArgs;
|
|
846
|
+
}
|
|
847
|
+
function createDryRunResult(def, args) {
|
|
848
|
+
const mappedArgs = providerArgsForTool(def, args);
|
|
849
|
+
const desktopToolName = resolvePaperDesktopToolName(def.name);
|
|
850
|
+
return {
|
|
851
|
+
content: [
|
|
852
|
+
{
|
|
853
|
+
type: "text",
|
|
854
|
+
text: `Dry run for ${desktopToolName}: would execute with arguments ${JSON.stringify(mappedArgs, null, 2)}. Pass dryRun=false to apply.`
|
|
855
|
+
}
|
|
856
|
+
],
|
|
857
|
+
details: { dryRun: true, mcpTool: desktopToolName, arguments: mappedArgs }
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function createPaperTool(def) {
|
|
861
|
+
return {
|
|
862
|
+
name: def.name,
|
|
863
|
+
label: def.label,
|
|
864
|
+
description: def.description,
|
|
865
|
+
parameters: def.parameters,
|
|
866
|
+
async execute(_toolCallId, params, signal) {
|
|
867
|
+
const args = params && typeof params === "object" ? params : {};
|
|
868
|
+
const desktopToolName = resolvePaperDesktopToolName(def.name);
|
|
869
|
+
const fixture = args._fixture;
|
|
870
|
+
if (fixture !== void 0 && !args.dryRun) {
|
|
871
|
+
return {
|
|
872
|
+
content: formatMcpResultWithinBudget(def, fixture),
|
|
873
|
+
details: { mcpTool: desktopToolName, fixture: true }
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
if (def.supportsDryRun && args.dryRun === true) {
|
|
877
|
+
return createDryRunResult(def, args);
|
|
878
|
+
}
|
|
879
|
+
let providerArgs;
|
|
880
|
+
try {
|
|
881
|
+
providerArgs = providerArgsForTool(def, args);
|
|
882
|
+
const fileId = optionalFileId(args.fileId);
|
|
883
|
+
if (fileId && def.fileIdPassthrough === false) {
|
|
884
|
+
await verifyPaperFileTarget(fileId, signal);
|
|
885
|
+
}
|
|
886
|
+
} catch (error) {
|
|
887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
888
|
+
return {
|
|
889
|
+
content: [{ type: "text", text: message }],
|
|
890
|
+
details: { error: message }
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
const result = def.name === "paper_get_screenshot" ? await callPaperScreenshotWithReadiness(providerArgs, signal) : await callPaperMcp(def.name, providerArgs, signal);
|
|
894
|
+
if (!result.ok) {
|
|
895
|
+
return {
|
|
896
|
+
content: [{ type: "text", text: result.error }],
|
|
897
|
+
details: { error: result.error }
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
content: formatMcpResultWithinBudget(def, result.result),
|
|
902
|
+
details: {
|
|
903
|
+
mcpTool: desktopToolName,
|
|
904
|
+
fileId: optionalFileId(args.fileId) ?? null
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
var dryRunSchema = Type.Optional(
|
|
911
|
+
Type.Boolean({ description: "If true, preview the operation without modifying the Paper file." })
|
|
912
|
+
);
|
|
913
|
+
var SOURCE_ACKNOWLEDGEMENTS = {
|
|
914
|
+
rename: "rename-source",
|
|
915
|
+
revise: "revise-source",
|
|
916
|
+
delete: "delete-source",
|
|
917
|
+
write: "write-source",
|
|
918
|
+
setTokens: "set-source-tokens",
|
|
919
|
+
move: "move-source"
|
|
920
|
+
};
|
|
921
|
+
var sessionCreatedNodeIds = /* @__PURE__ */ new Set();
|
|
922
|
+
var sessionFileKey = null;
|
|
923
|
+
function parseMcpJsonValue(result) {
|
|
924
|
+
const text = parseMcpTextContent(result);
|
|
925
|
+
if (text) {
|
|
926
|
+
try {
|
|
927
|
+
return JSON.parse(text);
|
|
928
|
+
} catch {
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
function parseMcpJsonObject(result) {
|
|
935
|
+
const parsed = parseMcpJsonValue(result);
|
|
936
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
937
|
+
}
|
|
938
|
+
function getMcpProviderError(result) {
|
|
939
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return void 0;
|
|
940
|
+
const record = result;
|
|
941
|
+
if (record.isError !== true) return void 0;
|
|
942
|
+
return parseMcpTextContent(result) ?? "Paper MCP reported an error.";
|
|
943
|
+
}
|
|
944
|
+
async function callRequiredPaperMcp(toolName, args, signal) {
|
|
945
|
+
const response = await callPaperMcp(toolName, args, signal);
|
|
946
|
+
if (!response.ok) throw new Error(response.error);
|
|
947
|
+
const providerError = getMcpProviderError(response.result);
|
|
948
|
+
if (providerError) throw new Error(`${toolName}: ${providerError}`);
|
|
949
|
+
return response.result;
|
|
950
|
+
}
|
|
951
|
+
var SCREENSHOT_RETRY_DELAYS_MS = [100, 200, 400, 800];
|
|
952
|
+
function mcpResultHasImage(result) {
|
|
953
|
+
return formatMcpResult(result).some((part) => part.type === "image" && part.data.length > 0);
|
|
954
|
+
}
|
|
955
|
+
async function waitForPaperReadiness(delayMs, signal) {
|
|
956
|
+
if (signal?.aborted) throw new Error("Paper screenshot readiness wait was aborted.");
|
|
957
|
+
await new Promise((resolve3, reject) => {
|
|
958
|
+
const timeout = setTimeout(resolve3, delayMs);
|
|
959
|
+
if (!signal) return;
|
|
960
|
+
signal.addEventListener(
|
|
961
|
+
"abort",
|
|
962
|
+
() => {
|
|
963
|
+
clearTimeout(timeout);
|
|
964
|
+
reject(new Error("Paper screenshot readiness wait was aborted."));
|
|
965
|
+
},
|
|
966
|
+
{ once: true }
|
|
967
|
+
);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async function callPaperScreenshotWithReadiness(args, signal) {
|
|
971
|
+
for (let attempt = 0; attempt <= SCREENSHOT_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
972
|
+
const response = await callPaperMcp("paper_get_screenshot", args, signal);
|
|
973
|
+
if (!response.ok || mcpResultHasImage(response.result)) return response;
|
|
974
|
+
if (attempt < SCREENSHOT_RETRY_DELAYS_MS.length) {
|
|
975
|
+
try {
|
|
976
|
+
await waitForPaperReadiness(SCREENSHOT_RETRY_DELAYS_MS[attempt], signal);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
return {
|
|
979
|
+
ok: false,
|
|
980
|
+
error: error instanceof Error ? error.message : String(error),
|
|
981
|
+
failure: "contract",
|
|
982
|
+
desktopToolName: response.desktopToolName
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
ok: false,
|
|
989
|
+
error: "Paper screenshot page not ready: get_screenshot returned no image after 5 readiness attempts. Retry after the requested file/page finishes opening.",
|
|
990
|
+
failure: "contract",
|
|
991
|
+
desktopToolName: response.desktopToolName
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
throw new Error("Paper screenshot readiness loop ended unexpectedly.");
|
|
995
|
+
}
|
|
996
|
+
async function verifyPaperFileTarget(fileId, signal) {
|
|
997
|
+
const result = await callRequiredPaperMcp("paper_get_basic_info", { fileId }, signal);
|
|
998
|
+
const parsed = parseMcpJsonObject(result);
|
|
999
|
+
const target = typeof parsed?.url === "string" ? parsed.url.trim() : typeof parsed?.fileId === "string" ? parsed.fileId.trim() : "";
|
|
1000
|
+
const expectedFileId = fileIdFromPaperTarget(fileId);
|
|
1001
|
+
const returnedFileId = fileIdFromPaperTarget(target);
|
|
1002
|
+
if (!parsed || !expectedFileId || returnedFileId !== expectedFileId) {
|
|
1003
|
+
throw new Error(`Paper did not prove that explicit fileId ${fileId} was targeted.`);
|
|
1004
|
+
}
|
|
1005
|
+
return parsed;
|
|
1006
|
+
}
|
|
1007
|
+
function maskProviderArguments(args) {
|
|
1008
|
+
return Object.fromEntries(
|
|
1009
|
+
Object.entries(args).map(([key, value]) => [
|
|
1010
|
+
key,
|
|
1011
|
+
/(?:api.?key|authorization|password|secret|token)/i.test(key) ? "[REDACTED]" : value
|
|
1012
|
+
])
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
function pageDiagnostic(providerTool, args) {
|
|
1016
|
+
return { providerTool, arguments: maskProviderArguments(args) };
|
|
1017
|
+
}
|
|
1018
|
+
function normalizeRequiredPageValue(value, label) {
|
|
1019
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1020
|
+
throw new TypeError(`An explicit non-empty Paper ${label} is required.`);
|
|
1021
|
+
}
|
|
1022
|
+
return value.trim();
|
|
1023
|
+
}
|
|
1024
|
+
function pageOperationError(operation, code, message, providerTool, args) {
|
|
1025
|
+
return new PaperPageOperationError(operation, code, message, pageDiagnostic(providerTool, args));
|
|
1026
|
+
}
|
|
1027
|
+
function providerFailureCode(result) {
|
|
1028
|
+
return result.failure === "unreachable" ? "PROVIDER_UNREACHABLE" : "PROVIDER_ERROR";
|
|
1029
|
+
}
|
|
1030
|
+
function pageIdFromProviderResult(result) {
|
|
1031
|
+
const parsed = parseMcpJsonObject(result);
|
|
1032
|
+
const pageId = typeof parsed?.pageId === "string" ? parsed.pageId.trim() : typeof parsed?.id === "string" ? parsed.id.trim() : "";
|
|
1033
|
+
return pageId;
|
|
1034
|
+
}
|
|
1035
|
+
function fileIdFromPaperTarget(target) {
|
|
1036
|
+
const routeMatch = target.match(/(?:^|\/)file\/([^/?#]+)/);
|
|
1037
|
+
if (routeMatch?.[1]) return routeMatch[1];
|
|
1038
|
+
return target.includes("/") || target.includes(":") ? void 0 : target;
|
|
1039
|
+
}
|
|
1040
|
+
function toPaperPageFailureReceipt(error) {
|
|
1041
|
+
return {
|
|
1042
|
+
status: "failed",
|
|
1043
|
+
operation: error.operation,
|
|
1044
|
+
error: { code: error.code, message: error.message },
|
|
1045
|
+
diagnostic: error.diagnostic
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
async function createPaperPage(input, signal) {
|
|
1049
|
+
const rawArgs = { fileId: input?.fileId, name: input?.name };
|
|
1050
|
+
let fileId;
|
|
1051
|
+
let name;
|
|
1052
|
+
try {
|
|
1053
|
+
fileId = normalizeRequiredPageValue(input?.fileId, "file ID");
|
|
1054
|
+
name = normalizeRequiredPageValue(input?.name, "page name");
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1057
|
+
throw pageOperationError("create", "INVALID_INPUT", message, "create_page", rawArgs);
|
|
1058
|
+
}
|
|
1059
|
+
const providerArgs = { fileId, name };
|
|
1060
|
+
const result = await callPaperMcp("paper_create_page", providerArgs, signal);
|
|
1061
|
+
if (!result.ok) {
|
|
1062
|
+
throw pageOperationError("create", providerFailureCode(result), result.error, "create_page", providerArgs);
|
|
1063
|
+
}
|
|
1064
|
+
const parsed = parseMcpJsonObject(result.result);
|
|
1065
|
+
const pageId = pageIdFromProviderResult(result.result);
|
|
1066
|
+
const returnedName = typeof parsed?.name === "string" ? parsed.name.trim() : void 0;
|
|
1067
|
+
if (!pageId) {
|
|
1068
|
+
throw pageOperationError(
|
|
1069
|
+
"create",
|
|
1070
|
+
"PROVIDER_SCHEMA_DRIFT",
|
|
1071
|
+
"Paper MCP tool create_page succeeded but returned no usable page ID.",
|
|
1072
|
+
"create_page",
|
|
1073
|
+
providerArgs
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return { fileId, pageId, name: returnedName || name, created: true };
|
|
1077
|
+
}
|
|
1078
|
+
async function openPaperPage(input, signal) {
|
|
1079
|
+
const rawArgs = { fileId: input?.fileId, pageId: input?.pageId };
|
|
1080
|
+
let fileId;
|
|
1081
|
+
let pageId;
|
|
1082
|
+
try {
|
|
1083
|
+
fileId = normalizeRequiredPageValue(input?.fileId, "file ID");
|
|
1084
|
+
pageId = normalizeRequiredPageValue(input?.pageId, "page ID");
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1087
|
+
throw pageOperationError("open", "INVALID_INPUT", message, "open_file", rawArgs);
|
|
1088
|
+
}
|
|
1089
|
+
const providerArgs = { fileId, pageId };
|
|
1090
|
+
const result = await callPaperMcp("paper_open_page", providerArgs, signal);
|
|
1091
|
+
if (!result.ok) {
|
|
1092
|
+
throw pageOperationError("open", providerFailureCode(result), result.error, "open_file", providerArgs);
|
|
1093
|
+
}
|
|
1094
|
+
const parsed = parseMcpJsonObject(result.result);
|
|
1095
|
+
const activePageId = typeof parsed?.pageId === "string" ? parsed.pageId.trim() : "";
|
|
1096
|
+
const activeUrl = typeof parsed?.url === "string" ? parsed.url.trim() : "";
|
|
1097
|
+
const expectedFileId = fileIdFromPaperTarget(fileId);
|
|
1098
|
+
const activeFileId = fileIdFromPaperTarget(activeUrl);
|
|
1099
|
+
if (!activePageId || activePageId !== pageId || !expectedFileId || activeFileId !== expectedFileId) {
|
|
1100
|
+
throw pageOperationError(
|
|
1101
|
+
"open",
|
|
1102
|
+
"PROVIDER_SCHEMA_DRIFT",
|
|
1103
|
+
"Paper MCP tool open_file did not prove the requested file and page became active.",
|
|
1104
|
+
"open_file",
|
|
1105
|
+
providerArgs
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
return { fileId, pageId, opened: true };
|
|
1109
|
+
}
|
|
1110
|
+
function pageOperationToolResult(receipt) {
|
|
1111
|
+
return {
|
|
1112
|
+
content: [{ type: "text", text: JSON.stringify(receipt, null, 2) }],
|
|
1113
|
+
details: { receipt }
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function pageOperationToolError(error) {
|
|
1117
|
+
if (error instanceof PaperPageOperationError) {
|
|
1118
|
+
const receipt = toPaperPageFailureReceipt(error);
|
|
1119
|
+
return {
|
|
1120
|
+
content: [{ type: "text", text: JSON.stringify(receipt, null, 2) }],
|
|
1121
|
+
details: { receipt, code: error.code, error: error.message }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1125
|
+
return { content: [{ type: "text", text: message }], details: { error: message } };
|
|
1126
|
+
}
|
|
1127
|
+
async function readNodeSnapshot(id, fileId, signal) {
|
|
1128
|
+
const result = await callRequiredPaperMcp("paper_get_node_info", { fileId, nodeId: id }, signal);
|
|
1129
|
+
const parsed = parseMcpJsonObject(result);
|
|
1130
|
+
const parsedId = typeof parsed?.id === "string" ? parsed.id.trim() : "";
|
|
1131
|
+
const name = typeof parsed?.name === "string" ? parsed.name.trim() : void 0;
|
|
1132
|
+
if (!parsed || parsedId !== id || name === void 0) {
|
|
1133
|
+
throw new Error(`Paper returned no usable node snapshot for ${id}.`);
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
id: parsedId,
|
|
1137
|
+
name,
|
|
1138
|
+
parentId: typeof parsed.parentId === "string" ? parsed.parentId : null,
|
|
1139
|
+
artboardId: typeof parsed.artboardId === "string" ? parsed.artboardId : null
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
async function readPaperFileKey(fileId, signal) {
|
|
1143
|
+
const parsed = await verifyPaperFileTarget(fileId, signal);
|
|
1144
|
+
const url = typeof parsed?.url === "string" ? parsed.url.trim() : "";
|
|
1145
|
+
const rootNodeId = typeof parsed?.rootNodeId === "string" ? parsed.rootNodeId.trim() : "";
|
|
1146
|
+
if (!url || !rootNodeId) throw new Error("Paper returned no stable file identity for session provenance.");
|
|
1147
|
+
return `${url}#${rootNodeId}`;
|
|
1148
|
+
}
|
|
1149
|
+
async function beginMaterializationScope(fileId, signal) {
|
|
1150
|
+
const currentFileKey = await readPaperFileKey(fileId, signal);
|
|
1151
|
+
if (sessionFileKey && sessionFileKey !== currentFileKey) sessionCreatedNodeIds.clear();
|
|
1152
|
+
sessionFileKey = currentFileKey;
|
|
1153
|
+
}
|
|
1154
|
+
async function reconcileSessionFileScope(fileId, signal) {
|
|
1155
|
+
if (sessionCreatedNodeIds.size === 0) return;
|
|
1156
|
+
const currentFileKey = await readPaperFileKey(fileId, signal);
|
|
1157
|
+
if (sessionFileKey !== currentFileKey) sessionCreatedNodeIds.clear();
|
|
1158
|
+
sessionFileKey = currentFileKey;
|
|
1159
|
+
}
|
|
1160
|
+
async function verifyNodeAbsent(id, fileId, signal) {
|
|
1161
|
+
const response = await callPaperMcp("paper_get_node_info", { fileId, nodeId: id }, signal);
|
|
1162
|
+
if (!response.ok) {
|
|
1163
|
+
if (response.failure === "contract" && /not found|deleted/i.test(response.error)) return;
|
|
1164
|
+
throw new Error(response.error);
|
|
1165
|
+
}
|
|
1166
|
+
const providerError = getMcpProviderError(response.result);
|
|
1167
|
+
if (providerError && /not found|deleted/i.test(providerError)) return;
|
|
1168
|
+
throw new Error(`Paper still returned node ${id} after deletion.`);
|
|
1169
|
+
}
|
|
1170
|
+
function normalizeNodeIds(value) {
|
|
1171
|
+
if (!Array.isArray(value)) throw new TypeError("At least one Paper node ID is required.");
|
|
1172
|
+
if (value.some((entry) => typeof entry !== "string" || !entry.trim())) {
|
|
1173
|
+
throw new TypeError("Every Paper node ID must be a non-empty string.");
|
|
1174
|
+
}
|
|
1175
|
+
const ids = value.map((entry) => entry.trim());
|
|
1176
|
+
if (ids.length === 0) throw new TypeError("At least one Paper node ID is required.");
|
|
1177
|
+
if (ids.length > 50) throw new TypeError("Operator mutations are limited to 50 Paper nodes per call.");
|
|
1178
|
+
if (new Set(ids).size !== ids.length) throw new TypeError("Each Paper node ID may appear only once per call.");
|
|
1179
|
+
return ids;
|
|
1180
|
+
}
|
|
1181
|
+
function normalizeRenameUpdates(value) {
|
|
1182
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1183
|
+
throw new TypeError("At least one Paper rename update is required.");
|
|
1184
|
+
}
|
|
1185
|
+
if (value.length > 50) throw new TypeError("Operator mutations are limited to 50 Paper nodes per call.");
|
|
1186
|
+
const updates = value.map((entry) => {
|
|
1187
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1188
|
+
throw new TypeError("Each rename update requires a node id and a non-empty name.");
|
|
1189
|
+
}
|
|
1190
|
+
const record = entry;
|
|
1191
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1192
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
1193
|
+
if (!id || !name) throw new TypeError("Each rename update requires a node id and a non-empty name.");
|
|
1194
|
+
return { id, name };
|
|
1195
|
+
});
|
|
1196
|
+
if (new Set(updates.map((update) => update.id)).size !== updates.length) {
|
|
1197
|
+
throw new TypeError("Each Paper node may appear only once in a rename call.");
|
|
1198
|
+
}
|
|
1199
|
+
return updates;
|
|
1200
|
+
}
|
|
1201
|
+
function normalizeLiteralHtml(value) {
|
|
1202
|
+
const html = typeof value === "string" ? value.trim() : "";
|
|
1203
|
+
if (!html) throw new TypeError("Revision requires non-empty literal inline-CSS HTML.");
|
|
1204
|
+
if (!/<[a-z][^>]*\sstyle\s*=/i.test(html)) {
|
|
1205
|
+
throw new TypeError("Revision HTML must use explicit inline style attributes.");
|
|
1206
|
+
}
|
|
1207
|
+
if (/\bclass(?:Name)?\s*=/i.test(html) || /var\s*\(/i.test(html) || /<(?:script|style)\b/i.test(html)) {
|
|
1208
|
+
throw new TypeError(
|
|
1209
|
+
"Revision accepts literal inline-CSS HTML only (no classes, CSS variables, style blocks, or scripts)."
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
const relativeDimension = /(?:^|;)\s*(?:width|height|min-width|max-width|min-height|max-height|top|right|bottom|left|margin(?:-[a-z]+)?|padding(?:-[a-z]+)?|gap|font-size|line-height|letter-spacing)\s*:[^;"]*(?:-?(?:\d+(?:\.\d+)?|\.\d+)(?:%|em|rem|vw|vh|vmin|vmax))(?=\s|;|$)/i;
|
|
1213
|
+
for (const match of html.matchAll(/\bstyle\s*=\s*(["'])(.*?)\1/gis)) {
|
|
1214
|
+
if (relativeDimension.test(match[2] ?? "")) {
|
|
1215
|
+
throw new TypeError(
|
|
1216
|
+
"Revision layout and typography values must be concrete pixels, numbers, or literal colors."
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return html;
|
|
1221
|
+
}
|
|
1222
|
+
function normalizeIncrementalHtml(value) {
|
|
1223
|
+
const html = typeof value === "string" ? value.trim() : "";
|
|
1224
|
+
if (!html) throw new TypeError("paper_write_html requires non-empty HTML.");
|
|
1225
|
+
if (!/<[a-z][^>]*\sstyle\s*=/i.test(html)) {
|
|
1226
|
+
throw new TypeError(
|
|
1227
|
+
"paper_write_html requires inline style attributes so Paper does not create invisible 0\xD70 nodes."
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (/<(?:script|style)\b/i.test(html)) {
|
|
1231
|
+
throw new TypeError("paper_write_html does not accept script or style blocks; use literal inline styles.");
|
|
1232
|
+
}
|
|
1233
|
+
return html;
|
|
1234
|
+
}
|
|
1235
|
+
var PAPER_TOKEN_TYPES = [
|
|
1236
|
+
"breakpoint",
|
|
1237
|
+
"color",
|
|
1238
|
+
"container",
|
|
1239
|
+
"fontFamily",
|
|
1240
|
+
"fontSize",
|
|
1241
|
+
"fontWeight",
|
|
1242
|
+
"letterSpacing",
|
|
1243
|
+
"lineHeight",
|
|
1244
|
+
"radius",
|
|
1245
|
+
"spacing"
|
|
1246
|
+
];
|
|
1247
|
+
var paperTokenTypeSet = new Set(PAPER_TOKEN_TYPES);
|
|
1248
|
+
var TOKEN_NAME_PATTERN = /^--[a-zA-Z0-9_-]+$/;
|
|
1249
|
+
var TOKEN_ALIAS_REJECTED_TYPES = /* @__PURE__ */ new Set(["fontSize", "radius", "spacing"]);
|
|
1250
|
+
function normalizeTokenName(value, label) {
|
|
1251
|
+
const name = typeof value === "string" ? value.trim() : "";
|
|
1252
|
+
if (!TOKEN_NAME_PATTERN.test(name)) {
|
|
1253
|
+
throw new TypeError(`${label} must be a CSS custom-property name such as --color-primary.`);
|
|
1254
|
+
}
|
|
1255
|
+
return name;
|
|
1256
|
+
}
|
|
1257
|
+
function normalizeTokenDescription(value) {
|
|
1258
|
+
if (value === void 0) return void 0;
|
|
1259
|
+
if (typeof value !== "string" || value.length > 1024) {
|
|
1260
|
+
throw new TypeError("Paper token descriptions must be strings no longer than 1024 characters.");
|
|
1261
|
+
}
|
|
1262
|
+
return value;
|
|
1263
|
+
}
|
|
1264
|
+
function validatePaperTokenValue(type, value) {
|
|
1265
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
1266
|
+
throw new TypeError(`Paper token ${type} values must be strings or numbers.`);
|
|
1267
|
+
}
|
|
1268
|
+
if (typeof value === "string" && /color-mix\s*\(/i.test(value)) {
|
|
1269
|
+
throw new TypeError(
|
|
1270
|
+
"Paper Desktop currently rejects color-mix() token values. Precompute a literal and preserve the original expression in description; upstream support is required to retain live provenance."
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
if (typeof value === "string" && /var\s*\(/i.test(value) && TOKEN_ALIAS_REJECTED_TYPES.has(type)) {
|
|
1274
|
+
throw new TypeError(
|
|
1275
|
+
`Paper Desktop currently rejects var() aliases for ${type} tokens despite its schema text. Use a computed literal plus an expression note until the provider fixes alias support.`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
return value;
|
|
1279
|
+
}
|
|
1280
|
+
function normalizeCreateTokens(value) {
|
|
1281
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1282
|
+
throw new TypeError("paper_create_tokens requires at least one token.");
|
|
1283
|
+
}
|
|
1284
|
+
if (value.length > 500) throw new TypeError("Paper token mutations are limited to 500 entries per call.");
|
|
1285
|
+
return value.map((entry) => {
|
|
1286
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1287
|
+
throw new TypeError("Each Paper token requires type, name, and value.");
|
|
1288
|
+
}
|
|
1289
|
+
const record = entry;
|
|
1290
|
+
if (typeof record.type !== "string" || !paperTokenTypeSet.has(record.type)) {
|
|
1291
|
+
throw new TypeError(
|
|
1292
|
+
`Unsupported Paper token type ${JSON.stringify(record.type)}. Supported types: ${PAPER_TOKEN_TYPES.join(", ")}. Shadow, duration, easing, and theme dimensions are not available upstream.`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const type = record.type;
|
|
1296
|
+
return {
|
|
1297
|
+
type,
|
|
1298
|
+
name: normalizeTokenName(record.name, "Paper token name"),
|
|
1299
|
+
value: validatePaperTokenValue(type, record.value),
|
|
1300
|
+
...record.description !== void 0 ? { description: normalizeTokenDescription(record.description) } : {}
|
|
1301
|
+
};
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
function normalizeSetTokens(value) {
|
|
1305
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1306
|
+
throw new TypeError("paper_set_tokens requires at least one token update.");
|
|
1307
|
+
}
|
|
1308
|
+
if (value.length > 500) throw new TypeError("Paper token mutations are limited to 500 entries per call.");
|
|
1309
|
+
return value.map((entry) => {
|
|
1310
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1311
|
+
throw new TypeError("Each Paper token update requires a token name.");
|
|
1312
|
+
}
|
|
1313
|
+
const record = entry;
|
|
1314
|
+
const name = normalizeTokenName(record.name, "Paper token name");
|
|
1315
|
+
const newName = record.newName === void 0 ? void 0 : normalizeTokenName(record.newName, "New token name");
|
|
1316
|
+
const description = normalizeTokenDescription(record.description);
|
|
1317
|
+
const deleting = record.delete === true;
|
|
1318
|
+
if (record.delete !== void 0 && typeof record.delete !== "boolean") {
|
|
1319
|
+
throw new TypeError("Paper token delete must be a boolean when provided.");
|
|
1320
|
+
}
|
|
1321
|
+
if (deleting && (newName !== void 0 || record.value !== void 0 || description !== void 0)) {
|
|
1322
|
+
throw new TypeError(`Delete update for ${name} cannot also rename or change its value/description.`);
|
|
1323
|
+
}
|
|
1324
|
+
if (!deleting && newName === void 0 && record.value === void 0 && description === void 0) {
|
|
1325
|
+
throw new TypeError(`Token update for ${name} must set newName, value, description, or delete=true.`);
|
|
1326
|
+
}
|
|
1327
|
+
if (typeof record.value === "string" && /(?:color-mix|var)\s*\(/i.test(record.value)) {
|
|
1328
|
+
throw new TypeError(
|
|
1329
|
+
`Paper Desktop may reject expression value ${record.value} without exposing the token type through set_tokens. Use a computed literal and preserve the source expression in description.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (record.value !== void 0 && typeof record.value !== "string" && typeof record.value !== "number") {
|
|
1333
|
+
throw new TypeError(`Paper token value for ${name} must be a string or number.`);
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
name,
|
|
1337
|
+
...newName !== void 0 ? { newName } : {},
|
|
1338
|
+
...record.value !== void 0 ? { value: record.value } : {},
|
|
1339
|
+
...description !== void 0 ? { description } : {},
|
|
1340
|
+
...deleting ? { delete: true } : {}
|
|
1341
|
+
};
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
function nonEmptyString(value, label) {
|
|
1345
|
+
if (typeof value !== "string" || !value.trim()) throw new TypeError(`${label} must be a non-empty string.`);
|
|
1346
|
+
return value.trim();
|
|
1347
|
+
}
|
|
1348
|
+
function normalizeMoves(value) {
|
|
1349
|
+
if (!Array.isArray(value) || value.length === 0) throw new TypeError("paper_move_nodes requires at least one move.");
|
|
1350
|
+
if (value.length > 50) throw new TypeError("Paper move batches are limited to 50 entries.");
|
|
1351
|
+
const moves = value.map((entry) => {
|
|
1352
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1353
|
+
throw new TypeError("Each Paper move must be an object.");
|
|
1354
|
+
}
|
|
1355
|
+
const record = entry;
|
|
1356
|
+
const nodeId = nonEmptyString(record.nodeId, "Move nodeId");
|
|
1357
|
+
const shapes = [record.before !== void 0, record.after !== void 0, record.parentId !== void 0].filter(
|
|
1358
|
+
Boolean
|
|
1359
|
+
);
|
|
1360
|
+
if (shapes.length !== 1) {
|
|
1361
|
+
throw new TypeError(`Move for ${nodeId} must use exactly one of before, after, or parentId.`);
|
|
1362
|
+
}
|
|
1363
|
+
if (record.before !== void 0) return { nodeId, before: nonEmptyString(record.before, "Move before") };
|
|
1364
|
+
if (record.after !== void 0) return { nodeId, after: nonEmptyString(record.after, "Move after") };
|
|
1365
|
+
const parentId = nonEmptyString(record.parentId, "Move parentId");
|
|
1366
|
+
if (record.index !== void 0 && (!Number.isInteger(record.index) || record.index < 0)) {
|
|
1367
|
+
throw new TypeError(`Move index for ${nodeId} must be a non-negative integer.`);
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
nodeId,
|
|
1371
|
+
parentId,
|
|
1372
|
+
...record.index !== void 0 ? { index: record.index } : {}
|
|
1373
|
+
};
|
|
1374
|
+
});
|
|
1375
|
+
if (new Set(moves.map((move) => move.nodeId)).size !== moves.length) {
|
|
1376
|
+
throw new TypeError("Each Paper node may appear only once in a guarded move batch.");
|
|
1377
|
+
}
|
|
1378
|
+
return moves;
|
|
1379
|
+
}
|
|
1380
|
+
function extractCreatedNodes(result) {
|
|
1381
|
+
const parsed = parseMcpJsonValue(result);
|
|
1382
|
+
const record = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1383
|
+
const entries = Array.isArray(record.createdNodes) ? record.createdNodes : [];
|
|
1384
|
+
return entries.flatMap((entry) => {
|
|
1385
|
+
if (typeof entry === "string" && entry.trim()) return [{ id: entry.trim() }];
|
|
1386
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
1387
|
+
const node = entry;
|
|
1388
|
+
const id = typeof node.id === "string" ? node.id.trim() : typeof node.nodeId === "string" ? node.nodeId.trim() : "";
|
|
1389
|
+
if (!id) return [];
|
|
1390
|
+
return [{ id, ...typeof node.name === "string" && node.name.trim() ? { name: node.name.trim() } : {} }];
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
function providerMutationEntries(result) {
|
|
1394
|
+
const parsed = parseMcpJsonValue(result);
|
|
1395
|
+
if (Array.isArray(parsed)) {
|
|
1396
|
+
return parsed.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
1397
|
+
}
|
|
1398
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
1399
|
+
const record = parsed;
|
|
1400
|
+
const entries = Array.isArray(record.results) ? record.results : Array.isArray(record.tokens) ? record.tokens : [];
|
|
1401
|
+
return entries.filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
1402
|
+
}
|
|
1403
|
+
function mutationEntryFailed(entry) {
|
|
1404
|
+
return entry.result === "error" || entry.status === "error" || entry.ok === false || typeof entry.error === "string";
|
|
1405
|
+
}
|
|
1406
|
+
function captureRouteSlug(route) {
|
|
1407
|
+
const slug = route.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1408
|
+
return slug || "root";
|
|
1409
|
+
}
|
|
1410
|
+
function buildCaptureArtboardName(input) {
|
|
1411
|
+
const suffix = `__${input.width}x${input.height}__${captureRouteSlug(input.theme)}__${input.date}`;
|
|
1412
|
+
const routeBudget = Math.max(1, 50 - suffix.length);
|
|
1413
|
+
return `${captureRouteSlug(input.route).slice(0, routeBudget)}${suffix}`;
|
|
1414
|
+
}
|
|
1415
|
+
function classifyTargets(ids) {
|
|
1416
|
+
const sessionCreatedTargetIds = ids.filter((id) => sessionCreatedNodeIds.has(id));
|
|
1417
|
+
return {
|
|
1418
|
+
sessionCreatedTargetIds,
|
|
1419
|
+
sourceTargetIds: ids.filter((id) => !sessionCreatedNodeIds.has(id))
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function createOperatorReceipt(options) {
|
|
1423
|
+
const locatorSuffix = options.targetIds.length > 0 ? options.targetIds.map(encodeURIComponent).join(",") : "none";
|
|
1424
|
+
const evidence = options.status === "applied" ? options.targetIds.map((id) => ({
|
|
1425
|
+
kind: `paper-${options.action}`,
|
|
1426
|
+
locator: `paper://node/${encodeURIComponent(id)}`,
|
|
1427
|
+
accessClass: "private-corpus"
|
|
1428
|
+
})) : [
|
|
1429
|
+
{
|
|
1430
|
+
kind: `paper-${options.action}-${options.status}`,
|
|
1431
|
+
locator: `paper://operator-control/${options.action}/${locatorSuffix}`,
|
|
1432
|
+
accessClass: "private"
|
|
1433
|
+
}
|
|
1434
|
+
];
|
|
1435
|
+
return {
|
|
1436
|
+
workflowId: options.workflowId,
|
|
1437
|
+
summary: options.summary,
|
|
1438
|
+
evidence,
|
|
1439
|
+
sourceNotes: options.sourceNotes,
|
|
1440
|
+
riskNotes: options.riskNotes,
|
|
1441
|
+
nextActions: options.nextActions,
|
|
1442
|
+
payload: options.payload
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function operatorReceiptResult(receipt, images = []) {
|
|
1446
|
+
return {
|
|
1447
|
+
content: [
|
|
1448
|
+
{ type: "text", text: `${receipt.summary}
|
|
1449
|
+
${JSON.stringify(receipt, null, 2)}` },
|
|
1450
|
+
...images.map((image) => ({ type: "image", ...image }))
|
|
1451
|
+
],
|
|
1452
|
+
details: { receipt }
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function operatorReceiptError(receipt, code, message) {
|
|
1456
|
+
const result = operatorReceiptResult(receipt);
|
|
1457
|
+
return { ...result, details: { receipt, code, error: message } };
|
|
1458
|
+
}
|
|
1459
|
+
function createRenameReceipt(summary, payload) {
|
|
1460
|
+
return createOperatorReceipt({
|
|
1461
|
+
workflowId: "paper.renameNodes.v1",
|
|
1462
|
+
action: "rename-nodes",
|
|
1463
|
+
status: payload.status,
|
|
1464
|
+
summary,
|
|
1465
|
+
targetIds: payload.updates.map((update) => update.id),
|
|
1466
|
+
sourceNotes: [
|
|
1467
|
+
"Paper Desktop remains the source of truth for node names.",
|
|
1468
|
+
"This operation can rename only the listed node IDs; it cannot change styles, content, hierarchy, or delete nodes.",
|
|
1469
|
+
"Session provenance is process- and file-local; after a restart or file switch, every target is treated as source work."
|
|
1470
|
+
],
|
|
1471
|
+
riskNotes: [
|
|
1472
|
+
"A live rename requires confirm=true.",
|
|
1473
|
+
`Targets not created in this session and active file also require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.rename}".`
|
|
1474
|
+
],
|
|
1475
|
+
nextActions: payload.status === "dry-run" ? ["Review the exact IDs and names, then rerun with the required confirmation fields."] : payload.status === "applied" ? ["Use the returned pre/post names as the rename verification record."] : ["Resolve the named gate or error before retrying; do not assume any unverified rename occurred."],
|
|
1476
|
+
payload
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function createReviseReceipt(summary, payload) {
|
|
1480
|
+
return createOperatorReceipt({
|
|
1481
|
+
workflowId: "paper.reviseArtboard.v1",
|
|
1482
|
+
action: "revise-artboard",
|
|
1483
|
+
status: payload.status,
|
|
1484
|
+
summary,
|
|
1485
|
+
targetIds: payload.artboardId ? [payload.artboardId] : [],
|
|
1486
|
+
sourceNotes: [
|
|
1487
|
+
"Paper Desktop remains the source of truth for artboard structure.",
|
|
1488
|
+
"This operation can replace only the children of the named artboard from validated literal inline-CSS HTML.",
|
|
1489
|
+
"It cannot replace the artboard root, touch siblings, use Tailwind classes, resolve CSS variables, or accept relative layout units.",
|
|
1490
|
+
"Session provenance is process- and file-local; after a restart or file switch, every target is treated as source work."
|
|
1491
|
+
],
|
|
1492
|
+
riskNotes: [
|
|
1493
|
+
"A live revision requires confirm=true and screenshots the resulting artboard.",
|
|
1494
|
+
`Artboards not created in this session and active file also require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.revise}".`
|
|
1495
|
+
],
|
|
1496
|
+
nextActions: payload.status === "dry-run" ? ["Review the literal HTML, target artboard, and source-protection gate before confirming."] : payload.status === "applied" ? ["Inspect the returned screenshot before accepting the revised artboard."] : [
|
|
1497
|
+
"Inspect the target artboard before retrying because a failed provider stage may have partially changed its children."
|
|
1498
|
+
],
|
|
1499
|
+
payload
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
function createDeleteReceipt(summary, payload) {
|
|
1503
|
+
return createOperatorReceipt({
|
|
1504
|
+
workflowId: "paper.deleteNodes.v1",
|
|
1505
|
+
action: "delete-nodes",
|
|
1506
|
+
status: payload.status,
|
|
1507
|
+
summary,
|
|
1508
|
+
targetIds: payload.requestedIds,
|
|
1509
|
+
sourceNotes: [
|
|
1510
|
+
"Paper Desktop remains the source of truth for canvas state.",
|
|
1511
|
+
"This operation can delete only the listed node IDs and their descendants; it cannot delete a root or page node.",
|
|
1512
|
+
"Session provenance is process- and file-local; after a restart or file switch, every target is treated as source work."
|
|
1513
|
+
],
|
|
1514
|
+
riskNotes: [
|
|
1515
|
+
"A live delete requires confirm=true and cannot be reversed through this tool.",
|
|
1516
|
+
`Nodes not created in this session and active file also require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.delete}".`
|
|
1517
|
+
],
|
|
1518
|
+
nextActions: payload.status === "dry-run" ? ["Review the exact node IDs before confirming deletion."] : payload.status === "applied" ? ["Retain this receipt as the exact removed-node and absence-verification record."] : [
|
|
1519
|
+
"Inspect Paper before retrying; do not assume a failed or unverified delete left every target unchanged."
|
|
1520
|
+
],
|
|
1521
|
+
payload
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
function createWriteHtmlReceipt(summary, payload) {
|
|
1525
|
+
return createOperatorReceipt({
|
|
1526
|
+
workflowId: "paper.writeHtml.v1",
|
|
1527
|
+
action: "write-html",
|
|
1528
|
+
status: payload.status,
|
|
1529
|
+
summary,
|
|
1530
|
+
targetIds: payload.createdNodes.length > 0 ? payload.createdNodes.map((node) => node.id) : [payload.targetId],
|
|
1531
|
+
sourceNotes: [
|
|
1532
|
+
"Paper Desktop remains the source of truth for created nodes.",
|
|
1533
|
+
"This guarded tool exposes insert-children only; destructive replacement stays behind paper_revise_artboard.",
|
|
1534
|
+
"The receipt preserves write_html.createdNodes so callers never have to guess nodeIds or orphan untracked layers."
|
|
1535
|
+
],
|
|
1536
|
+
riskNotes: [
|
|
1537
|
+
"A live write requires an explicit fileId, dry-run review, and confirm=true.",
|
|
1538
|
+
`Writing into a source target also requires sourceAcknowledgement="write-source".`
|
|
1539
|
+
],
|
|
1540
|
+
nextActions: payload.status === "dry-run" ? ["Review the exact file, parent ID, and HTML before confirming."] : payload.status === "applied" ? ["Use payload.createdNodes as the only authoritative IDs for follow-up work."] : [
|
|
1541
|
+
"Inspect the named provider failure before retrying; do not assume any unreceipted nodes are safe to ignore."
|
|
1542
|
+
],
|
|
1543
|
+
payload
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
function createTokenMutationReceipt(summary, payload) {
|
|
1547
|
+
return createOperatorReceipt({
|
|
1548
|
+
workflowId: `paper.${payload.operation}Tokens.v1`,
|
|
1549
|
+
action: `${payload.operation}-tokens`,
|
|
1550
|
+
status: payload.status,
|
|
1551
|
+
summary,
|
|
1552
|
+
targetIds: [],
|
|
1553
|
+
sourceNotes: [
|
|
1554
|
+
"Paper Desktop stores one flat, file-scoped token namespace with no light/dark dimension.",
|
|
1555
|
+
"Provider per-entry results are preserved verbatim in payload.providerResults."
|
|
1556
|
+
],
|
|
1557
|
+
riskNotes: [
|
|
1558
|
+
"A live token mutation requires an explicit fileId, dry-run review, and confirm=true.",
|
|
1559
|
+
'set_tokens changes or deletes existing source tokens and also requires sourceAcknowledgement="set-source-tokens".'
|
|
1560
|
+
],
|
|
1561
|
+
nextActions: payload.status === "dry-run" ? ["Review supported types, computed literals, descriptions, and exact token names before confirming."] : payload.status === "applied" ? ["Read back the affected names with paper_get_tokens in the same explicit file."] : ["Review every provider result; partial batches may already contain successful entries."],
|
|
1562
|
+
payload
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
function createMoveNodesReceipt(summary, payload) {
|
|
1566
|
+
return createOperatorReceipt({
|
|
1567
|
+
workflowId: "paper.moveNodes.v1",
|
|
1568
|
+
action: "move-nodes",
|
|
1569
|
+
status: payload.status,
|
|
1570
|
+
summary,
|
|
1571
|
+
targetIds: payload.moves.map((move) => move.nodeId),
|
|
1572
|
+
sourceNotes: [
|
|
1573
|
+
"move_nodes preserves node IDs while changing hierarchy or sibling order.",
|
|
1574
|
+
"The public live runtime requires every before, after, or parentId destination to be separately typed and fingerprinted in the approved brief.",
|
|
1575
|
+
"The provider's ambiguous parentId='root' alias and unverified cross-page roots are not authorized by the public live gate."
|
|
1576
|
+
],
|
|
1577
|
+
riskNotes: [
|
|
1578
|
+
'A live move requires an explicit fileId, dry-run review, confirm=true, and sourceAcknowledgement="move-source" for existing nodes.'
|
|
1579
|
+
],
|
|
1580
|
+
nextActions: payload.status === "dry-run" ? ["Canary one cross-page move before a large batch and retain the exact destination root ID."] : payload.status === "applied" ? ["Use affectedParents from the provider result to refresh the moved hierarchy without guessing."] : [
|
|
1581
|
+
"Inspect the file before retrying because sequential move batches may have partially applied upstream."
|
|
1582
|
+
],
|
|
1583
|
+
payload
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
var paperTokenTypeSchema = Type.Union([
|
|
1587
|
+
Type.Literal("breakpoint"),
|
|
1588
|
+
Type.Literal("color"),
|
|
1589
|
+
Type.Literal("container"),
|
|
1590
|
+
Type.Literal("fontFamily"),
|
|
1591
|
+
Type.Literal("fontSize"),
|
|
1592
|
+
Type.Literal("fontWeight"),
|
|
1593
|
+
Type.Literal("letterSpacing"),
|
|
1594
|
+
Type.Literal("lineHeight"),
|
|
1595
|
+
Type.Literal("radius"),
|
|
1596
|
+
Type.Literal("spacing")
|
|
1597
|
+
]);
|
|
1598
|
+
var paperMcpTools = [
|
|
1599
|
+
// ── Read tools ──────────────────────────────────────────────────────────
|
|
1600
|
+
createPaperTool({
|
|
1601
|
+
name: "paper_get_basic_info",
|
|
1602
|
+
label: "Paper: Get Basic Info",
|
|
1603
|
+
description: "Get file name, page name, node count, and list of artboards with dimensions from the currently open Paper file.",
|
|
1604
|
+
parameters: Type.Object({ fileId: optionalFileIdSchema })
|
|
1605
|
+
}),
|
|
1606
|
+
createPaperTool({
|
|
1607
|
+
name: "paper_get_selection",
|
|
1608
|
+
label: "Paper: Get Selection",
|
|
1609
|
+
description: "Get details about the currently selected nodes in Paper Desktop (IDs, names, types, size, artboard). Use this to see what the user is pointing at.",
|
|
1610
|
+
parameters: Type.Object({ fileId: optionalFileIdSchema })
|
|
1611
|
+
}),
|
|
1612
|
+
createPaperTool({
|
|
1613
|
+
name: "paper_get_node_info",
|
|
1614
|
+
label: "Paper: Get Node Info",
|
|
1615
|
+
description: "Get details for a specific node by ID: size, visibility, lock state, parent, children, text content.",
|
|
1616
|
+
parameters: Type.Object({
|
|
1617
|
+
fileId: optionalFileIdSchema,
|
|
1618
|
+
id: Type.String({ description: "Node ID to inspect" })
|
|
1619
|
+
}),
|
|
1620
|
+
mapArgs: (a) => ({ nodeId: a.id })
|
|
1621
|
+
}),
|
|
1622
|
+
createPaperTool({
|
|
1623
|
+
name: "paper_get_children",
|
|
1624
|
+
label: "Paper: Get Children",
|
|
1625
|
+
description: "Get direct children of a node: IDs, names, types, and child counts.",
|
|
1626
|
+
parameters: Type.Object({
|
|
1627
|
+
fileId: optionalFileIdSchema,
|
|
1628
|
+
id: Type.String({ description: "Parent node ID" })
|
|
1629
|
+
}),
|
|
1630
|
+
mapArgs: (a) => ({ nodeId: a.id })
|
|
1631
|
+
}),
|
|
1632
|
+
createPaperTool({
|
|
1633
|
+
name: "paper_get_tree_summary",
|
|
1634
|
+
label: "Paper: Get Tree Summary",
|
|
1635
|
+
description: "Get a bounded summary of one targeted subtree. Depth defaults to 4 and must be 1 through 8; start from an artboard or smaller subtree instead of a Snapshot-scale document root.",
|
|
1636
|
+
parameters: Type.Object({
|
|
1637
|
+
fileId: optionalFileIdSchema,
|
|
1638
|
+
id: Type.String({ description: "Root node ID" }),
|
|
1639
|
+
depth: Type.Optional(
|
|
1640
|
+
Type.Integer({
|
|
1641
|
+
minimum: PAPER_TREE_MIN_DEPTH,
|
|
1642
|
+
maximum: PAPER_TREE_MAX_DEPTH,
|
|
1643
|
+
default: PAPER_TREE_DEFAULT_DEPTH,
|
|
1644
|
+
description: "Max depth to traverse (default 4; minimum 1; maximum 8)"
|
|
1645
|
+
})
|
|
1646
|
+
)
|
|
1647
|
+
}),
|
|
1648
|
+
mapArgs: (a) => ({ nodeId: a.id, depth: boundedTreeSummaryDepth(a.depth) }),
|
|
1649
|
+
responseBudget: highVolumeReadBudget(
|
|
1650
|
+
"Retry with a smaller subtree ID or a shallower depth before continuing the tree walk."
|
|
1651
|
+
)
|
|
1652
|
+
}),
|
|
1653
|
+
createPaperTool({
|
|
1654
|
+
name: "paper_get_screenshot",
|
|
1655
|
+
label: "Paper: Get Screenshot",
|
|
1656
|
+
description: "Screenshot one targeted artboard or node. Start with scale 1; use scale 2 only for a specific detail after the scale-1 read is insufficient.",
|
|
1657
|
+
parameters: Type.Object({
|
|
1658
|
+
fileId: optionalFileIdSchema,
|
|
1659
|
+
id: Type.String({ description: "Node ID to screenshot" }),
|
|
1660
|
+
scale: Type.Optional(
|
|
1661
|
+
Type.Integer({
|
|
1662
|
+
minimum: 1,
|
|
1663
|
+
maximum: 2,
|
|
1664
|
+
default: 1,
|
|
1665
|
+
description: "Scale 1 (default) or 2; start with scale 1"
|
|
1666
|
+
})
|
|
1667
|
+
)
|
|
1668
|
+
}),
|
|
1669
|
+
mapArgs: (a) => ({ nodeId: a.id, ...a.scale !== void 0 ? { scale: a.scale } : {} }),
|
|
1670
|
+
responseBudget: highVolumeReadBudget(
|
|
1671
|
+
"Retry on a smaller artboard or node at scale 1; use scale 2 only for a narrowly targeted detail."
|
|
1672
|
+
)
|
|
1673
|
+
}),
|
|
1674
|
+
createPaperTool({
|
|
1675
|
+
name: "paper_get_jsx",
|
|
1676
|
+
label: "Paper: Get JSX",
|
|
1677
|
+
description: "Get JSX for one targeted node and its descendants. Request an artboard section or smaller subtree rather than a full Snapshot page; choose 'tailwind' or 'inline' styles.",
|
|
1678
|
+
parameters: Type.Object({
|
|
1679
|
+
fileId: optionalFileIdSchema,
|
|
1680
|
+
id: Type.String({ description: "Node ID" }),
|
|
1681
|
+
styleFormat: Type.Optional(Type.String({ description: "'tailwind' or 'inline' (default: tailwind)" }))
|
|
1682
|
+
}),
|
|
1683
|
+
mapArgs: (a) => ({ nodeId: a.id, ...a.styleFormat !== void 0 ? { format: a.styleFormat } : {} }),
|
|
1684
|
+
responseBudget: highVolumeReadBudget(
|
|
1685
|
+
"Retry with a smaller subtree ID, then summarize that section before reading the next one."
|
|
1686
|
+
)
|
|
1687
|
+
}),
|
|
1688
|
+
createPaperTool({
|
|
1689
|
+
name: "paper_get_computed_styles",
|
|
1690
|
+
label: "Paper: Get Computed Styles",
|
|
1691
|
+
description: "Get exact computed CSS values for 1 through 50 targeted nodes. Read only the subtree nodes needed for the current implementation decision.",
|
|
1692
|
+
parameters: Type.Object({
|
|
1693
|
+
fileId: optionalFileIdSchema,
|
|
1694
|
+
ids: Type.Array(Type.String({ minLength: 1 }), {
|
|
1695
|
+
minItems: 1,
|
|
1696
|
+
maxItems: PAPER_COMPUTED_STYLES_MAX_NODE_IDS,
|
|
1697
|
+
description: "Array of 1 through 50 targeted node IDs"
|
|
1698
|
+
})
|
|
1699
|
+
}),
|
|
1700
|
+
mapArgs: (a) => ({ nodeIds: boundedComputedStyleNodeIds(a.ids) }),
|
|
1701
|
+
responseBudget: highVolumeReadBudget(
|
|
1702
|
+
"Retry with fewer node IDs from a smaller subtree, then summarize the relevant style decisions."
|
|
1703
|
+
)
|
|
1704
|
+
}),
|
|
1705
|
+
createPaperTool({
|
|
1706
|
+
name: "paper_get_fill_image",
|
|
1707
|
+
label: "Paper: Get Fill Image",
|
|
1708
|
+
description: "Get image data from one exact node already confirmed to have an image fill. Avoid broad reference roots; the result is base64 JPEG data.",
|
|
1709
|
+
parameters: Type.Object({
|
|
1710
|
+
fileId: optionalFileIdSchema,
|
|
1711
|
+
id: Type.String({ description: "Node ID with image fill" })
|
|
1712
|
+
}),
|
|
1713
|
+
mapArgs: (a) => ({ nodeId: a.id }),
|
|
1714
|
+
responseBudget: highVolumeReadBudget(
|
|
1715
|
+
"Retry with the exact smaller image-fill node; use a targeted scale 1 screenshot when the full fill asset is unnecessary."
|
|
1716
|
+
)
|
|
1717
|
+
}),
|
|
1718
|
+
{
|
|
1719
|
+
name: "paper_find_nodes",
|
|
1720
|
+
label: "Paper: Find Nodes Safely",
|
|
1721
|
+
description: "Find Paper nodes by computed-style filters and/or text in an explicitly targeted file. scopeId must be an exact node ID; the belt preflights it and rejects names or stale IDs instead of returning a misleading empty result.",
|
|
1722
|
+
parameters: Type.Object({
|
|
1723
|
+
fileId: optionalFileIdSchema,
|
|
1724
|
+
scopeId: Type.Optional(
|
|
1725
|
+
Type.String({ description: "Exact Paper node ID whose descendants should be searched; not a node name" })
|
|
1726
|
+
),
|
|
1727
|
+
textValue: Type.Optional(Type.String({ description: "Case-insensitive text match with * wildcards" })),
|
|
1728
|
+
filters: Type.Optional(
|
|
1729
|
+
Type.Array(
|
|
1730
|
+
Type.Object({
|
|
1731
|
+
styleName: Type.Optional(Type.String({ description: "CSS property name or wildcard" })),
|
|
1732
|
+
styleValue: Type.Optional(Type.String({ description: "Literal, token reference, or wildcard" }))
|
|
1733
|
+
}),
|
|
1734
|
+
{ minItems: 1 }
|
|
1735
|
+
)
|
|
1736
|
+
)
|
|
1737
|
+
}),
|
|
1738
|
+
async execute(_toolCallId, params, signal) {
|
|
1739
|
+
const args = params && typeof params === "object" ? params : {};
|
|
1740
|
+
const fixture = args._fixture;
|
|
1741
|
+
if (fixture !== void 0) {
|
|
1742
|
+
return { content: formatMcpResult(fixture), details: { mcpTool: "find_nodes", fixture: true } };
|
|
1743
|
+
}
|
|
1744
|
+
let fileId;
|
|
1745
|
+
let scopeId;
|
|
1746
|
+
try {
|
|
1747
|
+
fileId = optionalFileId(args.fileId);
|
|
1748
|
+
scopeId = args.scopeId === void 0 ? void 0 : nonEmptyString(args.scopeId, "find_nodes scopeId");
|
|
1749
|
+
if (args.textValue === void 0 && args.filters === void 0) {
|
|
1750
|
+
throw new TypeError("paper_find_nodes requires textValue, filters, or both.");
|
|
1751
|
+
}
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1754
|
+
return {
|
|
1755
|
+
content: [{ type: "text", text: message }],
|
|
1756
|
+
details: { code: "INVALID_INPUT", error: message }
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
if (scopeId) {
|
|
1760
|
+
const scopeResult = await callPaperMcp(
|
|
1761
|
+
"paper_get_node_info",
|
|
1762
|
+
{ ...fileId ? { fileId } : {}, nodeId: scopeId },
|
|
1763
|
+
signal
|
|
1764
|
+
);
|
|
1765
|
+
if (!scopeResult.ok) {
|
|
1766
|
+
const message = `Invalid find_nodes scopeId ${JSON.stringify(scopeId)}: scopeId must be an exact Paper node ID, not a layer name. Resolve the ID first with paper_get_basic_info, paper_get_tree_summary, or paper_get_node_info. Provider detail: ${scopeResult.error}`;
|
|
1767
|
+
return {
|
|
1768
|
+
content: [{ type: "text", text: message }],
|
|
1769
|
+
details: { code: "INVALID_SCOPE_ID", error: message, fileId: fileId ?? null, scopeId }
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const providerArgs = {
|
|
1774
|
+
...fileId ? { fileId } : {},
|
|
1775
|
+
...scopeId ? { nodeId: scopeId } : {},
|
|
1776
|
+
...typeof args.textValue === "string" ? { textValue: args.textValue } : {},
|
|
1777
|
+
...Array.isArray(args.filters) ? { filters: args.filters } : {}
|
|
1778
|
+
};
|
|
1779
|
+
const response = await callPaperMcp("paper_find_nodes", providerArgs, signal);
|
|
1780
|
+
if (!response.ok) {
|
|
1781
|
+
return { content: [{ type: "text", text: response.error }], details: { error: response.error } };
|
|
1782
|
+
}
|
|
1783
|
+
return {
|
|
1784
|
+
content: formatMcpResult(response.result),
|
|
1785
|
+
details: { mcpTool: response.desktopToolName, fileId: fileId ?? null, validatedScopeId: scopeId ?? null }
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
createPaperTool({
|
|
1790
|
+
name: "paper_get_font_family_info",
|
|
1791
|
+
label: "Paper: Get Font Info",
|
|
1792
|
+
description: "Look up whether a font family is available on the user's machine or Google Fonts. Inspect weights and styles.",
|
|
1793
|
+
parameters: Type.Object({
|
|
1794
|
+
fileId: optionalFileIdSchema,
|
|
1795
|
+
family: Type.String({ description: "Font family name to look up" })
|
|
1796
|
+
}),
|
|
1797
|
+
fileIdPassthrough: false,
|
|
1798
|
+
mapArgs: (a) => ({ familyNames: [a.family] })
|
|
1799
|
+
}),
|
|
1800
|
+
createPaperTool({
|
|
1801
|
+
name: "paper_get_guide",
|
|
1802
|
+
label: "Paper: Get Guide",
|
|
1803
|
+
description: "Retrieve guided workflow documentation for a topic (e.g., 'figma-import' for Figma import steps).",
|
|
1804
|
+
parameters: Type.Object({
|
|
1805
|
+
fileId: optionalFileIdSchema,
|
|
1806
|
+
topic: Type.String({ description: "Guide topic name" })
|
|
1807
|
+
}),
|
|
1808
|
+
fileIdPassthrough: false
|
|
1809
|
+
}),
|
|
1810
|
+
createPaperTool({
|
|
1811
|
+
name: "paper_find_placement",
|
|
1812
|
+
label: "Paper: Find Placement",
|
|
1813
|
+
description: "Get suggested x/y coordinates on the canvas to place a new artboard without overlapping existing ones.",
|
|
1814
|
+
parameters: Type.Object({
|
|
1815
|
+
fileId: optionalFileIdSchema,
|
|
1816
|
+
width: Type.Optional(Type.Number({ description: "Artboard width" })),
|
|
1817
|
+
height: Type.Optional(Type.Number({ description: "Artboard height" }))
|
|
1818
|
+
})
|
|
1819
|
+
}),
|
|
1820
|
+
createPaperTool({
|
|
1821
|
+
name: "paper_get_tokens",
|
|
1822
|
+
label: "Paper: Get Tokens",
|
|
1823
|
+
description: "List the explicit file's flat design-token namespace in JSON, CSS, or Tailwind format. Paper currently supports breakpoint, color, container, fontFamily, fontSize, fontWeight, letterSpacing, lineHeight, radius, and spacing only; it has no shadow/duration/easing types or theme dimension.",
|
|
1824
|
+
parameters: Type.Object({
|
|
1825
|
+
fileId: optionalFileIdSchema,
|
|
1826
|
+
types: Type.Optional(Type.Array(paperTokenTypeSchema)),
|
|
1827
|
+
namePattern: Type.Optional(Type.String({ description: "Case-insensitive glob matched against token names" })),
|
|
1828
|
+
format: Type.Optional(Type.Union([Type.Literal("json"), Type.Literal("css"), Type.Literal("tailwind")]))
|
|
1829
|
+
})
|
|
1830
|
+
}),
|
|
1831
|
+
// ── Write tools ─────────────────────────────────────────────────────────
|
|
1832
|
+
createPaperTool({
|
|
1833
|
+
name: "paper_create_artboard",
|
|
1834
|
+
label: "Paper: Create Artboard",
|
|
1835
|
+
description: "Create a new artboard in the Paper file. Optionally specify name, width, height, and other styles.",
|
|
1836
|
+
supportsDryRun: true,
|
|
1837
|
+
parameters: Type.Object({
|
|
1838
|
+
fileId: requiredFileIdSchema,
|
|
1839
|
+
name: Type.Optional(Type.String({ description: "Artboard name" })),
|
|
1840
|
+
styles: Type.Optional(
|
|
1841
|
+
Type.Record(Type.String(), Type.Unknown(), { description: "CSS styles (width, height, etc.)" })
|
|
1842
|
+
),
|
|
1843
|
+
dryRun: dryRunSchema
|
|
1844
|
+
})
|
|
1845
|
+
}),
|
|
1846
|
+
createPaperTool({
|
|
1847
|
+
name: "paper_write_html",
|
|
1848
|
+
label: "Paper: Write HTML",
|
|
1849
|
+
description: "Parse HTML and add or replace nodes in the Paper file. Use mode 'insert-children' to add inside a node, or 'replace' to replace a node's content.",
|
|
1850
|
+
supportsDryRun: true,
|
|
1851
|
+
parameters: Type.Object({
|
|
1852
|
+
fileId: requiredFileIdSchema,
|
|
1853
|
+
id: Type.String({ description: "Target node ID" }),
|
|
1854
|
+
html: Type.String({ description: "HTML string to write" }),
|
|
1855
|
+
mode: Type.Optional(Type.String({ description: "'insert-children' or 'replace'" })),
|
|
1856
|
+
dryRun: dryRunSchema
|
|
1857
|
+
}),
|
|
1858
|
+
mapArgs: (a) => ({ targetNodeId: a.id, html: a.html, ...a.mode !== void 0 ? { mode: a.mode } : {} })
|
|
1859
|
+
}),
|
|
1860
|
+
createPaperTool({
|
|
1861
|
+
name: "paper_set_text_content",
|
|
1862
|
+
label: "Paper: Set Text",
|
|
1863
|
+
description: "Set the text content of one or more Text nodes (batch operation).",
|
|
1864
|
+
supportsDryRun: true,
|
|
1865
|
+
parameters: Type.Object({
|
|
1866
|
+
fileId: requiredFileIdSchema,
|
|
1867
|
+
updates: Type.Array(
|
|
1868
|
+
Type.Object({
|
|
1869
|
+
id: Type.String({ description: "Text node ID" }),
|
|
1870
|
+
text: Type.String({ description: "New text content" })
|
|
1871
|
+
}),
|
|
1872
|
+
{ description: "Array of { id, text } updates" }
|
|
1873
|
+
),
|
|
1874
|
+
dryRun: dryRunSchema
|
|
1875
|
+
}),
|
|
1876
|
+
mapArgs: (a) => ({
|
|
1877
|
+
updates: a.updates.map((u) => ({
|
|
1878
|
+
nodeId: u.id,
|
|
1879
|
+
textContent: u.text
|
|
1880
|
+
}))
|
|
1881
|
+
})
|
|
1882
|
+
}),
|
|
1883
|
+
createPaperTool({
|
|
1884
|
+
name: "paper_rename_nodes",
|
|
1885
|
+
label: "Paper: Rename Nodes",
|
|
1886
|
+
description: "Rename one or more layers in the Paper file (batch operation).",
|
|
1887
|
+
supportsDryRun: true,
|
|
1888
|
+
parameters: Type.Object({
|
|
1889
|
+
fileId: requiredFileIdSchema,
|
|
1890
|
+
updates: Type.Array(
|
|
1891
|
+
Type.Object({
|
|
1892
|
+
id: Type.String({ description: "Node ID" }),
|
|
1893
|
+
name: Type.String({ description: "New layer name" })
|
|
1894
|
+
}),
|
|
1895
|
+
{ description: "Array of { id, name } updates" }
|
|
1896
|
+
),
|
|
1897
|
+
dryRun: dryRunSchema
|
|
1898
|
+
}),
|
|
1899
|
+
mapArgs: (a) => ({
|
|
1900
|
+
updates: a.updates.map((u) => ({
|
|
1901
|
+
nodeId: u.id,
|
|
1902
|
+
name: u.name
|
|
1903
|
+
}))
|
|
1904
|
+
})
|
|
1905
|
+
}),
|
|
1906
|
+
createPaperTool({
|
|
1907
|
+
name: "paper_duplicate_nodes",
|
|
1908
|
+
label: "Paper: Duplicate Nodes",
|
|
1909
|
+
description: "Deep-clone one or more nodes. Returns new IDs and a descendant ID mapping.",
|
|
1910
|
+
supportsDryRun: true,
|
|
1911
|
+
parameters: Type.Object({
|
|
1912
|
+
fileId: requiredFileIdSchema,
|
|
1913
|
+
ids: Type.Array(Type.String(), { description: "Array of node IDs to duplicate" }),
|
|
1914
|
+
dryRun: dryRunSchema
|
|
1915
|
+
}),
|
|
1916
|
+
mapArgs: (a) => ({
|
|
1917
|
+
nodes: a.ids.map((id) => ({ id }))
|
|
1918
|
+
})
|
|
1919
|
+
}),
|
|
1920
|
+
createPaperTool({
|
|
1921
|
+
name: "paper_update_styles",
|
|
1922
|
+
label: "Paper: Update Styles",
|
|
1923
|
+
description: "Update CSS styles on one or more nodes in the Paper file.",
|
|
1924
|
+
supportsDryRun: true,
|
|
1925
|
+
parameters: Type.Object({
|
|
1926
|
+
fileId: requiredFileIdSchema,
|
|
1927
|
+
updates: Type.Array(
|
|
1928
|
+
Type.Object({
|
|
1929
|
+
id: Type.String({ description: "Node ID" }),
|
|
1930
|
+
styles: Type.Record(Type.String(), Type.Unknown(), { description: "CSS styles to apply" })
|
|
1931
|
+
}),
|
|
1932
|
+
{ description: "Array of { id, styles } updates" }
|
|
1933
|
+
),
|
|
1934
|
+
dryRun: dryRunSchema
|
|
1935
|
+
}),
|
|
1936
|
+
mapArgs: (a) => ({
|
|
1937
|
+
updates: a.updates.map((u) => ({
|
|
1938
|
+
nodeIds: [u.id],
|
|
1939
|
+
styles: u.styles
|
|
1940
|
+
}))
|
|
1941
|
+
})
|
|
1942
|
+
}),
|
|
1943
|
+
createPaperTool({
|
|
1944
|
+
name: "paper_delete_nodes",
|
|
1945
|
+
label: "Paper: Delete Nodes",
|
|
1946
|
+
description: "Delete one or more nodes and all their descendants from the Paper file.",
|
|
1947
|
+
supportsDryRun: true,
|
|
1948
|
+
parameters: Type.Object({
|
|
1949
|
+
fileId: requiredFileIdSchema,
|
|
1950
|
+
ids: Type.Array(Type.String(), { description: "Array of node IDs to delete" }),
|
|
1951
|
+
dryRun: dryRunSchema
|
|
1952
|
+
}),
|
|
1953
|
+
mapArgs: (a) => ({ nodeIds: a.ids })
|
|
1954
|
+
}),
|
|
1955
|
+
createPaperTool({
|
|
1956
|
+
name: "paper_finish_working_on_nodes",
|
|
1957
|
+
label: "Paper: Finish Working",
|
|
1958
|
+
description: "Clear the working indicator from artboards. Call this after you're done modifying artboards.",
|
|
1959
|
+
supportsDryRun: true,
|
|
1960
|
+
parameters: Type.Object({
|
|
1961
|
+
fileId: requiredFileIdSchema,
|
|
1962
|
+
ids: Type.Array(Type.String(), { description: "Array of artboard IDs to unmark" }),
|
|
1963
|
+
dryRun: dryRunSchema
|
|
1964
|
+
}),
|
|
1965
|
+
mapArgs: (a) => a.ids ? { nodeIds: a.ids } : {}
|
|
1966
|
+
}),
|
|
1967
|
+
// ── Turf-wrapped tools ───────────────────────────────────────────────────
|
|
1968
|
+
{
|
|
1969
|
+
name: "paper_mcp_reachability_check",
|
|
1970
|
+
label: "Paper: MCP Reachability Check",
|
|
1971
|
+
description: "Check whether Paper Desktop's MCP server is reachable on http://127.0.0.1:29979/mcp. Use before other Paper tools to decide whether to use live Paper state or a fixture.",
|
|
1972
|
+
parameters: Type.Object({ fileId: optionalFileIdSchema }),
|
|
1973
|
+
async execute(_toolCallId, params, _signal) {
|
|
1974
|
+
const args = params && typeof params === "object" ? params : {};
|
|
1975
|
+
let fileId;
|
|
1976
|
+
try {
|
|
1977
|
+
fileId = optionalFileId(args.fileId);
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1980
|
+
return { content: [{ type: "text", text: message }], details: { error: message } };
|
|
1981
|
+
}
|
|
1982
|
+
const reachable = await isPaperRunning(fileId);
|
|
1983
|
+
const text = reachable ? fileId ? `Paper Desktop MCP server is reachable and file ${fileId} can be targeted.` : "Paper Desktop MCP server is reachable." : "Paper Desktop is not running. Open a file in Paper Desktop to start the MCP server (http://127.0.0.1:29979/mcp).";
|
|
1984
|
+
return {
|
|
1985
|
+
content: [{ type: "text", text }],
|
|
1986
|
+
details: { reachable, fileId: fileId ?? null }
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: "paper_capture_provenance",
|
|
1992
|
+
label: "Paper: Prepare Capture Provenance",
|
|
1993
|
+
description: "Prepare a deterministic Paper capture/import name and provenance record before capture. The generated <=50-character artboardName carries route, viewport, theme, and UTC date; the receipt retains the full source URL for a sidecar manifest.",
|
|
1994
|
+
parameters: Type.Object({
|
|
1995
|
+
fileId: requiredFileIdSchema,
|
|
1996
|
+
sourceUrl: Type.String({ minLength: 1, description: "Full URL of the captured source" }),
|
|
1997
|
+
route: Type.Optional(
|
|
1998
|
+
Type.String({ minLength: 1, description: "Canonical route; defaults to the URL pathname" })
|
|
1999
|
+
),
|
|
2000
|
+
viewport: Type.Object({
|
|
2001
|
+
width: Type.Integer({ minimum: 1, maximum: 1e4 }),
|
|
2002
|
+
height: Type.Integer({ minimum: 1, maximum: 1e4 })
|
|
2003
|
+
}),
|
|
2004
|
+
theme: Type.String({ minLength: 1, description: "Theme label such as light, dark, system, or high-contrast" }),
|
|
2005
|
+
capturedAt: Type.Optional(
|
|
2006
|
+
Type.String({ description: "ISO-8601 capture timestamp; defaults to the current time" })
|
|
2007
|
+
)
|
|
2008
|
+
}),
|
|
2009
|
+
async execute(_toolCallId, params) {
|
|
2010
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2011
|
+
try {
|
|
2012
|
+
const fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2013
|
+
const sourceUrl = nonEmptyString(args.sourceUrl, "Capture sourceUrl");
|
|
2014
|
+
const parsedUrl = new URL(sourceUrl);
|
|
2015
|
+
const viewport = args.viewport && typeof args.viewport === "object" && !Array.isArray(args.viewport) ? args.viewport : {};
|
|
2016
|
+
const width = viewport.width;
|
|
2017
|
+
const height = viewport.height;
|
|
2018
|
+
if (typeof width !== "number" || !Number.isInteger(width) || width < 1 || width > 1e4 || typeof height !== "number" || !Number.isInteger(height) || height < 1 || height > 1e4) {
|
|
2019
|
+
throw new TypeError("Capture viewport width and height must be integers from 1 to 10000.");
|
|
2020
|
+
}
|
|
2021
|
+
const route = args.route === void 0 ? `${parsedUrl.pathname || "/"}${parsedUrl.search}` : nonEmptyString(args.route, "Capture route");
|
|
2022
|
+
const theme = nonEmptyString(args.theme, "Capture theme");
|
|
2023
|
+
const capturedAt = args.capturedAt === void 0 ? (/* @__PURE__ */ new Date()).toISOString() : nonEmptyString(args.capturedAt, "capturedAt");
|
|
2024
|
+
const parsedCapturedAt = new Date(capturedAt);
|
|
2025
|
+
if (Number.isNaN(parsedCapturedAt.valueOf()))
|
|
2026
|
+
throw new TypeError("capturedAt must be a valid ISO-8601 timestamp.");
|
|
2027
|
+
const normalizedCapturedAt = parsedCapturedAt.toISOString();
|
|
2028
|
+
const date = normalizedCapturedAt.slice(0, 10);
|
|
2029
|
+
const artboardName = buildCaptureArtboardName({ route, width, height, theme, date });
|
|
2030
|
+
const provenance = {
|
|
2031
|
+
schemaVersion: "paper-capture-provenance.v1",
|
|
2032
|
+
fileId,
|
|
2033
|
+
sourceUrl: parsedUrl.toString(),
|
|
2034
|
+
route,
|
|
2035
|
+
viewport: { width, height },
|
|
2036
|
+
theme,
|
|
2037
|
+
capturedAt: normalizedCapturedAt,
|
|
2038
|
+
artboardName,
|
|
2039
|
+
namingConvention: "{routeSlug}__{width}x{height}__{theme}__{YYYY-MM-DD}"
|
|
2040
|
+
};
|
|
2041
|
+
return {
|
|
2042
|
+
content: [{ type: "text", text: JSON.stringify(provenance, null, 2) }],
|
|
2043
|
+
details: { provenance }
|
|
2044
|
+
};
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2047
|
+
return {
|
|
2048
|
+
content: [{ type: "text", text: message }],
|
|
2049
|
+
details: { code: "INVALID_INPUT", error: message }
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
name: "paper_style_tokens_extract",
|
|
2056
|
+
label: "Paper: Extract Style Tokens",
|
|
2057
|
+
description: "Extract design tokens (colors, typography, spacing, fonts) from one or more Paper nodes by reading computed styles and font metadata, then emit a JSON token file. Useful for grounding code implementation in actual Paper styles.",
|
|
2058
|
+
parameters: Type.Object({
|
|
2059
|
+
fileId: requiredFileIdSchema,
|
|
2060
|
+
ids: Type.Array(Type.String(), { description: "Array of Paper node IDs to extract tokens from" }),
|
|
2061
|
+
outputPath: Type.Optional(
|
|
2062
|
+
Type.String({
|
|
2063
|
+
description: "Absolute path for the token JSON file. Live extraction rejects relative or omitted paths so session cwd and process cwd cannot diverge."
|
|
2064
|
+
})
|
|
2065
|
+
),
|
|
2066
|
+
dryRun: dryRunSchema
|
|
2067
|
+
}),
|
|
2068
|
+
async execute(_toolCallId, params, signal) {
|
|
2069
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2070
|
+
let fileId;
|
|
2071
|
+
try {
|
|
2072
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2075
|
+
return { content: [{ type: "text", text: message }], details: { error: message } };
|
|
2076
|
+
}
|
|
2077
|
+
const ids = Array.isArray(args.ids) ? args.ids : [];
|
|
2078
|
+
if (ids.length === 0) {
|
|
2079
|
+
return {
|
|
2080
|
+
content: [{ type: "text", text: "No node IDs provided." }],
|
|
2081
|
+
details: { error: "Missing ids" }
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
const outputPath = typeof args.outputPath === "string" ? args.outputPath.trim() : "";
|
|
2085
|
+
if (args.dryRun === true) {
|
|
2086
|
+
return {
|
|
2087
|
+
content: [
|
|
2088
|
+
{
|
|
2089
|
+
type: "text",
|
|
2090
|
+
text: `Dry run: would extract style tokens from ${ids.length} node(s) and write to ${outputPath || "an explicit absolute output path"}`
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
type: "text",
|
|
2094
|
+
text: JSON.stringify({ meta: { nodeIds: ids, dryRun: true }, tokens: {} }, null, 2)
|
|
2095
|
+
}
|
|
2096
|
+
],
|
|
2097
|
+
details: { dryRun: true, outputPath, tokenCount: 0 }
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
if (!outputPath || !isAbsolute2(outputPath)) {
|
|
2101
|
+
const error = "Live style-token extraction requires an absolute outputPath bound to the approved project scope.";
|
|
2102
|
+
return {
|
|
2103
|
+
content: [{ type: "text", text: error }],
|
|
2104
|
+
details: { code: "ABSOLUTE_OUTPUT_PATH_REQUIRED", error }
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
const stylesResult = await callPaperMcp("paper_get_computed_styles", { fileId, nodeIds: ids }, signal);
|
|
2108
|
+
if (!stylesResult.ok) {
|
|
2109
|
+
return {
|
|
2110
|
+
content: [{ type: "text", text: `Failed to read computed styles: ${stylesResult.error}` }],
|
|
2111
|
+
details: { error: stylesResult.error }
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const styles = parseMcpTextContent(stylesResult.result);
|
|
2115
|
+
let parsedStyles = {};
|
|
2116
|
+
try {
|
|
2117
|
+
parsedStyles = styles ? JSON.parse(styles) : {};
|
|
2118
|
+
} catch {
|
|
2119
|
+
parsedStyles = {};
|
|
2120
|
+
}
|
|
2121
|
+
const families = /* @__PURE__ */ new Set();
|
|
2122
|
+
for (const nodeStyles of Object.values(parsedStyles)) {
|
|
2123
|
+
if (typeof nodeStyles !== "object" || nodeStyles === null) continue;
|
|
2124
|
+
const ns = nodeStyles;
|
|
2125
|
+
if (typeof ns.fontFamily === "string") {
|
|
2126
|
+
for (const family of ns.fontFamily.split(",")) {
|
|
2127
|
+
families.add(family.trim());
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const fontInfo = {};
|
|
2132
|
+
for (const family of families) {
|
|
2133
|
+
const fontResult = await callPaperMcp("paper_get_font_family_info", { familyNames: [family] }, signal);
|
|
2134
|
+
if (fontResult.ok) {
|
|
2135
|
+
fontInfo[family] = parseMcpTextContent(fontResult.result) ?? {};
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const tokens = {
|
|
2139
|
+
meta: {
|
|
2140
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2141
|
+
nodeIds: ids,
|
|
2142
|
+
source: "paper_desktop_computed_styles"
|
|
2143
|
+
},
|
|
2144
|
+
colors: extractColorTokens(parsedStyles),
|
|
2145
|
+
typography: {
|
|
2146
|
+
fontFamilies: fontInfo,
|
|
2147
|
+
fontSizes: collectUniqueValues(parsedStyles, "fontSize"),
|
|
2148
|
+
fontWeights: collectUniqueValues(parsedStyles, "fontWeight"),
|
|
2149
|
+
lineHeights: collectUniqueValues(parsedStyles, "lineHeight"),
|
|
2150
|
+
letterSpacings: collectUniqueValues(parsedStyles, "letterSpacing")
|
|
2151
|
+
},
|
|
2152
|
+
spacing: {
|
|
2153
|
+
margins: collectUniqueValues(parsedStyles, "margin"),
|
|
2154
|
+
paddings: collectUniqueValues(parsedStyles, "padding"),
|
|
2155
|
+
gaps: collectUniqueValues(parsedStyles, "gap")
|
|
2156
|
+
},
|
|
2157
|
+
raw: parsedStyles
|
|
2158
|
+
};
|
|
2159
|
+
const dir = dirname2(outputPath);
|
|
2160
|
+
if (!existsSync2(dir)) {
|
|
2161
|
+
mkdirSync(dir, { recursive: true });
|
|
2162
|
+
}
|
|
2163
|
+
writeFileSync(outputPath, JSON.stringify(tokens, null, 2), "utf-8");
|
|
2164
|
+
return {
|
|
2165
|
+
content: [
|
|
2166
|
+
{ type: "text", text: `Extracted style tokens to ${outputPath}` },
|
|
2167
|
+
{ type: "text", text: JSON.stringify(tokens, null, 2) }
|
|
2168
|
+
],
|
|
2169
|
+
details: { outputPath, tokenCount: Object.keys(parsedStyles).length }
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
name: "paper_materialize_system_direction",
|
|
2175
|
+
label: "Paper: Materialize System Direction",
|
|
2176
|
+
description: "Create a new, non-destructive system-direction artboard from literal inline HTML, place it without overlap, screenshot the result, and return an execution receipt. Always dry-run first.",
|
|
2177
|
+
parameters: Type.Object({
|
|
2178
|
+
fileId: requiredFileIdSchema,
|
|
2179
|
+
name: Type.String({ description: "Name for the new system-direction artboard" }),
|
|
2180
|
+
width: Type.Number({ description: "Artboard width in pixels" }),
|
|
2181
|
+
height: Type.Number({ description: "Artboard height in pixels" }),
|
|
2182
|
+
html: Type.String({ description: "Literal inline-CSS HTML prepared by materializeSystemDirection" }),
|
|
2183
|
+
confirm: Type.Optional(
|
|
2184
|
+
Type.Boolean({ description: "Must be true for a live write after the dry-run receipt is approved" })
|
|
2185
|
+
),
|
|
2186
|
+
dryRun: dryRunSchema
|
|
2187
|
+
}),
|
|
2188
|
+
async execute(_toolCallId, params, signal) {
|
|
2189
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2190
|
+
try {
|
|
2191
|
+
const fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2192
|
+
if (args.confirm === true && args.dryRun !== true) {
|
|
2193
|
+
await beginMaterializationScope(fileId, signal);
|
|
2194
|
+
}
|
|
2195
|
+
const result = await materializeDirection(
|
|
2196
|
+
{
|
|
2197
|
+
name: typeof args.name === "string" ? args.name : "",
|
|
2198
|
+
width: typeof args.width === "number" ? args.width : 0,
|
|
2199
|
+
height: typeof args.height === "number" ? args.height : 0,
|
|
2200
|
+
html: typeof args.html === "string" ? args.html : "",
|
|
2201
|
+
confirm: args.confirm === true,
|
|
2202
|
+
dryRun: args.dryRun === true
|
|
2203
|
+
},
|
|
2204
|
+
{
|
|
2205
|
+
callTool: async (toolName, toolArgs = {}) => {
|
|
2206
|
+
const response = await callPaperMcp(toolName, { ...toolArgs, fileId }, signal);
|
|
2207
|
+
if (!response.ok) throw new Error(response.error);
|
|
2208
|
+
return response.result;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
);
|
|
2212
|
+
if (result.receipt.payload.status === "materialized" && result.receipt.payload.artboardId) {
|
|
2213
|
+
sessionCreatedNodeIds.add(result.receipt.payload.artboardId);
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
content: [
|
|
2217
|
+
{
|
|
2218
|
+
type: "text",
|
|
2219
|
+
text: `${result.receipt.summary}
|
|
2220
|
+
${JSON.stringify(result.receipt, null, 2)}`
|
|
2221
|
+
},
|
|
2222
|
+
...result.screenshot.map((image) => ({ type: "image", ...image }))
|
|
2223
|
+
],
|
|
2224
|
+
details: { receipt: result.receipt }
|
|
2225
|
+
};
|
|
2226
|
+
} catch (error) {
|
|
2227
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2228
|
+
if (error instanceof PaperMaterializationError && error.artboardId) {
|
|
2229
|
+
sessionCreatedNodeIds.add(error.artboardId);
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
content: [{ type: "text", text: detail }],
|
|
2233
|
+
details: {
|
|
2234
|
+
error: detail,
|
|
2235
|
+
...error instanceof PaperMaterializationError ? { stage: error.stage, ...error.artboardId ? { artboardId: error.artboardId } : {} } : {}
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
];
|
|
2242
|
+
var paperOperatorTools = [
|
|
2243
|
+
{
|
|
2244
|
+
name: "paper_create_page",
|
|
2245
|
+
label: "Paper: Create Page",
|
|
2246
|
+
description: "Create one explicitly named page in an explicit Paper file and return its page ID. This does not open, rename, overwrite, or delete any page.",
|
|
2247
|
+
parameters: Type.Object({
|
|
2248
|
+
fileId: Type.String({ minLength: 1, description: "Explicit Paper file ID" }),
|
|
2249
|
+
name: Type.String({ minLength: 1, description: "Non-empty display name for the new page" })
|
|
2250
|
+
}),
|
|
2251
|
+
async execute(_toolCallId, params, signal) {
|
|
2252
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2253
|
+
try {
|
|
2254
|
+
return pageOperationToolResult(
|
|
2255
|
+
await createPaperPage(
|
|
2256
|
+
{
|
|
2257
|
+
fileId: typeof args.fileId === "string" ? args.fileId : "",
|
|
2258
|
+
name: typeof args.name === "string" ? args.name : ""
|
|
2259
|
+
},
|
|
2260
|
+
signal
|
|
2261
|
+
)
|
|
2262
|
+
);
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
return pageOperationToolError(error);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
name: "paper_open_page",
|
|
2270
|
+
label: "Paper: Open Page",
|
|
2271
|
+
description: "Open an explicit Paper file and page, then return a receipt only when Paper reports that exact target as active. This cannot infer a target or alter page contents.",
|
|
2272
|
+
parameters: Type.Object({
|
|
2273
|
+
fileId: Type.String({ minLength: 1, description: "Explicit Paper file ID" }),
|
|
2274
|
+
pageId: Type.String({ minLength: 1, description: "Explicit Paper page ID to make active" })
|
|
2275
|
+
}),
|
|
2276
|
+
async execute(_toolCallId, params, signal) {
|
|
2277
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2278
|
+
try {
|
|
2279
|
+
return pageOperationToolResult(
|
|
2280
|
+
await openPaperPage(
|
|
2281
|
+
{
|
|
2282
|
+
fileId: typeof args.fileId === "string" ? args.fileId : "",
|
|
2283
|
+
pageId: typeof args.pageId === "string" ? args.pageId : ""
|
|
2284
|
+
},
|
|
2285
|
+
signal
|
|
2286
|
+
)
|
|
2287
|
+
);
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
return pageOperationToolError(error);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
},
|
|
2293
|
+
{
|
|
2294
|
+
name: "paper_create_tokens",
|
|
2295
|
+
label: "Paper: Create Tokens Safely",
|
|
2296
|
+
description: "Create file-scoped Paper design tokens through dry-run, explicit confirmation, and a per-entry receipt. The belt rejects unsupported types and known provider-invalid color-mix()/size-alias expressions before transport.",
|
|
2297
|
+
parameters: Type.Object({
|
|
2298
|
+
fileId: requiredFileIdSchema,
|
|
2299
|
+
tokens: Type.Array(
|
|
2300
|
+
Type.Object({
|
|
2301
|
+
type: paperTokenTypeSchema,
|
|
2302
|
+
name: Type.String({ pattern: "^--[a-zA-Z0-9_-]+$" }),
|
|
2303
|
+
value: Type.Union([Type.String(), Type.Number()]),
|
|
2304
|
+
description: Type.Optional(Type.String({ maxLength: 1024 }))
|
|
2305
|
+
}),
|
|
2306
|
+
{ minItems: 1, maxItems: 500 }
|
|
2307
|
+
),
|
|
2308
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live create after dry-run review" })),
|
|
2309
|
+
dryRun: dryRunSchema
|
|
2310
|
+
}),
|
|
2311
|
+
async execute(_toolCallId, params, signal) {
|
|
2312
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2313
|
+
let fileId = "";
|
|
2314
|
+
let tokens = [];
|
|
2315
|
+
try {
|
|
2316
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2317
|
+
tokens = normalizeCreateTokens(args.tokens);
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2320
|
+
const receipt = createTokenMutationReceipt(`Token creation validation failed: ${message}`, {
|
|
2321
|
+
fileId,
|
|
2322
|
+
status: "failed",
|
|
2323
|
+
operation: "create",
|
|
2324
|
+
requestedNames: [],
|
|
2325
|
+
providerResults: [],
|
|
2326
|
+
error: { code: "INVALID_INPUT", message }
|
|
2327
|
+
});
|
|
2328
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2329
|
+
}
|
|
2330
|
+
const basePayload = {
|
|
2331
|
+
fileId,
|
|
2332
|
+
operation: "create",
|
|
2333
|
+
requestedNames: tokens.map((token) => token.name),
|
|
2334
|
+
providerResults: []
|
|
2335
|
+
};
|
|
2336
|
+
if (args.dryRun === true) {
|
|
2337
|
+
return operatorReceiptResult(
|
|
2338
|
+
createTokenMutationReceipt(`Dry run: previewed creation of ${tokens.length} Paper token(s).`, {
|
|
2339
|
+
status: "dry-run",
|
|
2340
|
+
...basePayload
|
|
2341
|
+
})
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
if (args.confirm !== true) {
|
|
2345
|
+
const message = "Token creation refused: confirm=true is required after reviewing a dry run.";
|
|
2346
|
+
const receipt = createTokenMutationReceipt(message, {
|
|
2347
|
+
status: "refused",
|
|
2348
|
+
...basePayload,
|
|
2349
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2350
|
+
});
|
|
2351
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2352
|
+
}
|
|
2353
|
+
try {
|
|
2354
|
+
await verifyPaperFileTarget(fileId, signal);
|
|
2355
|
+
const providerResult = await callRequiredPaperMcp("paper_create_tokens", { fileId, tokens }, signal);
|
|
2356
|
+
const providerResults = providerMutationEntries(providerResult);
|
|
2357
|
+
if (providerResults.length === 0) {
|
|
2358
|
+
throw new Error(
|
|
2359
|
+
"Paper create_tokens returned no per-entry results; the provider response shape cannot be receipted."
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
const failures = providerResults.filter(mutationEntryFailed).length;
|
|
2363
|
+
const status = failures === 0 ? "applied" : failures === providerResults.length ? "failed" : "partial";
|
|
2364
|
+
const summary = failures === 0 ? `Created ${providerResults.length} Paper token(s).` : `Paper create_tokens returned ${failures} error result(s) across ${providerResults.length} entries.`;
|
|
2365
|
+
const receipt = createTokenMutationReceipt(summary, {
|
|
2366
|
+
status,
|
|
2367
|
+
...basePayload,
|
|
2368
|
+
providerResults,
|
|
2369
|
+
...failures > 0 ? { error: { code: "PAPER_TOKEN_ENTRY_ERRORS", message: summary } } : {}
|
|
2370
|
+
});
|
|
2371
|
+
return failures > 0 ? operatorReceiptError(receipt, "PAPER_TOKEN_ENTRY_ERRORS", summary) : operatorReceiptResult(receipt);
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2374
|
+
const receipt = createTokenMutationReceipt(`Token creation failed: ${message}`, {
|
|
2375
|
+
status: "failed",
|
|
2376
|
+
...basePayload,
|
|
2377
|
+
error: { code: "PAPER_TOKEN_CREATE_FAILED", message }
|
|
2378
|
+
});
|
|
2379
|
+
return operatorReceiptError(receipt, "PAPER_TOKEN_CREATE_FAILED", message);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
},
|
|
2383
|
+
{
|
|
2384
|
+
name: "paper_set_tokens",
|
|
2385
|
+
label: "Paper: Set Tokens Safely",
|
|
2386
|
+
description: "Rename, update, describe, or delete existing file-scoped Paper tokens through dry-run, explicit confirmation, source-token acknowledgement, and preserved per-entry results.",
|
|
2387
|
+
parameters: Type.Object({
|
|
2388
|
+
fileId: requiredFileIdSchema,
|
|
2389
|
+
tokens: Type.Array(
|
|
2390
|
+
Type.Object({
|
|
2391
|
+
name: Type.String({ pattern: "^--[a-zA-Z0-9_-]+$" }),
|
|
2392
|
+
newName: Type.Optional(Type.String({ pattern: "^--[a-zA-Z0-9_-]+$" })),
|
|
2393
|
+
value: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
|
2394
|
+
description: Type.Optional(Type.String({ maxLength: 1024 })),
|
|
2395
|
+
delete: Type.Optional(Type.Boolean())
|
|
2396
|
+
}),
|
|
2397
|
+
{ minItems: 1, maxItems: 500 }
|
|
2398
|
+
),
|
|
2399
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live update after dry-run review" })),
|
|
2400
|
+
sourceAcknowledgement: Type.Optional(
|
|
2401
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.setTokens, {
|
|
2402
|
+
description: "Required because set_tokens changes or deletes existing source tokens"
|
|
2403
|
+
})
|
|
2404
|
+
),
|
|
2405
|
+
dryRun: dryRunSchema
|
|
2406
|
+
}),
|
|
2407
|
+
async execute(_toolCallId, params, signal) {
|
|
2408
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2409
|
+
let fileId = "";
|
|
2410
|
+
let tokens = [];
|
|
2411
|
+
try {
|
|
2412
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2413
|
+
tokens = normalizeSetTokens(args.tokens);
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2416
|
+
const receipt = createTokenMutationReceipt(`Token update validation failed: ${message}`, {
|
|
2417
|
+
fileId,
|
|
2418
|
+
status: "failed",
|
|
2419
|
+
operation: "set",
|
|
2420
|
+
requestedNames: [],
|
|
2421
|
+
providerResults: [],
|
|
2422
|
+
error: { code: "INVALID_INPUT", message }
|
|
2423
|
+
});
|
|
2424
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2425
|
+
}
|
|
2426
|
+
const basePayload = {
|
|
2427
|
+
fileId,
|
|
2428
|
+
operation: "set",
|
|
2429
|
+
requestedNames: tokens.map((token) => token.name),
|
|
2430
|
+
providerResults: []
|
|
2431
|
+
};
|
|
2432
|
+
if (args.dryRun === true) {
|
|
2433
|
+
return operatorReceiptResult(
|
|
2434
|
+
createTokenMutationReceipt(`Dry run: previewed ${tokens.length} Paper token update(s).`, {
|
|
2435
|
+
status: "dry-run",
|
|
2436
|
+
...basePayload
|
|
2437
|
+
})
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
if (args.confirm !== true) {
|
|
2441
|
+
const message = "Token update refused: confirm=true is required after reviewing a dry run.";
|
|
2442
|
+
const receipt = createTokenMutationReceipt(message, {
|
|
2443
|
+
status: "refused",
|
|
2444
|
+
...basePayload,
|
|
2445
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2446
|
+
});
|
|
2447
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2448
|
+
}
|
|
2449
|
+
if (args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.setTokens) {
|
|
2450
|
+
const message = `Token update refused: sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.setTokens}" is required.`;
|
|
2451
|
+
const receipt = createTokenMutationReceipt(message, {
|
|
2452
|
+
status: "refused",
|
|
2453
|
+
...basePayload,
|
|
2454
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
2455
|
+
});
|
|
2456
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
await verifyPaperFileTarget(fileId, signal);
|
|
2460
|
+
const providerResult = await callRequiredPaperMcp("paper_set_tokens", { fileId, tokens }, signal);
|
|
2461
|
+
const providerResults = providerMutationEntries(providerResult);
|
|
2462
|
+
if (providerResults.length === 0) {
|
|
2463
|
+
throw new Error(
|
|
2464
|
+
"Paper set_tokens returned no per-entry results; the provider response shape cannot be receipted."
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const failures = providerResults.filter(mutationEntryFailed).length;
|
|
2468
|
+
const status = failures === 0 ? "applied" : failures === providerResults.length ? "failed" : "partial";
|
|
2469
|
+
const summary = failures === 0 ? `Updated ${providerResults.length} Paper token(s).` : `Paper set_tokens returned ${failures} error result(s) across ${providerResults.length} entries.`;
|
|
2470
|
+
const receipt = createTokenMutationReceipt(summary, {
|
|
2471
|
+
status,
|
|
2472
|
+
...basePayload,
|
|
2473
|
+
providerResults,
|
|
2474
|
+
...failures > 0 ? { error: { code: "PAPER_TOKEN_ENTRY_ERRORS", message: summary } } : {}
|
|
2475
|
+
});
|
|
2476
|
+
return failures > 0 ? operatorReceiptError(receipt, "PAPER_TOKEN_ENTRY_ERRORS", summary) : operatorReceiptResult(receipt);
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2479
|
+
const receipt = createTokenMutationReceipt(`Token update failed: ${message}`, {
|
|
2480
|
+
status: "failed",
|
|
2481
|
+
...basePayload,
|
|
2482
|
+
error: { code: "PAPER_TOKEN_SET_FAILED", message }
|
|
2483
|
+
});
|
|
2484
|
+
return operatorReceiptError(receipt, "PAPER_TOKEN_SET_FAILED", message);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
},
|
|
2488
|
+
{
|
|
2489
|
+
name: "paper_write_html",
|
|
2490
|
+
label: "Paper: Write HTML Safely",
|
|
2491
|
+
description: "Insert one incremental inline-HTML item under an exact target after dry-run and confirmation. The receipt always surfaces the provider response as payload.createdNodes (for example {createdNodes:[{id,name}]}); replacement is intentionally handled by paper_revise_artboard.",
|
|
2492
|
+
parameters: Type.Object({
|
|
2493
|
+
fileId: requiredFileIdSchema,
|
|
2494
|
+
id: Type.String({ minLength: 1, description: "Exact target parent node ID" }),
|
|
2495
|
+
html: Type.String({ minLength: 1, description: "One incremental HTML item with literal inline styles" }),
|
|
2496
|
+
mode: Type.Optional(
|
|
2497
|
+
Type.Literal("insert-children", { description: "Guarded writes support insert-children only" })
|
|
2498
|
+
),
|
|
2499
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live write after dry-run review" })),
|
|
2500
|
+
sourceAcknowledgement: Type.Optional(
|
|
2501
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.write, {
|
|
2502
|
+
description: "Required when inserting into a target not created in this process session and file"
|
|
2503
|
+
})
|
|
2504
|
+
),
|
|
2505
|
+
dryRun: dryRunSchema
|
|
2506
|
+
}),
|
|
2507
|
+
async execute(_toolCallId, params, signal) {
|
|
2508
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2509
|
+
let fileId = "";
|
|
2510
|
+
let targetId = "";
|
|
2511
|
+
let html = "";
|
|
2512
|
+
try {
|
|
2513
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2514
|
+
targetId = nonEmptyString(args.id, "paper_write_html target id");
|
|
2515
|
+
html = normalizeIncrementalHtml(args.html);
|
|
2516
|
+
if (args.mode !== void 0 && args.mode !== "insert-children") {
|
|
2517
|
+
throw new TypeError(
|
|
2518
|
+
"paper_write_html supports insert-children only; use paper_revise_artboard for replacement."
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2523
|
+
const receipt = createWriteHtmlReceipt(`Write validation failed: ${message}`, {
|
|
2524
|
+
fileId,
|
|
2525
|
+
status: "failed",
|
|
2526
|
+
targetId,
|
|
2527
|
+
mode: "insert-children",
|
|
2528
|
+
htmlBytes: typeof args.html === "string" ? Buffer.byteLength(args.html) : 0,
|
|
2529
|
+
createdNodes: [],
|
|
2530
|
+
sessionCreatedTargetIds: [],
|
|
2531
|
+
sourceTargetIds: targetId ? [targetId] : [],
|
|
2532
|
+
verifiedBy: null,
|
|
2533
|
+
error: { code: "INVALID_INPUT", message }
|
|
2534
|
+
});
|
|
2535
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2536
|
+
}
|
|
2537
|
+
let classification = classifyTargets([targetId]);
|
|
2538
|
+
const basePayload = {
|
|
2539
|
+
fileId,
|
|
2540
|
+
targetId,
|
|
2541
|
+
mode: "insert-children",
|
|
2542
|
+
htmlBytes: Buffer.byteLength(html),
|
|
2543
|
+
createdNodes: [],
|
|
2544
|
+
...classification,
|
|
2545
|
+
verifiedBy: null
|
|
2546
|
+
};
|
|
2547
|
+
if (args.dryRun === true) {
|
|
2548
|
+
return operatorReceiptResult(
|
|
2549
|
+
createWriteHtmlReceipt(`Dry run: previewed one HTML insertion under ${targetId}; no mutation ran.`, {
|
|
2550
|
+
status: "dry-run",
|
|
2551
|
+
...basePayload
|
|
2552
|
+
})
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
if (args.confirm !== true) {
|
|
2556
|
+
const message = "Write refused: confirm=true is required after reviewing a dry run.";
|
|
2557
|
+
const receipt = createWriteHtmlReceipt(message, {
|
|
2558
|
+
status: "refused",
|
|
2559
|
+
...basePayload,
|
|
2560
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2561
|
+
});
|
|
2562
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2563
|
+
}
|
|
2564
|
+
try {
|
|
2565
|
+
await reconcileSessionFileScope(fileId, signal);
|
|
2566
|
+
classification = classifyTargets([targetId]);
|
|
2567
|
+
if (classification.sourceTargetIds.length > 0 && args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.write) {
|
|
2568
|
+
const message = `Write refused: source targets require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.write}".`;
|
|
2569
|
+
const receipt = createWriteHtmlReceipt(message, {
|
|
2570
|
+
status: "refused",
|
|
2571
|
+
...basePayload,
|
|
2572
|
+
...classification,
|
|
2573
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
2574
|
+
});
|
|
2575
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
2576
|
+
}
|
|
2577
|
+
await readNodeSnapshot(targetId, fileId, signal);
|
|
2578
|
+
const providerResult = await callRequiredPaperMcp(
|
|
2579
|
+
"paper_write_html",
|
|
2580
|
+
{ fileId, targetNodeId: targetId, html, mode: "insert-children" },
|
|
2581
|
+
signal
|
|
2582
|
+
);
|
|
2583
|
+
const createdNodes = extractCreatedNodes(providerResult);
|
|
2584
|
+
if (createdNodes.length === 0) {
|
|
2585
|
+
throw new Error(
|
|
2586
|
+
"Paper write_html reported success but returned no usable createdNodes. Stop and inspect the target before retrying; the belt will not invent IDs."
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
for (const node of createdNodes) sessionCreatedNodeIds.add(node.id);
|
|
2590
|
+
return operatorReceiptResult(
|
|
2591
|
+
createWriteHtmlReceipt(`Created and receipted ${createdNodes.length} Paper node(s) under ${targetId}.`, {
|
|
2592
|
+
status: "applied",
|
|
2593
|
+
...basePayload,
|
|
2594
|
+
...classification,
|
|
2595
|
+
createdNodes,
|
|
2596
|
+
verifiedBy: "write_html.createdNodes"
|
|
2597
|
+
})
|
|
2598
|
+
);
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2601
|
+
const receipt = createWriteHtmlReceipt(`Write was not fully receipted: ${message}`, {
|
|
2602
|
+
status: "failed",
|
|
2603
|
+
...basePayload,
|
|
2604
|
+
...classification,
|
|
2605
|
+
error: { code: "PAPER_WRITE_FAILED", message }
|
|
2606
|
+
});
|
|
2607
|
+
return operatorReceiptError(receipt, "PAPER_WRITE_FAILED", message);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
},
|
|
2611
|
+
{
|
|
2612
|
+
name: "paper_move_nodes",
|
|
2613
|
+
label: "Paper: Move Nodes Safely",
|
|
2614
|
+
description: "Move exact node IDs through dry-run, confirmation, and a receipt while preserving identity. Every live destination must be separately typed and fingerprinted in the approved brief; the root alias and unverified cross-page roots are rejected by the public runtime. The receipt surfaces resolved moves and affectedParents.",
|
|
2615
|
+
parameters: Type.Object({
|
|
2616
|
+
fileId: requiredFileIdSchema,
|
|
2617
|
+
moves: Type.Array(
|
|
2618
|
+
Type.Union([
|
|
2619
|
+
Type.Object({
|
|
2620
|
+
nodeId: Type.String({ minLength: 1 }),
|
|
2621
|
+
before: Type.String({ minLength: 1, description: "Destination sibling ID" })
|
|
2622
|
+
}),
|
|
2623
|
+
Type.Object({
|
|
2624
|
+
nodeId: Type.String({ minLength: 1 }),
|
|
2625
|
+
after: Type.String({ minLength: 1, description: "Destination sibling ID" })
|
|
2626
|
+
}),
|
|
2627
|
+
Type.Object({
|
|
2628
|
+
nodeId: Type.String({ minLength: 1 }),
|
|
2629
|
+
parentId: Type.String({
|
|
2630
|
+
minLength: 1,
|
|
2631
|
+
description: "Exact destination parent ID. Live use requires the same ID as a typed approved move destination; the root alias is not authorized."
|
|
2632
|
+
}),
|
|
2633
|
+
index: Type.Optional(Type.Integer({ minimum: 0 }))
|
|
2634
|
+
})
|
|
2635
|
+
]),
|
|
2636
|
+
{ minItems: 1, maxItems: 50 }
|
|
2637
|
+
),
|
|
2638
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live move after dry-run review" })),
|
|
2639
|
+
sourceAcknowledgement: Type.Optional(
|
|
2640
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.move, {
|
|
2641
|
+
description: "Required for moved nodes not created in this process session and file"
|
|
2642
|
+
})
|
|
2643
|
+
),
|
|
2644
|
+
dryRun: dryRunSchema
|
|
2645
|
+
}),
|
|
2646
|
+
async execute(_toolCallId, params, signal) {
|
|
2647
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2648
|
+
let fileId = "";
|
|
2649
|
+
let moves = [];
|
|
2650
|
+
try {
|
|
2651
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2652
|
+
moves = normalizeMoves(args.moves);
|
|
2653
|
+
} catch (error) {
|
|
2654
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2655
|
+
const receipt = createMoveNodesReceipt(`Move validation failed: ${message}`, {
|
|
2656
|
+
fileId,
|
|
2657
|
+
status: "failed",
|
|
2658
|
+
moves: [],
|
|
2659
|
+
sessionCreatedTargetIds: [],
|
|
2660
|
+
sourceTargetIds: [],
|
|
2661
|
+
providerResult: null,
|
|
2662
|
+
verifiedBy: null,
|
|
2663
|
+
error: { code: "INVALID_INPUT", message }
|
|
2664
|
+
});
|
|
2665
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2666
|
+
}
|
|
2667
|
+
const targetIds = moves.map((move) => move.nodeId);
|
|
2668
|
+
let classification = classifyTargets(targetIds);
|
|
2669
|
+
const basePayload = {
|
|
2670
|
+
fileId,
|
|
2671
|
+
moves,
|
|
2672
|
+
...classification,
|
|
2673
|
+
providerResult: null,
|
|
2674
|
+
verifiedBy: null
|
|
2675
|
+
};
|
|
2676
|
+
if (args.dryRun === true) {
|
|
2677
|
+
return operatorReceiptResult(
|
|
2678
|
+
createMoveNodesReceipt(`Dry run: previewed ${moves.length} identity-preserving Paper move(s).`, {
|
|
2679
|
+
status: "dry-run",
|
|
2680
|
+
...basePayload
|
|
2681
|
+
})
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
if (args.confirm !== true) {
|
|
2685
|
+
const message = "Move refused: confirm=true is required after reviewing a dry run.";
|
|
2686
|
+
const receipt = createMoveNodesReceipt(message, {
|
|
2687
|
+
status: "refused",
|
|
2688
|
+
...basePayload,
|
|
2689
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2690
|
+
});
|
|
2691
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2692
|
+
}
|
|
2693
|
+
try {
|
|
2694
|
+
await reconcileSessionFileScope(fileId, signal);
|
|
2695
|
+
classification = classifyTargets(targetIds);
|
|
2696
|
+
if (classification.sourceTargetIds.length > 0 && args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.move) {
|
|
2697
|
+
const message = `Move refused: source targets require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.move}".`;
|
|
2698
|
+
const receipt = createMoveNodesReceipt(message, {
|
|
2699
|
+
status: "refused",
|
|
2700
|
+
...basePayload,
|
|
2701
|
+
...classification,
|
|
2702
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
2703
|
+
});
|
|
2704
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
2705
|
+
}
|
|
2706
|
+
const snapshots = await Promise.all(targetIds.map((id) => readNodeSnapshot(id, fileId, signal)));
|
|
2707
|
+
const protectedNodes = snapshots.filter((snapshot) => !snapshot.parentId);
|
|
2708
|
+
if (protectedNodes.length > 0) {
|
|
2709
|
+
throw new Error(
|
|
2710
|
+
`Root or page nodes cannot be moved: ${protectedNodes.map((node) => node.id).join(", ")}.`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
const rawProviderResult = await callRequiredPaperMcp("paper_move_nodes", { fileId, moves }, signal);
|
|
2714
|
+
const providerResult = parseMcpJsonValue(rawProviderResult);
|
|
2715
|
+
const providerRecord = providerResult && typeof providerResult === "object" && !Array.isArray(providerResult) ? providerResult : void 0;
|
|
2716
|
+
if (!Array.isArray(providerRecord?.affectedParents)) {
|
|
2717
|
+
throw new Error(
|
|
2718
|
+
"Paper move_nodes returned no affectedParents receipt. Stop before assuming the hierarchy is clean."
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
return operatorReceiptResult(
|
|
2722
|
+
createMoveNodesReceipt(`Moved and receipted ${moves.length} Paper node(s) without changing IDs.`, {
|
|
2723
|
+
status: "applied",
|
|
2724
|
+
...basePayload,
|
|
2725
|
+
...classification,
|
|
2726
|
+
providerResult,
|
|
2727
|
+
verifiedBy: "move_nodes.affectedParents"
|
|
2728
|
+
})
|
|
2729
|
+
);
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2732
|
+
const receipt = createMoveNodesReceipt(`Move was not fully receipted: ${message}`, {
|
|
2733
|
+
status: "failed",
|
|
2734
|
+
...basePayload,
|
|
2735
|
+
...classification,
|
|
2736
|
+
error: { code: "PAPER_MOVE_FAILED", message }
|
|
2737
|
+
});
|
|
2738
|
+
return operatorReceiptError(receipt, "PAPER_MOVE_FAILED", message);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
},
|
|
2742
|
+
{
|
|
2743
|
+
name: "paper_rename_nodes",
|
|
2744
|
+
label: "Paper: Rename Nodes Safely",
|
|
2745
|
+
description: "Rename exactly the listed Paper nodes through a dry-run and explicit-confirmation receipt. This cannot edit styles, content, hierarchy, or delete nodes. Targets not created in this process session and active file require sourceAcknowledgement='rename-source'.",
|
|
2746
|
+
parameters: Type.Object({
|
|
2747
|
+
fileId: requiredFileIdSchema,
|
|
2748
|
+
updates: Type.Array(
|
|
2749
|
+
Type.Object({
|
|
2750
|
+
id: Type.String({ description: "Paper node ID to rename" }),
|
|
2751
|
+
name: Type.String({ description: "New non-empty node name" })
|
|
2752
|
+
}),
|
|
2753
|
+
{ description: "Exact node IDs and replacement names" }
|
|
2754
|
+
),
|
|
2755
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live rename after dry-run review" })),
|
|
2756
|
+
sourceAcknowledgement: Type.Optional(
|
|
2757
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.rename, {
|
|
2758
|
+
description: "Required with confirm for targets not created in this process session and active file"
|
|
2759
|
+
})
|
|
2760
|
+
),
|
|
2761
|
+
dryRun: dryRunSchema
|
|
2762
|
+
}),
|
|
2763
|
+
async execute(_toolCallId, params, signal) {
|
|
2764
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2765
|
+
let fileId = "";
|
|
2766
|
+
let updates = [];
|
|
2767
|
+
try {
|
|
2768
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2769
|
+
updates = normalizeRenameUpdates(args.updates);
|
|
2770
|
+
} catch (error) {
|
|
2771
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2772
|
+
const receipt = createRenameReceipt(`Rename validation failed: ${message}`, {
|
|
2773
|
+
fileId,
|
|
2774
|
+
status: "failed",
|
|
2775
|
+
updates: [],
|
|
2776
|
+
sessionCreatedTargetIds: [],
|
|
2777
|
+
sourceTargetIds: [],
|
|
2778
|
+
verifiedBy: null,
|
|
2779
|
+
error: { code: "INVALID_INPUT", message }
|
|
2780
|
+
});
|
|
2781
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2782
|
+
}
|
|
2783
|
+
const targetIds = updates.map((update) => update.id);
|
|
2784
|
+
let classification = classifyTargets(targetIds);
|
|
2785
|
+
const previewUpdates = updates.map((update) => ({ id: update.id, from: null, to: update.name }));
|
|
2786
|
+
if (args.dryRun === true) {
|
|
2787
|
+
return operatorReceiptResult(
|
|
2788
|
+
createRenameReceipt(`Dry run: previewed ${updates.length} Paper node rename(s); no mutation ran.`, {
|
|
2789
|
+
fileId,
|
|
2790
|
+
status: "dry-run",
|
|
2791
|
+
updates: previewUpdates,
|
|
2792
|
+
...classification,
|
|
2793
|
+
verifiedBy: null
|
|
2794
|
+
})
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
if (args.confirm !== true) {
|
|
2798
|
+
const message = "Rename refused: confirm=true is required after reviewing a dry run.";
|
|
2799
|
+
const receipt = createRenameReceipt(message, {
|
|
2800
|
+
fileId,
|
|
2801
|
+
status: "refused",
|
|
2802
|
+
updates: previewUpdates,
|
|
2803
|
+
...classification,
|
|
2804
|
+
verifiedBy: null,
|
|
2805
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2806
|
+
});
|
|
2807
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2808
|
+
}
|
|
2809
|
+
try {
|
|
2810
|
+
await reconcileSessionFileScope(fileId, signal);
|
|
2811
|
+
classification = classifyTargets(targetIds);
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2814
|
+
const receipt = createRenameReceipt(
|
|
2815
|
+
`Rename refused because file provenance could not be verified: ${message}`,
|
|
2816
|
+
{
|
|
2817
|
+
fileId,
|
|
2818
|
+
status: "failed",
|
|
2819
|
+
updates: previewUpdates,
|
|
2820
|
+
...classification,
|
|
2821
|
+
verifiedBy: null,
|
|
2822
|
+
error: { code: "FILE_PROVENANCE_FAILED", message }
|
|
2823
|
+
}
|
|
2824
|
+
);
|
|
2825
|
+
return operatorReceiptError(receipt, "FILE_PROVENANCE_FAILED", message);
|
|
2826
|
+
}
|
|
2827
|
+
if (classification.sourceTargetIds.length > 0 && args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.rename) {
|
|
2828
|
+
const message = `Rename refused: source targets require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.rename}".`;
|
|
2829
|
+
const receipt = createRenameReceipt(message, {
|
|
2830
|
+
fileId,
|
|
2831
|
+
status: "refused",
|
|
2832
|
+
updates: previewUpdates,
|
|
2833
|
+
...classification,
|
|
2834
|
+
verifiedBy: null,
|
|
2835
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
2836
|
+
});
|
|
2837
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
2838
|
+
}
|
|
2839
|
+
let before = [];
|
|
2840
|
+
try {
|
|
2841
|
+
before = await Promise.all(targetIds.map((id) => readNodeSnapshot(id, fileId, signal)));
|
|
2842
|
+
const protectedNodes = before.filter((snapshot) => !snapshot.parentId);
|
|
2843
|
+
if (protectedNodes.length > 0) {
|
|
2844
|
+
throw new Error(
|
|
2845
|
+
`Root or page nodes cannot be renamed: ${protectedNodes.map((node) => node.id).join(", ")}.`
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
await callRequiredPaperMcp(
|
|
2849
|
+
"paper_rename_nodes",
|
|
2850
|
+
{ fileId, updates: updates.map((update) => ({ nodeId: update.id, name: update.name })) },
|
|
2851
|
+
signal
|
|
2852
|
+
);
|
|
2853
|
+
const after = await Promise.all(targetIds.map((id) => readNodeSnapshot(id, fileId, signal)));
|
|
2854
|
+
const expectedNames = new Map(updates.map((update) => [update.id, update.name]));
|
|
2855
|
+
const mismatches = after.filter((snapshot) => expectedNames.get(snapshot.id) !== snapshot.name);
|
|
2856
|
+
if (mismatches.length > 0) {
|
|
2857
|
+
throw new Error(
|
|
2858
|
+
`Paper did not verify the requested name for: ${mismatches.map((node) => node.id).join(", ")}.`
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
const previousNames = new Map(before.map((snapshot) => [snapshot.id, snapshot.name]));
|
|
2862
|
+
const verifiedUpdates = updates.map((update) => ({
|
|
2863
|
+
id: update.id,
|
|
2864
|
+
from: previousNames.get(update.id) ?? null,
|
|
2865
|
+
to: update.name
|
|
2866
|
+
}));
|
|
2867
|
+
return operatorReceiptResult(
|
|
2868
|
+
createRenameReceipt(`Renamed and verified ${updates.length} Paper node(s).`, {
|
|
2869
|
+
fileId,
|
|
2870
|
+
status: "applied",
|
|
2871
|
+
updates: verifiedUpdates,
|
|
2872
|
+
...classification,
|
|
2873
|
+
verifiedBy: "paper_get_node_info"
|
|
2874
|
+
})
|
|
2875
|
+
);
|
|
2876
|
+
} catch (error) {
|
|
2877
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2878
|
+
const previousNames = new Map(before.map((snapshot) => [snapshot.id, snapshot.name]));
|
|
2879
|
+
const receipt = createRenameReceipt(`Rename was not fully verified: ${message}`, {
|
|
2880
|
+
fileId,
|
|
2881
|
+
status: "failed",
|
|
2882
|
+
updates: updates.map((update) => ({
|
|
2883
|
+
id: update.id,
|
|
2884
|
+
from: previousNames.get(update.id) ?? null,
|
|
2885
|
+
to: update.name
|
|
2886
|
+
})),
|
|
2887
|
+
...classification,
|
|
2888
|
+
verifiedBy: null,
|
|
2889
|
+
error: { code: "PAPER_RENAME_FAILED", message }
|
|
2890
|
+
});
|
|
2891
|
+
return operatorReceiptError(receipt, "PAPER_RENAME_FAILED", message);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
name: "paper_revise_artboard",
|
|
2897
|
+
label: "Paper: Revise Artboard Safely",
|
|
2898
|
+
description: "Replace only the children of one existing Paper artboard from validated literal inline-CSS HTML, then screenshot the result. This cannot replace the artboard root or touch siblings. Artboards not created in this process session and active file require sourceAcknowledgement='revise-source'.",
|
|
2899
|
+
parameters: Type.Object({
|
|
2900
|
+
fileId: requiredFileIdSchema,
|
|
2901
|
+
id: Type.String({ description: "Existing Paper artboard ID whose children will be replaced" }),
|
|
2902
|
+
html: Type.String({ description: "Literal inline-CSS HTML with concrete layout and typography values" }),
|
|
2903
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live revision after dry-run review" })),
|
|
2904
|
+
sourceAcknowledgement: Type.Optional(
|
|
2905
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.revise, {
|
|
2906
|
+
description: "Required with confirm for an artboard not created in this process session and active file"
|
|
2907
|
+
})
|
|
2908
|
+
),
|
|
2909
|
+
dryRun: dryRunSchema
|
|
2910
|
+
}),
|
|
2911
|
+
async execute(_toolCallId, params, signal) {
|
|
2912
|
+
const args = params && typeof params === "object" ? params : {};
|
|
2913
|
+
let fileId = "";
|
|
2914
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
2915
|
+
let html = "";
|
|
2916
|
+
try {
|
|
2917
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
2918
|
+
if (!id) throw new TypeError("Revision requires one existing Paper artboard ID.");
|
|
2919
|
+
html = normalizeLiteralHtml(args.html);
|
|
2920
|
+
} catch (error) {
|
|
2921
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2922
|
+
const receipt = createReviseReceipt(`Revision validation failed: ${message}`, {
|
|
2923
|
+
fileId,
|
|
2924
|
+
status: "failed",
|
|
2925
|
+
artboardId: id,
|
|
2926
|
+
artboardName: null,
|
|
2927
|
+
htmlBytes: typeof args.html === "string" ? Buffer.byteLength(args.html) : 0,
|
|
2928
|
+
mode: "replace",
|
|
2929
|
+
sessionCreatedTargetIds: [],
|
|
2930
|
+
sourceTargetIds: id ? [id] : [],
|
|
2931
|
+
verifiedBy: null,
|
|
2932
|
+
error: { code: "INVALID_INPUT", message }
|
|
2933
|
+
});
|
|
2934
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
2935
|
+
}
|
|
2936
|
+
let classification = classifyTargets([id]);
|
|
2937
|
+
let basePayload = {
|
|
2938
|
+
fileId,
|
|
2939
|
+
artboardId: id,
|
|
2940
|
+
artboardName: null,
|
|
2941
|
+
htmlBytes: Buffer.byteLength(html),
|
|
2942
|
+
mode: "replace",
|
|
2943
|
+
...classification,
|
|
2944
|
+
verifiedBy: null
|
|
2945
|
+
};
|
|
2946
|
+
if (args.dryRun === true) {
|
|
2947
|
+
return operatorReceiptResult(
|
|
2948
|
+
createReviseReceipt(`Dry run: previewed replacement of artboard ${id} children; no mutation ran.`, {
|
|
2949
|
+
status: "dry-run",
|
|
2950
|
+
...basePayload
|
|
2951
|
+
})
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
if (args.confirm !== true) {
|
|
2955
|
+
const message = "Revision refused: confirm=true is required after reviewing a dry run.";
|
|
2956
|
+
const receipt = createReviseReceipt(message, {
|
|
2957
|
+
status: "refused",
|
|
2958
|
+
...basePayload,
|
|
2959
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
2960
|
+
});
|
|
2961
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
2962
|
+
}
|
|
2963
|
+
try {
|
|
2964
|
+
await reconcileSessionFileScope(fileId, signal);
|
|
2965
|
+
classification = classifyTargets([id]);
|
|
2966
|
+
basePayload = {
|
|
2967
|
+
...basePayload,
|
|
2968
|
+
...classification
|
|
2969
|
+
};
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2972
|
+
const receipt = createReviseReceipt(
|
|
2973
|
+
`Revision refused because file provenance could not be verified: ${message}`,
|
|
2974
|
+
{
|
|
2975
|
+
status: "failed",
|
|
2976
|
+
...basePayload,
|
|
2977
|
+
error: { code: "FILE_PROVENANCE_FAILED", message }
|
|
2978
|
+
}
|
|
2979
|
+
);
|
|
2980
|
+
return operatorReceiptError(receipt, "FILE_PROVENANCE_FAILED", message);
|
|
2981
|
+
}
|
|
2982
|
+
if (classification.sourceTargetIds.length > 0 && args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.revise) {
|
|
2983
|
+
const message = `Revision refused: source artboards require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.revise}".`;
|
|
2984
|
+
const receipt = createReviseReceipt(message, {
|
|
2985
|
+
status: "refused",
|
|
2986
|
+
...basePayload,
|
|
2987
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
2988
|
+
});
|
|
2989
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
2990
|
+
}
|
|
2991
|
+
let artboardName = null;
|
|
2992
|
+
try {
|
|
2993
|
+
const snapshot = await readNodeSnapshot(id, fileId, signal);
|
|
2994
|
+
artboardName = snapshot.name;
|
|
2995
|
+
if (!snapshot.parentId?.startsWith("root_node_")) {
|
|
2996
|
+
throw new Error(`${id} is not a top-level Paper artboard; revise-artboard cannot touch it.`);
|
|
2997
|
+
}
|
|
2998
|
+
await callRequiredPaperMcp("paper_write_html", { fileId, targetNodeId: id, html, mode: "replace" }, signal);
|
|
2999
|
+
const screenshotResponse = await callPaperScreenshotWithReadiness({ fileId, nodeId: id, scale: 1 }, signal);
|
|
3000
|
+
if (!screenshotResponse.ok) throw new Error(screenshotResponse.error);
|
|
3001
|
+
const screenshotResult = screenshotResponse.result;
|
|
3002
|
+
const images = formatMcpResult(screenshotResult).filter((part) => part.type === "image").map(({ data, mimeType }) => ({ data, mimeType }));
|
|
3003
|
+
if (images.length === 0) throw new Error(`Paper returned no screenshot for revised artboard ${id}.`);
|
|
3004
|
+
await callRequiredPaperMcp("paper_finish_working_on_nodes", { fileId, nodeIds: [id] }, signal);
|
|
3005
|
+
return operatorReceiptResult(
|
|
3006
|
+
createReviseReceipt(`Replaced and screenshot-verified the children of \u201C${artboardName}\u201D (${id}).`, {
|
|
3007
|
+
status: "applied",
|
|
3008
|
+
...basePayload,
|
|
3009
|
+
artboardName,
|
|
3010
|
+
verifiedBy: "paper_get_screenshot"
|
|
3011
|
+
}),
|
|
3012
|
+
images
|
|
3013
|
+
);
|
|
3014
|
+
} catch (error) {
|
|
3015
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3016
|
+
const receipt = createReviseReceipt(`Revision was not fully verified: ${message}`, {
|
|
3017
|
+
status: "failed",
|
|
3018
|
+
...basePayload,
|
|
3019
|
+
artboardName,
|
|
3020
|
+
error: { code: "PAPER_REVISION_FAILED", message }
|
|
3021
|
+
});
|
|
3022
|
+
return operatorReceiptError(receipt, "PAPER_REVISION_FAILED", message);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
},
|
|
3026
|
+
{
|
|
3027
|
+
name: "paper_delete_nodes",
|
|
3028
|
+
label: "Paper: Delete Nodes Safely",
|
|
3029
|
+
description: "Delete exactly the listed Paper nodes and descendants after a dry run, explicit confirmation, named-node preflight, and absence verification. This cannot delete root or page nodes. Targets not created in this process session and active file require sourceAcknowledgement='delete-source'.",
|
|
3030
|
+
parameters: Type.Object({
|
|
3031
|
+
fileId: requiredFileIdSchema,
|
|
3032
|
+
ids: Type.Array(Type.String(), { description: "Exact Paper node IDs to delete" }),
|
|
3033
|
+
confirm: Type.Optional(Type.Boolean({ description: "Must be true for a live deletion after dry-run review" })),
|
|
3034
|
+
sourceAcknowledgement: Type.Optional(
|
|
3035
|
+
Type.Literal(SOURCE_ACKNOWLEDGEMENTS.delete, {
|
|
3036
|
+
description: "Required with confirm for nodes not created in this process session and active file"
|
|
3037
|
+
})
|
|
3038
|
+
),
|
|
3039
|
+
dryRun: dryRunSchema
|
|
3040
|
+
}),
|
|
3041
|
+
async execute(_toolCallId, params, signal) {
|
|
3042
|
+
const args = params && typeof params === "object" ? params : {};
|
|
3043
|
+
let fileId = "";
|
|
3044
|
+
let ids = [];
|
|
3045
|
+
try {
|
|
3046
|
+
fileId = normalizeRequiredPageValue(args.fileId, "file ID");
|
|
3047
|
+
ids = normalizeNodeIds(args.ids);
|
|
3048
|
+
} catch (error) {
|
|
3049
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3050
|
+
const receipt = createDeleteReceipt(`Delete validation failed: ${message}`, {
|
|
3051
|
+
fileId,
|
|
3052
|
+
status: "failed",
|
|
3053
|
+
requestedIds: [],
|
|
3054
|
+
removed: [],
|
|
3055
|
+
sessionCreatedTargetIds: [],
|
|
3056
|
+
sourceTargetIds: [],
|
|
3057
|
+
verifiedBy: null,
|
|
3058
|
+
error: { code: "INVALID_INPUT", message }
|
|
3059
|
+
});
|
|
3060
|
+
return operatorReceiptError(receipt, "INVALID_INPUT", message);
|
|
3061
|
+
}
|
|
3062
|
+
let classification = classifyTargets(ids);
|
|
3063
|
+
const previewRemoved = ids.map((id) => ({ id, name: null }));
|
|
3064
|
+
if (args.dryRun === true) {
|
|
3065
|
+
return operatorReceiptResult(
|
|
3066
|
+
createDeleteReceipt(`Dry run: previewed deletion of ${ids.length} Paper node(s); no mutation ran.`, {
|
|
3067
|
+
fileId,
|
|
3068
|
+
status: "dry-run",
|
|
3069
|
+
requestedIds: ids,
|
|
3070
|
+
removed: previewRemoved,
|
|
3071
|
+
...classification,
|
|
3072
|
+
verifiedBy: null
|
|
3073
|
+
})
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
3076
|
+
if (args.confirm !== true) {
|
|
3077
|
+
const message = "Delete refused: confirm=true is required after reviewing a dry run.";
|
|
3078
|
+
const receipt = createDeleteReceipt(message, {
|
|
3079
|
+
fileId,
|
|
3080
|
+
status: "refused",
|
|
3081
|
+
requestedIds: ids,
|
|
3082
|
+
removed: previewRemoved,
|
|
3083
|
+
...classification,
|
|
3084
|
+
verifiedBy: null,
|
|
3085
|
+
error: { code: "CONFIRMATION_REQUIRED", message }
|
|
3086
|
+
});
|
|
3087
|
+
return operatorReceiptError(receipt, "CONFIRMATION_REQUIRED", message);
|
|
3088
|
+
}
|
|
3089
|
+
try {
|
|
3090
|
+
await reconcileSessionFileScope(fileId, signal);
|
|
3091
|
+
classification = classifyTargets(ids);
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3094
|
+
const receipt = createDeleteReceipt(
|
|
3095
|
+
`Delete refused because file provenance could not be verified: ${message}`,
|
|
3096
|
+
{
|
|
3097
|
+
fileId,
|
|
3098
|
+
status: "failed",
|
|
3099
|
+
requestedIds: ids,
|
|
3100
|
+
removed: previewRemoved,
|
|
3101
|
+
...classification,
|
|
3102
|
+
verifiedBy: null,
|
|
3103
|
+
error: { code: "FILE_PROVENANCE_FAILED", message }
|
|
3104
|
+
}
|
|
3105
|
+
);
|
|
3106
|
+
return operatorReceiptError(receipt, "FILE_PROVENANCE_FAILED", message);
|
|
3107
|
+
}
|
|
3108
|
+
if (classification.sourceTargetIds.length > 0 && args.sourceAcknowledgement !== SOURCE_ACKNOWLEDGEMENTS.delete) {
|
|
3109
|
+
const message = `Delete refused: source nodes require sourceAcknowledgement="${SOURCE_ACKNOWLEDGEMENTS.delete}".`;
|
|
3110
|
+
const receipt = createDeleteReceipt(message, {
|
|
3111
|
+
fileId,
|
|
3112
|
+
status: "refused",
|
|
3113
|
+
requestedIds: ids,
|
|
3114
|
+
removed: previewRemoved,
|
|
3115
|
+
...classification,
|
|
3116
|
+
verifiedBy: null,
|
|
3117
|
+
error: { code: "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message }
|
|
3118
|
+
});
|
|
3119
|
+
return operatorReceiptError(receipt, "SOURCE_ACKNOWLEDGEMENT_REQUIRED", message);
|
|
3120
|
+
}
|
|
3121
|
+
let snapshots = [];
|
|
3122
|
+
try {
|
|
3123
|
+
snapshots = await Promise.all(ids.map((id) => readNodeSnapshot(id, fileId, signal)));
|
|
3124
|
+
const protectedNodes = snapshots.filter((snapshot) => !snapshot.parentId);
|
|
3125
|
+
if (protectedNodes.length > 0) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`Root or page nodes cannot be deleted: ${protectedNodes.map((node) => node.id).join(", ")}.`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
await callRequiredPaperMcp("paper_delete_nodes", { fileId, nodeIds: ids }, signal);
|
|
3131
|
+
for (const id of ids) sessionCreatedNodeIds.delete(id);
|
|
3132
|
+
await Promise.all(ids.map((id) => verifyNodeAbsent(id, fileId, signal)));
|
|
3133
|
+
const removed = snapshots.map((snapshot) => ({ id: snapshot.id, name: snapshot.name }));
|
|
3134
|
+
return operatorReceiptResult(
|
|
3135
|
+
createDeleteReceipt(
|
|
3136
|
+
`Deleted and absence-verified ${removed.map((node) => `\u201C${node.name}\u201D (${node.id})`).join(", ")}.`,
|
|
3137
|
+
{
|
|
3138
|
+
fileId,
|
|
3139
|
+
status: "applied",
|
|
3140
|
+
requestedIds: ids,
|
|
3141
|
+
removed,
|
|
3142
|
+
...classification,
|
|
3143
|
+
verifiedBy: "paper_get_node_info:not-found"
|
|
3144
|
+
}
|
|
3145
|
+
)
|
|
3146
|
+
);
|
|
3147
|
+
} catch (error) {
|
|
3148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3149
|
+
const receipt = createDeleteReceipt(`Delete was not fully verified: ${message}`, {
|
|
3150
|
+
fileId,
|
|
3151
|
+
status: "failed",
|
|
3152
|
+
requestedIds: ids,
|
|
3153
|
+
removed: snapshots.length > 0 ? snapshots.map((snapshot) => ({ id: snapshot.id, name: snapshot.name })) : previewRemoved,
|
|
3154
|
+
...classification,
|
|
3155
|
+
verifiedBy: null,
|
|
3156
|
+
error: { code: "PAPER_DELETE_FAILED", message }
|
|
3157
|
+
});
|
|
3158
|
+
return operatorReceiptError(receipt, "PAPER_DELETE_FAILED", message);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
];
|
|
3163
|
+
var RAW_PROVIDER_MUTATION_TOOLS = /* @__PURE__ */ new Set([
|
|
3164
|
+
"paper_create_artboard",
|
|
3165
|
+
"paper_write_html",
|
|
3166
|
+
"paper_set_text_content",
|
|
3167
|
+
"paper_rename_nodes",
|
|
3168
|
+
"paper_duplicate_nodes",
|
|
3169
|
+
"paper_update_styles",
|
|
3170
|
+
"paper_delete_nodes",
|
|
3171
|
+
"paper_finish_working_on_nodes"
|
|
3172
|
+
]);
|
|
3173
|
+
var paperTools = [
|
|
3174
|
+
...paperMcpTools.filter((tool) => !RAW_PROVIDER_MUTATION_TOOLS.has(tool.name)),
|
|
3175
|
+
...paperOperatorTools
|
|
3176
|
+
];
|
|
3177
|
+
var paperToolNames = new Set(paperTools.map((t) => t.name));
|
|
3178
|
+
async function isPaperRunning(fileId) {
|
|
3179
|
+
const result = await callPaperMcp("paper_get_basic_info", fileId ? { fileId } : {}, void 0, 3e3);
|
|
3180
|
+
return result.ok;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
// src/tool-contract.ts
|
|
3184
|
+
var CONTRACT_PATH = join2(getPackageDir(), "tools.contract.json");
|
|
3185
|
+
var paperLoadedToolContract = loadMonoToolContract(CONTRACT_PATH);
|
|
3186
|
+
assertPaperContractIdentity(paperLoadedToolContract.contract);
|
|
3187
|
+
var paperToolContract = paperLoadedToolContract.contract;
|
|
3188
|
+
var PAPER_TOOL_CONTRACT_PATH = paperLoadedToolContract.contractPath;
|
|
3189
|
+
var PAPER_TOOL_CONTRACT_HASH = paperLoadedToolContract.hash;
|
|
3190
|
+
var PAPER_DOMAIN_TOOL_NAMES = paperToolContract.tools.map((tool) => tool.id);
|
|
3191
|
+
function stableJsonValue(value) {
|
|
3192
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
3193
|
+
if (value && typeof value === "object") {
|
|
3194
|
+
const record = value;
|
|
3195
|
+
return Object.fromEntries(
|
|
3196
|
+
Object.keys(record).sort().map((key) => [key, stableJsonValue(record[key])])
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
return value;
|
|
3200
|
+
}
|
|
3201
|
+
var PAPER_TOOL_SCHEMA_HASHES = Object.fromEntries(
|
|
3202
|
+
paperTools.map((tool) => [
|
|
3203
|
+
tool.name,
|
|
3204
|
+
`sha256:${createHash2("sha256").update(JSON.stringify(stableJsonValue(tool.parameters))).digest("hex")}`
|
|
3205
|
+
])
|
|
3206
|
+
);
|
|
3207
|
+
function resolvePaperAgentHome(requested) {
|
|
3208
|
+
const configured = resolve2(getAgentDir());
|
|
3209
|
+
if (!requested || resolve2(requested) === configured) return configured;
|
|
3210
|
+
return resolveMonoAgentHome(paperToolContract, requested);
|
|
3211
|
+
}
|
|
3212
|
+
function renderPaperToolInventoryXml() {
|
|
3213
|
+
return renderMonoToolPromptInventory(paperToolContract);
|
|
3214
|
+
}
|
|
3215
|
+
function extractPaperPromptToolIds(prompt) {
|
|
3216
|
+
return extractMonoPromptToolIds(prompt);
|
|
3217
|
+
}
|
|
3218
|
+
function paperDomainToolIdsFromActive(toolIds) {
|
|
3219
|
+
return toolIds.filter((id) => id.startsWith("paper_"));
|
|
3220
|
+
}
|
|
3221
|
+
function assertPaperToolContractHandshake(input) {
|
|
3222
|
+
const agentHome = resolvePaperAgentHome(input.agentHome);
|
|
3223
|
+
const handshake = assertMonoToolContractHandshake({
|
|
3224
|
+
contract: paperToolContract,
|
|
3225
|
+
contractHash: PAPER_TOOL_CONTRACT_HASH,
|
|
3226
|
+
registeredToolIds: input.registeredToolIds,
|
|
3227
|
+
customDefinitionToolIds: input.registeredToolIds,
|
|
3228
|
+
prompt: input.prompt,
|
|
3229
|
+
activeToolIds: input.activeToolIds ? paperDomainToolIdsFromActive(input.activeToolIds) : void 0
|
|
3230
|
+
});
|
|
3231
|
+
return { ...handshake, agentHome };
|
|
3232
|
+
}
|
|
3233
|
+
function resolvePaperContractRef(reference) {
|
|
3234
|
+
return resolveMonoContractRef(PAPER_TOOL_CONTRACT_PATH, reference);
|
|
3235
|
+
}
|
|
3236
|
+
function assertPaperContractFixturesExist() {
|
|
3237
|
+
assertMonoContractFixturesExist(paperLoadedToolContract);
|
|
3238
|
+
}
|
|
3239
|
+
function assertPaperContractIdentity(contract) {
|
|
3240
|
+
if (contract.specialist !== "paper" || contract.ownerCommand !== "paper mono" || contract.agentHome !== "~/.paper/agent") {
|
|
3241
|
+
throw new Error("Paper tools.contract.json identity does not match the owner runtime.");
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
export {
|
|
3246
|
+
assertMonoToolContractHandshake,
|
|
3247
|
+
resolveMonoAgentHome,
|
|
3248
|
+
resolveMonoContractRef,
|
|
3249
|
+
callPaperMcp,
|
|
3250
|
+
paperTools,
|
|
3251
|
+
paperLoadedToolContract,
|
|
3252
|
+
paperToolContract,
|
|
3253
|
+
PAPER_TOOL_CONTRACT_PATH,
|
|
3254
|
+
PAPER_TOOL_CONTRACT_HASH,
|
|
3255
|
+
PAPER_DOMAIN_TOOL_NAMES,
|
|
3256
|
+
PAPER_TOOL_SCHEMA_HASHES,
|
|
3257
|
+
resolvePaperAgentHome,
|
|
3258
|
+
renderPaperToolInventoryXml,
|
|
3259
|
+
extractPaperPromptToolIds,
|
|
3260
|
+
paperDomainToolIdsFromActive,
|
|
3261
|
+
assertPaperToolContractHandshake,
|
|
3262
|
+
resolvePaperContractRef,
|
|
3263
|
+
assertPaperContractFixturesExist
|
|
3264
|
+
};
|