@sanjibdevnath/mcp-excalidraw-local 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +458 -0
  3. package/dist/db.d.ts +58 -0
  4. package/dist/db.d.ts.map +1 -0
  5. package/dist/db.js +379 -0
  6. package/dist/db.js.map +1 -0
  7. package/dist/frontend/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
  8. package/dist/frontend/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
  9. package/dist/frontend/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
  10. package/dist/frontend/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
  11. package/dist/frontend/assets/Tableau10-B-NsZVaP.js +1 -0
  12. package/dist/frontend/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
  13. package/dist/frontend/assets/advancedFormat-BvOvfnfC.js +1 -0
  14. package/dist/frontend/assets/ar-SA-G6X2FPQ2-75HMOOy8.js +10 -0
  15. package/dist/frontend/assets/arc-D-322MQz.js +1 -0
  16. package/dist/frontend/assets/array-BKyUJesY.js +1 -0
  17. package/dist/frontend/assets/az-AZ-76LH7QW2-DPDwkDvh.js +1 -0
  18. package/dist/frontend/assets/band-dPffDWoQ.js +1 -0
  19. package/dist/frontend/assets/bg-BG-XCXSNQG7-DrFYc9eo.js +5 -0
  20. package/dist/frontend/assets/blockDiagram-38ab4fdb-Ch8bwO7g.js +118 -0
  21. package/dist/frontend/assets/blockDiagram-68f4deed-BVqzkDiu.js +118 -0
  22. package/dist/frontend/assets/bn-BD-2XOGV67Q-B1Y75Cvj.js +5 -0
  23. package/dist/frontend/assets/c4Diagram-15b5d702-D5U2mSdf.js +10 -0
  24. package/dist/frontend/assets/c4Diagram-3d4e48cf-eT2EEN_c.js +10 -0
  25. package/dist/frontend/assets/ca-ES-6MX7JW3Y-00BTiK3Z.js +8 -0
  26. package/dist/frontend/assets/channel-CudwHHli.js +1 -0
  27. package/dist/frontend/assets/classDiagram-70f12bd4-CcNOdQHv.js +2 -0
  28. package/dist/frontend/assets/classDiagram-d40c83e7-nRIgRTMT.js +2 -0
  29. package/dist/frontend/assets/classDiagram-v2-d5a6b087-Cfbvao44.js +2 -0
  30. package/dist/frontend/assets/classDiagram-v2-f2320105-1Sjp5Uqh.js +2 -0
  31. package/dist/frontend/assets/clone-D_tGm99B.js +1 -0
  32. package/dist/frontend/assets/createText-2e5e7dd3-Bpmkp1eZ.js +5 -0
  33. package/dist/frontend/assets/createText-d213de94-3MLB4fd8.js +5 -0
  34. package/dist/frontend/assets/cs-CZ-2BRQDIVT-R7SCWLLF.js +11 -0
  35. package/dist/frontend/assets/cytoscape-cose-bilkent-CoIxD6ON.js +331 -0
  36. package/dist/frontend/assets/da-DK-5WZEPLOC-Db1yebad.js +5 -0
  37. package/dist/frontend/assets/de-DE-XR44H4JA-HRE-6fuh.js +8 -0
  38. package/dist/frontend/assets/directory-open-01563666-DWU9wJ6I.js +1 -0
  39. package/dist/frontend/assets/directory-open-4ed118d0-BzWybGaI.js +1 -0
  40. package/dist/frontend/assets/edges-332bd1c7-DZAOA9uP.js +4 -0
  41. package/dist/frontend/assets/edges-e0da2a9e-CP-XTLb4.js +4 -0
  42. package/dist/frontend/assets/el-GR-BZB4AONW-CfNczSdx.js +10 -0
  43. package/dist/frontend/assets/elk.bundled-BZDcWavb.js +26 -0
  44. package/dist/frontend/assets/erDiagram-880f2ed8-Bk96tDga.js +51 -0
  45. package/dist/frontend/assets/erDiagram-9861fffd-BvkEkcRK.js +51 -0
  46. package/dist/frontend/assets/es-ES-U4NZUMDT-BBJZ1_wD.js +9 -0
  47. package/dist/frontend/assets/eu-ES-A7QVB2H4-CCLNmdnk.js +11 -0
  48. package/dist/frontend/assets/fa-IR-HGAKTJCU-BtKS5FOW.js +8 -0
  49. package/dist/frontend/assets/fi-FI-Z5N7JZ37-DEQi6vbL.js +6 -0
  50. package/dist/frontend/assets/file-open-002ab408-DIuFHtCF.js +1 -0
  51. package/dist/frontend/assets/file-open-7c801643-684qeFg4.js +1 -0
  52. package/dist/frontend/assets/file-save-3189631c-x92wctJd.js +1 -0
  53. package/dist/frontend/assets/file-save-745eba88-Bb9F9Kg7.js +1 -0
  54. package/dist/frontend/assets/flowDb-7c981674-JJMg1ttK.js +10 -0
  55. package/dist/frontend/assets/flowDb-956e92f1-CVVUllPW.js +10 -0
  56. package/dist/frontend/assets/flowDiagram-66a62f08-wGFuUp6y.js +4 -0
  57. package/dist/frontend/assets/flowDiagram-cbd28bf7-CXKT_tHC.js +4 -0
  58. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CN4ht1EM.js +1 -0
  59. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CFiBItzu.js +1 -0
  60. package/dist/frontend/assets/flowchart-elk-definition-36e2d292-Cam5JBwn.js +114 -0
  61. package/dist/frontend/assets/flowchart-elk-definition-4a651766-BoyD4myW.js +114 -0
  62. package/dist/frontend/assets/fr-FR-RHASNOE6-_AQjPuKS.js +9 -0
  63. package/dist/frontend/assets/ganttDiagram-04f9e578-DrDI9_oS.js +257 -0
  64. package/dist/frontend/assets/ganttDiagram-c361ad54-CKSyNc2k.js +257 -0
  65. package/dist/frontend/assets/gitGraphDiagram-21fc4d3e-BHBdnwSb.js +70 -0
  66. package/dist/frontend/assets/gitGraphDiagram-72cf32ee-BHN9qiXg.js +70 -0
  67. package/dist/frontend/assets/gl-ES-HMX3MZ6V-Bp2h6sBC.js +10 -0
  68. package/dist/frontend/assets/graph-CRb9j7zI.js +1 -0
  69. package/dist/frontend/assets/graph-EK5j_nPe.js +1 -0
  70. package/dist/frontend/assets/he-IL-6SHJWFNN-hsaAKZ5K.js +10 -0
  71. package/dist/frontend/assets/hi-IN-IWLTKZ5I-sgYSNzoz.js +4 -0
  72. package/dist/frontend/assets/hu-HU-A5ZG7DT2-DxYZr0yq.js +7 -0
  73. package/dist/frontend/assets/id-ID-SAP4L64H-z0RzSKPQ.js +10 -0
  74. package/dist/frontend/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
  75. package/dist/frontend/assets/index-3862675e-CQPsxwvk.js +1 -0
  76. package/dist/frontend/assets/index-6079d271-pTR-OMc-.js +1 -0
  77. package/dist/frontend/assets/index-B9Rh8YyQ.css +1 -0
  78. package/dist/frontend/assets/index-BcHA28Dx.js +87 -0
  79. package/dist/frontend/assets/index-DGmpr33w.js +3 -0
  80. package/dist/frontend/assets/index-DPgZw9ew.js +349 -0
  81. package/dist/frontend/assets/infoDiagram-4a4f5b27-OIxyK2_N.js +7 -0
  82. package/dist/frontend/assets/infoDiagram-f8f76790-BTkoanKB.js +7 -0
  83. package/dist/frontend/assets/init-Gi6I4Gst.js +1 -0
  84. package/dist/frontend/assets/it-IT-JPQ66NNP-Cu6RM7DP.js +11 -0
  85. package/dist/frontend/assets/ja-JP-DBVTYXUO-lD7U4Zkf.js +8 -0
  86. package/dist/frontend/assets/journeyDiagram-29694f62-BS4Xl0A-.js +139 -0
  87. package/dist/frontend/assets/journeyDiagram-49397b02-BbBAwEfu.js +139 -0
  88. package/dist/frontend/assets/kaa-6HZHGXH3-DM9LwXUP.js +1 -0
  89. package/dist/frontend/assets/kab-KAB-ZGHBKWFO-BAojmp2_.js +8 -0
  90. package/dist/frontend/assets/katex-ChWnQ-fc.js +261 -0
  91. package/dist/frontend/assets/kk-KZ-P5N5QNE5-Dp0K1W81.js +1 -0
  92. package/dist/frontend/assets/km-KH-HSX4SM5Z-BzYGKbAg.js +11 -0
  93. package/dist/frontend/assets/ko-KR-MTYHY66A-DOvEMk4H.js +9 -0
  94. package/dist/frontend/assets/ku-TR-6OUDTVRD-B6l-ghqp.js +9 -0
  95. package/dist/frontend/assets/layout-CGydnLJa.js +1 -0
  96. package/dist/frontend/assets/layout-DbdMIGYe.js +1 -0
  97. package/dist/frontend/assets/line-CbImtxDK.js +1 -0
  98. package/dist/frontend/assets/linear-DvIsU3aM.js +1 -0
  99. package/dist/frontend/assets/lt-LT-XHIRWOB4-BYcRk8Uj.js +3 -0
  100. package/dist/frontend/assets/lv-LV-5QDEKY6T-DS3krNIe.js +7 -0
  101. package/dist/frontend/assets/mindmap-definition-ac74a2e8-C0Sp7ICZ.js +95 -0
  102. package/dist/frontend/assets/mindmap-definition-fc14e90a-BZrjRbkr.js +95 -0
  103. package/dist/frontend/assets/mr-IN-CRQNXWMA-BfxQL7Vh.js +13 -0
  104. package/dist/frontend/assets/my-MM-5M5IBNSE-C3EfnOvD.js +1 -0
  105. package/dist/frontend/assets/nb-NO-T6EIAALU-BIbPZokm.js +10 -0
  106. package/dist/frontend/assets/nl-NL-IS3SIHDZ-BqQloGBT.js +8 -0
  107. package/dist/frontend/assets/nn-NO-6E72VCQL-zGR8NYQf.js +8 -0
  108. package/dist/frontend/assets/oc-FR-POXYY2M6-B8-HsJFE.js +8 -0
  109. package/dist/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/dist/frontend/assets/pa-IN-N4M65BXN-B2Ta58Tu.js +4 -0
  111. package/dist/frontend/assets/path-CbwjOpE9.js +1 -0
  112. package/dist/frontend/assets/pica-DSD-O3at.js +7 -0
  113. package/dist/frontend/assets/pie-Dk_pQnuO.js +1 -0
  114. package/dist/frontend/assets/pieDiagram-421022e6-9oAq5fk_.js +35 -0
  115. package/dist/frontend/assets/pieDiagram-8a3498a8-B5SMrdDh.js +35 -0
  116. package/dist/frontend/assets/pl-PL-T2D74RX3-rZKvQ0zQ.js +9 -0
  117. package/dist/frontend/assets/pt-BR-5N22H2LF-ij6wtU6I.js +9 -0
  118. package/dist/frontend/assets/pt-PT-UZXXM6DQ-BIgtUnbW.js +9 -0
  119. package/dist/frontend/assets/quadrantDiagram-0957ecba-Cr3mj6c1.js +7 -0
  120. package/dist/frontend/assets/quadrantDiagram-120e2f19-CQnc4s0f.js +7 -0
  121. package/dist/frontend/assets/requirementDiagram-23d650b8-Bs7pP1vJ.js +52 -0
  122. package/dist/frontend/assets/requirementDiagram-deff3bca-G5e-Qxao.js +52 -0
  123. package/dist/frontend/assets/ro-RO-JPDTUUEW-DPj_79nt.js +11 -0
  124. package/dist/frontend/assets/roundRect-0PYZxl1G.js +1 -0
  125. package/dist/frontend/assets/ru-RU-B4JR7IUQ-fdYiaqbX.js +9 -0
  126. package/dist/frontend/assets/sankeyDiagram-04a897e0-CJogadkF.js +8 -0
  127. package/dist/frontend/assets/sankeyDiagram-23345273-DKUWMCrX.js +8 -0
  128. package/dist/frontend/assets/sankeyLinkHorizontal-DgqkLiUE.js +1 -0
  129. package/dist/frontend/assets/selectAll-tNeSnQY6.js +1 -0
  130. package/dist/frontend/assets/sequenceDiagram-17ac3bff-DCw9xUbw.js +122 -0
  131. package/dist/frontend/assets/sequenceDiagram-704730f1-BgClSrOI.js +122 -0
  132. package/dist/frontend/assets/si-LK-N5RQ5JYF-DfPBk-rU.js +1 -0
  133. package/dist/frontend/assets/sk-SK-C5VTKIMK-Cbj4yoD_.js +6 -0
  134. package/dist/frontend/assets/sl-SI-NN7IZMDC-C_rL7eDE.js +6 -0
  135. package/dist/frontend/assets/stateDiagram-587899a1-DuFGG-SI.js +1 -0
  136. package/dist/frontend/assets/stateDiagram-9c5f0230-Bwj38hfH.js +1 -0
  137. package/dist/frontend/assets/stateDiagram-v2-51a3dcff-3c0yKNdL.js +1 -0
  138. package/dist/frontend/assets/stateDiagram-v2-d93cdb3a-CAaqB4wm.js +1 -0
  139. package/dist/frontend/assets/styles-2ab5d517-Dxg7wKah.js +116 -0
  140. package/dist/frontend/assets/styles-5f03d8d2-DD32XMGL.js +160 -0
  141. package/dist/frontend/assets/styles-6aaf32cf-B5DxK_RW.js +207 -0
  142. package/dist/frontend/assets/styles-9a916d00-C6L6Mj2P.js +160 -0
  143. package/dist/frontend/assets/styles-c10674c1-BPM_bB3H.js +116 -0
  144. package/dist/frontend/assets/styles-edf9a4b0-CbQDxrwP.js +207 -0
  145. package/dist/frontend/assets/subset-shared.chunk-B_DQsaBC.js +84 -0
  146. package/dist/frontend/assets/subset-worker.chunk-DL6tLP7M.js +1 -0
  147. package/dist/frontend/assets/sv-SE-XGPEYMSR-BmmcOaVK.js +10 -0
  148. package/dist/frontend/assets/svgDrawCommon-08f97a94-aUx8qfJx.js +1 -0
  149. package/dist/frontend/assets/svgDrawCommon-3ba9043b-1JM8RiLc.js +1 -0
  150. package/dist/frontend/assets/ta-IN-2NMHFXQM-Kxnb_Mwk.js +9 -0
  151. package/dist/frontend/assets/th-TH-HPSO5L25-BqTLgxJz.js +2 -0
  152. package/dist/frontend/assets/timeline-definition-7e6b55e7-BbFhIPTl.js +61 -0
  153. package/dist/frontend/assets/timeline-definition-85554ec2-C1G9H6m5.js +61 -0
  154. package/dist/frontend/assets/tr-TR-DEFEU3FU-DhlYP6tL.js +7 -0
  155. package/dist/frontend/assets/uk-UA-QMV73CPH-pMrN1qBS.js +6 -0
  156. package/dist/frontend/assets/union-Cu1rbD_D.js +1 -0
  157. package/dist/frontend/assets/vi-VN-M7AON7JQ-BPMcH84R.js +5 -0
  158. package/dist/frontend/assets/xml-BOsq7VnW.js +1 -0
  159. package/dist/frontend/assets/xychartDiagram-b6496bcd-BDm9pYtk.js +7 -0
  160. package/dist/frontend/assets/xychartDiagram-e933f94c-BlrTBDHC.js +7 -0
  161. package/dist/frontend/assets/zh-CN-LNUGB5OW-B8kYYibM.js +10 -0
  162. package/dist/frontend/assets/zh-HK-E62DVLB3-CaI0gehP.js +1 -0
  163. package/dist/frontend/assets/zh-TW-RAJ6MFWO-DKCVg17j.js +9 -0
  164. package/dist/frontend/assets/zipObject-iRVIFf6r.js +1 -0
  165. package/dist/frontend/index.html +420 -0
  166. package/dist/index.d.ts +4 -0
  167. package/dist/index.d.ts.map +1 -0
  168. package/dist/index.js +2241 -0
  169. package/dist/index.js.map +1 -0
  170. package/dist/server.d.ts +6 -0
  171. package/dist/server.d.ts.map +1 -0
  172. package/dist/server.js +980 -0
  173. package/dist/server.js.map +1 -0
  174. package/dist/types.d.ts +225 -0
  175. package/dist/types.d.ts.map +1 -0
  176. package/dist/types.js +30 -0
  177. package/dist/types.js.map +1 -0
  178. package/dist/utils/logger.d.ts +4 -0
  179. package/dist/utils/logger.d.ts.map +1 -0
  180. package/dist/utils/logger.js +23 -0
  181. package/dist/utils/logger.js.map +1 -0
  182. package/package.json +108 -0
  183. package/skills/excalidraw-skill/SKILL.md +370 -0
  184. package/skills/excalidraw-skill/references/cheatsheet.md +195 -0
  185. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +38 -0
  186. package/skills/excalidraw-skill/scripts/create-element.cjs +68 -0
  187. package/skills/excalidraw-skill/scripts/delete-element.cjs +48 -0
  188. package/skills/excalidraw-skill/scripts/export-elements.cjs +53 -0
  189. package/skills/excalidraw-skill/scripts/healthcheck.cjs +35 -0
  190. package/skills/excalidraw-skill/scripts/import-elements.cjs +81 -0
  191. package/skills/excalidraw-skill/scripts/update-element.cjs +70 -0
