@trohde/agentic-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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/SECURITY.md +19 -0
- package/dist/cli/index.js +1873 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/web/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/web/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/web/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/web/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/web/assets/ar-SA-G6X2FPQ2-Bx_JGCzc.js +10 -0
- package/dist/web/assets/arc-CZEYm-X2.js +1 -0
- package/dist/web/assets/architecture-7EHR7CIX-BW08P3jh.js +1 -0
- package/dist/web/assets/architectureDiagram-3BPJPVTR-BX4UqKAe.js +36 -0
- package/dist/web/assets/array-BifhSqXX.js +1 -0
- package/dist/web/assets/az-AZ-76LH7QW2-CQnzCEm_.js +1 -0
- package/dist/web/assets/bg-BG-XCXSNQG7-B6OhJNg1.js +5 -0
- package/dist/web/assets/blockDiagram-GPEHLZMM-DOaLZNH2.js +132 -0
- package/dist/web/assets/bn-BD-2XOGV67Q-CL8DmeD-.js +5 -0
- package/dist/web/assets/c4Diagram-AAUBKEIU-BOYbD4o9.js +10 -0
- package/dist/web/assets/ca-ES-6MX7JW3Y-DqXuGb3N.js +8 -0
- package/dist/web/assets/channel-jlcmNkDM.js +1 -0
- package/dist/web/assets/chunk-2J33WTMH-9pRDhFmL.js +1 -0
- package/dist/web/assets/chunk-3OPIFGDE-Cxtej-eZ.js +62 -0
- package/dist/web/assets/chunk-4BX2VUAB-WGnRPgB4.js +1 -0
- package/dist/web/assets/chunk-55IACEB6-By23tM0L.js +1 -0
- package/dist/web/assets/chunk-5ZQYHXKU-BbvpmZDT.js +2 -0
- package/dist/web/assets/chunk-727SXJPM-DeN-6nx2.js +206 -0
- package/dist/web/assets/chunk-AQP2D5EJ-3vlT2osY.js +231 -0
- package/dist/web/assets/chunk-BSJP7CBP-ggZwEsYs.js +1 -0
- package/dist/web/assets/chunk-CSCIHK7Q-CkCt7BGB.js +124 -0
- package/dist/web/assets/chunk-EIO257PC-DhAXVvAc.js +22 -0
- package/dist/web/assets/chunk-FMBD7UC4-BWW1dpup.js +15 -0
- package/dist/web/assets/chunk-KSCS5N6A-ByGpruIk.js +10 -0
- package/dist/web/assets/chunk-L5ZTLDWV-Brj903IS.js +1 -0
- package/dist/web/assets/chunk-LZXEDZCA-B0E6SbHa.js +2 -0
- package/dist/web/assets/chunk-ND2GUHAM-cgO1vAyy.js +1 -0
- package/dist/web/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/dist/web/assets/chunk-NZK2D7GU-rJf6zpwf.js +1 -0
- package/dist/web/assets/chunk-O5CBEL6O-DZpUP7B-.js +70 -0
- package/dist/web/assets/chunk-QZHKN3VN-B2hsWqvV.js +1 -0
- package/dist/web/assets/chunk-WU5MYG2G-ad-_f9bA.js +1 -0
- package/dist/web/assets/chunk-XPW4576I-DRG_5uRX.js +32 -0
- package/dist/web/assets/classDiagram-4FO5ZUOK-pgvxJ5K2.js +1 -0
- package/dist/web/assets/classDiagram-v2-Q7XG4LA2-pgvxJ5K2.js +1 -0
- package/dist/web/assets/cose-bilkent-S5V4N54A-Dzdtq3ax.js +1 -0
- package/dist/web/assets/cs-CZ-2BRQDIVT-C9Hi60ft.js +11 -0
- package/dist/web/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/dist/web/assets/da-DK-5WZEPLOC-DuscK2dU.js +5 -0
- package/dist/web/assets/dagre-BM42HDAG-DG-1WRTH.js +4 -0
- package/dist/web/assets/dagre-Bx709z4p.js +1 -0
- package/dist/web/assets/de-DE-XR44H4JA-BUknVi9m.js +8 -0
- package/dist/web/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/web/assets/diagram-2AECGRRQ-C01yy19V.js +43 -0
- package/dist/web/assets/diagram-5GNKFQAL-9Ly54gPf.js +10 -0
- package/dist/web/assets/diagram-KO2AKTUF-Bnzn_Fk2.js +3 -0
- package/dist/web/assets/diagram-LMA3HP47-CkQJH7qS.js +24 -0
- package/dist/web/assets/diagram-OG6HWLK6-sYTSt9S6.js +24 -0
- package/dist/web/assets/directory-open-01563666-D4xXyWb_.js +1 -0
- package/dist/web/assets/directory-open-4ed118d0-1i309Asm.js +1 -0
- package/dist/web/assets/dist--k9jX8ko.js +27 -0
- package/dist/web/assets/dist-CHteZrRU.js +1 -0
- package/dist/web/assets/el-GR-BZB4AONW-CBx8IJCe.js +10 -0
- package/dist/web/assets/en-B4ZKOASM-D04FpeSQ.js +1 -0
- package/dist/web/assets/erDiagram-TEJ5UH35-kBTcPhaQ.js +85 -0
- package/dist/web/assets/es-ES-U4NZUMDT-nQPOkQIb.js +9 -0
- package/dist/web/assets/eu-ES-A7QVB2H4-OVL5j0Bz.js +11 -0
- package/dist/web/assets/eventmodeling-FCH6USID-3Z5bJ280.js +1 -0
- package/dist/web/assets/fa-IR-HGAKTJCU-BXJbxYFk.js +8 -0
- package/dist/web/assets/fi-FI-Z5N7JZ37-DoZfr4yv.js +6 -0
- package/dist/web/assets/file-open-002ab408-BHUWm0Sh.js +1 -0
- package/dist/web/assets/file-open-7c801643-DZtJT5zp.js +1 -0
- package/dist/web/assets/file-save-3189631c-CO9S4HFW.js +1 -0
- package/dist/web/assets/file-save-745eba88-Bwdfz6OZ.js +1 -0
- package/dist/web/assets/flowDiagram-I6XJVG4X-CwQyslgb.js +162 -0
- package/dist/web/assets/fr-FR-RHASNOE6-CXLv0m_p.js +9 -0
- package/dist/web/assets/ganttDiagram-6RSMTGT7-B-1gME9q.js +292 -0
- package/dist/web/assets/gitGraph-WXDBUCRP-BC3bsb5A.js +1 -0
- package/dist/web/assets/gitGraphDiagram-PVQCEYII-DUh_E2g-.js +106 -0
- package/dist/web/assets/gl-ES-HMX3MZ6V-DMYtQjyy.js +10 -0
- package/dist/web/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/web/assets/he-IL-6SHJWFNN-BXviMgxV.js +10 -0
- package/dist/web/assets/hi-IN-IWLTKZ5I-B6s0F0bv.js +4 -0
- package/dist/web/assets/hu-HU-A5ZG7DT2-DZY88thP.js +7 -0
- package/dist/web/assets/id-ID-SAP4L64H-CsRPE_UY.js +10 -0
- package/dist/web/assets/image-GAAHSSAO-Ufj_572K.js +1 -0
- package/dist/web/assets/image-blob-reduce.esm-NzmDxm1v.js +2 -0
- package/dist/web/assets/index-DedR31Vl.css +1 -0
- package/dist/web/assets/index-Do_WCY2x.js +207 -0
- package/dist/web/assets/info-J43DQDTF-DgcAwO7L.js +1 -0
- package/dist/web/assets/infoDiagram-5YYISTIA-BVoRZtf6.js +2 -0
- package/dist/web/assets/init-D6jRqBbL.js +1 -0
- package/dist/web/assets/ishikawaDiagram-YF4QCWOH-Pc0lSZjn.js +70 -0
- package/dist/web/assets/it-IT-JPQ66NNP-D6QIIUKx.js +11 -0
- package/dist/web/assets/ja-JP-DBVTYXUO-BKIOpiiN.js +8 -0
- package/dist/web/assets/journeyDiagram-JHISSGLW-CUTIkP_3.js +139 -0
- package/dist/web/assets/kaa-6HZHGXH3-Ck2PqClI.js +1 -0
- package/dist/web/assets/kab-KAB-ZGHBKWFO-9067fQ1h.js +8 -0
- package/dist/web/assets/kanban-definition-UN3LZRKU-f9h-Kl6S.js +89 -0
- package/dist/web/assets/katex-Vhh-h91d.js +257 -0
- package/dist/web/assets/kk-KZ-P5N5QNE5-Ds5pD5Rs.js +1 -0
- package/dist/web/assets/km-KH-HSX4SM5Z-BvgABkMn.js +11 -0
- package/dist/web/assets/ko-KR-MTYHY66A-BI-VJ_qS.js +9 -0
- package/dist/web/assets/ku-TR-6OUDTVRD-CzO0QxpT.js +9 -0
- package/dist/web/assets/line-BM7n-WSY.js +1 -0
- package/dist/web/assets/linear-CoV0e-iv.js +1 -0
- package/dist/web/assets/lt-LT-XHIRWOB4-Wdr8437y.js +3 -0
- package/dist/web/assets/lv-LV-5QDEKY6T-Baqs6ETz.js +7 -0
- package/dist/web/assets/mermaid-parser.core-BxU7L1C-.js +4 -0
- package/dist/web/assets/mindmap-definition-RKZ34NQL-e2CkjxCV.js +96 -0
- package/dist/web/assets/mr-IN-CRQNXWMA-_P3j2iZu.js +13 -0
- package/dist/web/assets/my-MM-5M5IBNSE-u-YXjqOx.js +1 -0
- package/dist/web/assets/nb-NO-T6EIAALU-BVTkGOfM.js +10 -0
- package/dist/web/assets/nl-NL-IS3SIHDZ-Dz45KRHt.js +8 -0
- package/dist/web/assets/nn-NO-6E72VCQL-Bv1T99Os.js +8 -0
- package/dist/web/assets/oc-FR-POXYY2M6-CWPx0BPy.js +8 -0
- package/dist/web/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/web/assets/pa-IN-N4M65BXN-CUAufnLD.js +4 -0
- package/dist/web/assets/packet-YPE3B663-DSUdqwD6.js +1 -0
- package/dist/web/assets/path-BWPyau1x.js +1 -0
- package/dist/web/assets/percentages-BXMCSKIN-BRi-lUYV.js +1 -0
- package/dist/web/assets/pica-lghYzniR.js +2 -0
- package/dist/web/assets/pie-LRSECV5Y-CtaLKxkL.js +1 -0
- package/dist/web/assets/pieDiagram-4H26LBE5-KQxiMI7y.js +30 -0
- package/dist/web/assets/pl-PL-T2D74RX3-BYDGKTrw.js +9 -0
- package/dist/web/assets/pt-BR-5N22H2LF-DiLHA1Fv.js +9 -0
- package/dist/web/assets/pt-PT-UZXXM6DQ-DO4nqrjh.js +9 -0
- package/dist/web/assets/quadrantDiagram-W4KKPZXB-DYnhMBii.js +7 -0
- package/dist/web/assets/radar-GUYGQ44K-BWVL_5jd.js +1 -0
- package/dist/web/assets/requirementDiagram-4Y6WPE33-CLSyCDmb.js +84 -0
- package/dist/web/assets/ro-RO-JPDTUUEW-CkArAo01.js +11 -0
- package/dist/web/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/web/assets/roundRect-D01gJrlt.js +1 -0
- package/dist/web/assets/ru-RU-B4JR7IUQ-CF7qZR2L.js +9 -0
- package/dist/web/assets/sankeyDiagram-5OEKKPKP-fCrXqVCu.js +40 -0
- package/dist/web/assets/sequenceDiagram-3UESZ5HK-Ibg1UsPm.js +162 -0
- package/dist/web/assets/si-LK-N5RQ5JYF-SlyZ_B_5.js +1 -0
- package/dist/web/assets/sk-SK-C5VTKIMK-B59JZXsV.js +6 -0
- package/dist/web/assets/sl-SI-NN7IZMDC-CkVIpRnH.js +6 -0
- package/dist/web/assets/src-Dc-yDLup.js +1 -0
- package/dist/web/assets/stateDiagram-AJRCARHV-Cg6ervwo.js +1 -0
- package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Z9e_4KHc.js +1 -0
- package/dist/web/assets/subset-shared.chunk-CnjPFGnW.js +1 -0
- package/dist/web/assets/subset-worker.chunk-DN4FBbFb.js +1 -0
- package/dist/web/assets/sv-SE-XGPEYMSR-BwnaFVSC.js +10 -0
- package/dist/web/assets/ta-IN-2NMHFXQM-Digwj7d-.js +9 -0
- package/dist/web/assets/th-TH-HPSO5L25-Ck3FgDpQ.js +2 -0
- package/dist/web/assets/timeline-definition-PNZ67QCA-Sxfm8qnw.js +120 -0
- package/dist/web/assets/tr-TR-DEFEU3FU-CZntbglt.js +7 -0
- package/dist/web/assets/treeView-BLDUP644-Cx8t5kj4.js +1 -0
- package/dist/web/assets/treemap-LRROVOQU-Dfh_IQDp.js +1 -0
- package/dist/web/assets/uk-UA-QMV73CPH-B0wjKowt.js +6 -0
- package/dist/web/assets/vennDiagram-CIIHVFJN-BMEOJ0gl.js +34 -0
- package/dist/web/assets/vi-VN-M7AON7JQ-Dj0BUYxm.js +5 -0
- package/dist/web/assets/wardley-L42UT6IY-EJTnFNq7.js +1 -0
- package/dist/web/assets/wardleyDiagram-YWT4CUSO-BJjdV56L.js +78 -0
- package/dist/web/assets/xychartDiagram-2RQKCTM6-CBQ88pV0.js +7 -0
- package/dist/web/assets/zh-CN-LNUGB5OW-K1_YaWy1.js +10 -0
- package/dist/web/assets/zh-HK-E62DVLB3-DCU_gXiJ.js +1 -0
- package/dist/web/assets/zh-TW-RAJ6MFWO-1ApXhCfP.js +9 -0
- package/dist/web/index.html +13 -0
- package/package.json +96 -0
|
@@ -0,0 +1,1873 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
import open from "open";
|
|
6
|
+
|
|
7
|
+
// src/server/httpServer.ts
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { createServer } from "http";
|
|
10
|
+
import net from "net";
|
|
11
|
+
import path4 from "path";
|
|
12
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13
|
+
|
|
14
|
+
// src/plugins/excalidraw/index.ts
|
|
15
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
16
|
+
|
|
17
|
+
// src/core/scene.ts
|
|
18
|
+
function isElementEndpoint(endpoint) {
|
|
19
|
+
return "elementId" in endpoint;
|
|
20
|
+
}
|
|
21
|
+
function cloneScene(scene) {
|
|
22
|
+
return {
|
|
23
|
+
elements: scene.elements.map((element) => ({ ...element })),
|
|
24
|
+
appState: { ...scene.appState },
|
|
25
|
+
files: { ...scene.files },
|
|
26
|
+
version: scene.version
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/plugins/excalidraw/adapter.ts
|
|
31
|
+
var SUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
32
|
+
"rectangle",
|
|
33
|
+
"ellipse",
|
|
34
|
+
"diamond",
|
|
35
|
+
"line",
|
|
36
|
+
"arrow",
|
|
37
|
+
"text",
|
|
38
|
+
"frame"
|
|
39
|
+
]);
|
|
40
|
+
function isSupportedElement(element) {
|
|
41
|
+
return SUPPORTED_TYPES.has(element.type) && !element.isDeleted;
|
|
42
|
+
}
|
|
43
|
+
function toCanvasObject(element) {
|
|
44
|
+
if (!isSupportedElement(element)) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: element.id,
|
|
49
|
+
type: element.type,
|
|
50
|
+
x: element.x,
|
|
51
|
+
y: element.y,
|
|
52
|
+
width: element.width,
|
|
53
|
+
height: element.height,
|
|
54
|
+
text: element.text,
|
|
55
|
+
points: element.points,
|
|
56
|
+
style: {
|
|
57
|
+
strokeColor: element.strokeColor,
|
|
58
|
+
backgroundColor: element.backgroundColor,
|
|
59
|
+
fillStyle: normalizeFillStyle(element.fillStyle),
|
|
60
|
+
strokeWidth: normalizeStrokeWidth(element.strokeWidth),
|
|
61
|
+
strokeStyle: element.strokeStyle,
|
|
62
|
+
roughness: normalizeRoughness(element.roughness),
|
|
63
|
+
opacity: element.opacity,
|
|
64
|
+
fontSize: element.fontSize,
|
|
65
|
+
textAlign: element.textAlign
|
|
66
|
+
},
|
|
67
|
+
containerId: element.containerId ?? void 0,
|
|
68
|
+
groupIds: element.groupIds,
|
|
69
|
+
frameId: element.frameId,
|
|
70
|
+
raw: element
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function toCanvasObjectSummary(element) {
|
|
74
|
+
const object = toCanvasObject(element);
|
|
75
|
+
if (!object) {
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
id: object.id,
|
|
80
|
+
type: object.type,
|
|
81
|
+
x: object.x,
|
|
82
|
+
y: object.y,
|
|
83
|
+
width: object.width,
|
|
84
|
+
height: object.height,
|
|
85
|
+
text: object.text
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function normalizeStrokeWidth(width) {
|
|
89
|
+
if (width === 1 || width === 4) {
|
|
90
|
+
return width;
|
|
91
|
+
}
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
function normalizeFillStyle(fillStyle) {
|
|
95
|
+
if (fillStyle === "hachure" || fillStyle === "cross-hatch") {
|
|
96
|
+
return fillStyle;
|
|
97
|
+
}
|
|
98
|
+
return "solid";
|
|
99
|
+
}
|
|
100
|
+
function normalizeRoughness(roughness) {
|
|
101
|
+
if (roughness === 0 || roughness === 2) {
|
|
102
|
+
return roughness;
|
|
103
|
+
}
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/plugins/excalidraw/elements.ts
|
|
108
|
+
import { randomUUID } from "crypto";
|
|
109
|
+
var DEFAULT_WIDTH = 160;
|
|
110
|
+
var DEFAULT_HEIGHT = 80;
|
|
111
|
+
var ORDER_KEY_DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
112
|
+
var elementIndexCounter = 0;
|
|
113
|
+
function buildElement(spec, options = {}) {
|
|
114
|
+
const type = spec.type;
|
|
115
|
+
const width = spec.width ?? defaultWidth(type);
|
|
116
|
+
const height = spec.height ?? defaultHeight(type);
|
|
117
|
+
const points = spec.points ?? defaultPoints(type, width, height);
|
|
118
|
+
const base = {
|
|
119
|
+
id: options.id ?? randomUUID(),
|
|
120
|
+
index: nextElementIndex(),
|
|
121
|
+
type,
|
|
122
|
+
x: spec.x,
|
|
123
|
+
y: spec.y,
|
|
124
|
+
width,
|
|
125
|
+
height,
|
|
126
|
+
angle: 0,
|
|
127
|
+
strokeColor: spec.style?.strokeColor ?? "#1e1e1e",
|
|
128
|
+
backgroundColor: spec.style?.backgroundColor ?? "transparent",
|
|
129
|
+
fillStyle: spec.style?.fillStyle ?? "solid",
|
|
130
|
+
strokeWidth: spec.style?.strokeWidth ?? 2,
|
|
131
|
+
strokeStyle: spec.style?.strokeStyle ?? "solid",
|
|
132
|
+
roughness: spec.style?.roughness ?? 1,
|
|
133
|
+
opacity: spec.style?.opacity ?? 100,
|
|
134
|
+
groupIds: options.groupIds ?? spec.groupIds ?? [],
|
|
135
|
+
frameId: options.frameId ?? null,
|
|
136
|
+
roundness: roundnessFor(type),
|
|
137
|
+
seed: randomInt31(),
|
|
138
|
+
version: 1,
|
|
139
|
+
versionNonce: randomInt31(),
|
|
140
|
+
isDeleted: false,
|
|
141
|
+
boundElements: options.boundElements ?? null,
|
|
142
|
+
updated: Date.now(),
|
|
143
|
+
link: null,
|
|
144
|
+
locked: false
|
|
145
|
+
};
|
|
146
|
+
if (type === "text") {
|
|
147
|
+
return {
|
|
148
|
+
...base,
|
|
149
|
+
width: spec.width ?? Math.max(40, (options.text ?? spec.text ?? "").length * 10),
|
|
150
|
+
height: spec.height ?? spec.style?.fontSize ?? 24,
|
|
151
|
+
text: options.text ?? spec.text ?? "",
|
|
152
|
+
originalText: options.text ?? spec.text ?? "",
|
|
153
|
+
fontSize: spec.style?.fontSize ?? 20,
|
|
154
|
+
fontFamily: 1,
|
|
155
|
+
textAlign: spec.style?.textAlign ?? "center",
|
|
156
|
+
verticalAlign: "middle",
|
|
157
|
+
containerId: options.containerId ?? spec.containerId ?? null,
|
|
158
|
+
lineHeight: 1.25,
|
|
159
|
+
autoResize: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (type === "line" || type === "arrow") {
|
|
163
|
+
return {
|
|
164
|
+
...base,
|
|
165
|
+
points,
|
|
166
|
+
width: linearWidth(points),
|
|
167
|
+
height: linearHeight(points),
|
|
168
|
+
lastCommittedPoint: null,
|
|
169
|
+
startBinding: options.startBinding ?? null,
|
|
170
|
+
endBinding: options.endBinding ?? null,
|
|
171
|
+
startArrowhead: null,
|
|
172
|
+
endArrowhead: type === "arrow" ? "arrow" : null
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (type === "frame") {
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
backgroundColor: spec.style?.backgroundColor ?? "transparent",
|
|
179
|
+
name: options.name ?? spec.text ?? null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return base;
|
|
183
|
+
}
|
|
184
|
+
function makeBinding(elementId) {
|
|
185
|
+
return { elementId, focus: 0, gap: 0 };
|
|
186
|
+
}
|
|
187
|
+
function addBoundElement(target, bound) {
|
|
188
|
+
const current = target.boundElements ?? [];
|
|
189
|
+
if (!current.some((entry) => entry.id === bound.id)) {
|
|
190
|
+
target.boundElements = [...current, bound];
|
|
191
|
+
touchElement(target);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function touchElement(element) {
|
|
195
|
+
element.version += 1;
|
|
196
|
+
element.versionNonce = randomInt31();
|
|
197
|
+
element.updated = Date.now();
|
|
198
|
+
}
|
|
199
|
+
function defaultWidth(type) {
|
|
200
|
+
return type === "text" ? 80 : DEFAULT_WIDTH;
|
|
201
|
+
}
|
|
202
|
+
function defaultHeight(type) {
|
|
203
|
+
return type === "text" ? 24 : DEFAULT_HEIGHT;
|
|
204
|
+
}
|
|
205
|
+
function defaultPoints(type, width, height) {
|
|
206
|
+
if (type === "line" || type === "arrow") {
|
|
207
|
+
return [
|
|
208
|
+
[0, 0],
|
|
209
|
+
[width, height]
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
function linearWidth(points) {
|
|
215
|
+
const xs = points.map(([x]) => x);
|
|
216
|
+
return Math.max(...xs) - Math.min(...xs);
|
|
217
|
+
}
|
|
218
|
+
function linearHeight(points) {
|
|
219
|
+
const ys = points.map(([, y]) => y);
|
|
220
|
+
return Math.max(...ys) - Math.min(...ys);
|
|
221
|
+
}
|
|
222
|
+
function roundnessFor(type) {
|
|
223
|
+
if (type === "rectangle" || type === "diamond") {
|
|
224
|
+
return { type: 3 };
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function randomInt31() {
|
|
229
|
+
return Math.floor(Math.random() * 2147483647);
|
|
230
|
+
}
|
|
231
|
+
function nextElementIndex() {
|
|
232
|
+
return orderKeyForInteger(elementIndexCounter++);
|
|
233
|
+
}
|
|
234
|
+
function orderKeyForInteger(value) {
|
|
235
|
+
let remaining = value;
|
|
236
|
+
for (let width = 1; width <= 26; width += 1) {
|
|
237
|
+
const capacity = ORDER_KEY_DIGITS.length ** width;
|
|
238
|
+
if (remaining < capacity) {
|
|
239
|
+
return `${String.fromCharCode("a".charCodeAt(0) + width - 1)}${encodeOrderDigits(
|
|
240
|
+
remaining,
|
|
241
|
+
width
|
|
242
|
+
)}`;
|
|
243
|
+
}
|
|
244
|
+
remaining -= capacity;
|
|
245
|
+
}
|
|
246
|
+
throw new Error("Element order key range exhausted");
|
|
247
|
+
}
|
|
248
|
+
function encodeOrderDigits(value, width) {
|
|
249
|
+
let encoded = "";
|
|
250
|
+
let remaining = value;
|
|
251
|
+
for (let index = 0; index < width; index += 1) {
|
|
252
|
+
encoded = ORDER_KEY_DIGITS[remaining % ORDER_KEY_DIGITS.length] + encoded;
|
|
253
|
+
remaining = Math.floor(remaining / ORDER_KEY_DIGITS.length);
|
|
254
|
+
}
|
|
255
|
+
return encoded;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/plugins/excalidraw/format.ts
|
|
259
|
+
function serializeScene(scene) {
|
|
260
|
+
return {
|
|
261
|
+
type: "excalidraw",
|
|
262
|
+
version: 2,
|
|
263
|
+
source: "agentic-canvas",
|
|
264
|
+
elements: scene.elements,
|
|
265
|
+
appState: {
|
|
266
|
+
viewBackgroundColor: scene.appState.viewBackgroundColor
|
|
267
|
+
},
|
|
268
|
+
files: scene.files
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function deserializeScene(raw) {
|
|
272
|
+
const parsed = JSON.parse(raw);
|
|
273
|
+
if (parsed.type !== "excalidraw" || !Array.isArray(parsed.elements)) {
|
|
274
|
+
throw new Error("Invalid .excalidraw file");
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
elements: parsed.elements,
|
|
278
|
+
appState: {
|
|
279
|
+
viewBackgroundColor: parsed.appState?.viewBackgroundColor ?? "#ffffff"
|
|
280
|
+
},
|
|
281
|
+
files: parsed.files ?? {},
|
|
282
|
+
version: 0
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/plugins/excalidraw/geometry.ts
|
|
287
|
+
function centerPoint(element) {
|
|
288
|
+
return {
|
|
289
|
+
x: element.x + element.width / 2,
|
|
290
|
+
y: element.y + element.height / 2
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function edgePoint(element, towardX, towardY) {
|
|
294
|
+
const center = centerPoint(element);
|
|
295
|
+
const dx = towardX - center.x;
|
|
296
|
+
const dy = towardY - center.y;
|
|
297
|
+
if (dx === 0 && dy === 0) {
|
|
298
|
+
return center;
|
|
299
|
+
}
|
|
300
|
+
const halfWidth = element.width / 2;
|
|
301
|
+
const halfHeight = element.height / 2;
|
|
302
|
+
if (halfWidth <= 0 || halfHeight <= 0) {
|
|
303
|
+
return center;
|
|
304
|
+
}
|
|
305
|
+
if (element.type === "ellipse") {
|
|
306
|
+
const scale2 = 1 / Math.sqrt(dx * dx / (halfWidth * halfWidth) + dy * dy / (halfHeight * halfHeight));
|
|
307
|
+
return {
|
|
308
|
+
x: center.x + dx * scale2,
|
|
309
|
+
y: center.y + dy * scale2
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (element.type === "diamond") {
|
|
313
|
+
const scale2 = 1 / (Math.abs(dx) / halfWidth + Math.abs(dy) / halfHeight);
|
|
314
|
+
return {
|
|
315
|
+
x: center.x + dx * scale2,
|
|
316
|
+
y: center.y + dy * scale2
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const xScale = dx === 0 ? Number.POSITIVE_INFINITY : halfWidth / Math.abs(dx);
|
|
320
|
+
const yScale = dy === 0 ? Number.POSITIVE_INFINITY : halfHeight / Math.abs(dy);
|
|
321
|
+
const scale = Math.min(xScale, yScale);
|
|
322
|
+
return {
|
|
323
|
+
x: center.x + dx * scale,
|
|
324
|
+
y: center.y + dy * scale
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/plugins/excalidraw/tools.ts
|
|
329
|
+
import { z as z2 } from "zod";
|
|
330
|
+
|
|
331
|
+
// src/mcp/schemas.ts
|
|
332
|
+
import { z } from "zod";
|
|
333
|
+
var canvasObjectTypeSchema = z.enum([
|
|
334
|
+
"rectangle",
|
|
335
|
+
"ellipse",
|
|
336
|
+
"diamond",
|
|
337
|
+
"line",
|
|
338
|
+
"arrow",
|
|
339
|
+
"text",
|
|
340
|
+
"frame"
|
|
341
|
+
]);
|
|
342
|
+
var styleSchema = z.object({
|
|
343
|
+
strokeColor: z.string().optional(),
|
|
344
|
+
backgroundColor: z.string().optional(),
|
|
345
|
+
fillStyle: z.enum(["hachure", "cross-hatch", "solid"]).optional(),
|
|
346
|
+
strokeWidth: z.union([z.literal(1), z.literal(2), z.literal(4)]).optional(),
|
|
347
|
+
strokeStyle: z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
348
|
+
roughness: z.union([z.literal(0), z.literal(1), z.literal(2)]).optional(),
|
|
349
|
+
opacity: z.number().min(0).max(100).optional(),
|
|
350
|
+
fontSize: z.number().positive().optional(),
|
|
351
|
+
textAlign: z.enum(["left", "center", "right"]).optional()
|
|
352
|
+
});
|
|
353
|
+
var endpointSchema = z.union([
|
|
354
|
+
z.object({ elementId: z.string() }),
|
|
355
|
+
z.object({ x: z.number(), y: z.number() })
|
|
356
|
+
]);
|
|
357
|
+
var pointSchema = z.array(z.number()).length(2);
|
|
358
|
+
var pointsSchema = z.array(pointSchema);
|
|
359
|
+
var createObjectShape = {
|
|
360
|
+
type: canvasObjectTypeSchema,
|
|
361
|
+
x: z.number(),
|
|
362
|
+
y: z.number(),
|
|
363
|
+
width: z.number().optional(),
|
|
364
|
+
height: z.number().optional(),
|
|
365
|
+
text: z.string().optional(),
|
|
366
|
+
points: pointsSchema.optional(),
|
|
367
|
+
style: styleSchema.optional(),
|
|
368
|
+
start: endpointSchema.optional(),
|
|
369
|
+
end: endpointSchema.optional(),
|
|
370
|
+
containerId: z.string().optional(),
|
|
371
|
+
groupIds: z.array(z.string()).optional()
|
|
372
|
+
};
|
|
373
|
+
var updateObjectShape = {
|
|
374
|
+
id: z.string(),
|
|
375
|
+
x: z.number().optional(),
|
|
376
|
+
y: z.number().optional(),
|
|
377
|
+
width: z.number().optional(),
|
|
378
|
+
height: z.number().optional(),
|
|
379
|
+
text: z.string().optional(),
|
|
380
|
+
points: pointsSchema.optional(),
|
|
381
|
+
style: styleSchema.optional(),
|
|
382
|
+
start: endpointSchema.optional(),
|
|
383
|
+
end: endpointSchema.optional(),
|
|
384
|
+
containerId: z.string().optional(),
|
|
385
|
+
groupIds: z.array(z.string()).optional()
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// src/plugins/excalidraw/flowchart.ts
|
|
389
|
+
function planFlowchart(input) {
|
|
390
|
+
const direction = input.direction ?? "LR";
|
|
391
|
+
const spacingX = input.spacingX ?? 220;
|
|
392
|
+
const spacingY = input.spacingY ?? 140;
|
|
393
|
+
const levels = assignLevels(input);
|
|
394
|
+
const levelGroups = groupLevels(input.nodes, levels);
|
|
395
|
+
return {
|
|
396
|
+
nodes: input.nodes.map((node, index) => {
|
|
397
|
+
const level = levels.get(node.id) ?? index;
|
|
398
|
+
const peers = levelGroups.get(level) ?? [node.id];
|
|
399
|
+
const peerIndex = Math.max(0, peers.indexOf(node.id));
|
|
400
|
+
const crossAxisOffset = peerOffset(
|
|
401
|
+
peerIndex,
|
|
402
|
+
peers.length,
|
|
403
|
+
direction === "LR" ? spacingY : spacingX
|
|
404
|
+
);
|
|
405
|
+
return {
|
|
406
|
+
key: node.id,
|
|
407
|
+
spec: {
|
|
408
|
+
type: node.shape ?? "rectangle",
|
|
409
|
+
x: node.x ?? (direction === "LR" ? level * spacingX : crossAxisOffset),
|
|
410
|
+
y: node.y ?? (direction === "TB" ? level * spacingY : crossAxisOffset),
|
|
411
|
+
width: 160,
|
|
412
|
+
height: 60,
|
|
413
|
+
text: node.label
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}),
|
|
417
|
+
edges: input.edges
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function assignLevels(input) {
|
|
421
|
+
const nodeIds = new Set(input.nodes.map((node) => node.id));
|
|
422
|
+
const indegrees = new Map(input.nodes.map((node) => [node.id, 0]));
|
|
423
|
+
for (const edge of input.edges) {
|
|
424
|
+
if (nodeIds.has(edge.to)) {
|
|
425
|
+
indegrees.set(edge.to, (indegrees.get(edge.to) ?? 0) + 1);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const roots = input.nodes.filter((node) => (indegrees.get(node.id) ?? 0) === 0);
|
|
429
|
+
const levels = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const root of roots.length > 0 ? roots : input.nodes.slice(0, 1)) {
|
|
431
|
+
levels.set(root.id, 0);
|
|
432
|
+
}
|
|
433
|
+
for (let pass = 0; pass < input.nodes.length; pass += 1) {
|
|
434
|
+
let changed = false;
|
|
435
|
+
for (const edge of input.edges) {
|
|
436
|
+
const fromLevel = levels.get(edge.from);
|
|
437
|
+
if (fromLevel === void 0 || !nodeIds.has(edge.to)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const nextLevel = fromLevel + 1;
|
|
441
|
+
if ((levels.get(edge.to) ?? -1) < nextLevel) {
|
|
442
|
+
levels.set(edge.to, nextLevel);
|
|
443
|
+
changed = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!changed) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const [index, node] of input.nodes.entries()) {
|
|
451
|
+
if (!levels.has(node.id)) {
|
|
452
|
+
levels.set(node.id, index);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return levels;
|
|
456
|
+
}
|
|
457
|
+
function groupLevels(nodes, levels) {
|
|
458
|
+
const groups = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const node of nodes) {
|
|
460
|
+
const level = levels.get(node.id) ?? 0;
|
|
461
|
+
groups.set(level, [...groups.get(level) ?? [], node.id]);
|
|
462
|
+
}
|
|
463
|
+
return groups;
|
|
464
|
+
}
|
|
465
|
+
function peerOffset(index, count, spacing) {
|
|
466
|
+
return (index - (count - 1) / 2) * spacing;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/plugins/excalidraw/tools.ts
|
|
470
|
+
var shapeInput = {
|
|
471
|
+
x: z2.number(),
|
|
472
|
+
y: z2.number(),
|
|
473
|
+
width: z2.number(),
|
|
474
|
+
height: z2.number(),
|
|
475
|
+
text: z2.string().optional(),
|
|
476
|
+
style: styleSchema.optional()
|
|
477
|
+
};
|
|
478
|
+
function registerExcalidrawTools(server, context) {
|
|
479
|
+
registerShapeTool(server, context, "draw_rectangle", "rectangle");
|
|
480
|
+
registerShapeTool(server, context, "draw_ellipse", "ellipse");
|
|
481
|
+
registerShapeTool(server, context, "draw_diamond", "diamond");
|
|
482
|
+
server.registerTool(
|
|
483
|
+
"draw_line",
|
|
484
|
+
{
|
|
485
|
+
description: "Draw an Excalidraw polyline.",
|
|
486
|
+
inputSchema: {
|
|
487
|
+
x: z2.number(),
|
|
488
|
+
y: z2.number(),
|
|
489
|
+
points: pointsSchema.min(2),
|
|
490
|
+
style: styleSchema.optional()
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
async ({ x, y, points, style }) => {
|
|
494
|
+
const object = context.controller.createObject({
|
|
495
|
+
type: "line",
|
|
496
|
+
x,
|
|
497
|
+
y,
|
|
498
|
+
points,
|
|
499
|
+
style
|
|
500
|
+
});
|
|
501
|
+
return textResult({ id: object.id });
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
server.registerTool(
|
|
505
|
+
"draw_arrow",
|
|
506
|
+
{
|
|
507
|
+
description: "Draw an Excalidraw arrow, optionally bound to existing elements.",
|
|
508
|
+
inputSchema: {
|
|
509
|
+
start: endpointSchema,
|
|
510
|
+
end: endpointSchema,
|
|
511
|
+
text: z2.string().optional(),
|
|
512
|
+
style: styleSchema.optional()
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
async ({ start, end, text, style }) => {
|
|
516
|
+
try {
|
|
517
|
+
const id = context.controller.transaction(
|
|
518
|
+
() => context.controller.createObject({
|
|
519
|
+
type: "arrow",
|
|
520
|
+
x: "x" in start ? start.x : 0,
|
|
521
|
+
y: "y" in start ? start.y : 0,
|
|
522
|
+
start,
|
|
523
|
+
end,
|
|
524
|
+
text,
|
|
525
|
+
style
|
|
526
|
+
}).id
|
|
527
|
+
);
|
|
528
|
+
return textResult({ id });
|
|
529
|
+
} catch (error) {
|
|
530
|
+
return errorResult(error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
);
|
|
534
|
+
server.registerTool(
|
|
535
|
+
"add_text",
|
|
536
|
+
{
|
|
537
|
+
description: "Add standalone text or a label bound to a container element.",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
x: z2.number().optional(),
|
|
540
|
+
y: z2.number().optional(),
|
|
541
|
+
text: z2.string(),
|
|
542
|
+
containerId: z2.string().optional(),
|
|
543
|
+
style: styleSchema.optional()
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
async ({ x, y, text, containerId, style }) => {
|
|
547
|
+
const container = containerId ? context.controller.getObject(containerId) : void 0;
|
|
548
|
+
if (containerId && !container) {
|
|
549
|
+
return errorResult(`Object not found: ${containerId}`);
|
|
550
|
+
}
|
|
551
|
+
const object = context.controller.createObject({
|
|
552
|
+
type: "text",
|
|
553
|
+
x: x ?? (container ? container.x + 12 : 0),
|
|
554
|
+
y: y ?? (container ? container.y + container.height / 2 - 12 : 0),
|
|
555
|
+
width: container ? Math.max(40, container.width - 24) : void 0,
|
|
556
|
+
text,
|
|
557
|
+
containerId,
|
|
558
|
+
style
|
|
559
|
+
});
|
|
560
|
+
return textResult({ id: object.id });
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
server.registerTool(
|
|
564
|
+
"create_frame",
|
|
565
|
+
{
|
|
566
|
+
description: "Create a frame and optionally assign existing elements to it.",
|
|
567
|
+
inputSchema: {
|
|
568
|
+
x: z2.number(),
|
|
569
|
+
y: z2.number(),
|
|
570
|
+
width: z2.number(),
|
|
571
|
+
height: z2.number(),
|
|
572
|
+
name: z2.string().optional(),
|
|
573
|
+
childIds: z2.array(z2.string()).optional()
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
async ({ x, y, width, height, name, childIds }) => {
|
|
577
|
+
const result = context.controller.transaction(() => {
|
|
578
|
+
const frame = context.controller.createObject({
|
|
579
|
+
type: "frame",
|
|
580
|
+
x,
|
|
581
|
+
y,
|
|
582
|
+
width,
|
|
583
|
+
height,
|
|
584
|
+
text: name
|
|
585
|
+
});
|
|
586
|
+
const updated = context.controller.mutateScene(
|
|
587
|
+
(scene) => setFrameOnChildren(scene, childIds ?? [], frame.id)
|
|
588
|
+
);
|
|
589
|
+
return { id: frame.id, childIds: updated };
|
|
590
|
+
});
|
|
591
|
+
return textResult(result);
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
server.registerTool(
|
|
595
|
+
"group_objects",
|
|
596
|
+
{
|
|
597
|
+
description: "Group existing Excalidraw elements.",
|
|
598
|
+
inputSchema: {
|
|
599
|
+
ids: z2.array(z2.string()).min(1)
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
async ({ ids }) => {
|
|
603
|
+
const groupId = context.controller.mutateScene((scene) => groupElements(scene, ids));
|
|
604
|
+
return textResult({ groupId });
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
server.registerTool(
|
|
608
|
+
"create_flowchart",
|
|
609
|
+
{
|
|
610
|
+
description: "Create a simple deterministic flowchart.",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
nodes: z2.array(
|
|
613
|
+
z2.object({
|
|
614
|
+
id: z2.string(),
|
|
615
|
+
label: z2.string(),
|
|
616
|
+
shape: z2.enum(["rectangle", "ellipse", "diamond"]).optional(),
|
|
617
|
+
x: z2.number().optional(),
|
|
618
|
+
y: z2.number().optional()
|
|
619
|
+
})
|
|
620
|
+
).min(1),
|
|
621
|
+
edges: z2.array(
|
|
622
|
+
z2.object({ from: z2.string(), to: z2.string(), label: z2.string().optional() })
|
|
623
|
+
),
|
|
624
|
+
direction: z2.enum(["TB", "LR"]).optional(),
|
|
625
|
+
spacingX: z2.number().positive().optional(),
|
|
626
|
+
spacingY: z2.number().positive().optional()
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async (input) => {
|
|
630
|
+
try {
|
|
631
|
+
const plan = planFlowchart(input);
|
|
632
|
+
const result = context.controller.transaction(() => {
|
|
633
|
+
const nodeIds = {};
|
|
634
|
+
for (const node of plan.nodes) {
|
|
635
|
+
const object = context.controller.createObject(node.spec);
|
|
636
|
+
nodeIds[node.key] = object.id;
|
|
637
|
+
}
|
|
638
|
+
const arrowIds = [];
|
|
639
|
+
for (const edge of plan.edges) {
|
|
640
|
+
const from = nodeIds[edge.from];
|
|
641
|
+
const to = nodeIds[edge.to];
|
|
642
|
+
if (!from || !to) {
|
|
643
|
+
throw new Error(`Flowchart edge references missing node: ${edge.from} -> ${edge.to}`);
|
|
644
|
+
}
|
|
645
|
+
const arrow = context.controller.createObject({
|
|
646
|
+
type: "arrow",
|
|
647
|
+
x: 0,
|
|
648
|
+
y: 0,
|
|
649
|
+
start: { elementId: from },
|
|
650
|
+
end: { elementId: to },
|
|
651
|
+
text: edge.label
|
|
652
|
+
});
|
|
653
|
+
arrowIds.push(arrow.id);
|
|
654
|
+
}
|
|
655
|
+
return { nodeIds, arrowIds };
|
|
656
|
+
});
|
|
657
|
+
return textResult(result);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
return errorResult(error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
function registerShapeTool(server, context, name, type) {
|
|
665
|
+
server.registerTool(
|
|
666
|
+
name,
|
|
667
|
+
{
|
|
668
|
+
description: `Draw an Excalidraw ${type}.`,
|
|
669
|
+
inputSchema: shapeInput
|
|
670
|
+
},
|
|
671
|
+
async ({ x, y, width, height, text, style }) => {
|
|
672
|
+
const object = context.controller.createObject({ type, x, y, width, height, text, style });
|
|
673
|
+
return textResult({ id: object.id });
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function textResult(value) {
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function errorResult(error) {
|
|
683
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
684
|
+
return {
|
|
685
|
+
isError: true,
|
|
686
|
+
content: [{ type: "text", text: message }]
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/plugins/excalidraw/index.ts
|
|
691
|
+
function createExcalidrawPlugin() {
|
|
692
|
+
return {
|
|
693
|
+
name: "excalidraw",
|
|
694
|
+
createInitialScene,
|
|
695
|
+
getMetadata,
|
|
696
|
+
listObjects,
|
|
697
|
+
getObject,
|
|
698
|
+
createObject,
|
|
699
|
+
updateObject,
|
|
700
|
+
deleteObjects,
|
|
701
|
+
clear,
|
|
702
|
+
serialize,
|
|
703
|
+
deserialize,
|
|
704
|
+
registerTools
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function createInitialScene() {
|
|
708
|
+
return {
|
|
709
|
+
elements: [],
|
|
710
|
+
appState: {
|
|
711
|
+
viewBackgroundColor: "#ffffff"
|
|
712
|
+
},
|
|
713
|
+
files: {},
|
|
714
|
+
version: 0
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function getMetadata(scene) {
|
|
718
|
+
return {
|
|
719
|
+
canvas: "excalidraw",
|
|
720
|
+
version: scene.version,
|
|
721
|
+
objectCount: listObjects(scene).length,
|
|
722
|
+
viewBackgroundColor: scene.appState.viewBackgroundColor
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function listObjects(scene, type) {
|
|
726
|
+
return scene.elements.map(toCanvasObjectSummary).filter((object) => Boolean(object)).filter((object) => !type || object.type === type);
|
|
727
|
+
}
|
|
728
|
+
function getObject(scene, id) {
|
|
729
|
+
const element = findElement(scene, id);
|
|
730
|
+
return element ? toCanvasObject(element) : void 0;
|
|
731
|
+
}
|
|
732
|
+
function createObject(scene, spec) {
|
|
733
|
+
const element = buildElementWithBindings(scene, spec);
|
|
734
|
+
scene.elements.push(element);
|
|
735
|
+
if (spec.text && canCreateBoundLabel(spec.type)) {
|
|
736
|
+
const label = createBoundLabel(element, spec.text, spec.style);
|
|
737
|
+
scene.elements.push(label);
|
|
738
|
+
}
|
|
739
|
+
const object = toCanvasObject(element);
|
|
740
|
+
if (!object) {
|
|
741
|
+
throw new Error(`Unsupported object type: ${spec.type}`);
|
|
742
|
+
}
|
|
743
|
+
return object;
|
|
744
|
+
}
|
|
745
|
+
function updateObject(scene, id, patch) {
|
|
746
|
+
const element = findElement(scene, id);
|
|
747
|
+
if (!element) {
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
if (patch.x !== void 0) {
|
|
751
|
+
element.x = patch.x;
|
|
752
|
+
}
|
|
753
|
+
if (patch.y !== void 0) {
|
|
754
|
+
element.y = patch.y;
|
|
755
|
+
}
|
|
756
|
+
if (patch.width !== void 0) {
|
|
757
|
+
element.width = patch.width;
|
|
758
|
+
}
|
|
759
|
+
if (patch.height !== void 0) {
|
|
760
|
+
element.height = patch.height;
|
|
761
|
+
}
|
|
762
|
+
if (patch.points !== void 0) {
|
|
763
|
+
element.points = patch.points;
|
|
764
|
+
element.width = linearWidth2(patch.points);
|
|
765
|
+
element.height = linearHeight2(patch.points);
|
|
766
|
+
}
|
|
767
|
+
if (patch.groupIds !== void 0) {
|
|
768
|
+
element.groupIds = patch.groupIds;
|
|
769
|
+
}
|
|
770
|
+
if (patch.containerId !== void 0) {
|
|
771
|
+
element.containerId = patch.containerId;
|
|
772
|
+
}
|
|
773
|
+
if (patch.style) {
|
|
774
|
+
applyStyle(element, patch.style);
|
|
775
|
+
}
|
|
776
|
+
if (patch.text !== void 0) {
|
|
777
|
+
if (element.type === "text") {
|
|
778
|
+
element.text = patch.text;
|
|
779
|
+
element.originalText = patch.text;
|
|
780
|
+
} else {
|
|
781
|
+
upsertContainerLabel(scene, element, patch.text, patch.style);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
touchElement(element);
|
|
785
|
+
return toCanvasObject(element);
|
|
786
|
+
}
|
|
787
|
+
function deleteObjects(scene, ids) {
|
|
788
|
+
const idSet = new Set(ids);
|
|
789
|
+
const before = scene.elements.length;
|
|
790
|
+
scene.elements = scene.elements.filter(
|
|
791
|
+
(element) => !idSet.has(element.id) && !idSet.has(element.containerId ?? "")
|
|
792
|
+
);
|
|
793
|
+
return before === scene.elements.length ? [] : ids.filter((id) => !findElement(scene, id));
|
|
794
|
+
}
|
|
795
|
+
function clear(scene) {
|
|
796
|
+
scene.elements = [];
|
|
797
|
+
scene.files = {};
|
|
798
|
+
}
|
|
799
|
+
function serialize(scene) {
|
|
800
|
+
return serializeScene(scene);
|
|
801
|
+
}
|
|
802
|
+
function deserialize(raw) {
|
|
803
|
+
return deserializeScene(raw);
|
|
804
|
+
}
|
|
805
|
+
function registerTools(server, context) {
|
|
806
|
+
registerExcalidrawTools(server, context);
|
|
807
|
+
}
|
|
808
|
+
function buildElementWithBindings(scene, spec) {
|
|
809
|
+
if (spec.type !== "arrow" && spec.type !== "line") {
|
|
810
|
+
return buildElement(spec);
|
|
811
|
+
}
|
|
812
|
+
const start = spec.start ? resolveEndpoint(scene, spec.start) : void 0;
|
|
813
|
+
const end = spec.end ? resolveEndpoint(scene, spec.end) : void 0;
|
|
814
|
+
const defaultStart = { x: spec.x, y: spec.y };
|
|
815
|
+
const defaultEnd = {
|
|
816
|
+
x: spec.x + (spec.width ?? 160),
|
|
817
|
+
y: spec.y + (spec.height ?? 80)
|
|
818
|
+
};
|
|
819
|
+
const startCenter = start?.point ?? defaultStart;
|
|
820
|
+
const endCenter = end?.point ?? defaultEnd;
|
|
821
|
+
const startPoint = start?.element ? edgePoint(start.element, endCenter.x, endCenter.y) : startCenter;
|
|
822
|
+
const endPoint = end?.element ? edgePoint(end.element, startCenter.x, startCenter.y) : endCenter;
|
|
823
|
+
const points = spec.points ?? [
|
|
824
|
+
[0, 0],
|
|
825
|
+
[endPoint.x - startPoint.x, endPoint.y - startPoint.y]
|
|
826
|
+
];
|
|
827
|
+
const element = buildElement(
|
|
828
|
+
{
|
|
829
|
+
...spec,
|
|
830
|
+
x: startPoint.x,
|
|
831
|
+
y: startPoint.y,
|
|
832
|
+
points
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
startBinding: start?.element ? makeBinding(start.element.id) : null,
|
|
836
|
+
endBinding: end?.element ? makeBinding(end.element.id) : null
|
|
837
|
+
}
|
|
838
|
+
);
|
|
839
|
+
if (spec.type === "arrow") {
|
|
840
|
+
if (start?.element) {
|
|
841
|
+
addBoundElementById(scene, start.element.id, { id: element.id, type: "arrow" });
|
|
842
|
+
}
|
|
843
|
+
if (end?.element) {
|
|
844
|
+
addBoundElementById(scene, end.element.id, { id: element.id, type: "arrow" });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return element;
|
|
848
|
+
}
|
|
849
|
+
function resolveEndpoint(scene, endpoint) {
|
|
850
|
+
if (isElementEndpoint(endpoint)) {
|
|
851
|
+
const element = findElement(scene, endpoint.elementId);
|
|
852
|
+
if (!element) {
|
|
853
|
+
throw new Error(`Object not found: ${endpoint.elementId}`);
|
|
854
|
+
}
|
|
855
|
+
return {
|
|
856
|
+
point: centerPoint(element),
|
|
857
|
+
element
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return { point: endpoint };
|
|
861
|
+
}
|
|
862
|
+
function upsertContainerLabel(scene, container, text, style) {
|
|
863
|
+
const existing = scene.elements.find(
|
|
864
|
+
(element) => element.type === "text" && element.containerId === container.id
|
|
865
|
+
);
|
|
866
|
+
if (existing) {
|
|
867
|
+
existing.text = text;
|
|
868
|
+
existing.originalText = text;
|
|
869
|
+
if (style) {
|
|
870
|
+
applyStyle(existing, style);
|
|
871
|
+
}
|
|
872
|
+
touchElement(existing);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
scene.elements.push(createBoundLabel(container, text, style));
|
|
876
|
+
}
|
|
877
|
+
function createBoundLabel(container, text, style) {
|
|
878
|
+
const labelStyle = { ...style, textAlign: style?.textAlign ?? "center" };
|
|
879
|
+
const position = labelPosition(container, text, labelStyle);
|
|
880
|
+
const label = buildElement(
|
|
881
|
+
{
|
|
882
|
+
type: "text",
|
|
883
|
+
x: position.x,
|
|
884
|
+
y: position.y,
|
|
885
|
+
width: position.width,
|
|
886
|
+
height: position.height,
|
|
887
|
+
text,
|
|
888
|
+
style: labelStyle
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
containerId: container.id
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
label.autoResize = false;
|
|
895
|
+
addBoundElement(container, { id: label.id, type: "text" });
|
|
896
|
+
return label;
|
|
897
|
+
}
|
|
898
|
+
function labelPosition(container, text, style) {
|
|
899
|
+
const fontSize = style.fontSize ?? 20;
|
|
900
|
+
const lineHeight = fontSize * 1.25;
|
|
901
|
+
if (isLinearElement(container)) {
|
|
902
|
+
const midpoint = linearMidpoint(container);
|
|
903
|
+
const width = Math.max(40, text.length * fontSize * 0.6);
|
|
904
|
+
const height = lineHeight;
|
|
905
|
+
return {
|
|
906
|
+
x: midpoint.x - width / 2,
|
|
907
|
+
y: midpoint.y - height / 2,
|
|
908
|
+
width,
|
|
909
|
+
height
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
x: container.x,
|
|
914
|
+
y: container.y + container.height / 2 - lineHeight / 2,
|
|
915
|
+
width: Math.max(40, container.width),
|
|
916
|
+
height: lineHeight
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function isLinearElement(element) {
|
|
920
|
+
return element.type === "line" || element.type === "arrow";
|
|
921
|
+
}
|
|
922
|
+
function canCreateBoundLabel(type) {
|
|
923
|
+
return type !== "text" && type !== "frame" && type !== "line";
|
|
924
|
+
}
|
|
925
|
+
function linearMidpoint(element) {
|
|
926
|
+
const points = element.points ?? [
|
|
927
|
+
[0, 0],
|
|
928
|
+
[element.width, element.height]
|
|
929
|
+
];
|
|
930
|
+
const first = points[0] ?? [0, 0];
|
|
931
|
+
const last = points[points.length - 1] ?? first;
|
|
932
|
+
return {
|
|
933
|
+
x: element.x + (first[0] + last[0]) / 2,
|
|
934
|
+
y: element.y + (first[1] + last[1]) / 2
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function addBoundElementById(scene, elementId, bound) {
|
|
938
|
+
const element = findElement(scene, elementId);
|
|
939
|
+
if (element) {
|
|
940
|
+
addBoundElement(element, bound);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function findElement(scene, id) {
|
|
944
|
+
return scene.elements.find((element) => element.id === id && !element.isDeleted);
|
|
945
|
+
}
|
|
946
|
+
function setFrameOnChildren(scene, childIds, frameId) {
|
|
947
|
+
const updated = [];
|
|
948
|
+
for (const childId of childIds) {
|
|
949
|
+
const child = findElement(scene, childId);
|
|
950
|
+
if (child) {
|
|
951
|
+
child.frameId = frameId;
|
|
952
|
+
touchElement(child);
|
|
953
|
+
updated.push(child.id);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return updated;
|
|
957
|
+
}
|
|
958
|
+
function groupElements(scene, ids) {
|
|
959
|
+
const groupId = randomUUID2();
|
|
960
|
+
for (const id of ids) {
|
|
961
|
+
const element = findElement(scene, id);
|
|
962
|
+
if (element && !element.groupIds.includes(groupId)) {
|
|
963
|
+
element.groupIds = [...element.groupIds, groupId];
|
|
964
|
+
touchElement(element);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return groupId;
|
|
968
|
+
}
|
|
969
|
+
function applyStyle(element, style) {
|
|
970
|
+
if (style.strokeColor !== void 0) {
|
|
971
|
+
element.strokeColor = style.strokeColor;
|
|
972
|
+
}
|
|
973
|
+
if (style.backgroundColor !== void 0) {
|
|
974
|
+
element.backgroundColor = style.backgroundColor;
|
|
975
|
+
}
|
|
976
|
+
if (style.fillStyle !== void 0) {
|
|
977
|
+
element.fillStyle = style.fillStyle;
|
|
978
|
+
}
|
|
979
|
+
if (style.strokeWidth !== void 0) {
|
|
980
|
+
element.strokeWidth = style.strokeWidth;
|
|
981
|
+
}
|
|
982
|
+
if (style.strokeStyle !== void 0) {
|
|
983
|
+
element.strokeStyle = style.strokeStyle;
|
|
984
|
+
}
|
|
985
|
+
if (style.roughness !== void 0) {
|
|
986
|
+
element.roughness = style.roughness;
|
|
987
|
+
}
|
|
988
|
+
if (style.opacity !== void 0) {
|
|
989
|
+
element.opacity = style.opacity;
|
|
990
|
+
}
|
|
991
|
+
if (style.fontSize !== void 0) {
|
|
992
|
+
element.fontSize = style.fontSize;
|
|
993
|
+
element.height = style.fontSize * 1.25;
|
|
994
|
+
}
|
|
995
|
+
if (style.textAlign !== void 0) {
|
|
996
|
+
element.textAlign = style.textAlign;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function linearWidth2(points) {
|
|
1000
|
+
const xs = points.map(([x]) => x);
|
|
1001
|
+
return Math.max(...xs) - Math.min(...xs);
|
|
1002
|
+
}
|
|
1003
|
+
function linearHeight2(points) {
|
|
1004
|
+
const ys = points.map(([, y]) => y);
|
|
1005
|
+
return Math.max(...ys) - Math.min(...ys);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/server/app.ts
|
|
1009
|
+
import path2 from "path";
|
|
1010
|
+
import express from "express";
|
|
1011
|
+
|
|
1012
|
+
// src/mcp/buildServer.ts
|
|
1013
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1014
|
+
|
|
1015
|
+
// src/shared/packageInfo.ts
|
|
1016
|
+
import { readFileSync } from "fs";
|
|
1017
|
+
import path from "path";
|
|
1018
|
+
import { fileURLToPath } from "url";
|
|
1019
|
+
var CLI_NAME = "agentic-canvas";
|
|
1020
|
+
var MCP_SERVER_NAME = "agentic-canvas";
|
|
1021
|
+
var PACKAGE_NAME = "@trohde/agentic-canvas";
|
|
1022
|
+
function readPackageInfo() {
|
|
1023
|
+
const packageJsonPath = path.resolve(
|
|
1024
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
1025
|
+
"../../package.json"
|
|
1026
|
+
);
|
|
1027
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1028
|
+
return {
|
|
1029
|
+
name: typeof parsed.name === "string" ? parsed.name : PACKAGE_NAME,
|
|
1030
|
+
version: typeof parsed.version === "string" ? parsed.version : "0.0.0"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/mcp/baselineTools.ts
|
|
1035
|
+
import { z as z3 } from "zod";
|
|
1036
|
+
function registerBaselineTools(server, context) {
|
|
1037
|
+
server.registerTool(
|
|
1038
|
+
"get_canvas_state",
|
|
1039
|
+
{
|
|
1040
|
+
description: "Get canvas metadata and current scene state summary.",
|
|
1041
|
+
inputSchema: {}
|
|
1042
|
+
},
|
|
1043
|
+
async () => textResult2(context.controller.getMetadata(context.clientsConnected()))
|
|
1044
|
+
);
|
|
1045
|
+
server.registerTool(
|
|
1046
|
+
"list_objects",
|
|
1047
|
+
{
|
|
1048
|
+
description: "List normalized canvas objects.",
|
|
1049
|
+
inputSchema: {
|
|
1050
|
+
type: canvasObjectTypeSchema.optional()
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
async ({ type }) => textResult2(context.controller.listObjects(type))
|
|
1054
|
+
);
|
|
1055
|
+
server.registerTool(
|
|
1056
|
+
"get_object",
|
|
1057
|
+
{
|
|
1058
|
+
description: "Get one normalized canvas object by id.",
|
|
1059
|
+
inputSchema: {
|
|
1060
|
+
id: z3.string()
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
async ({ id }) => {
|
|
1064
|
+
const object = context.controller.getObject(id);
|
|
1065
|
+
return object ? textResult2(object) : errorResult2(`Object not found: ${id}`);
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
server.registerTool(
|
|
1069
|
+
"create_object",
|
|
1070
|
+
{
|
|
1071
|
+
description: "Create a normalized canvas object.",
|
|
1072
|
+
inputSchema: createObjectShape
|
|
1073
|
+
},
|
|
1074
|
+
async (input) => {
|
|
1075
|
+
try {
|
|
1076
|
+
const object = context.controller.createObject(input);
|
|
1077
|
+
return textResult2({ id: object.id });
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
return errorResult2(error);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
);
|
|
1083
|
+
server.registerTool(
|
|
1084
|
+
"update_object",
|
|
1085
|
+
{
|
|
1086
|
+
description: "Patch a normalized canvas object.",
|
|
1087
|
+
inputSchema: updateObjectShape
|
|
1088
|
+
},
|
|
1089
|
+
async ({ id, ...patch }) => {
|
|
1090
|
+
const object = context.controller.updateObject(id, patch);
|
|
1091
|
+
return object ? textResult2({ id: object.id }) : errorResult2(`Object not found: ${id}`);
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
server.registerTool(
|
|
1095
|
+
"delete_object",
|
|
1096
|
+
{
|
|
1097
|
+
description: "Delete one or more canvas objects.",
|
|
1098
|
+
inputSchema: {
|
|
1099
|
+
ids: z3.array(z3.string()).min(1)
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
async ({ ids }) => textResult2({ deleted: context.controller.deleteObjects(ids) })
|
|
1103
|
+
);
|
|
1104
|
+
server.registerTool(
|
|
1105
|
+
"clear_canvas",
|
|
1106
|
+
{
|
|
1107
|
+
description: "Clear all canvas objects.",
|
|
1108
|
+
inputSchema: {}
|
|
1109
|
+
},
|
|
1110
|
+
async () => {
|
|
1111
|
+
context.controller.clear();
|
|
1112
|
+
return textResult2({ cleared: true });
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
server.registerTool(
|
|
1116
|
+
"save_canvas",
|
|
1117
|
+
{
|
|
1118
|
+
description: "Save the current canvas to a .excalidraw file inside the workspace.",
|
|
1119
|
+
inputSchema: {
|
|
1120
|
+
path: z3.string().optional()
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
async ({ path: path5 }) => {
|
|
1124
|
+
try {
|
|
1125
|
+
const written = await context.workspace.writeText(
|
|
1126
|
+
path5 ?? "canvas.excalidraw",
|
|
1127
|
+
context.controller.serialize()
|
|
1128
|
+
);
|
|
1129
|
+
return textResult2({ path: written });
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
return errorResult2(error);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
);
|
|
1135
|
+
server.registerTool(
|
|
1136
|
+
"open_canvas",
|
|
1137
|
+
{
|
|
1138
|
+
description: "Open a .excalidraw file from inside the workspace.",
|
|
1139
|
+
inputSchema: {
|
|
1140
|
+
path: z3.string()
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
async ({ path: path5 }) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const file = await context.workspace.readText(path5);
|
|
1146
|
+
context.controller.deserialize(file.text);
|
|
1147
|
+
return textResult2({
|
|
1148
|
+
path: file.path,
|
|
1149
|
+
objectCount: context.controller.listObjects().length
|
|
1150
|
+
});
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
return errorResult2(friendlyOpenError(error, path5));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
server.registerTool(
|
|
1157
|
+
"screenshot",
|
|
1158
|
+
{
|
|
1159
|
+
description: "Export a PNG screenshot through a connected browser.",
|
|
1160
|
+
inputSchema: {
|
|
1161
|
+
path: z3.string().optional(),
|
|
1162
|
+
exportPadding: z3.number().min(0).max(200).optional()
|
|
1163
|
+
}
|
|
1164
|
+
},
|
|
1165
|
+
async ({ path: path5, exportPadding }) => {
|
|
1166
|
+
try {
|
|
1167
|
+
const exported = await context.requestExport({ exportPadding });
|
|
1168
|
+
const content = [{ type: "image", data: exported.base64, mimeType: exported.mimeType }];
|
|
1169
|
+
if (path5) {
|
|
1170
|
+
const written = await context.workspace.writeBinary(
|
|
1171
|
+
path5,
|
|
1172
|
+
Buffer.from(exported.base64, "base64")
|
|
1173
|
+
);
|
|
1174
|
+
content.push({ type: "text", text: JSON.stringify({ path: written }) });
|
|
1175
|
+
}
|
|
1176
|
+
return { content };
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
return errorResult2(error);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
);
|
|
1182
|
+
server.registerTool(
|
|
1183
|
+
"get_selected_objects",
|
|
1184
|
+
{
|
|
1185
|
+
description: "Return normalized objects currently selected in the connected browser.",
|
|
1186
|
+
inputSchema: {}
|
|
1187
|
+
},
|
|
1188
|
+
async () => {
|
|
1189
|
+
try {
|
|
1190
|
+
const selection = await context.requestSelection();
|
|
1191
|
+
const objects = [];
|
|
1192
|
+
const missingIds = [];
|
|
1193
|
+
for (const id of selection.selectedIds) {
|
|
1194
|
+
const object = context.controller.getObject(id);
|
|
1195
|
+
if (object) {
|
|
1196
|
+
objects.push(object);
|
|
1197
|
+
} else {
|
|
1198
|
+
missingIds.push(id);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return textResult2({
|
|
1202
|
+
version: context.controller.currentVersion(),
|
|
1203
|
+
selectedIds: selection.selectedIds,
|
|
1204
|
+
objects,
|
|
1205
|
+
missingIds
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
return errorResult2(error);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
function textResult2(value) {
|
|
1214
|
+
return {
|
|
1215
|
+
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
function errorResult2(error) {
|
|
1219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1220
|
+
return {
|
|
1221
|
+
isError: true,
|
|
1222
|
+
content: [{ type: "text", text: message }]
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function friendlyOpenError(error, requestedPath) {
|
|
1226
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
|
|
1227
|
+
return `No such canvas file: ${requestedPath}`;
|
|
1228
|
+
}
|
|
1229
|
+
return error;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/mcp/buildServer.ts
|
|
1233
|
+
function buildMcpServer(options) {
|
|
1234
|
+
const packageInfo = readPackageInfo();
|
|
1235
|
+
const server = new McpServer({
|
|
1236
|
+
name: MCP_SERVER_NAME,
|
|
1237
|
+
version: packageInfo.version
|
|
1238
|
+
});
|
|
1239
|
+
registerBaselineTools(server, {
|
|
1240
|
+
controller: options.controller,
|
|
1241
|
+
workspace: options.workspace,
|
|
1242
|
+
clientsConnected: options.clientsConnected,
|
|
1243
|
+
requestExport: options.requestExport,
|
|
1244
|
+
requestSelection: options.requestSelection
|
|
1245
|
+
});
|
|
1246
|
+
options.plugin.registerTools(server, {
|
|
1247
|
+
controller: options.controller
|
|
1248
|
+
});
|
|
1249
|
+
return server;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/server/mcpHttp.ts
|
|
1253
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1254
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1255
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
1256
|
+
function mountMcpHttp(router, createServer2, allowedHosts) {
|
|
1257
|
+
const transports = /* @__PURE__ */ new Map();
|
|
1258
|
+
router.post("/mcp", async (req, res) => {
|
|
1259
|
+
try {
|
|
1260
|
+
const sessionId = headerValue(req, "mcp-session-id");
|
|
1261
|
+
let transport = sessionId ? transports.get(sessionId) : void 0;
|
|
1262
|
+
if (!transport) {
|
|
1263
|
+
if (!isInitialize(req.body)) {
|
|
1264
|
+
res.status(400).json({ error: "Missing or invalid MCP session" });
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
transport = new StreamableHTTPServerTransport({
|
|
1268
|
+
sessionIdGenerator: () => randomUUID3(),
|
|
1269
|
+
enableDnsRebindingProtection: true,
|
|
1270
|
+
allowedHosts,
|
|
1271
|
+
onsessioninitialized: (newSessionId) => {
|
|
1272
|
+
transports.set(newSessionId, transport);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
transport.onclose = () => {
|
|
1276
|
+
const currentSessionId = transport?.sessionId;
|
|
1277
|
+
if (currentSessionId) {
|
|
1278
|
+
transports.delete(currentSessionId);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
await createServer2().connect(transport);
|
|
1282
|
+
}
|
|
1283
|
+
await transport.handleRequest(req, res, req.body);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
sendTransportError(res, error);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
router.get("/mcp", async (req, res) => {
|
|
1289
|
+
await handleSessionRequest(req, res, transports);
|
|
1290
|
+
});
|
|
1291
|
+
router.delete("/mcp", async (req, res) => {
|
|
1292
|
+
await handleSessionRequest(req, res, transports);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async function handleSessionRequest(req, res, transports) {
|
|
1296
|
+
const sessionId = headerValue(req, "mcp-session-id");
|
|
1297
|
+
const transport = sessionId ? transports.get(sessionId) : void 0;
|
|
1298
|
+
if (!transport) {
|
|
1299
|
+
res.status(400).json({ error: "Missing or invalid MCP session" });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
await transport.handleRequest(req, res);
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
sendTransportError(res, error);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function isInitialize(body) {
|
|
1309
|
+
const messages = Array.isArray(body) ? body : [body];
|
|
1310
|
+
return messages.some((message) => isInitializeRequest(message));
|
|
1311
|
+
}
|
|
1312
|
+
function headerValue(req, name) {
|
|
1313
|
+
const value = req.headers[name];
|
|
1314
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1315
|
+
}
|
|
1316
|
+
function sendTransportError(res, error) {
|
|
1317
|
+
if (res.headersSent) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1321
|
+
res.status(500).json({ error: message });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// src/server/app.ts
|
|
1325
|
+
function createApp(options) {
|
|
1326
|
+
const app = express();
|
|
1327
|
+
app.use(express.json({ limit: "10mb" }));
|
|
1328
|
+
app.get("/healthz", (_req, res) => {
|
|
1329
|
+
res.json({
|
|
1330
|
+
status: "ok",
|
|
1331
|
+
canvas: options.plugin.name,
|
|
1332
|
+
version: options.controller.getSnapshot().version
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
mountMcpHttp(app, () => buildMcpServer(options), options.allowedHosts);
|
|
1336
|
+
app.use(express.static(options.webDistDir));
|
|
1337
|
+
app.get("*", (_req, res) => {
|
|
1338
|
+
res.sendFile(path2.join(options.webDistDir, "index.html"), (error) => {
|
|
1339
|
+
if (error) {
|
|
1340
|
+
res.status(404).send("Agentic Canvas web build not found. Run npm run build first.");
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
return app;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/server/canvasController.ts
|
|
1348
|
+
var CanvasController = class {
|
|
1349
|
+
constructor(plugin) {
|
|
1350
|
+
this.plugin = plugin;
|
|
1351
|
+
this.scene = plugin.createInitialScene();
|
|
1352
|
+
}
|
|
1353
|
+
plugin;
|
|
1354
|
+
scene;
|
|
1355
|
+
listener;
|
|
1356
|
+
txDepth = 0;
|
|
1357
|
+
txDirty = false;
|
|
1358
|
+
txOrigin;
|
|
1359
|
+
get canvasName() {
|
|
1360
|
+
return this.plugin.name;
|
|
1361
|
+
}
|
|
1362
|
+
setChangeListener(listener) {
|
|
1363
|
+
this.listener = listener;
|
|
1364
|
+
}
|
|
1365
|
+
getScene() {
|
|
1366
|
+
return cloneScene(this.scene);
|
|
1367
|
+
}
|
|
1368
|
+
getSnapshot() {
|
|
1369
|
+
return {
|
|
1370
|
+
version: this.scene.version,
|
|
1371
|
+
elements: this.scene.elements,
|
|
1372
|
+
appState: this.scene.appState,
|
|
1373
|
+
files: this.scene.files
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
currentVersion() {
|
|
1377
|
+
return this.scene.version;
|
|
1378
|
+
}
|
|
1379
|
+
getMetadata(clientsConnected = 0) {
|
|
1380
|
+
return {
|
|
1381
|
+
...this.plugin.getMetadata(this.scene),
|
|
1382
|
+
clientsConnected
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
listObjects(type) {
|
|
1386
|
+
return this.plugin.listObjects(this.scene, type);
|
|
1387
|
+
}
|
|
1388
|
+
getObject(id) {
|
|
1389
|
+
return this.plugin.getObject(this.scene, id);
|
|
1390
|
+
}
|
|
1391
|
+
createObject(spec) {
|
|
1392
|
+
return this.mutateScene((scene) => this.plugin.createObject(scene, spec));
|
|
1393
|
+
}
|
|
1394
|
+
updateObject(id, patch) {
|
|
1395
|
+
return this.mutateScene((scene) => this.plugin.updateObject(scene, id, patch));
|
|
1396
|
+
}
|
|
1397
|
+
deleteObjects(ids) {
|
|
1398
|
+
return this.mutateScene((scene) => this.plugin.deleteObjects(scene, ids));
|
|
1399
|
+
}
|
|
1400
|
+
clear() {
|
|
1401
|
+
this.mutateScene((scene) => this.plugin.clear(scene));
|
|
1402
|
+
}
|
|
1403
|
+
serialize() {
|
|
1404
|
+
return JSON.stringify(this.plugin.serialize(this.scene), null, 2);
|
|
1405
|
+
}
|
|
1406
|
+
deserialize(raw) {
|
|
1407
|
+
const currentVersion = this.scene.version;
|
|
1408
|
+
this.scene = this.plugin.deserialize(raw);
|
|
1409
|
+
this.scene.version = currentVersion;
|
|
1410
|
+
this.bumpAndNotify();
|
|
1411
|
+
}
|
|
1412
|
+
replaceFromBrowser(elements, appState, files, origin) {
|
|
1413
|
+
this.scene.elements = elements;
|
|
1414
|
+
this.scene.appState = {
|
|
1415
|
+
viewBackgroundColor: appState?.viewBackgroundColor ?? this.scene.appState.viewBackgroundColor
|
|
1416
|
+
};
|
|
1417
|
+
this.scene.files = files ?? this.scene.files;
|
|
1418
|
+
this.bumpAndNotify(origin);
|
|
1419
|
+
}
|
|
1420
|
+
mutateScene(mutator, origin) {
|
|
1421
|
+
const result = mutator(this.scene);
|
|
1422
|
+
this.bumpAndNotify(origin);
|
|
1423
|
+
return result;
|
|
1424
|
+
}
|
|
1425
|
+
transaction(fn, origin) {
|
|
1426
|
+
this.txDepth += 1;
|
|
1427
|
+
try {
|
|
1428
|
+
return fn();
|
|
1429
|
+
} finally {
|
|
1430
|
+
this.txDepth -= 1;
|
|
1431
|
+
if (this.txDepth === 0 && this.txDirty) {
|
|
1432
|
+
const txOrigin = this.txOrigin;
|
|
1433
|
+
this.txDirty = false;
|
|
1434
|
+
this.txOrigin = void 0;
|
|
1435
|
+
this.commit(txOrigin ?? origin);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
bumpAndNotify(origin) {
|
|
1440
|
+
if (this.txDepth > 0) {
|
|
1441
|
+
this.txDirty = true;
|
|
1442
|
+
this.txOrigin = origin ?? this.txOrigin;
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
this.commit(origin);
|
|
1446
|
+
}
|
|
1447
|
+
commit(origin) {
|
|
1448
|
+
this.scene.version += 1;
|
|
1449
|
+
this.listener?.(this.getSnapshot(), origin);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
// src/server/workspace.ts
|
|
1454
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1455
|
+
import path3 from "path";
|
|
1456
|
+
var Workspace = class {
|
|
1457
|
+
root;
|
|
1458
|
+
constructor(root) {
|
|
1459
|
+
this.root = path3.resolve(root);
|
|
1460
|
+
}
|
|
1461
|
+
async ensure() {
|
|
1462
|
+
await mkdir(this.root, { recursive: true });
|
|
1463
|
+
}
|
|
1464
|
+
resolveInWorkspace(userPath) {
|
|
1465
|
+
if (!userPath || userPath.trim().length === 0) {
|
|
1466
|
+
throw new Error("Path is required");
|
|
1467
|
+
}
|
|
1468
|
+
const resolved = path3.isAbsolute(userPath) ? path3.resolve(userPath) : path3.resolve(this.root, userPath);
|
|
1469
|
+
const relative = path3.relative(this.root, resolved);
|
|
1470
|
+
const staysInside = relative === "" || !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
1471
|
+
if (!staysInside) {
|
|
1472
|
+
throw new Error(`Path is outside workspace: ${userPath}`);
|
|
1473
|
+
}
|
|
1474
|
+
return resolved;
|
|
1475
|
+
}
|
|
1476
|
+
async readText(userPath) {
|
|
1477
|
+
const resolved = this.resolveInWorkspace(userPath);
|
|
1478
|
+
return {
|
|
1479
|
+
path: resolved,
|
|
1480
|
+
text: await readFile(resolved, "utf8")
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
async writeText(userPath, text) {
|
|
1484
|
+
const resolved = this.resolveInWorkspace(userPath);
|
|
1485
|
+
await mkdir(path3.dirname(resolved), { recursive: true });
|
|
1486
|
+
await writeFile(resolved, text, "utf8");
|
|
1487
|
+
return resolved;
|
|
1488
|
+
}
|
|
1489
|
+
async writeBinary(userPath, data) {
|
|
1490
|
+
const resolved = this.resolveInWorkspace(userPath);
|
|
1491
|
+
await mkdir(path3.dirname(resolved), { recursive: true });
|
|
1492
|
+
await writeFile(resolved, data);
|
|
1493
|
+
return resolved;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/server/wsBridge.ts
|
|
1498
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1499
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
1500
|
+
|
|
1501
|
+
// src/shared/protocol.ts
|
|
1502
|
+
function parseBrowserMessage(data) {
|
|
1503
|
+
try {
|
|
1504
|
+
const parsed = JSON.parse(data);
|
|
1505
|
+
if (typeof parsed === "object" && parsed !== null && "type" in parsed) {
|
|
1506
|
+
return parsed;
|
|
1507
|
+
}
|
|
1508
|
+
} catch {
|
|
1509
|
+
return void 0;
|
|
1510
|
+
}
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// src/server/wsBridge.ts
|
|
1515
|
+
var WsBridge = class {
|
|
1516
|
+
constructor(controller) {
|
|
1517
|
+
this.controller = controller;
|
|
1518
|
+
}
|
|
1519
|
+
controller;
|
|
1520
|
+
clients = /* @__PURE__ */ new Map();
|
|
1521
|
+
pendingExports = /* @__PURE__ */ new Map();
|
|
1522
|
+
pendingSelections = /* @__PURE__ */ new Map();
|
|
1523
|
+
syncOrder = 0;
|
|
1524
|
+
wss;
|
|
1525
|
+
attach(server, path5 = "/ws") {
|
|
1526
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
1527
|
+
this.wss.on("connection", (socket) => this.handleConnection(socket));
|
|
1528
|
+
server.on("upgrade", (request, socket, head) => {
|
|
1529
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1530
|
+
if (url.pathname !== path5 || !this.wss) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1534
|
+
this.wss?.emit("connection", ws, request);
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
connectedClientCount() {
|
|
1539
|
+
return this.clients.size;
|
|
1540
|
+
}
|
|
1541
|
+
broadcastScene(snapshot, origin) {
|
|
1542
|
+
for (const { socket } of this.clients.values()) {
|
|
1543
|
+
if (socket !== origin) {
|
|
1544
|
+
this.sendScene(socket, snapshot);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (origin instanceof WebSocket) {
|
|
1548
|
+
this.markClientSynced(origin, snapshot.version);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
requestExport(options = {}) {
|
|
1552
|
+
const client = this.mostRecentlySyncedClient();
|
|
1553
|
+
if (!client) {
|
|
1554
|
+
return Promise.reject(new Error("No browser canvas client is connected"));
|
|
1555
|
+
}
|
|
1556
|
+
const id = randomUUID4();
|
|
1557
|
+
const request = {
|
|
1558
|
+
type: "export:request",
|
|
1559
|
+
id,
|
|
1560
|
+
mimeType: "image/png",
|
|
1561
|
+
exportPadding: options.exportPadding
|
|
1562
|
+
};
|
|
1563
|
+
return new Promise((resolve, reject) => {
|
|
1564
|
+
const timer = setTimeout(() => {
|
|
1565
|
+
this.pendingExports.delete(id);
|
|
1566
|
+
reject(new Error("Screenshot export timed out"));
|
|
1567
|
+
}, options.timeoutMs ?? 5e3);
|
|
1568
|
+
this.pendingExports.set(id, { resolve, reject, timer });
|
|
1569
|
+
client.socket.send(JSON.stringify(request));
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
requestSelection(options = {}) {
|
|
1573
|
+
const client = this.mostRecentlySyncedClient();
|
|
1574
|
+
if (!client) {
|
|
1575
|
+
return Promise.reject(new Error("No browser canvas client is connected"));
|
|
1576
|
+
}
|
|
1577
|
+
const id = randomUUID4();
|
|
1578
|
+
const request = {
|
|
1579
|
+
type: "selection:request",
|
|
1580
|
+
id
|
|
1581
|
+
};
|
|
1582
|
+
return new Promise((resolve, reject) => {
|
|
1583
|
+
const timer = setTimeout(() => {
|
|
1584
|
+
this.pendingSelections.delete(id);
|
|
1585
|
+
reject(new Error("Selection request timed out"));
|
|
1586
|
+
}, options.timeoutMs ?? 5e3);
|
|
1587
|
+
this.pendingSelections.set(id, { resolve, reject, timer });
|
|
1588
|
+
client.socket.send(JSON.stringify(request));
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
close() {
|
|
1592
|
+
for (const { socket } of this.clients.values()) {
|
|
1593
|
+
socket.close();
|
|
1594
|
+
}
|
|
1595
|
+
this.wss?.close();
|
|
1596
|
+
}
|
|
1597
|
+
handleConnection(socket) {
|
|
1598
|
+
this.clients.set(socket, {
|
|
1599
|
+
socket,
|
|
1600
|
+
lastSyncedOrder: 0,
|
|
1601
|
+
lastSyncedVersion: -1
|
|
1602
|
+
});
|
|
1603
|
+
socket.on("message", (data) => this.handleMessage(socket, data.toString()));
|
|
1604
|
+
socket.on("close", () => {
|
|
1605
|
+
this.clients.delete(socket);
|
|
1606
|
+
});
|
|
1607
|
+
this.sendScene(socket, this.controller.getSnapshot());
|
|
1608
|
+
}
|
|
1609
|
+
handleMessage(socket, data) {
|
|
1610
|
+
const message = parseBrowserMessage(data);
|
|
1611
|
+
if (!message) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (message.type === "hello") {
|
|
1615
|
+
this.sendScene(socket, this.controller.getSnapshot());
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (message.type === "scene:changed") {
|
|
1619
|
+
if (message.baseVersion < this.controller.currentVersion()) {
|
|
1620
|
+
this.sendScene(socket, this.controller.getSnapshot());
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
this.controller.replaceFromBrowser(
|
|
1624
|
+
message.elements,
|
|
1625
|
+
message.appState,
|
|
1626
|
+
message.files,
|
|
1627
|
+
socket
|
|
1628
|
+
);
|
|
1629
|
+
this.markClientSynced(socket, this.controller.currentVersion());
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (message.type === "export:result" || message.type === "export:error") {
|
|
1633
|
+
const pending = this.pendingExports.get(message.id);
|
|
1634
|
+
if (!pending) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
clearTimeout(pending.timer);
|
|
1638
|
+
this.pendingExports.delete(message.id);
|
|
1639
|
+
if (message.type === "export:result") {
|
|
1640
|
+
pending.resolve({ mimeType: message.mimeType, base64: message.base64 });
|
|
1641
|
+
} else {
|
|
1642
|
+
pending.reject(new Error(message.message));
|
|
1643
|
+
}
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
if (message.type === "selection:result" || message.type === "selection:error") {
|
|
1647
|
+
const pending = this.pendingSelections.get(message.id);
|
|
1648
|
+
if (!pending) {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
clearTimeout(pending.timer);
|
|
1652
|
+
this.pendingSelections.delete(message.id);
|
|
1653
|
+
if (message.type === "selection:result") {
|
|
1654
|
+
pending.resolve({ selectedIds: message.selectedIds });
|
|
1655
|
+
} else {
|
|
1656
|
+
pending.reject(new Error(message.message));
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
sendScene(socket, snapshot) {
|
|
1661
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
socket.send(
|
|
1665
|
+
JSON.stringify({
|
|
1666
|
+
type: "scene:set",
|
|
1667
|
+
version: snapshot.version,
|
|
1668
|
+
elements: snapshot.elements,
|
|
1669
|
+
appState: snapshot.appState,
|
|
1670
|
+
files: snapshot.files
|
|
1671
|
+
})
|
|
1672
|
+
);
|
|
1673
|
+
this.markClientSynced(socket, snapshot.version);
|
|
1674
|
+
}
|
|
1675
|
+
markClientSynced(socket, version) {
|
|
1676
|
+
const client = this.clients.get(socket);
|
|
1677
|
+
if (!client) {
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
client.lastSyncedOrder = ++this.syncOrder;
|
|
1681
|
+
client.lastSyncedVersion = version;
|
|
1682
|
+
}
|
|
1683
|
+
mostRecentlySyncedClient() {
|
|
1684
|
+
return [...this.clients.values()].filter((candidate) => candidate.socket.readyState === WebSocket.OPEN).sort((left, right) => right.lastSyncedOrder - left.lastSyncedOrder)[0];
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
// src/server/httpServer.ts
|
|
1689
|
+
async function startHttpServer(options) {
|
|
1690
|
+
const port = await findFreePort(options.host, options.port);
|
|
1691
|
+
const workspace = new Workspace(options.workspace);
|
|
1692
|
+
await workspace.ensure();
|
|
1693
|
+
const plugin = createExcalidrawPlugin();
|
|
1694
|
+
const controller = new CanvasController(plugin);
|
|
1695
|
+
const bridge = new WsBridge(controller);
|
|
1696
|
+
controller.setChangeListener((snapshot, origin) => bridge.broadcastScene(snapshot, origin));
|
|
1697
|
+
const app = createApp({
|
|
1698
|
+
plugin,
|
|
1699
|
+
controller,
|
|
1700
|
+
workspace,
|
|
1701
|
+
webDistDir: findWebDistDir(),
|
|
1702
|
+
allowedHosts: allowedHostsFor(options.host, port),
|
|
1703
|
+
clientsConnected: () => bridge.connectedClientCount(),
|
|
1704
|
+
requestExport: (exportOptions) => bridge.requestExport(exportOptions),
|
|
1705
|
+
requestSelection: (selectionOptions) => bridge.requestSelection(selectionOptions)
|
|
1706
|
+
});
|
|
1707
|
+
const server = createServer(app);
|
|
1708
|
+
bridge.attach(server);
|
|
1709
|
+
await new Promise((resolve, reject) => {
|
|
1710
|
+
server.once("error", reject);
|
|
1711
|
+
server.listen(port, options.host, () => {
|
|
1712
|
+
server.off("error", reject);
|
|
1713
|
+
resolve();
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
return {
|
|
1717
|
+
host: options.host,
|
|
1718
|
+
port,
|
|
1719
|
+
canvasUrl: `http://${options.host}:${port}`,
|
|
1720
|
+
mcpUrl: `http://${options.host}:${port}/mcp`,
|
|
1721
|
+
server,
|
|
1722
|
+
controller,
|
|
1723
|
+
bridge,
|
|
1724
|
+
close: () => new Promise((resolve, reject) => {
|
|
1725
|
+
bridge.close();
|
|
1726
|
+
server.close((error) => {
|
|
1727
|
+
if (error) {
|
|
1728
|
+
reject(error);
|
|
1729
|
+
} else {
|
|
1730
|
+
resolve();
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
})
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
async function findFreePort(host, requestedPort) {
|
|
1737
|
+
for (let port = requestedPort; port < requestedPort + 20; port += 1) {
|
|
1738
|
+
if (await canListen(host, port)) {
|
|
1739
|
+
return port;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
throw new Error(`No free port found starting at ${requestedPort}`);
|
|
1743
|
+
}
|
|
1744
|
+
function canListen(host, port) {
|
|
1745
|
+
return new Promise((resolve) => {
|
|
1746
|
+
const probe = net.createServer();
|
|
1747
|
+
probe.once("error", () => resolve(false));
|
|
1748
|
+
probe.listen(port, host, () => {
|
|
1749
|
+
probe.close(() => resolve(true));
|
|
1750
|
+
});
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function allowedHostsFor(host, port) {
|
|
1754
|
+
const hosts = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", host]);
|
|
1755
|
+
for (const hostname of [...hosts]) {
|
|
1756
|
+
hosts.add(`${hostname}:${port}`);
|
|
1757
|
+
}
|
|
1758
|
+
return [...hosts];
|
|
1759
|
+
}
|
|
1760
|
+
function findWebDistDir() {
|
|
1761
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
1762
|
+
const candidates = [
|
|
1763
|
+
path4.resolve(here, "../web"),
|
|
1764
|
+
path4.resolve(here, "../../dist/web"),
|
|
1765
|
+
path4.resolve(process.cwd(), "dist/web")
|
|
1766
|
+
];
|
|
1767
|
+
return candidates.find((candidate) => existsSync(path4.join(candidate, "index.html"))) ?? candidates[2];
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/shared/logger.ts
|
|
1771
|
+
var levelOrder = {
|
|
1772
|
+
debug: 10,
|
|
1773
|
+
info: 20,
|
|
1774
|
+
warn: 30,
|
|
1775
|
+
error: 40
|
|
1776
|
+
};
|
|
1777
|
+
function createLogger(minLevel = "info") {
|
|
1778
|
+
const min = levelOrder[minLevel];
|
|
1779
|
+
const write = (level, message, details) => {
|
|
1780
|
+
if (levelOrder[level] < min) {
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const suffix = details === void 0 ? "" : ` ${formatDetails(details)}`;
|
|
1784
|
+
console.error(`[agentic-canvas] ${level}: ${message}${suffix}`);
|
|
1785
|
+
};
|
|
1786
|
+
return {
|
|
1787
|
+
debug: (message, details) => write("debug", message, details),
|
|
1788
|
+
info: (message, details) => write("info", message, details),
|
|
1789
|
+
warn: (message, details) => write("warn", message, details),
|
|
1790
|
+
error: (message, details) => write("error", message, details)
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function formatDetails(details) {
|
|
1794
|
+
if (details instanceof Error) {
|
|
1795
|
+
return details.stack ?? details.message;
|
|
1796
|
+
}
|
|
1797
|
+
if (typeof details === "string") {
|
|
1798
|
+
return details;
|
|
1799
|
+
}
|
|
1800
|
+
try {
|
|
1801
|
+
return JSON.stringify(details);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return String(details);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/cli/index.ts
|
|
1808
|
+
var logger = createLogger("info");
|
|
1809
|
+
async function main() {
|
|
1810
|
+
const packageInfo = readPackageInfo();
|
|
1811
|
+
const args = parseArgs({
|
|
1812
|
+
allowPositionals: false,
|
|
1813
|
+
allowNegative: true,
|
|
1814
|
+
options: {
|
|
1815
|
+
canvas: { type: "string", default: "excalidraw" },
|
|
1816
|
+
port: { type: "string", default: process.env.AGENTIC_CANVAS_PORT ?? "3333" },
|
|
1817
|
+
host: { type: "string", default: process.env.AGENTIC_CANVAS_HOST ?? "127.0.0.1" },
|
|
1818
|
+
workspace: { type: "string", default: process.env.AGENTIC_CANVAS_WORKSPACE ?? process.cwd() },
|
|
1819
|
+
open: { type: "boolean", default: true },
|
|
1820
|
+
help: { type: "boolean", short: "h" },
|
|
1821
|
+
version: { type: "boolean", short: "v" }
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
if (args.values.help) {
|
|
1825
|
+
printHelp();
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
if (args.values.version) {
|
|
1829
|
+
console.log(packageInfo.version);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (args.values.canvas !== "excalidraw") {
|
|
1833
|
+
throw new Error(`Unknown canvas "${args.values.canvas}". Available canvases: excalidraw`);
|
|
1834
|
+
}
|
|
1835
|
+
const requestedPort = Number(args.values.port);
|
|
1836
|
+
if (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
|
|
1837
|
+
throw new Error(`Invalid port: ${args.values.port}`);
|
|
1838
|
+
}
|
|
1839
|
+
const running = await startHttpServer({
|
|
1840
|
+
canvas: "excalidraw",
|
|
1841
|
+
host: String(args.values.host),
|
|
1842
|
+
port: requestedPort,
|
|
1843
|
+
workspace: String(args.values.workspace)
|
|
1844
|
+
});
|
|
1845
|
+
console.log(`Canvas: ${running.canvasUrl}`);
|
|
1846
|
+
console.log(`MCP: ${running.mcpUrl}`);
|
|
1847
|
+
if (args.values.open) {
|
|
1848
|
+
await open(running.canvasUrl);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function printHelp() {
|
|
1852
|
+
console.log(`${CLI_NAME}
|
|
1853
|
+
|
|
1854
|
+
Start a local Excalidraw canvas with an MCP Streamable HTTP endpoint.
|
|
1855
|
+
|
|
1856
|
+
Usage:
|
|
1857
|
+
npx ${PACKAGE_NAME} --canvas excalidraw
|
|
1858
|
+
${CLI_NAME} --canvas excalidraw
|
|
1859
|
+
|
|
1860
|
+
Options:
|
|
1861
|
+
--canvas <name> Canvas plugin, currently excalidraw (default: excalidraw)
|
|
1862
|
+
--port <n> Port (default: 3333 or AGENTIC_CANVAS_PORT)
|
|
1863
|
+
--host <host> Bind host (default: 127.0.0.1 or AGENTIC_CANVAS_HOST)
|
|
1864
|
+
--workspace <dir> Save/open/screenshot workspace (default: cwd)
|
|
1865
|
+
--open, --no-open Open browser on startup (default: open)
|
|
1866
|
+
-h, --help Show help
|
|
1867
|
+
-v, --version Show version`);
|
|
1868
|
+
}
|
|
1869
|
+
main().catch((error) => {
|
|
1870
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
1871
|
+
process.exitCode = 1;
|
|
1872
|
+
});
|
|
1873
|
+
//# sourceMappingURL=index.js.map
|