@pixi-board/board-plugin-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1131 @@
1
+ // ../board-domain/src/geometry.ts
2
+ function nodeBounds(node) {
3
+ const corners = nodeWorldCorners(node);
4
+ return {
5
+ id: node.id,
6
+ minX: Math.min(...corners.map((point) => point.x)),
7
+ minY: Math.min(...corners.map((point) => point.y)),
8
+ maxX: Math.max(...corners.map((point) => point.x)),
9
+ maxY: Math.max(...corners.map((point) => point.y))
10
+ };
11
+ }
12
+ function nodeWorldCorners(node) {
13
+ const localCorners = [
14
+ { x: 0, y: 0 },
15
+ { x: node.width, y: 0 },
16
+ { x: node.width, y: node.height },
17
+ { x: 0, y: node.height }
18
+ ];
19
+ if (node.rotation === 0) {
20
+ return localCorners.map((point) => ({
21
+ x: node.x + point.x,
22
+ y: node.y + point.y
23
+ }));
24
+ }
25
+ const cos = Math.cos(node.rotation);
26
+ const sin = Math.sin(node.rotation);
27
+ return localCorners.map((point) => ({
28
+ x: node.x + point.x * cos - point.y * sin,
29
+ y: node.y + point.x * sin + point.y * cos
30
+ }));
31
+ }
32
+
33
+ // ../board-domain/src/nodeNames.ts
34
+ var UNNAMED_NODE_NAME = "\u672A\u547D\u540D\u8282\u70B9";
35
+ function displayNodeName(name) {
36
+ const trimmed = name?.trim();
37
+ return trimmed ? trimmed : UNNAMED_NODE_NAME;
38
+ }
39
+
40
+ // src/dto.ts
41
+ import path from "path";
42
+ function toNodeDto(node) {
43
+ const bounds = nodeBounds(node);
44
+ return {
45
+ id: node.id,
46
+ type: node.type,
47
+ name: node.name,
48
+ displayName: displayNodeName(node.name),
49
+ x: node.x,
50
+ y: node.y,
51
+ width: node.width,
52
+ height: node.height,
53
+ rotation: node.rotation,
54
+ zIndex: node.zIndex,
55
+ locked: node.locked ?? false,
56
+ assetId: node.assetId,
57
+ options: node.options,
58
+ bounds: {
59
+ minX: bounds.minX,
60
+ minY: bounds.minY,
61
+ maxX: bounds.maxX,
62
+ maxY: bounds.maxY
63
+ }
64
+ };
65
+ }
66
+ function toAssetDto(projectRoot, asset) {
67
+ return {
68
+ id: asset.id,
69
+ kind: asset.kind,
70
+ fileName: asset.fileName,
71
+ mimeType: asset.mimeType,
72
+ size: asset.size,
73
+ original: asset.localPath ? {
74
+ localPath: asset.localPath,
75
+ absolutePath: path.resolve(projectRoot, asset.localPath),
76
+ sourceUrl: asset.sourceUrl,
77
+ webLink: asset.webLink
78
+ } : void 0,
79
+ derivatives: Object.entries(asset.derivatives ?? {}).map(([variant, derivative]) => ({
80
+ variant,
81
+ localPath: derivative.localPath,
82
+ absolutePath: path.resolve(projectRoot, derivative.localPath),
83
+ extension: derivative.extension,
84
+ updatedAt: derivative.updatedAt
85
+ })),
86
+ metadata: {
87
+ width: asset.width,
88
+ height: asset.height,
89
+ duration: asset.duration,
90
+ format: asset.format,
91
+ hash: asset.hash,
92
+ metadata: asset.metadata,
93
+ createdAt: asset.createdAt,
94
+ updatedAt: asset.updatedAt
95
+ }
96
+ };
97
+ }
98
+ function toProjectInfo(projectRoot) {
99
+ return {
100
+ name: path.basename(projectRoot),
101
+ rootPath: projectRoot,
102
+ boardPath: path.join(projectRoot, "board.json"),
103
+ assetsPath: path.join(projectRoot, "assets.json")
104
+ };
105
+ }
106
+ function toSnapshotDto(projectRoot, snapshot) {
107
+ return {
108
+ project: toProjectInfo(projectRoot),
109
+ viewport: snapshot.viewport ?? null,
110
+ nodes: snapshot.nodes.map(toNodeDto),
111
+ assets: snapshot.assets.map((asset) => ({
112
+ id: asset.id,
113
+ kind: asset.kind,
114
+ fileName: asset.fileName,
115
+ mimeType: asset.mimeType,
116
+ size: asset.size,
117
+ metadata: asset.metadata,
118
+ width: asset.width,
119
+ height: asset.height,
120
+ duration: asset.duration,
121
+ updatedAt: asset.updatedAt
122
+ }))
123
+ };
124
+ }
125
+
126
+ // src/errors.ts
127
+ var BoardToolUserError = class extends Error {
128
+ constructor(message) {
129
+ super(message);
130
+ this.name = "BoardToolUserError";
131
+ }
132
+ };
133
+ function errorMessage(error) {
134
+ if (error instanceof Error) return error.message;
135
+ return String(error);
136
+ }
137
+
138
+ // src/filter.ts
139
+ var NODE_TYPE_VALUES = ["image", "video", "audio", "model", "text", "markdown", "html", "importing", "generating"];
140
+ function filterNodes(nodes, filter) {
141
+ const parsed = parseFilter(filter);
142
+ return nodes.filter((node) => matchesType(node, parsed) && matchesKeyword(node, parsed) && matchesBounds(node, parsed));
143
+ }
144
+ function parseFilter(filter) {
145
+ if (!filter || typeof filter !== "object" || Array.isArray(filter)) {
146
+ return {};
147
+ }
148
+ const source = filter;
149
+ const parsed = {};
150
+ if (typeof source.type === "string" && NODE_TYPE_VALUES.includes(source.type)) {
151
+ parsed.type = source.type;
152
+ }
153
+ if (typeof source.keyword === "string") {
154
+ parsed.keyword = source.keyword;
155
+ } else if (typeof source.name === "string") {
156
+ parsed.keyword = source.name;
157
+ } else if (typeof source.text === "string") {
158
+ parsed.keyword = source.text;
159
+ }
160
+ if (source.bounds && typeof source.bounds === "object" && !Array.isArray(source.bounds)) {
161
+ parsed.bounds = source.bounds;
162
+ }
163
+ return parsed;
164
+ }
165
+ function matchesType(node, filter) {
166
+ return !filter.type || node.type === filter.type;
167
+ }
168
+ function matchesKeyword(node, filter) {
169
+ const keyword = filter.keyword?.trim().toLowerCase();
170
+ if (!keyword) return true;
171
+ const haystack = [node.id, node.type, node.name ?? "", node.assetId].join("\n").toLowerCase();
172
+ return haystack.includes(keyword);
173
+ }
174
+ function matchesBounds(node, filter) {
175
+ if (!filter.bounds) return true;
176
+ const bounds = normalizeBounds(filter.bounds);
177
+ if (!bounds) return true;
178
+ const current = nodeBounds(node);
179
+ return current.maxX >= bounds.minX && current.minX <= bounds.maxX && current.maxY >= bounds.minY && current.minY <= bounds.maxY;
180
+ }
181
+ function normalizeBounds(bounds) {
182
+ if (typeof bounds.minX === "number" && typeof bounds.minY === "number" && typeof bounds.maxX === "number" && typeof bounds.maxY === "number") {
183
+ return {
184
+ minX: bounds.minX,
185
+ minY: bounds.minY,
186
+ maxX: bounds.maxX,
187
+ maxY: bounds.maxY
188
+ };
189
+ }
190
+ if (typeof bounds.x === "number" && typeof bounds.y === "number" && typeof bounds.width === "number" && typeof bounds.height === "number") {
191
+ return {
192
+ minX: bounds.x,
193
+ minY: bounds.y,
194
+ maxX: bounds.x + bounds.width,
195
+ maxY: bounds.y + bounds.height
196
+ };
197
+ }
198
+ return null;
199
+ }
200
+
201
+ // src/reader.ts
202
+ import { promises as fs2 } from "fs";
203
+ import path3 from "path";
204
+
205
+ // src/projects.ts
206
+ import { promises as fs } from "fs";
207
+ import net from "net";
208
+ import os from "os";
209
+ import path2 from "path";
210
+ var APP_IDENTIFIER = "com.codex.pixi-board";
211
+ var ACTIVE_PROJECT = "active";
212
+ var LAST_PROJECT_FILE = ".last-canvas";
213
+ var KNOWN_PROJECTS_FILE = ".known-canvas-projects.json";
214
+ var BRIDGE_FILE = ".canvas-mcp-bridge.json";
215
+ var BRIDGE_CONNECT_TIMEOUT_MS = 250;
216
+ async function resolveProjectRoot(projectRoot, options) {
217
+ if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
218
+ throw new BoardToolUserError('projectRoot must be a non-empty string or "active"');
219
+ }
220
+ if (projectRoot.trim() === ACTIVE_PROJECT) {
221
+ return resolveActiveProjectRoot(options);
222
+ }
223
+ return realpathForProject(projectRoot);
224
+ }
225
+ async function listKnownProjects(options) {
226
+ const appRoot = resolveAppRoot(options);
227
+ const roots = await readKnownProjectRoots(appRoot);
228
+ roots.sort((left, right) => projectDisplayName(left).localeCompare(projectDisplayName(right), void 0, {
229
+ sensitivity: "accent"
230
+ }));
231
+ const bridgeAvailability = await Promise.all(
232
+ roots.map(async (root) => ({
233
+ root,
234
+ bridgeAvailable: await isBridgeAvailable(root)
235
+ }))
236
+ );
237
+ return bridgeAvailability.map(({ root, bridgeAvailable }) => ({
238
+ ...toProjectInfo(root),
239
+ bridgeAvailable
240
+ }));
241
+ }
242
+ async function resolveActiveProjectRoot(options) {
243
+ const appRoot = resolveAppRoot(options);
244
+ const root = await readLastProject(appRoot);
245
+ if (!root) {
246
+ throw new BoardToolUserError("No active canvas project. Open a project in the desktop app first.");
247
+ }
248
+ return root;
249
+ }
250
+ async function readKnownProjectRoots(appRoot) {
251
+ const registryPath = path2.join(appRoot, KNOWN_PROJECTS_FILE);
252
+ let parsed = [];
253
+ try {
254
+ parsed = JSON.parse(await fs.readFile(registryPath, "utf8"));
255
+ } catch (error) {
256
+ if (!isMissing(error)) {
257
+ await writeKnownProjectRoots(registryPath, []);
258
+ }
259
+ return [];
260
+ }
261
+ if (!Array.isArray(parsed)) {
262
+ await writeKnownProjectRoots(registryPath, []);
263
+ return [];
264
+ }
265
+ const validRoots = [];
266
+ for (const entry of parsed) {
267
+ if (typeof entry !== "string" || entry.trim() === "") continue;
268
+ try {
269
+ const root = await fs.realpath(entry);
270
+ if (await isCanvasProject(root)) {
271
+ validRoots.push(root);
272
+ }
273
+ } catch {
274
+ }
275
+ }
276
+ const dedupedRoots = dedupeProjectRoots(validRoots);
277
+ const rawRoots = parsed.filter((entry) => typeof entry === "string" && entry.trim() !== "");
278
+ if (!sameProjectLists(rawRoots, dedupedRoots)) {
279
+ await writeKnownProjectRoots(registryPath, dedupedRoots);
280
+ }
281
+ return dedupedRoots;
282
+ }
283
+ async function readLastProject(appRoot) {
284
+ const lastProjectPath = path2.join(appRoot, LAST_PROJECT_FILE);
285
+ let stored;
286
+ try {
287
+ stored = await fs.readFile(lastProjectPath, "utf8");
288
+ } catch (error) {
289
+ if (isMissing(error)) return null;
290
+ throw error;
291
+ }
292
+ const candidate = stored.trim();
293
+ if (!candidate) return null;
294
+ try {
295
+ const root = await fs.realpath(candidate);
296
+ return await isCanvasProject(root) ? root : null;
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+ async function writeKnownProjectRoots(registryPath, roots) {
302
+ await fs.mkdir(path2.dirname(registryPath), { recursive: true });
303
+ await fs.writeFile(`${registryPath}.tmp`, `${JSON.stringify(roots, null, 2)}
304
+ `, "utf8");
305
+ await fs.rename(`${registryPath}.tmp`, registryPath);
306
+ }
307
+ async function isCanvasProject(projectRoot) {
308
+ try {
309
+ const [board, assets] = await Promise.all([
310
+ fs.stat(path2.join(projectRoot, "board.json")),
311
+ fs.stat(path2.join(projectRoot, "assets.json"))
312
+ ]);
313
+ return board.isFile() && assets.isFile();
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+ async function realpathForProject(projectRoot) {
319
+ try {
320
+ const root = await fs.realpath(projectRoot);
321
+ const stat = await fs.stat(root);
322
+ if (!stat.isDirectory()) {
323
+ throw new BoardToolUserError(`projectRoot is not a directory: ${projectRoot}`);
324
+ }
325
+ if (!await isCanvasProject(root)) {
326
+ throw new BoardToolUserError(`invalid canvas project: ${projectRoot}`);
327
+ }
328
+ return root;
329
+ } catch (error) {
330
+ if (error instanceof BoardToolUserError) throw error;
331
+ throw new BoardToolUserError(`Cannot access projectRoot ${projectRoot}: ${String(error)}`);
332
+ }
333
+ }
334
+ async function isBridgeAvailable(projectRoot) {
335
+ const bridgePath = path2.join(projectRoot, BRIDGE_FILE);
336
+ let value;
337
+ try {
338
+ value = JSON.parse(await fs.readFile(bridgePath, "utf8"));
339
+ } catch {
340
+ return false;
341
+ }
342
+ if (!isEndpoint(value)) return false;
343
+ return canConnectToBridge(value.host, value.port);
344
+ }
345
+ function canConnectToBridge(host, port) {
346
+ return new Promise((resolve) => {
347
+ const socket = net.createConnection({ host, port });
348
+ let settled = false;
349
+ const settle = (result) => {
350
+ if (settled) return;
351
+ settled = true;
352
+ socket.destroy();
353
+ resolve(result);
354
+ };
355
+ const timer = setTimeout(() => settle(false), BRIDGE_CONNECT_TIMEOUT_MS);
356
+ socket.once("connect", () => {
357
+ clearTimeout(timer);
358
+ settle(true);
359
+ });
360
+ socket.once("error", () => {
361
+ clearTimeout(timer);
362
+ settle(false);
363
+ });
364
+ socket.once("close", () => {
365
+ clearTimeout(timer);
366
+ settle(false);
367
+ });
368
+ });
369
+ }
370
+ function resolveAppRoot(options) {
371
+ if (options?.appRoot) {
372
+ return path2.resolve(options.appRoot);
373
+ }
374
+ if (process.platform === "darwin") {
375
+ return path2.join(os.homedir(), "Library", "Application Support", APP_IDENTIFIER);
376
+ }
377
+ if (process.platform === "win32") {
378
+ const appData = process.env.APPDATA;
379
+ if (!appData) {
380
+ throw new BoardToolUserError("APPDATA is not set; cannot resolve the desktop app project root");
381
+ }
382
+ return path2.join(appData, APP_IDENTIFIER);
383
+ }
384
+ const baseDir = process.env.XDG_DATA_HOME ?? path2.join(os.homedir(), ".local", "share");
385
+ return path2.join(baseDir, APP_IDENTIFIER);
386
+ }
387
+ function dedupeProjectRoots(roots) {
388
+ const seen = /* @__PURE__ */ new Set();
389
+ const deduped = [];
390
+ for (const root of roots) {
391
+ const key = normalizeRootKey(root);
392
+ if (seen.has(key)) continue;
393
+ seen.add(key);
394
+ deduped.push(root);
395
+ }
396
+ return deduped;
397
+ }
398
+ function sameProjectLists(left, right) {
399
+ if (left.length !== right.length) return false;
400
+ return left.every((value, index) => normalizeRootKey(value) === normalizeRootKey(right[index]));
401
+ }
402
+ function normalizeRootKey(root) {
403
+ return process.platform === "win32" ? root.toLowerCase() : root;
404
+ }
405
+ function projectDisplayName(projectRoot) {
406
+ return path2.basename(projectRoot).toLowerCase();
407
+ }
408
+ function isMissing(error) {
409
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
410
+ }
411
+ function isEndpoint(value) {
412
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
413
+ const endpoint = value;
414
+ return endpoint.version === 1 && endpoint.host === "127.0.0.1" && typeof endpoint.port === "number" && Number.isInteger(endpoint.port) && endpoint.port > 0 && typeof endpoint.token === "string" && endpoint.token.length > 0 && typeof endpoint.updatedAt === "number";
415
+ }
416
+
417
+ // src/reader.ts
418
+ async function resolveProjectFiles(projectRoot) {
419
+ const root = await resolveProjectRoot(projectRoot);
420
+ const boardPath = path3.join(root, "board.json");
421
+ const assetsPath = path3.join(root, "assets.json");
422
+ await assertReadableFile(boardPath, "board.json");
423
+ await assertReadableFile(assetsPath, "assets.json");
424
+ return { root, boardPath, assetsPath };
425
+ }
426
+ async function loadProject(projectRoot) {
427
+ const files = await resolveProjectFiles(projectRoot);
428
+ const [boardResult, assetsResult] = await Promise.all([
429
+ readJsonFile(files.boardPath, "board.json"),
430
+ readJsonFile(files.assetsPath, "assets.json")
431
+ ]);
432
+ const board = parseBoardFile(boardResult.value, files.boardPath);
433
+ const assets = parseAssetFile(assetsResult.value, files.assetsPath);
434
+ return {
435
+ ...files,
436
+ boardUpdatedAt: board.updatedAt ?? boardResult.mtimeMs,
437
+ assetsUpdatedAt: assets.updatedAt ?? assetsResult.mtimeMs,
438
+ snapshot: {
439
+ nodes: board.nodes,
440
+ assets: assets.assets,
441
+ viewport: board.viewport ?? null
442
+ }
443
+ };
444
+ }
445
+ function getNodeOrThrow(snapshot, nodeId) {
446
+ if (typeof nodeId !== "string" || nodeId.trim() === "") {
447
+ throw new BoardToolUserError("nodeId must be a non-empty string");
448
+ }
449
+ const node = snapshot.nodes.find((entry) => entry.id === nodeId);
450
+ if (!node) {
451
+ throw new BoardToolUserError(`Node not found: ${nodeId}`);
452
+ }
453
+ return node;
454
+ }
455
+ function getAssetOrThrow(snapshot, assetId) {
456
+ if (typeof assetId !== "string" || assetId.trim() === "") {
457
+ throw new BoardToolUserError("assetId must be a non-empty string");
458
+ }
459
+ const asset = snapshot.assets.find((entry) => entry.id === assetId);
460
+ if (!asset) {
461
+ throw new BoardToolUserError(`Asset not found: ${assetId}`);
462
+ }
463
+ return asset;
464
+ }
465
+ function getOriginAssetForNode(snapshot, nodeId) {
466
+ const node = getNodeOrThrow(snapshot, nodeId);
467
+ const asset = getAssetOrThrow(snapshot, node.assetId);
468
+ if (!asset.localPath) {
469
+ throw new BoardToolUserError(`Asset has no original file: ${asset.id}`);
470
+ }
471
+ return asset;
472
+ }
473
+ async function assertReadableFile(filePath, label) {
474
+ try {
475
+ const stat = await fs2.stat(filePath);
476
+ if (!stat.isFile()) {
477
+ throw new BoardToolUserError(`${label} is not a file: ${filePath}`);
478
+ }
479
+ } catch (error) {
480
+ if (error instanceof BoardToolUserError) throw error;
481
+ throw new BoardToolUserError(`Cannot read ${label} at ${filePath}: ${errorMessage(error)}`);
482
+ }
483
+ }
484
+ async function readJsonFile(filePath, label) {
485
+ let text;
486
+ let mtimeMs = 0;
487
+ try {
488
+ const [content, stat] = await Promise.all([fs2.readFile(filePath, "utf8"), fs2.stat(filePath)]);
489
+ text = content;
490
+ mtimeMs = Math.trunc(stat.mtimeMs);
491
+ } catch (error) {
492
+ throw new BoardToolUserError(`Failed to read ${label} at ${filePath}: ${errorMessage(error)}`);
493
+ }
494
+ try {
495
+ return { value: JSON.parse(text), mtimeMs };
496
+ } catch (error) {
497
+ throw new BoardToolUserError(
498
+ `Failed to parse ${label} at ${filePath}: ${errorMessage(error)}. The file may be mid-write; retry the read.`
499
+ );
500
+ }
501
+ }
502
+ function parseBoardFile(value, filePath) {
503
+ if (!isRecord(value)) {
504
+ throw new BoardToolUserError(`Invalid board.json at ${filePath}: expected an object`);
505
+ }
506
+ if (!Array.isArray(value.nodes)) {
507
+ throw new BoardToolUserError(`Invalid board.json at ${filePath}: missing nodes array`);
508
+ }
509
+ return {
510
+ schemaVersion: numberOrUndefined(value.schemaVersion),
511
+ updatedAt: numberOrUndefined(value.updatedAt),
512
+ viewport: parseViewport(value.viewport),
513
+ nodes: value.nodes
514
+ };
515
+ }
516
+ function parseAssetFile(value, filePath) {
517
+ if (!isRecord(value)) {
518
+ throw new BoardToolUserError(`Invalid assets.json at ${filePath}: expected an object`);
519
+ }
520
+ if (!Array.isArray(value.assets)) {
521
+ throw new BoardToolUserError(`Invalid assets.json at ${filePath}: missing assets array`);
522
+ }
523
+ return {
524
+ schemaVersion: numberOrUndefined(value.schemaVersion),
525
+ updatedAt: numberOrUndefined(value.updatedAt),
526
+ assets: value.assets
527
+ };
528
+ }
529
+ function parseViewport(value) {
530
+ if (value === null || value === void 0) return null;
531
+ if (!isRecord(value) || typeof value.scale !== "number" || !isRecord(value.offset)) {
532
+ return null;
533
+ }
534
+ if (typeof value.offset.x !== "number" || typeof value.offset.y !== "number") {
535
+ return null;
536
+ }
537
+ return {
538
+ scale: value.scale,
539
+ offset: {
540
+ x: value.offset.x,
541
+ y: value.offset.y
542
+ }
543
+ };
544
+ }
545
+ function numberOrUndefined(value) {
546
+ return typeof value === "number" ? value : void 0;
547
+ }
548
+ function isRecord(value) {
549
+ return typeof value === "object" && value !== null && !Array.isArray(value);
550
+ }
551
+
552
+ // src/writerClient.ts
553
+ import { promises as fs3 } from "fs";
554
+ import net2 from "net";
555
+ import path4 from "path";
556
+ var BRIDGE_FILE2 = ".canvas-mcp-bridge.json";
557
+ var CONNECT_TIMEOUT_MS = 1500;
558
+ var RESPONSE_TIMEOUT_MS = 3e4;
559
+ async function sendWriteCommand(command) {
560
+ const files = await resolveProjectFiles(command.projectRoot);
561
+ const endpoint = await readBridgeEndpoint(files.root);
562
+ const response = await sendJsonLine(endpoint, {
563
+ token: endpoint.token,
564
+ request: {
565
+ ...command,
566
+ projectRoot: files.root
567
+ }
568
+ });
569
+ if (!response.ok) {
570
+ throw new BoardToolUserError(response.error);
571
+ }
572
+ return response.data;
573
+ }
574
+ async function readBridgeEndpoint(projectRoot) {
575
+ const bridgePath = path4.join(projectRoot, BRIDGE_FILE2);
576
+ let value;
577
+ try {
578
+ value = JSON.parse(await fs3.readFile(bridgePath, "utf8"));
579
+ } catch (error) {
580
+ throw new BoardToolUserError(
581
+ `Desktop app is not available for writes: cannot read ${BRIDGE_FILE2} in ${projectRoot}. Start the desktop app and open this project. Details: ${errorMessage(error)}`
582
+ );
583
+ }
584
+ if (!isEndpoint2(value)) {
585
+ throw new BoardToolUserError(`Desktop app bridge file is invalid: ${bridgePath}`);
586
+ }
587
+ return value;
588
+ }
589
+ function sendJsonLine(endpoint, payload) {
590
+ return new Promise((resolve, reject) => {
591
+ const socket = net2.createConnection({ host: endpoint.host, port: endpoint.port });
592
+ let buffer = "";
593
+ let settled = false;
594
+ const connectTimer = setTimeout(() => {
595
+ socket.destroy();
596
+ reject(new BoardToolUserError("Desktop app is not available for writes: bridge connection timed out"));
597
+ }, CONNECT_TIMEOUT_MS);
598
+ const responseTimer = setTimeout(() => {
599
+ socket.destroy();
600
+ reject(new BoardToolUserError("Desktop app did not finish the write command before the timeout"));
601
+ }, RESPONSE_TIMEOUT_MS);
602
+ const settle = (fn) => {
603
+ if (settled) return;
604
+ settled = true;
605
+ clearTimeout(connectTimer);
606
+ clearTimeout(responseTimer);
607
+ fn();
608
+ };
609
+ socket.setEncoding("utf8");
610
+ socket.on("connect", () => {
611
+ clearTimeout(connectTimer);
612
+ socket.write(`${JSON.stringify(payload)}
613
+ `);
614
+ });
615
+ socket.on("data", (chunk) => {
616
+ buffer += chunk;
617
+ const newline = buffer.indexOf("\n");
618
+ if (newline < 0) return;
619
+ const line = buffer.slice(0, newline);
620
+ settle(() => {
621
+ socket.end();
622
+ try {
623
+ const parsed = JSON.parse(line);
624
+ if (isBridgeResponse(parsed)) {
625
+ resolve(parsed);
626
+ } else {
627
+ reject(new BoardToolUserError("Desktop app returned an invalid bridge response"));
628
+ }
629
+ } catch (error) {
630
+ reject(new BoardToolUserError(`Desktop app returned invalid JSON: ${errorMessage(error)}`));
631
+ }
632
+ });
633
+ });
634
+ socket.on("error", (error) => {
635
+ settle(() => {
636
+ reject(
637
+ new BoardToolUserError(
638
+ `Desktop app is not available for writes: failed to connect to bridge ${endpoint.host}:${endpoint.port}. Details: ${error.message}`
639
+ )
640
+ );
641
+ });
642
+ });
643
+ socket.on("close", () => {
644
+ if (settled) return;
645
+ settle(() => {
646
+ reject(new BoardToolUserError("Desktop app bridge closed before returning a write result"));
647
+ });
648
+ });
649
+ });
650
+ }
651
+ function isEndpoint2(value) {
652
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
653
+ const endpoint = value;
654
+ return endpoint.version === 1 && endpoint.host === "127.0.0.1" && typeof endpoint.port === "number" && Number.isInteger(endpoint.port) && endpoint.port > 0 && typeof endpoint.token === "string" && endpoint.token.length > 0 && typeof endpoint.updatedAt === "number";
655
+ }
656
+ function isBridgeResponse(value) {
657
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
658
+ const response = value;
659
+ if (response.ok === true) return typeof response.data === "object" && response.data !== null;
660
+ if (response.ok === false) return typeof response.error === "string";
661
+ return false;
662
+ }
663
+
664
+ // src/index.ts
665
+ var PROJECT_ROOT_DESCRIPTION = 'Canvas project root absolute path, or "active" for the current canvas';
666
+ var ASSET_KIND_VALUES = ["image", "video", "audio", "model", "text", "markdown", "html", "importing", "generating"];
667
+ var NODE_TYPE_VALUES2 = ASSET_KIND_VALUES;
668
+ var MODEL_FORMAT_VALUES = [
669
+ "glb",
670
+ "gltf",
671
+ "obj",
672
+ "fbx",
673
+ "stl",
674
+ "ply",
675
+ "dae",
676
+ "3mf",
677
+ "3ds",
678
+ "vrml",
679
+ "wrl",
680
+ "zip"
681
+ ];
682
+ var anyOutputSchema = {
683
+ type: "object",
684
+ additionalProperties: true
685
+ };
686
+ var canvasPlugin = {
687
+ name: "@pixi-board/board-plugin-canvas",
688
+ version: "0.1.0",
689
+ register(api) {
690
+ for (const tool2 of canvasTools) {
691
+ api.registerTool(tool2);
692
+ }
693
+ }
694
+ };
695
+ var plugin = canvasPlugin;
696
+ var canvasTools = [
697
+ tool({
698
+ name: "canvas.get_project_list",
699
+ description: "List known canvas projects from the shared canvas project registry.",
700
+ inputSchema: objectSchema({}, []),
701
+ async run() {
702
+ return {
703
+ projects: await listKnownProjects()
704
+ };
705
+ }
706
+ }),
707
+ tool({
708
+ name: "canvas.get_board_snapshot",
709
+ description: "Read a compact board snapshot from local project files. Does not require the desktop app.",
710
+ inputSchema: objectSchema({
711
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION)
712
+ }),
713
+ async run(input) {
714
+ const project = await loadProject(readString(input, "projectRoot"));
715
+ return toSnapshotDto(project.root, project.snapshot);
716
+ }
717
+ }),
718
+ tool({
719
+ name: "canvas.list_nodes",
720
+ description: "List compact board nodes, optionally filtered by bounds or keyword.",
721
+ inputSchema: objectSchema({
722
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
723
+ filter: {
724
+ type: "object",
725
+ additionalProperties: true,
726
+ properties: {
727
+ type: { type: "string", enum: NODE_TYPE_VALUES2 },
728
+ keyword: { type: "string" },
729
+ name: { type: "string" },
730
+ text: { type: "string" },
731
+ bounds: {
732
+ type: "object",
733
+ additionalProperties: false,
734
+ properties: {
735
+ x: { type: "number" },
736
+ y: { type: "number" },
737
+ width: { type: "number" },
738
+ height: { type: "number" },
739
+ minX: { type: "number" },
740
+ minY: { type: "number" },
741
+ maxX: { type: "number" },
742
+ maxY: { type: "number" }
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }),
748
+ async run(input) {
749
+ const args = asRecord(input);
750
+ const project = await loadProject(readString(args, "projectRoot"));
751
+ return {
752
+ nodes: filterNodes(project.snapshot.nodes, args.filter).map(toNodeDto)
753
+ };
754
+ }
755
+ }),
756
+ tool({
757
+ name: "canvas.get_node",
758
+ description: "Read compact details for one board node from local project files.",
759
+ inputSchema: objectSchema({
760
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
761
+ nodeId: stringSchema("Node id")
762
+ }, ["projectRoot", "nodeId"]),
763
+ async run(input) {
764
+ const args = asRecord(input);
765
+ const project = await loadProject(readString(args, "projectRoot"));
766
+ return {
767
+ node: toNodeDto(getNodeOrThrow(project.snapshot, args.nodeId))
768
+ };
769
+ }
770
+ }),
771
+ tool({
772
+ name: "canvas.create_nodes",
773
+ description: "Ask the running desktop app to create asset-driven BoardNode entries from local files, then save through desktop logic. Pass a file path unless kind is generating. Write text, markdown, or html content to a source file first and pass that path. Do not provide x/y; the desktop canvas chooses placement and returns the created node coordinates.",
774
+ inputSchema: objectSchema({
775
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
776
+ nodes: {
777
+ type: "array",
778
+ minItems: 1,
779
+ items: {
780
+ type: "object",
781
+ additionalProperties: true,
782
+ properties: {
783
+ path: { type: "string" },
784
+ kind: {
785
+ type: "string",
786
+ description: "Optional compatibility hint. Use generating to create a lightweight placeholder without a path; otherwise the desktop importer determines the actual asset kind from the file.",
787
+ enum: ASSET_KIND_VALUES
788
+ },
789
+ width: { type: "number" },
790
+ height: { type: "number" },
791
+ options: {
792
+ type: "object",
793
+ description: "Renderer parameters only. Do not put text, markdown, or html document content here.",
794
+ additionalProperties: true
795
+ },
796
+ name: { type: "string" }
797
+ }
798
+ }
799
+ }
800
+ }, ["projectRoot", "nodes"]),
801
+ async run(input) {
802
+ const args = asRecord(input);
803
+ const command = {
804
+ kind: "create_nodes",
805
+ projectRoot: readString(args, "projectRoot"),
806
+ nodes: readCreateNodes(args.nodes)
807
+ };
808
+ const result = await sendWriteCommand(command);
809
+ const project = await loadProject(command.projectRoot);
810
+ return {
811
+ nodes: result.nodes.map(toNodeDto),
812
+ assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset))
813
+ };
814
+ }
815
+ }),
816
+ tool({
817
+ name: "canvas.generating_node_install",
818
+ description: "Replace an existing generating placeholder node with a real local file asset. The desktop app imports the file, updates the node type/asset/name, and resizes the node to the real asset dimensions.",
819
+ inputSchema: objectSchema({
820
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
821
+ nodeId: stringSchema("Generating board node id"),
822
+ path: stringSchema("Local file path to install into the generating node")
823
+ }, ["projectRoot", "nodeId", "path"]),
824
+ async run(input) {
825
+ const args = asRecord(input);
826
+ const command = {
827
+ kind: "generating_node_install",
828
+ projectRoot: readString(args, "projectRoot"),
829
+ nodeId: readString(args, "nodeId"),
830
+ path: readString(args, "path")
831
+ };
832
+ const result = await sendWriteCommand(command);
833
+ const project = await loadProject(command.projectRoot);
834
+ return {
835
+ nodes: result.nodes.map(toNodeDto),
836
+ assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset))
837
+ };
838
+ }
839
+ }),
840
+ tool({
841
+ name: "canvas.update_nodes",
842
+ description: "Ask the running desktop app to merge node geometry, name, and node option updates, then save through desktop logic. Do not send text, markdown, or html content here; edit the source file for content changes.",
843
+ inputSchema: objectSchema({
844
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
845
+ updates: {
846
+ type: "array",
847
+ minItems: 1,
848
+ items: {
849
+ type: "object",
850
+ additionalProperties: true,
851
+ required: ["id"],
852
+ properties: {
853
+ id: { type: "string" },
854
+ name: { type: "string" },
855
+ options: {
856
+ type: "object",
857
+ description: "Renderer parameters only. Do not put text, markdown, or html document content here.",
858
+ additionalProperties: true
859
+ },
860
+ x: { type: "number" },
861
+ y: { type: "number" },
862
+ width: { type: "number" },
863
+ height: { type: "number" },
864
+ rotation: { type: "number" },
865
+ zIndex: { type: "number" },
866
+ locked: { type: "boolean" }
867
+ }
868
+ }
869
+ }
870
+ }, ["projectRoot", "updates"]),
871
+ async run(input) {
872
+ const args = asRecord(input);
873
+ const command = {
874
+ kind: "update_nodes",
875
+ projectRoot: readString(args, "projectRoot"),
876
+ updates: readUpdates(args.updates)
877
+ };
878
+ const result = await sendWriteCommand(command);
879
+ return {
880
+ nodes: result.nodes.map(toNodeDto)
881
+ };
882
+ }
883
+ }),
884
+ tool({
885
+ name: "canvas.update_assets",
886
+ description: "Ask the running desktop app to replace asset metadata and media dimensions, then save through desktop logic. Do not use this to write text, markdown, or html content; edit the source file instead.",
887
+ inputSchema: objectSchema({
888
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
889
+ assets: {
890
+ type: "array",
891
+ minItems: 1,
892
+ items: {
893
+ type: "object",
894
+ additionalProperties: true,
895
+ required: ["id"],
896
+ properties: {
897
+ id: { type: "string" },
898
+ metadata: { type: "object", additionalProperties: true },
899
+ width: { type: "number" },
900
+ height: { type: "number" },
901
+ duration: { type: "number" },
902
+ format: {
903
+ type: "string",
904
+ enum: MODEL_FORMAT_VALUES
905
+ }
906
+ }
907
+ }
908
+ }
909
+ }, ["projectRoot", "assets"]),
910
+ async run(input) {
911
+ const args = asRecord(input);
912
+ const command = {
913
+ kind: "update_assets",
914
+ projectRoot: readString(args, "projectRoot"),
915
+ assets: readAssetUpdates(args.assets)
916
+ };
917
+ const result = await sendWriteCommand(command);
918
+ const project = await loadProject(command.projectRoot);
919
+ return {
920
+ assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset))
921
+ };
922
+ }
923
+ }),
924
+ tool({
925
+ name: "canvas.refresh_node_preview",
926
+ description: "Ask the running desktop app to regenerate a text, markdown, or html node preview from its source file using the node's current bounds.",
927
+ inputSchema: objectSchema({
928
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
929
+ nodeId: stringSchema("Text, markdown, or html board node id")
930
+ }, ["projectRoot", "nodeId"]),
931
+ async run(input) {
932
+ const args = asRecord(input);
933
+ const command = {
934
+ kind: "refresh_node_preview",
935
+ projectRoot: readString(args, "projectRoot"),
936
+ nodeId: readString(args, "nodeId")
937
+ };
938
+ const result = await sendWriteCommand(command);
939
+ const project = await loadProject(command.projectRoot);
940
+ return {
941
+ nodes: result.nodes.map(toNodeDto),
942
+ assets: (result.assets ?? []).map((asset) => toAssetDto(project.root, asset))
943
+ };
944
+ }
945
+ }),
946
+ tool({
947
+ name: "canvas.get_origin_asset_by_node",
948
+ description: "Return original asset information for a file-backed board node.",
949
+ inputSchema: objectSchema({
950
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
951
+ nodeId: stringSchema("Board node id")
952
+ }, ["projectRoot", "nodeId"]),
953
+ async run(input) {
954
+ const args = asRecord(input);
955
+ const project = await loadProject(readString(args, "projectRoot"));
956
+ const node = getNodeOrThrow(project.snapshot, args.nodeId);
957
+ const asset = getOriginAssetForNode(project.snapshot, args.nodeId);
958
+ return {
959
+ node: toNodeDto(node),
960
+ asset: {
961
+ ...toAssetDto(project.root, asset),
962
+ accessibleUrl: asset.webLink ?? asset.sourceUrl ?? null
963
+ }
964
+ };
965
+ }
966
+ }),
967
+ tool({
968
+ name: "canvas.get_asset",
969
+ description: "Return compact asset details including original, derivatives, and metadata.",
970
+ inputSchema: objectSchema({
971
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION),
972
+ assetId: stringSchema("Asset id")
973
+ }, ["projectRoot", "assetId"]),
974
+ async run(input) {
975
+ const args = asRecord(input);
976
+ const project = await loadProject(readString(args, "projectRoot"));
977
+ return {
978
+ asset: toAssetDto(project.root, getAssetOrThrow(project.snapshot, args.assetId))
979
+ };
980
+ }
981
+ }),
982
+ tool({
983
+ name: "canvas.read_project_info",
984
+ description: "Read compact project info, counts, viewport, and update timestamps.",
985
+ inputSchema: objectSchema({
986
+ projectRoot: stringSchema(PROJECT_ROOT_DESCRIPTION)
987
+ }),
988
+ async run(input) {
989
+ const project = await loadProject(readString(input, "projectRoot"));
990
+ return {
991
+ projectRoot: project.root,
992
+ boardPath: project.boardPath,
993
+ assetsPath: project.assetsPath,
994
+ nodeCount: project.snapshot.nodes.length,
995
+ assetCount: project.snapshot.assets.length,
996
+ viewport: project.snapshot.viewport ?? null,
997
+ updatedAt: Math.max(project.boardUpdatedAt ?? 0, project.assetsUpdatedAt ?? 0),
998
+ boardUpdatedAt: project.boardUpdatedAt,
999
+ assetsUpdatedAt: project.assetsUpdatedAt
1000
+ };
1001
+ }
1002
+ })
1003
+ ];
1004
+ function tool(definition) {
1005
+ return {
1006
+ kind: "builtin",
1007
+ outputSchema: definition.outputSchema ?? anyOutputSchema,
1008
+ ...definition
1009
+ };
1010
+ }
1011
+ function objectSchema(properties, required = ["projectRoot"]) {
1012
+ return {
1013
+ type: "object",
1014
+ additionalProperties: false,
1015
+ required,
1016
+ properties
1017
+ };
1018
+ }
1019
+ function stringSchema(description) {
1020
+ return { type: "string", description };
1021
+ }
1022
+ function readCreateNodes(value) {
1023
+ if (!Array.isArray(value) || value.length === 0) {
1024
+ throw new BoardToolUserError("nodes must be a non-empty array");
1025
+ }
1026
+ return value.map((entry, index) => {
1027
+ const node = asRecord(entry, `nodes[${index}]`);
1028
+ if (node.x !== void 0 || node.y !== void 0) {
1029
+ throw new BoardToolUserError(`nodes[${index}].x/y is not supported; the desktop canvas chooses placement`);
1030
+ }
1031
+ if (node.kind !== void 0 && !isAssetKind(node.kind)) {
1032
+ throw new BoardToolUserError(`nodes[${index}].kind must be one of ${ASSET_KIND_VALUES.join(", ")}`);
1033
+ }
1034
+ const kind = node.kind;
1035
+ const hasPath = typeof node.path === "string" && node.path.trim() !== "";
1036
+ if (kind === "generating") {
1037
+ if (node.path !== void 0 && !hasPath) {
1038
+ throw new BoardToolUserError(`nodes[${index}].path must be a non-empty string when provided`);
1039
+ }
1040
+ } else if (!hasPath) {
1041
+ throw new BoardToolUserError(`nodes[${index}].path must be a non-empty string unless kind is generating`);
1042
+ }
1043
+ if (node.metadata !== void 0) {
1044
+ throw new BoardToolUserError(`nodes[${index}].metadata is not supported; write content to a source file and pass path`);
1045
+ }
1046
+ if (node.text !== void 0) {
1047
+ throw new BoardToolUserError(`nodes[${index}].text is not supported; write content to a source file and pass path`);
1048
+ }
1049
+ if (node.options !== void 0) {
1050
+ asRecord(node.options, `nodes[${index}].options`);
1051
+ }
1052
+ return node;
1053
+ });
1054
+ }
1055
+ function isAssetKind(value) {
1056
+ return typeof value === "string" && ASSET_KIND_VALUES.includes(value);
1057
+ }
1058
+ function readAssetUpdates(value) {
1059
+ if (!Array.isArray(value) || value.length === 0) {
1060
+ throw new BoardToolUserError("assets must be a non-empty array");
1061
+ }
1062
+ return value.map((entry, index) => {
1063
+ const update = asRecord(entry, `assets[${index}]`);
1064
+ readString(update, "id");
1065
+ readOptionalNumber(update, "width", `assets[${index}]`);
1066
+ readOptionalNumber(update, "height", `assets[${index}]`);
1067
+ readOptionalNumber(update, "duration", `assets[${index}]`);
1068
+ if (update.format !== void 0 && !MODEL_FORMAT_VALUES.includes(update.format)) {
1069
+ throw new BoardToolUserError(`assets[${index}].format must be a supported model format`);
1070
+ }
1071
+ if (update.metadata !== void 0) {
1072
+ const metadata = asRecord(update.metadata, `assets[${index}].metadata`);
1073
+ assertNoContentMetadata(metadata, `assets[${index}].metadata`);
1074
+ }
1075
+ return update;
1076
+ });
1077
+ }
1078
+ function assertNoContentMetadata(metadata, label) {
1079
+ for (const key of ["html", "markdown", "text", "content"]) {
1080
+ if (metadata[key] !== void 0) {
1081
+ throw new BoardToolUserError(`${label}.${key} is not supported; edit the source file instead`);
1082
+ }
1083
+ }
1084
+ }
1085
+ function readUpdates(value) {
1086
+ if (!Array.isArray(value) || value.length === 0) {
1087
+ throw new BoardToolUserError("updates must be a non-empty array");
1088
+ }
1089
+ return value.map((entry, index) => {
1090
+ const update = asRecord(entry, `updates[${index}]`);
1091
+ readString(update, "id");
1092
+ if ("text" in update) {
1093
+ throw new BoardToolUserError(`updates[${index}].text is not supported; edit the source file instead`);
1094
+ }
1095
+ readOptionalNumber(update, "x", `updates[${index}]`);
1096
+ readOptionalNumber(update, "y", `updates[${index}]`);
1097
+ readOptionalNumber(update, "width", `updates[${index}]`);
1098
+ readOptionalNumber(update, "height", `updates[${index}]`);
1099
+ readOptionalNumber(update, "rotation", `updates[${index}]`);
1100
+ readOptionalNumber(update, "zIndex", `updates[${index}]`);
1101
+ if (update.options !== void 0) {
1102
+ asRecord(update.options, `updates[${index}].options`);
1103
+ }
1104
+ return update;
1105
+ });
1106
+ }
1107
+ function readString(input, key) {
1108
+ const record = asRecord(input);
1109
+ const value = record[key];
1110
+ if (typeof value !== "string" || value.trim() === "") {
1111
+ throw new BoardToolUserError(`${key} must be a non-empty string`);
1112
+ }
1113
+ return value;
1114
+ }
1115
+ function readOptionalNumber(input, key, label) {
1116
+ const value = input[key];
1117
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) {
1118
+ throw new BoardToolUserError(`${label}.${key} must be a finite number`);
1119
+ }
1120
+ }
1121
+ function asRecord(input, label = "input") {
1122
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
1123
+ throw new BoardToolUserError(`${label} must be an object`);
1124
+ }
1125
+ return input;
1126
+ }
1127
+ export {
1128
+ canvasPlugin,
1129
+ canvasTools,
1130
+ plugin
1131
+ };