@@ -0,0 +1,370 @@
1
+ ---
2
+ name: excalidraw-skill
3
+ description: Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools (32 tools) or REST API with real-time canvas sync, multi-tenant workspace isolation, SQLite persistence, project management, full-text search, and element version history. Use when an agent needs to draw or lay out diagrams on a live canvas, iteratively refine diagrams using screenshots, manage workspaces/tenants and projects, export/import .excalidraw files or PNG/SVG images, search elements, view change history, save/restore canvas snapshots, or perform element-level CRUD. Canvas server port is configurable via CANVAS_PORT env var (default 3000).
4
+ ---
5
+
6
+ # Excalidraw Skill
7
+
8
+ ## Step 0: Detect Connection Mode
9
+
10
+ Run these checks **in order**:
11
+
12
+ 1. **MCP Server** (best): If tools like `batch_create_elements` are available → use MCP mode.
13
+ 2. **REST API** (fallback): `curl -s http://localhost:3000/health` returns `{"status":"ok"}` → use REST API mode.
14
+ 3. **Nothing works**: Guide user to install (clone `sanjibdevnathlabs/mcp-excalidraw-local`, build, configure MCP).
15
+
16
+ See `references/cheatsheet.md` for the full MCP-vs-REST mapping and REST API gotchas.
17
+
18
+ ## Core Principles (Read Before Any Diagram)
19
+
20
+ These principles were learned through extensive iterative use. Violating them produces bad diagrams.
21
+
22
+ ### 1. Never Trust Blind Output — Use the Write-Check-Review Cycle
23
+
24
+ Every diagram iteration follows this mandatory loop:
25
+
26
+ ```
27
+ WRITE (create/update elements)
28
+ → CHECK (screenshot to see actual rendering)
29
+ → REVIEW (critically evaluate against Quality Checklist)
30
+ → FIX (if issues found, fix and re-screenshot)
31
+ → only proceed when ALL checks pass
32
+ ```
33
+
34
+ **Screenshot strategy**: `get_canvas_screenshot` may return empty images. When it fails, use Chrome DevTools MCP (`take_screenshot` after `navigate_page` to canvas URL) as a reliable fallback.
35
+
36
+ ### 2. Use batch_create_elements, Not Mermaid
37
+
38
+ The `create_from_mermaid` tool produces **low-quality output**: overlapping text, poor spacing, unreadable labels. It is a quick preview tool, not a production tool.
39
+
40
+ For quality diagrams, **always use `batch_create_elements`** with precise coordinates, explicit sizing, and color coding. The extra planning time pays for itself in fewer fix iterations.
41
+
42
+ ### 3. Shapes First, Arrows Second — Two Separate Batches
43
+
44
+ Create shapes in one batch, then arrows in a separate batch. Arrow binding (`startElementId`/`endElementId`) requires shapes to already exist in the scene. Mixing both in one call can work but often produces binding errors.
45
+
46
+ ### 4. Multiple Diagrams on One Canvas
47
+
48
+ **Never clear the canvas** between diagrams. Place them side-by-side or in a grid:
49
+
50
+ ```
51
+ Diagram 1: x=0 to ~1100
52
+ Diagram 2: x=1400 onward (300px gap)
53
+ — or —
54
+ Row 1: y=0 to ~800
55
+ Row 2: y=1100 onward (300px gap)
56
+ ```
57
+
58
+ Use a title text element above each diagram to label it.
59
+
60
+ ### 5. Set roughness: 0 for Clean Diagrams
61
+
62
+ Excalidraw defaults to hand-drawn style (roughness > 0). For professional, readable diagrams, always set `"roughness": 0` on every element. Also use `"strokeWidth": 2` for arrows to ensure visibility.
63
+
64
+ ## Sizing Rules (Critical — Prevents Truncation)
65
+
66
+ Excalidraw's Virgil font is ~30% wider than standard fonts. These rules account for that.
67
+
68
+ ### Rectangles
69
+
70
+ ```
71
+ width: max(200, characterCount * 11)
72
+ height: 70 (1 line), 80 (2 lines), 100 (3 lines)
73
+ fontSize: 16-20
74
+ ```
75
+
76
+ ### Diamonds (Decision Nodes)
77
+
78
+ Diamond usable text area is ~50% of the bounding box. **Double your width estimate.**
79
+
80
+ ```
81
+ width: max(400, longestLineChars * 18)
82
+ height: max(160, lineCount * 50)
83
+ fontSize: 16
84
+ ```
85
+
86
+ A diamond with text "Behavioral guideline\nor project standard?" (20 chars) needs at least 400x160.
87
+
88
+ ### Ellipses
89
+
90
+ Ellipse text area is ~60% of bounding box. Size generously.
91
+
92
+ ```
93
+ width: max(280, characterCount * 14)
94
+ height: max(65, lineCount * 35)
95
+ fontSize: 16-18
96
+ ```
97
+
98
+ ### Text Elements (Standalone Titles)
99
+
100
+ ```
101
+ fontSize: 24-28 for diagram titles
102
+ fontSize: 16-20 for annotations
103
+ ```
104
+
105
+ ## Arrow Visibility Rules (Critical — Prevents Invisible Arrows)
106
+
107
+ When arrows are bound to shapes via `startElementId`/`endElementId`, the actual rendered arrow length equals the **gap between shape edges minus binding padding (8px each side)**. If shapes are too close, arrows shrink to 0px and become invisible.
108
+
109
+ ### Minimum Gap Between Connected Shapes
110
+
111
+ | Connection Direction | Minimum Gap | Recommended Gap |
112
+ |---------------------|-------------|-----------------|
113
+ | Vertical (top-down flow) | 80px | 120px |
114
+ | Horizontal (left-right) | 100px | 140px |
115
+
116
+ ### Calculating Vertical Gap for Flowcharts
117
+
118
+ ```
119
+ gap = nextShapeY - (currentShapeY + currentShapeHeight)
120
+
121
+ Example (diamonds h=160, gap needed ≥ 120):
122
+ Q1: y=260, h=160 → bottom edge = 420
123
+ Q2: y=540 → gap = 540 - 420 = 120px ✓
124
+ ```
125
+
126
+ If the gap is < 80px, arrows will be too short to see — especially with labels like "YES"/"NO".
127
+
128
+ ## Workflow: Draw A Diagram
129
+
130
+ ### Phase 1: Plan
131
+
132
+ Before writing any JSON, plan on paper:
133
+
134
+ 1. **List all elements**: shapes, labels, connections
135
+ 2. **Choose layout direction**: top-down (flowcharts), left-right (timelines), grid (architecture)
136
+ 3. **Assign coordinates**: use the sizing rules above to compute widths/heights, then lay out with proper gaps
137
+ 4. **Assign IDs**: every shape needs a custom `id` so arrows can reference it
138
+
139
+ ### Phase 2: Create Shapes (Batch 1)
140
+
141
+ ```json
142
+ {"elements": [
143
+ {"id": "title", "type": "text", "x": 100, "y": 0,
144
+ "text": "MY DIAGRAM", "fontSize": 28, "strokeColor": "#1e1e1e"},
145
+ {"id": "box-a", "type": "rectangle", "x": 0, "y": 80,
146
+ "width": 200, "height": 70, "text": "Service A",
147
+ "backgroundColor": "#a5d8ff", "strokeColor": "#1971c2",
148
+ "roughness": 0, "fontSize": 18},
149
+ {"id": "box-b", "type": "rectangle", "x": 0, "y": 280,
150
+ "width": 200, "height": 70, "text": "Service B",
151
+ "backgroundColor": "#b2f2bb", "strokeColor": "#2f9e44",
152
+ "roughness": 0, "fontSize": 18}
153
+ ]}
154
+ ```
155
+
156
+ ### Phase 3: Create Arrows (Batch 2)
157
+
158
+ ```json
159
+ {"elements": [
160
+ {"type": "arrow", "x": 100, "y": 150,
161
+ "startElementId": "box-a", "endElementId": "box-b",
162
+ "width": 0, "height": 130, "text": "calls",
163
+ "strokeColor": "#1e1e1e", "roughness": 0, "strokeWidth": 2,
164
+ "endArrowhead": "arrow"}
165
+ ]}
166
+ ```
167
+
168
+ ### Phase 4: Check (MANDATORY)
169
+
170
+ 1. `set_viewport` with `scrollToContent: true`
171
+ 2. Wait 1-2 seconds for render
172
+ 3. Take screenshot (MCP `get_canvas_screenshot` or Chrome DevTools `take_screenshot`)
173
+ 4. **Critically evaluate** against the Quality Checklist below
174
+ 5. Fix any issues, re-screenshot, repeat until clean
175
+
176
+ ## Quality Checklist
177
+
178
+ After EVERY batch of elements, verify ALL of these:
179
+
180
+ | Check | What to Look For | Fix |
181
+ |-------|-----------------|-----|
182
+ | **Text truncation** | Any label cut off or hidden? | Increase shape width/height |
183
+ | **Invisible arrows** | Can you see arrows between all connected shapes? | Increase gap between shapes to ≥ 120px |
184
+ | **Arrow labels** | Do YES/NO/labels overlap with shapes? | Shorten labels or increase gap |
185
+ | **Overlap** | Do any elements share space? | Reposition with more spacing |
186
+ | **Readability** | Can all text be read at 50-70% zoom? | Increase fontSize to ≥ 16 |
187
+ | **Spacing** | At least 40px gap between unconnected elements? | Spread elements apart |
188
+
189
+ ### If ANY Check Fails
190
+
191
+ **STOP.** Do not add more elements. Fix the issue first:
192
+
193
+ 1. Use `update_element` to resize/reposition
194
+ 2. Or `delete_element` + recreate with better coordinates
195
+ 3. Re-screenshot to verify the fix
196
+ 4. Only proceed when ALL checks pass
197
+
198
+ ### How to Honestly Evaluate a Screenshot
199
+
200
+ - Zoom into different regions — don't just glance at the overview
201
+ - Check every label individually for truncation
202
+ - Trace every arrow path for visibility
203
+ - **If you see ANY issue, say "I see [issue], fixing it"** — never say "looks great" unless it truly is
204
+
205
+ ## Color Palette
206
+
207
+ Use consistent colors from this palette:
208
+
209
+ | Role | Fill | Stroke | Use For |
210
+ |------|------|--------|---------|
211
+ | Primary | #a5d8ff | #1971c2 | Main flow, services |
212
+ | Success | #b2f2bb | #2f9e44 | Approved, healthy, YES paths |
213
+ | Warning | #ffd8a8 | #e8590c | Attention, agents |
214
+ | Error | #ffc9c9 | #e03131 | Critical, NO paths, failures |
215
+ | Purple | #eebefa | #9c36b5 | Rules, governance |
216
+ | Cyan | #99e9f2 | #0c8599 | Data stores, MCP |
217
+ | Neutral | #e9ecef | #868e96 | Secondary, annotations |
218
+ | Default | #ffffff | #1e1e1e | Decisions, generic |
219
+
220
+ ## Flowchart Template (Tested & Verified)
221
+
222
+ This template produces clean, readable decision flowcharts:
223
+
224
+ ```
225
+ Layout:
226
+ Diamonds: w=400, h=160, fontSize=16, gap=120px vertical
227
+ Answer boxes: w=300, h=80, fontSize=20, offset 130px right of diamonds
228
+ Start ellipse: w=340, h=70, fontSize=18
229
+ Title: fontSize=28
230
+ Arrows: strokeWidth=2, roughness=0
231
+ YES arrows: strokeColor=#2f9e44 (green), horizontal right
232
+ NO arrows: strokeColor=#e03131 (red), vertical down
233
+ All elements: roughness=0
234
+ ```
235
+
236
+ ## Architecture Diagram Template
237
+
238
+ ```
239
+ Layout:
240
+ Zones: large rectangles, backgroundColor=#e9ecef, opacity=30
241
+ Services: w=200, h=70, fontSize=18, spaced 60px apart
242
+ Data stores: w=180, h=60, fontSize=16, strokeColor=#0c8599
243
+ Arrows: solid for sync, dashed (strokeStyle="dashed") for async
244
+ Title: fontSize=24 above each zone
245
+ ```
246
+
247
+ ## Workflow: Iterative Refinement
248
+
249
+ ```
250
+ create shapes (batch 1)
251
+ → create arrows (batch 2)
252
+ → set_viewport(scrollToContent: true)
253
+ → wait 1-2s
254
+ → screenshot
255
+ → evaluate quality checklist
256
+ → issues? fix → re-screenshot → re-evaluate
257
+ → clean? proceed to next diagram section
258
+ ```
259
+
260
+ For multi-diagram canvases, offset each new diagram by 300px+ from the previous one's bounding box.
261
+
262
+ ## Workflow: Multi-Tenancy (Workspaces)
263
+
264
+ The MCP is multi-tenant. Each Cursor workspace automatically gets its own tenant (identified by a SHA-256 hash of the workspace path). All elements, projects, and snapshots are scoped to the active tenant.
265
+
266
+ ### Automatic Tenant Detection
267
+
268
+ On MCP startup, the server:
269
+ 1. Creates a tenant from `process.cwd()` (initial guess)
270
+ 2. After connecting, calls `server.listRoots()` to get the real workspace path from Cursor
271
+ 3. If different, re-creates/switches to the correct tenant and notifies the canvas
272
+
273
+ This means globally-configured MCPs (`~/.cursor/mcp.json`) correctly detect the per-window workspace — no manual setup needed.
274
+
275
+ ### Tenant Operations
276
+
277
+ | Task | Tool | Notes |
278
+ |------|------|-------|
279
+ | See all workspaces | `list_tenants` | Returns id, name, workspace_path, created_at |
280
+ | Switch workspace | `switch_tenant` with `tenantId` | Canvas reloads that tenant's elements via WebSocket |
281
+ | Check current tenant | (from describe_scene or frontend header) | Shows "Workspace: [name]" in canvas |
282
+
283
+ ### Multiple Cursor Instances
284
+
285
+ Each instance sends its own `X-Tenant-Id` header on every HTTP/MCP request. SQLite uses `busy_timeout` for concurrent write safety. No state conflicts between windows.
286
+
287
+ ## Workflow: Projects (Within a Tenant)
288
+
289
+ Projects group diagrams within a tenant. Each tenant has a "Default Project" created automatically. Use projects to organize different diagram sets (e.g., "Architecture", "User Flows", "Sprint Planning").
290
+
291
+ | Task | Tool | Notes |
292
+ |------|------|-------|
293
+ | List projects | `list_projects` | Shows all projects in active tenant |
294
+ | Switch project | `switch_project` with `projectId` | Elements change to that project's set |
295
+ | Create new project | `switch_project` with `createName` | Creates and switches in one call |
296
+
297
+ ## Workflow: Search & History
298
+
299
+ ### Full-Text Search
300
+
301
+ `search_elements` with `query` — searches across element labels and text content in the active project. Useful for finding specific elements in large diagrams.
302
+
303
+ ### Element Version History
304
+
305
+ `element_history` — view create/update/delete operations for:
306
+ - A specific element: pass `elementId`
307
+ - Entire active project: omit `elementId`
308
+ - Control result count with `limit` (default 50)
309
+
310
+ Use history to debug unexpected changes or audit what was modified.
311
+
312
+ ## Workflow: Refine An Existing Diagram
313
+
314
+ 1. `describe_scene` to understand current state
315
+ 2. Identify targets by `id` or label text (or use `search_elements` for text search)
316
+ 3. `update_element` to move/resize/recolor
317
+ 4. Screenshot to verify
318
+ 5. If updates fail: check element id exists (`get_element`), element isn't locked
319
+ 6. Use `element_history` to see what changed if something looks wrong
320
+
321
+ ## Workflow: File I/O
322
+
323
+ - Export: `export_scene` (optional `filePath`)
324
+ - Import: `import_scene` with `mode: "replace"` or `"merge"`
325
+ - Image export: `export_to_image` with `format: "png"` or `"svg"` (requires browser)
326
+
327
+ ## Workflow: Snapshots
328
+
329
+ 1. `snapshot_scene` with a name before risky changes
330
+ 2. Make changes, screenshot to evaluate
331
+ 3. `restore_snapshot` to rollback if needed
332
+
333
+ **Note**: Snapshot restore may not always reload elements into the active view. If the canvas appears empty after restore, re-fetch elements or recreate.
334
+
335
+ ## Workflow: Viewport Control
336
+
337
+ - `scrollToContent: true` — auto-fit all elements
338
+ - `scrollToElementId: "my-element"` — center on specific element
339
+ - `zoom: 0.7, offsetX: 100, offsetY: 50` — manual camera for close-up review
340
+
341
+ ## Anti-Patterns (Common Mistakes)
342
+
343
+ | Mistake | Why It Fails | Do This Instead |
344
+ |---------|-------------|-----------------|
345
+ | Using `create_from_mermaid` for final diagrams | Overlapping text, poor layout | Use `batch_create_elements` with coordinates |
346
+ | Shapes too small for text | Truncation, especially in diamonds | Use sizing formulas above |
347
+ | No gap between connected shapes | Arrows become invisible (0px length) | Maintain 120px+ vertical gap |
348
+ | Clearing canvas between diagrams | Loses previous work | Place diagrams side-by-side |
349
+ | Skipping screenshot verification | Invisible defects compound | Screenshot after EVERY batch |
350
+ | Shapes + arrows in one batch | Binding errors | Shapes first, arrows second |
351
+ | Default roughness (hand-drawn look) | Unprofessional for technical diagrams | Set `roughness: 0` on all elements |
352
+ | Trusting MCP screenshot alone | May return empty image | Use Chrome DevTools as fallback |
353
+
354
+ ## MCP Tool Quick Reference (32 Tools)
355
+
356
+ | Category | Tools |
357
+ |----------|-------|
358
+ | Element CRUD (9) | `create_element`, `get_element`, `update_element`, `delete_element`, `query_elements`, `batch_create_elements`, `duplicate_elements`, `search_elements`, `element_history` |
359
+ | Layout (6) | `align_elements`, `distribute_elements`, `group_elements`, `ungroup_elements`, `lock_elements`, `unlock_elements` |
360
+ | Scene (4) | `describe_scene`, `get_canvas_screenshot`, `get_resource`, `read_diagram_guide` |
361
+ | File I/O (4) | `export_scene`, `import_scene`, `export_to_image`, `export_to_excalidraw_url` |
362
+ | State (3) | `clear_canvas`, `snapshot_scene`, `restore_snapshot` |
363
+ | Viewport (1) | `set_viewport` |
364
+ | Tenants (2) | `list_tenants`, `switch_tenant` |
365
+ | Projects (2) | `list_projects`, `switch_project` |
366
+ | Conversion (1) | `create_from_mermaid` (⚠ low quality — use `batch_create_elements` instead) |
367
+
368
+ ## References
369
+
370
+ - `references/cheatsheet.md`: Complete MCP tool list (32 tools) + REST API endpoints + payload shapes + env vars
@@ -0,0 +1,195 @@
1
+ # Excalidraw Skill Cheatsheet
2
+
3
+ ## Defaults
4
+
5
+ - Canvas base URL: configurable via `CANVAS_PORT` env var (default `3000`), resolves to `http://localhost:<CANVAS_PORT>`
6
+ - Canvas health: `GET /health`
7
+ - Data persistence: SQLite database at `~/.excalidraw-mcp/excalidraw.db`
8
+ - Multi-tenancy: each Cursor workspace auto-creates a tenant (hash of workspace path)
9
+
10
+ ## MCP Tools (32 total)
11
+
12
+ ### Element CRUD
13
+
14
+ | Tool | Description | Required params |
15
+ |------|-------------|-----------------|
16
+ | `create_element` | Create shape/text/arrow/line | `type`, `x`, `y` |
17
+ | `get_element` | Get single element by ID | `id` |
18
+ | `update_element` | Update element properties | `id` |
19
+ | `delete_element` | Delete element | `id` |
20
+ | `query_elements` | Query by type | (optional) `type` |
21
+ | `batch_create_elements` | Create many at once (recommended) | `elements[]` |
22
+ | `duplicate_elements` | Clone with offset | `elementIds[]`, (optional) `offsetX`, `offsetY` |
23
+ | `search_elements` | Full-text search over labels/text | `query` |
24
+ | `element_history` | View version history (create/update/delete ops) | (optional) `elementId`, `limit` (default 50) |
25
+
26
+ ### Layout & Organization
27
+
28
+ | Tool | Description | Required params |
29
+ |------|-------------|-----------------|
30
+ | `align_elements` | Align to left/center/right/top/middle/bottom | `elementIds[]`, `alignment` |
31
+ | `distribute_elements` | Even spacing horizontal/vertical | `elementIds[]`, `direction` |
32
+ | `group_elements` | Group elements | `elementIds[]` |
33
+ | `ungroup_elements` | Ungroup | `groupId` |
34
+ | `lock_elements` | Lock elements | `elementIds[]` |
35
+ | `unlock_elements` | Unlock elements | `elementIds[]` |
36
+
37
+ ### Scene Awareness (Iterative Refinement)
38
+
39
+ | Tool | Description | Required params |
40
+ |------|-------------|-----------------|
41
+ | `describe_scene` | AI-readable scene description (types, positions, labels, connections, bounding box) | (none) |
42
+ | `get_canvas_screenshot` | Returns PNG image of canvas for visual verification (may return empty — use Chrome DevTools as fallback) | (optional) `background` |
43
+ | `get_resource` | Get scene/library/theme/elements | `resource` |
44
+ | `read_diagram_guide` | Get design best practices (colors, sizing, layout, anti-patterns) | (none) |
45
+
46
+ ### File I/O & Export
47
+
48
+ | Tool | Description | Required params |
49
+ |------|-------------|-----------------|
50
+ | `export_scene` | Export to .excalidraw JSON | (optional) `filePath` |
51
+ | `import_scene` | Import from .excalidraw JSON | `mode` ("replace"\|"merge"), `filePath` or `data` |
52
+ | `export_to_image` | Export to PNG/SVG (needs browser) | `format` ("png"\|"svg"), (optional) `filePath`, `background` |
53
+ | `export_to_excalidraw_url` | Upload & get shareable excalidraw.com URL (may fail if org blocks excalidraw.com) | (none) |
54
+
55
+ ### State Management
56
+
57
+ | Tool | Description | Required params |
58
+ |------|-------------|-----------------|
59
+ | `clear_canvas` | Remove all elements from active project | (none) |
60
+ | `snapshot_scene` | Save named snapshot of current canvas state | `name` |
61
+ | `restore_snapshot` | Restore from snapshot (may not reload into view — re-fetch if canvas appears empty) | `name` |
62
+
63
+ ### Viewport & Camera
64
+
65
+ | Tool | Description | Required params |
66
+ |------|-------------|-----------------|
67
+ | `set_viewport` | Control camera: zoom-to-fit, center on element, manual zoom/scroll (needs browser) | (optional) `scrollToContent`, `scrollToElementId`, `zoom`, `offsetX`, `offsetY` |
68
+
69
+ ### Multi-Tenancy (Workspaces)
70
+
71
+ | Tool | Description | Required params |
72
+ |------|-------------|-----------------|
73
+ | `list_tenants` | List all tenants (workspaces). Each tenant maps to a Cursor workspace. | (none) |
74
+ | `switch_tenant` | Switch active tenant. All later operations use that tenant's projects/elements. | `tenantId` |
75
+
76
+ ### Projects (Within a Tenant)
77
+
78
+ | Tool | Description | Required params |
79
+ |------|-------------|-----------------|
80
+ | `list_projects` | List all diagram projects in the active tenant | (none) |
81
+ | `switch_project` | Switch active project or create a new one | (optional) `projectId`, `createName`, `createDescription` |
82
+
83
+ ### Conversion
84
+
85
+ | Tool | Description | Required params |
86
+ |------|-------------|-----------------|
87
+ | `create_from_mermaid` | Mermaid diagram to Excalidraw (⚠ produces low-quality output — use `batch_create_elements` for production diagrams) | `mermaidDiagram` |
88
+
89
+ ## Key Notes
90
+
91
+ ### MCP vs REST API Format Differences
92
+
93
+ | Concept | MCP Tool Format | REST API Format |
94
+ |---------|----------------|-----------------|
95
+ | Shape labels | `"text": "My Label"` (auto-converts) | `"label": {"text": "My Label"}` |
96
+ | Arrow binding | `"startElementId": "id"` / `"endElementId": "id"` | `"start": {"id": "id"}` / `"end": {"id": "id"}` |
97
+ | `fontFamily` | String `"1"` or omit | String `"1"` or omit (never a number) |
98
+ | Tenant scoping | Auto (uses active tenant) | Include `X-Tenant-Id` header on every request |
99
+
100
+ ### Element Creation Best Practices
101
+
102
+ - **Always set `roughness: 0`** for clean, professional diagrams (default is hand-drawn).
103
+ - **Always set `strokeWidth: 2`** on arrows for visibility.
104
+ - **Create shapes first, arrows second** (two separate `batch_create_elements` calls).
105
+ - **Assign custom `id`** to every shape so arrows can reference it.
106
+ - **Size shapes for their text** — Virgil font is ~30% wider than standard. Use sizing formulas from SKILL.md.
107
+ - `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects — normalized automatically.
108
+ - **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points. **Elbowed arrows**: Use `"elbowed": true`.
109
+
110
+ ### Multi-Tenancy Architecture
111
+
112
+ - **Tenant**: Maps to a Cursor workspace. Auto-created on MCP startup from workspace path hash.
113
+ - **Project**: Groups diagrams within a tenant. Default project created per tenant.
114
+ - **Elements**: Belong to the active project within the active tenant.
115
+ - **Hierarchy**: Tenant → Project → Elements
116
+ - **Concurrent instances**: Multiple Cursor windows each send `X-Tenant-Id` header for isolation. SQLite `busy_timeout` handles concurrent writes.
117
+ - **Frontend workspace switcher**: Dropdown in the canvas UI header labeled "Workspace: [name] ▾" with search filter.
118
+
119
+ ## Canvas REST API (HTTP)
120
+
121
+ All endpoints accept an optional `X-Tenant-Id` header to scope operations to a specific tenant.
122
+
123
+ ### Elements
124
+
125
+ | Method | Endpoint | Description |
126
+ |--------|----------|-------------|
127
+ | `GET` | `/api/elements` | List all elements in active project |
128
+ | `GET` | `/api/elements/:id` | Get element by ID |
129
+ | `POST` | `/api/elements` | Create element |
130
+ | `PUT` | `/api/elements/:id` | Update element |
131
+ | `DELETE` | `/api/elements/:id` | Delete element |
132
+ | `DELETE` | `/api/elements/clear` | Clear all elements |
133
+ | `GET` | `/api/elements/search?type=...` | Search with filters |
134
+ | `POST` | `/api/elements/batch` | Batch create |
135
+ | `POST` | `/api/elements/sync` | Full sync (clear + write all elements) |
136
+ | `POST` | `/api/elements/from-mermaid` | Mermaid conversion via frontend |
137
+
138
+ ### Tenants
139
+
140
+ | Method | Endpoint | Description |
141
+ |--------|----------|-------------|
142
+ | `GET` | `/api/tenants` | List all tenants |
143
+ | `GET` | `/api/tenant/active` | Get active tenant |
144
+ | `PUT` | `/api/tenant/active` | Switch active tenant `{"tenantId": "..."}` (broadcasts to all WebSocket clients) |
145
+
146
+ ### Export
147
+
148
+ | Method | Endpoint | Description |
149
+ |--------|----------|-------------|
150
+ | `POST` | `/api/export/image` | Request image export (needs browser) |
151
+ | `POST` | `/api/export/image/result` | Frontend posts export result back |
152
+
153
+ ### Viewport
154
+
155
+ | Method | Endpoint | Description |
156
+ |--------|----------|-------------|
157
+ | `POST` | `/api/viewport` | Set viewport/camera (needs browser) |
158
+ | `POST` | `/api/viewport/result` | Frontend posts viewport result back |
159
+
160
+ ### Snapshots
161
+
162
+ | Method | Endpoint | Description |
163
+ |--------|----------|-------------|
164
+ | `POST` | `/api/snapshots` | Save snapshot `{"name": "..."}` |
165
+ | `GET` | `/api/snapshots` | List all snapshots |
166
+ | `GET` | `/api/snapshots/:name` | Get snapshot by name |
167
+
168
+ ### System
169
+
170
+ | Method | Endpoint | Description |
171
+ |--------|----------|-------------|
172
+ | `GET` | `/health` | Health check |
173
+ | `GET` | `/api/sync/status` | Element count and WebSocket stats |
174
+
175
+ ## Skill Scripts
176
+
177
+ All scripts accept `--url <canvasUrl>` (defaults to `EXPRESS_SERVER_URL`).
178
+
179
+ ```bash
180
+ node scripts/healthcheck.cjs
181
+ node scripts/clear-canvas.cjs
182
+ node scripts/export-elements.cjs --out diagram.elements.json
183
+ node scripts/import-elements.cjs --in diagram.elements.json --mode batch|sync
184
+ node scripts/create-element.cjs --data '{...}'
185
+ node scripts/update-element.cjs --id <id> --data '{...}'
186
+ node scripts/delete-element.cjs --id <id>
187
+ ```
188
+
189
+ ## Environment Variables
190
+
191
+ | Variable | Default | Description |
192
+ |----------|---------|-------------|
193
+ | `CANVAS_PORT` | `3000` | Port the canvas server listens on |
194
+ | `EXPRESS_SERVER_URL` | `http://localhost:3000` | Full canvas URL (derived from CANVAS_PORT if not set) |
195
+ | `EXCALIDRAW_EXPORT_DIR` | `process.cwd()` | Allowed base directory for file exports (path traversal protection) |
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
5
+
6
+ function parseArgs(argv) {
7
+ const out = { url: DEFAULT_URL };
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const a = argv[i];
10
+ if (a === "--url") out.url = argv[++i];
11
+ }
12
+ return out;
13
+ }
14
+
15
+ async function main() {
16
+ if (typeof fetch !== "function") {
17
+ throw new Error("This script requires Node 18+ (global fetch).");
18
+ }
19
+
20
+ const { url } = parseArgs(process.argv.slice(2));
21
+ const baseUrl = url.replace(/\/$/, "");
22
+
23
+ const res = await fetch(`${baseUrl}/api/elements/clear`, {
24
+ method: "DELETE",
25
+ });
26
+
27
+ const json = await res.json().catch(() => null);
28
+ if (!res.ok || !json || json.success !== true) {
29
+ throw new Error(`Failed to clear canvas: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`);
30
+ }
31
+
32
+ console.log(`Cleared canvas (${json.count} elements removed)`);
33
+ }
34
+
35
+ main().catch((err) => {
36
+ console.error(err?.stack || String(err));
37
+ process.exit(1);
38
+ });
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require("node:fs");
5
+
6
+ const DEFAULT_URL = process.env.EXPRESS_SERVER_URL || "http://localhost:3000";
7
+
8
+ function usage() {
9
+ console.error(
10
+ [
11
+ "Usage:",
12
+ " node scripts/create-element.cjs (--data <json> | --file <path>) [--url <canvasUrl>]",
13
+ "",
14
+ "Examples:",
15
+ ' node scripts/create-element.cjs --data \'{"type":"rectangle","x":100,"y":100,"width":300,"height":200}\'',
16
+ " node scripts/create-element.cjs --file element.json",
17
+ ].join("\n"),
18
+ );
19
+ process.exit(2);
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const out = { url: DEFAULT_URL, data: null, file: null };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === "--url") out.url = argv[++i];
27
+ else if (a === "--data") out.data = argv[++i];
28
+ else if (a === "--file") out.file = argv[++i];
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function readJson({ data, file }) {
34
+ if (data) return JSON.parse(data);
35
+ if (file) return JSON.parse(fs.readFileSync(file, "utf8"));
36
+ usage();
37
+ }
38
+
39
+ async function main() {
40
+ if (typeof fetch !== "function") {
41
+ throw new Error("This script requires Node 18+ (global fetch).");
42
+ }
43
+
44
+ const args = parseArgs(process.argv.slice(2));
45
+ const payload = readJson(args);
46
+
47
+ const baseUrl = args.url.replace(/\/$/, "");
48
+ const res = await fetch(`${baseUrl}/api/elements`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify(payload),
52
+ });
53
+
54
+ const json = await res.json().catch(() => null);
55
+ if (!res.ok || !json || json.success !== true) {
56
+ throw new Error(
57
+ `Failed to create element: ${res.status} ${res.statusText} ${json?.error ? `- ${json.error}` : ""}`,
58
+ );
59
+ }
60
+
61
+ process.stdout.write(JSON.stringify(json.element, null, 2) + "\n");
62
+ }
63
+
64
+ main().catch((err) => {
65
+ console.error(err?.stack || String(err));
66
+ process.exit(1);
67
+ });
68
+