excalidrop 0.1.4 → 0.2.1

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 (222) hide show
  1. package/README.md +58 -131
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +488 -394
  4. package/dist/cli.js.map +1 -1
  5. package/dist/frontend/assets/{advancedFormat-tiBAHNGo.js → advancedFormat-CsxROTkH.js} +1 -1
  6. package/dist/frontend/assets/{ar-SA-G6X2FPQ2-DVxiWV5Z.js → ar-SA-G6X2FPQ2-Bu8X6mOi.js} +1 -1
  7. package/dist/frontend/assets/arc-B9VNLSzj.js +1 -0
  8. package/dist/frontend/assets/{az-AZ-76LH7QW2-Z_2BqDo-.js → az-AZ-76LH7QW2-DpvzY31_.js} +1 -1
  9. package/dist/frontend/assets/{bg-BG-XCXSNQG7-DrxGJZOG.js → bg-BG-XCXSNQG7-DCtZnLyo.js} +1 -1
  10. package/dist/frontend/assets/{blockDiagram-38ab4fdb-CDnkTFfP.js → blockDiagram-38ab4fdb-4cfDkUsw.js} +1 -1
  11. package/dist/frontend/assets/{blockDiagram-68f4deed-Bb0W6mxN.js → blockDiagram-68f4deed-CsgWL6uC.js} +1 -1
  12. package/dist/frontend/assets/{bn-BD-2XOGV67Q-BkGJ-w8k.js → bn-BD-2XOGV67Q-B-IdLNKT.js} +1 -1
  13. package/dist/frontend/assets/{c4Diagram-15b5d702-ByRV0tka.js → c4Diagram-15b5d702-7BAqo4qT.js} +1 -1
  14. package/dist/frontend/assets/{c4Diagram-3d4e48cf-QSVnzYYK.js → c4Diagram-3d4e48cf-CDW-O-vg.js} +1 -1
  15. package/dist/frontend/assets/{ca-ES-6MX7JW3Y-BBJvOPE1.js → ca-ES-6MX7JW3Y-DM8olClc.js} +1 -1
  16. package/dist/frontend/assets/channel-jdNM4v-F.js +1 -0
  17. package/dist/frontend/assets/{classDiagram-70f12bd4-DDFBNGUt.js → classDiagram-70f12bd4-D2U4i4yC.js} +1 -1
  18. package/dist/frontend/assets/{classDiagram-d40c83e7-cgC3s7Nz.js → classDiagram-d40c83e7-DtBHRJc9.js} +1 -1
  19. package/dist/frontend/assets/classDiagram-v2-d5a6b087-Drykcxns.js +2 -0
  20. package/dist/frontend/assets/classDiagram-v2-f2320105-D8ruMBA6.js +2 -0
  21. package/dist/frontend/assets/clone-DFO_zGRO.js +1 -0
  22. package/dist/frontend/assets/{createText-2e5e7dd3-CqY00v-4.js → createText-2e5e7dd3-1KPUhDpM.js} +1 -1
  23. package/dist/frontend/assets/{createText-d213de94-D59sBfqq.js → createText-d213de94-C6EhGbUN.js} +4 -4
  24. package/dist/frontend/assets/{cs-CZ-2BRQDIVT-CK09MAsg.js → cs-CZ-2BRQDIVT-DgjTgz6K.js} +1 -1
  25. package/dist/frontend/assets/{cytoscape-cose-bilkent-CKy_tuGZ.js → cytoscape-cose-bilkent-BnIHl72R.js} +1 -1
  26. package/dist/frontend/assets/{da-DK-5WZEPLOC-V4hdLKuT.js → da-DK-5WZEPLOC-uEs3xGKF.js} +1 -1
  27. package/dist/frontend/assets/{de-DE-XR44H4JA-Dw8qkP9g.js → de-DE-XR44H4JA-CBglLw42.js} +1 -1
  28. package/dist/frontend/assets/{edges-332bd1c7-D-u0KMZB.js → edges-332bd1c7-cWCBwcrg.js} +1 -1
  29. package/dist/frontend/assets/{edges-e0da2a9e-B3UFqLqY.js → edges-e0da2a9e-DKeDUJ_F.js} +1 -1
  30. package/dist/frontend/assets/{el-GR-BZB4AONW-BUdBQbYg.js → el-GR-BZB4AONW-B-_eub00.js} +1 -1
  31. package/dist/frontend/assets/{elk.bundled-BZA3Xcoo.js → elk.bundled-Ch9TeAu4.js} +1 -1
  32. package/dist/frontend/assets/{erDiagram-880f2ed8-CNxm7e1x.js → erDiagram-880f2ed8-CtDf_58r.js} +4 -4
  33. package/dist/frontend/assets/{erDiagram-9861fffd-C2u_ziAM.js → erDiagram-9861fffd-CAlUeljk.js} +1 -1
  34. package/dist/frontend/assets/{es-ES-U4NZUMDT-DS0FAZqX.js → es-ES-U4NZUMDT-BIN3bSVD.js} +1 -1
  35. package/dist/frontend/assets/{eu-ES-A7QVB2H4-D-D5uidc.js → eu-ES-A7QVB2H4-TyQet3-N.js} +1 -1
  36. package/dist/frontend/assets/{fa-IR-HGAKTJCU-mAW_2-jg.js → fa-IR-HGAKTJCU-DVmjjr57.js} +1 -1
  37. package/dist/frontend/assets/{fi-FI-Z5N7JZ37-B9mW6nxS.js → fi-FI-Z5N7JZ37-ZW9kXLa8.js} +1 -1
  38. package/dist/frontend/assets/{flowDb-7c981674-6JB2gd0I.js → flowDb-7c981674-B5ppx8EN.js} +1 -1
  39. package/dist/frontend/assets/{flowDb-956e92f1-BbpkWRi7.js → flowDb-956e92f1-Dqi00Wq-.js} +1 -1
  40. package/dist/frontend/assets/{flowDiagram-66a62f08-DrhrQuXc.js → flowDiagram-66a62f08-v3Liu0m_.js} +1 -1
  41. package/dist/frontend/assets/{flowDiagram-cbd28bf7-BmNY7bvJ.js → flowDiagram-cbd28bf7-DJ_YHRwa.js} +3 -3
  42. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-6vlgZ0Bg.js +1 -0
  43. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-FmnI78Vm.js +1 -0
  44. package/dist/frontend/assets/{flowchart-elk-definition-36e2d292-4G1YiIlk.js → flowchart-elk-definition-36e2d292-BleYul6b.js} +2 -2
  45. package/dist/frontend/assets/{flowchart-elk-definition-4a651766-Cm94SrrQ.js → flowchart-elk-definition-4a651766-D6lLh4Ih.js} +1 -1
  46. package/dist/frontend/assets/{fr-FR-RHASNOE6-BXnmUxTA.js → fr-FR-RHASNOE6-CxLR66tH.js} +1 -1
  47. package/dist/frontend/assets/{ganttDiagram-04f9e578-X0BweAnG.js → ganttDiagram-04f9e578-DJSOyr8B.js} +1 -1
  48. package/dist/frontend/assets/{ganttDiagram-c361ad54-D3vxZ5MQ.js → ganttDiagram-c361ad54-CJmbCLvp.js} +1 -1
  49. package/dist/frontend/assets/{gitGraphDiagram-21fc4d3e-wznrpRWn.js → gitGraphDiagram-21fc4d3e-DiRq_FJc.js} +6 -6
  50. package/dist/frontend/assets/{gitGraphDiagram-72cf32ee-B9zCS3Pw.js → gitGraphDiagram-72cf32ee-MfvDzoAb.js} +1 -1
  51. package/dist/frontend/assets/{gl-ES-HMX3MZ6V-CrUx-ab6.js → gl-ES-HMX3MZ6V-CxWj2itK.js} +1 -1
  52. package/dist/frontend/assets/{graph-BQZ6SIVr.js → graph-CT_r46y9.js} +1 -1
  53. package/dist/frontend/assets/{graph-Cda8xSTx.js → graph-DJt_Vezo.js} +1 -1
  54. package/dist/frontend/assets/{he-IL-6SHJWFNN-D33eaw6-.js → he-IL-6SHJWFNN-5v2WFGO3.js} +1 -1
  55. package/dist/frontend/assets/{hi-IN-IWLTKZ5I-DEnStF-g.js → hi-IN-IWLTKZ5I-BJGbvgRu.js} +1 -1
  56. package/dist/frontend/assets/{hu-HU-A5ZG7DT2-BjH2w8Sn.js → hu-HU-A5ZG7DT2-REQT2EcE.js} +1 -1
  57. package/dist/frontend/assets/{id-ID-SAP4L64H-Bc6R-JEz.js → id-ID-SAP4L64H-aZWQYWdg.js} +1 -1
  58. package/dist/frontend/assets/{index-3862675e-CpYfS3Db.js → index-3862675e-BgrvgbsM.js} +1 -1
  59. package/dist/frontend/assets/{index-6079d271-Y2bgyRpa.js → index-6079d271-Cp8jFHTC.js} +1 -1
  60. package/dist/frontend/assets/{index-D7eYnAQp.js → index-DBHHeSXy.js} +4 -4
  61. package/dist/frontend/assets/index-GTq6y6g6.js +349 -0
  62. package/dist/frontend/assets/{infoDiagram-4a4f5b27-Bm3Oq-Rq.js → infoDiagram-4a4f5b27-C1ZJJtWv.js} +3 -3
  63. package/dist/frontend/assets/{infoDiagram-f8f76790-oNVitBKM.js → infoDiagram-f8f76790-GeS0JKxo.js} +1 -1
  64. package/dist/frontend/assets/{it-IT-JPQ66NNP-BcxQABF2.js → it-IT-JPQ66NNP-EyeA2Ty1.js} +1 -1
  65. package/dist/frontend/assets/{ja-JP-DBVTYXUO-D1zIM-nt.js → ja-JP-DBVTYXUO-BLcE45D9.js} +1 -1
  66. package/dist/frontend/assets/{journeyDiagram-29694f62-CamPk_3P.js → journeyDiagram-29694f62-4Z_lpK4g.js} +5 -5
  67. package/dist/frontend/assets/{journeyDiagram-49397b02-D6w3tLtf.js → journeyDiagram-49397b02-ChwU3Zo6.js} +1 -1
  68. package/dist/frontend/assets/{kaa-6HZHGXH3-CoILysVg.js → kaa-6HZHGXH3-CkpTcX9X.js} +1 -1
  69. package/dist/frontend/assets/{kab-KAB-ZGHBKWFO-B9ZbybBt.js → kab-KAB-ZGHBKWFO-CChq0y6o.js} +1 -1
  70. package/dist/frontend/assets/{kk-KZ-P5N5QNE5-Dxi4GIQ7.js → kk-KZ-P5N5QNE5-CwtoTY4z.js} +1 -1
  71. package/dist/frontend/assets/{km-KH-HSX4SM5Z-DaJ7EtO2.js → km-KH-HSX4SM5Z-D26sdohj.js} +1 -1
  72. package/dist/frontend/assets/{ko-KR-MTYHY66A-DG57Ah12.js → ko-KR-MTYHY66A-DvnDcJQA.js} +1 -1
  73. package/dist/frontend/assets/{ku-TR-6OUDTVRD-DnDf9E8a.js → ku-TR-6OUDTVRD-qdHbLgKg.js} +1 -1
  74. package/dist/frontend/assets/{layout-Drr-66M9.js → layout-C-wUeNvb.js} +1 -1
  75. package/dist/frontend/assets/{layout-BsWfhUAF.js → layout-YRLNN4au.js} +1 -1
  76. package/dist/frontend/assets/{line-DwQObOhP.js → line-BEgMClKG.js} +1 -1
  77. package/dist/frontend/assets/{linear-C36MjJps.js → linear-DDHpSdYw.js} +1 -1
  78. package/dist/frontend/assets/{lt-LT-XHIRWOB4-D_zqS0yg.js → lt-LT-XHIRWOB4-DT4LYe_d.js} +1 -1
  79. package/dist/frontend/assets/{lv-LV-5QDEKY6T-owIsIh1b.js → lv-LV-5QDEKY6T-Dbc3ivwT.js} +1 -1
  80. package/dist/frontend/assets/{mindmap-definition-ac74a2e8-Cf9JbrBJ.js → mindmap-definition-ac74a2e8-CY2RhWUN.js} +6 -6
  81. package/dist/frontend/assets/{mindmap-definition-fc14e90a-BpsNCNCp.js → mindmap-definition-fc14e90a-VT4JDi6N.js} +1 -1
  82. package/dist/frontend/assets/{mr-IN-CRQNXWMA-CS9XUj74.js → mr-IN-CRQNXWMA-D75p1fVc.js} +1 -1
  83. package/dist/frontend/assets/{my-MM-5M5IBNSE-BKB060k9.js → my-MM-5M5IBNSE-DnEoBXyE.js} +1 -1
  84. package/dist/frontend/assets/{nb-NO-T6EIAALU-CkmLUrvm.js → nb-NO-T6EIAALU-BL4ThJxl.js} +1 -1
  85. package/dist/frontend/assets/{nl-NL-IS3SIHDZ-DdiLyJoz.js → nl-NL-IS3SIHDZ-u18nUblY.js} +1 -1
  86. package/dist/frontend/assets/{nn-NO-6E72VCQL-Yevp6f_Y.js → nn-NO-6E72VCQL-B9LJ0oXc.js} +1 -1
  87. package/dist/frontend/assets/{oc-FR-POXYY2M6-D_M6GHup.js → oc-FR-POXYY2M6-BKG5kIcO.js} +1 -1
  88. package/dist/frontend/assets/{pa-IN-N4M65BXN-1vuACN9n.js → pa-IN-N4M65BXN-CmpnjbkL.js} +1 -1
  89. package/dist/frontend/assets/{pica-DDDx0iqx.js → pica-BcpcM0v1.js} +1 -1
  90. package/dist/frontend/assets/{pie-CFIpuQMO.js → pie-Swzl5NrI.js} +1 -1
  91. package/dist/frontend/assets/{pieDiagram-421022e6-DO7e_Qgw.js → pieDiagram-421022e6-ndz96BLe.js} +5 -5
  92. package/dist/frontend/assets/{pieDiagram-8a3498a8-BEoqChXo.js → pieDiagram-8a3498a8-D5rcV68O.js} +1 -1
  93. package/dist/frontend/assets/{pl-PL-T2D74RX3-Cc6Q5nSh.js → pl-PL-T2D74RX3-CrR66Rgj.js} +1 -1
  94. package/dist/frontend/assets/{pt-BR-5N22H2LF-3eEiAax3.js → pt-BR-5N22H2LF-Crv7jZiH.js} +1 -1
  95. package/dist/frontend/assets/{pt-PT-UZXXM6DQ-CgnF0Le7.js → pt-PT-UZXXM6DQ-Bkaa5ESR.js} +1 -1
  96. package/dist/frontend/assets/{quadrantDiagram-0957ecba-C1Eo-ZUY.js → quadrantDiagram-0957ecba-DOdyxeTP.js} +3 -3
  97. package/dist/frontend/assets/{quadrantDiagram-120e2f19-Bdfsl_O7.js → quadrantDiagram-120e2f19-beQD7R1C.js} +1 -1
  98. package/dist/frontend/assets/relay-BIOL2uw2.js +1 -0
  99. package/dist/frontend/assets/{requirementDiagram-23d650b8-DiOK6QW3.js → requirementDiagram-23d650b8-B2i-OB97.js} +1 -1
  100. package/dist/frontend/assets/{requirementDiagram-deff3bca-BDJ-7T4h.js → requirementDiagram-deff3bca-beS-SvfY.js} +1 -1
  101. package/dist/frontend/assets/{ro-RO-JPDTUUEW-CbJ3IB7a.js → ro-RO-JPDTUUEW-DsrnQm7K.js} +1 -1
  102. package/dist/frontend/assets/{ru-RU-B4JR7IUQ-PQGmeGG4.js → ru-RU-B4JR7IUQ-DrnGGdUs.js} +1 -1
  103. package/dist/frontend/assets/{sankeyDiagram-04a897e0-DV25bgN2.js → sankeyDiagram-04a897e0-CVad45bN.js} +1 -1
  104. package/dist/frontend/assets/{sankeyDiagram-23345273-szSb_Sei.js → sankeyDiagram-23345273-ghWBXXF2.js} +1 -1
  105. package/dist/frontend/assets/{selectAll-DsrPvhqV.js → selectAll-XplaO988.js} +1 -1
  106. package/dist/frontend/assets/{sequenceDiagram-17ac3bff-Dg88bSlc.js → sequenceDiagram-17ac3bff-DBI-EdWx.js} +1 -1
  107. package/dist/frontend/assets/{sequenceDiagram-704730f1-DRIjLCxx.js → sequenceDiagram-704730f1-BnJ__mBt.js} +1 -1
  108. package/dist/frontend/assets/{si-LK-N5RQ5JYF-BtIIudm3.js → si-LK-N5RQ5JYF-BxAwuRM5.js} +1 -1
  109. package/dist/frontend/assets/{sk-SK-C5VTKIMK-CIwXticU.js → sk-SK-C5VTKIMK-CnDyiLRf.js} +1 -1
  110. package/dist/frontend/assets/{sl-SI-NN7IZMDC-CXEdg57y.js → sl-SI-NN7IZMDC-BXHXIdFA.js} +1 -1
  111. package/dist/frontend/assets/{stateDiagram-587899a1-BwRwgRo6.js → stateDiagram-587899a1-D3Ahxsgc.js} +1 -1
  112. package/dist/frontend/assets/{stateDiagram-9c5f0230-C8n96rER.js → stateDiagram-9c5f0230-8XbLWvC0.js} +1 -1
  113. package/dist/frontend/assets/{stateDiagram-v2-51a3dcff-B7e5E4J7.js → stateDiagram-v2-51a3dcff-Cqf-MUGD.js} +1 -1
  114. package/dist/frontend/assets/{stateDiagram-v2-d93cdb3a-B18iZQLw.js → stateDiagram-v2-d93cdb3a-DjGrud9D.js} +1 -1
  115. package/dist/frontend/assets/{styles-2ab5d517-CfWubwio.js → styles-2ab5d517-B4qdHOgu.js} +3 -3
  116. package/dist/frontend/assets/{styles-5f03d8d2-C-_n19AB.js → styles-5f03d8d2-DOD2v5V8.js} +3 -3
  117. package/dist/frontend/assets/{styles-6aaf32cf-Dh-DHkVP.js → styles-6aaf32cf-bP3XXcmf.js} +1 -1
  118. package/dist/frontend/assets/{styles-9a916d00-Boy0hUEG.js → styles-9a916d00-QLeuL_Dy.js} +1 -1
  119. package/dist/frontend/assets/{styles-c10674c1-BfxiK3Ns.js → styles-c10674c1-Lpvh1u4f.js} +1 -1
  120. package/dist/frontend/assets/{styles-edf9a4b0-T7mKViRF.js → styles-edf9a4b0-D4W9aPm7.js} +1 -1
  121. package/dist/frontend/assets/subset-shared.chunk.js +9 -9
  122. package/dist/frontend/assets/subset-worker.chunk.js +1 -1
  123. package/dist/frontend/assets/{sv-SE-XGPEYMSR-dVrUAqld.js → sv-SE-XGPEYMSR-YrCQQBuo.js} +1 -1
  124. package/dist/frontend/assets/{svgDrawCommon-08f97a94-fsWk6SQc.js → svgDrawCommon-08f97a94-CwNv3PjS.js} +1 -1
  125. package/dist/frontend/assets/{svgDrawCommon-3ba9043b-BJ8X3qS-.js → svgDrawCommon-3ba9043b-DZbT0Bqu.js} +1 -1
  126. package/dist/frontend/assets/{ta-IN-2NMHFXQM-CtVVZfIT.js → ta-IN-2NMHFXQM-qR82aBSW.js} +1 -1
  127. package/dist/frontend/assets/{th-TH-HPSO5L25-Do90CyDN.js → th-TH-HPSO5L25-C65V7hE5.js} +1 -1
  128. package/dist/frontend/assets/{timeline-definition-7e6b55e7-D_iHA485.js → timeline-definition-7e6b55e7-GkPy3QeG.js} +3 -3
  129. package/dist/frontend/assets/{timeline-definition-85554ec2-nhT8QMsO.js → timeline-definition-85554ec2-CCdkBaC0.js} +3 -3
  130. package/dist/frontend/assets/{tr-TR-DEFEU3FU-MfR-IQke.js → tr-TR-DEFEU3FU-CFzbwAKV.js} +1 -1
  131. package/dist/frontend/assets/{uk-UA-QMV73CPH-CPet8B1O.js → uk-UA-QMV73CPH-ePt5WVz3.js} +1 -1
  132. package/dist/frontend/assets/{union-BgYwhB7T.js → union-CVZobYBh.js} +1 -1
  133. package/dist/frontend/assets/{vi-VN-M7AON7JQ-3HD06T_8.js → vi-VN-M7AON7JQ-CaPKykgJ.js} +1 -1
  134. package/dist/frontend/assets/{xychartDiagram-b6496bcd-D5vSaFT2.js → xychartDiagram-b6496bcd-CV5WRc81.js} +4 -4
  135. package/dist/frontend/assets/{xychartDiagram-e933f94c-DLIHXsUi.js → xychartDiagram-e933f94c-BH-ySdQf.js} +1 -1
  136. package/dist/frontend/assets/{zh-CN-LNUGB5OW-t2RyWPw2.js → zh-CN-LNUGB5OW-CK8AEj8o.js} +1 -1
  137. package/dist/frontend/assets/{zh-HK-E62DVLB3-Bj22hc7e.js → zh-HK-E62DVLB3-CUINiFgH.js} +1 -1
  138. package/dist/frontend/assets/{zh-TW-RAJ6MFWO-CBRhqhU1.js → zh-TW-RAJ6MFWO-BEBY7iJR.js} +1 -1
  139. package/dist/frontend/assets/zipObject-W8SLG3F1.js +1 -0
  140. package/dist/frontend/index.html +167 -235
  141. package/dist/{index.d.ts → mcp.d.ts} +1 -1
  142. package/dist/mcp.d.ts.map +1 -0
  143. package/dist/mcp.js +1183 -0
  144. package/dist/mcp.js.map +1 -0
  145. package/package.json +19 -34
  146. package/skills/excalidraw-skill/SKILL.md +42 -55
  147. package/skills/excalidraw-skill/references/cheatsheet.md +23 -56
  148. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +9 -32
  149. package/skills/excalidraw-skill/scripts/create-element.cjs +18 -61
  150. package/skills/excalidraw-skill/scripts/delete-element.cjs +12 -42
  151. package/skills/excalidraw-skill/scripts/export-elements.cjs +17 -44
  152. package/skills/excalidraw-skill/scripts/gh-helper.cjs +58 -0
  153. package/skills/excalidraw-skill/scripts/healthcheck.cjs +8 -29
  154. package/skills/excalidraw-skill/scripts/import-elements.cjs +20 -70
  155. package/skills/excalidraw-skill/scripts/update-element.cjs +19 -63
  156. package/tsconfig.json +3 -2
  157. package/vite.config.js +3 -10
  158. package/dist/frontend/assets/arc-Cc3ncJ1n.js +0 -1
  159. package/dist/frontend/assets/channel-BQSIb3i3.js +0 -1
  160. package/dist/frontend/assets/classDiagram-v2-d5a6b087-BgB8AG2e.js +0 -2
  161. package/dist/frontend/assets/classDiagram-v2-f2320105-Dtywkpeg.js +0 -2
  162. package/dist/frontend/assets/clone-BoZziNL9.js +0 -1
  163. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-Z3H2d3Wj.js +0 -1
  164. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CIQOMghC.js +0 -1
  165. package/dist/frontend/assets/index-DIz4WrSB.js +0 -349
  166. package/dist/frontend/assets/zipObject-DMfHW2o8.js +0 -1
  167. package/dist/index.d.ts.map +0 -1
  168. package/dist/index.js +0 -2279
  169. package/dist/index.js.map +0 -1
  170. package/dist/remote.d.ts +0 -5
  171. package/dist/remote.d.ts.map +0 -1
  172. package/dist/remote.js +0 -129
  173. package/dist/remote.js.map +0 -1
  174. package/dist/server.d.ts +0 -4
  175. package/dist/server.d.ts.map +0 -1
  176. package/dist/server.js +0 -1208
  177. package/dist/server.js.map +0 -1
  178. package/dist/target.d.ts +0 -38
  179. package/dist/target.d.ts.map +0 -1
  180. package/dist/target.js +0 -225
  181. package/dist/target.js.map +0 -1
  182. package/dist/types.d.ts +0 -228
  183. package/dist/types.d.ts.map +0 -1
  184. package/dist/types.js +0 -45
  185. package/dist/types.js.map +0 -1
  186. package/dist/utils/auth.d.ts +0 -14
  187. package/dist/utils/auth.d.ts.map +0 -1
  188. package/dist/utils/auth.js +0 -67
  189. package/dist/utils/auth.js.map +0 -1
  190. package/dist/utils/github.d.ts +0 -24
  191. package/dist/utils/github.d.ts.map +0 -1
  192. package/dist/utils/github.js +0 -132
  193. package/dist/utils/github.js.map +0 -1
  194. package/dist/utils/logger.d.ts +0 -4
  195. package/dist/utils/logger.d.ts.map +0 -1
  196. package/dist/utils/logger.js +0 -25
  197. package/dist/utils/logger.js.map +0 -1
  198. package/dist/utils/r2store.d.ts +0 -12
  199. package/dist/utils/r2store.d.ts.map +0 -1
  200. package/dist/utils/r2store.js +0 -75
  201. package/dist/utils/r2store.js.map +0 -1
  202. package/frontend/drop.png +0 -0
  203. package/frontend/index.html +0 -339
  204. package/frontend/src/App.tsx +0 -1091
  205. package/frontend/src/main.tsx +0 -15
  206. package/frontend/src/utils/ghSync.ts +0 -234
  207. package/frontend/src/utils/mermaidConverter.ts +0 -43
  208. package/frontend/src/utils/webmcp.ts +0 -265
  209. package/scripts/check-local-bind.mjs +0 -164
  210. package/scripts/postinstall.mjs +0 -23
  211. package/scripts/publish-pages.sh +0 -39
  212. package/scripts/seed-r2.mjs +0 -9
  213. package/src/cli.ts +0 -528
  214. package/src/index.ts +0 -2538
  215. package/src/remote.ts +0 -92
  216. package/src/server.ts +0 -1349
  217. package/src/target.ts +0 -198
  218. package/src/types.ts +0 -336
  219. package/src/utils/auth.ts +0 -61
  220. package/src/utils/github.ts +0 -121
  221. package/src/utils/logger.ts +0 -35
  222. package/src/utils/r2store.ts +0 -70
package/dist/server.js DELETED
@@ -1,1208 +0,0 @@
1
- import express from 'express';
2
- import cors from 'cors';
3
- import { WebSocketServer } from 'ws';
4
- import { createServer } from 'http';
5
- import net from 'net';
6
- import path from 'path';
7
- import { fileURLToPath } from 'url';
8
- import dotenv from 'dotenv';
9
- import logger from './utils/logger.js';
10
- import { getR2Config, r2Get, r2Put } from './utils/r2store.js';
11
- import { authOptional, requireWriteAuth, requireReadAuth, signSession, allowedLogins } from './utils/auth.js';
12
- import { elements, files, snapshots, generateId, EXCALIDRAW_ELEMENT_TYPES, normalizeFontFamily } from './types.js';
13
- import { z } from 'zod';
14
- import WebSocket from 'ws';
15
- dotenv.config();
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
- const app = express();
19
- const server = createServer(app);
20
- const wss = new WebSocketServer({ server });
21
- app.use(cors());
22
- app.use(express.json({ limit: '10mb' }));
23
- app.use(authOptional);
24
- const SCENE_KEY = `${process.env.GITHUB_REPOSITORY?.replace('/', '__') || process.env.SCENE_ID || 'default'}.json`;
25
- let r2Cfg = getR2Config();
26
- let persistTimer = null;
27
- async function loadFromR2() {
28
- r2Cfg = getR2Config();
29
- if (!r2Cfg)
30
- return;
31
- try {
32
- const doc = await r2Get(r2Cfg, SCENE_KEY);
33
- if (doc?.elements) {
34
- elements.clear();
35
- for (const el of doc.elements)
36
- elements.set(el.id, el);
37
- if (doc.files)
38
- for (const f of doc.files)
39
- files.set(f.id, f);
40
- logger.info(`Loaded ${doc.elements.length} elements from R2`);
41
- }
42
- }
43
- catch (e) {
44
- logger.warn('R2 load failed: ' + e.message);
45
- }
46
- }
47
- function schedulePersist() {
48
- r2Cfg = getR2Config();
49
- if (!r2Cfg)
50
- return;
51
- if (persistTimer)
52
- clearTimeout(persistTimer);
53
- persistTimer = setTimeout(async () => {
54
- try {
55
- await r2Put(r2Cfg, SCENE_KEY, { elements: Array.from(elements.values()), files: Array.from(files.values()), updatedAt: new Date().toISOString() });
56
- logger.info('Persisted scene to R2');
57
- }
58
- catch (e) {
59
- logger.warn('R2 persist failed: ' + e.message);
60
- }
61
- }, 800);
62
- }
63
- void loadFromR2();
64
- app.get('/api/auth/login', (req, res) => {
65
- const cid = process.env.GITHUB_CLIENT_ID;
66
- if (!cid)
67
- return res.status(500).json({ success: false, error: 'GITHUB_CLIENT_ID not configured' });
68
- const redirect = `${req.protocol}://${req.get('host')}/api/auth/callback`;
69
- const url = `https://github.com/login/oauth/authorize?client_id=${cid}&redirect_uri=${encodeURIComponent(redirect)}&scope=read:user`;
70
- res.redirect(url);
71
- });
72
- app.get('/api/auth/callback', async (req, res) => {
73
- try {
74
- const code = String(req.query.code || '');
75
- const r = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: process.env.GITHUB_CLIENT_ID, client_secret: process.env.GITHUB_CLIENT_SECRET, code }) });
76
- const tok = await r.json();
77
- if (!tok.access_token)
78
- throw new Error(tok.error_description || 'OAuth exchange failed');
79
- const me = await (await fetch('https://api.github.com/user', { headers: { Authorization: `Bearer ${tok.access_token}`, 'User-Agent': 'excalidrop' } })).json();
80
- const allow = allowedLogins();
81
- if (allow.length && !allow.includes(String(me.login).toLowerCase()))
82
- return res.status(403).send('User not allowed');
83
- const token = signSession({ login: me.login, id: me.id, iat: Math.floor(Date.now() / 1000) });
84
- res.setHeader('Set-Cookie', `excalidrop_token=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${30 * 24 * 3600}`);
85
- res.send(`<script>localStorage.setItem('excalidrop_token','${token}');location.href='/'</script>Logged in as ${me.login}. <a href="/">Open canvas</a>`);
86
- }
87
- catch (e) {
88
- res.status(500).send('Login failed: ' + e.message);
89
- }
90
- });
91
- app.get('/api/auth/me', (req, res) => res.json({ user: req.user || null, publicRead: String(process.env.PUBLIC_READ || 'false') }));
92
- app.post('/api/auth/logout', (_req, res) => { res.setHeader('Set-Cookie', 'excalidrop_token=; Path=/; Max-Age=0'); res.json({ success: true }); });
93
- const OPEN_WRITE_PATHS = new Set(['/api/export/image/result', '/api/viewport/result']);
94
- app.use('/api/', (req, res, next) => {
95
- if (req.path.startsWith('/auth'))
96
- return next();
97
- if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
98
- if (OPEN_WRITE_PATHS.has(req.path))
99
- return next();
100
- return requireWriteAuth(req, res, next);
101
- }
102
- if (req.path.startsWith('/elements') || req.path.startsWith('/files') || req.path.startsWith('/snapshots'))
103
- return requireReadAuth(req, res, next);
104
- next();
105
- });
106
- const persist = () => schedulePersist();
107
- const staticDir = path.join(__dirname, '../dist');
108
- app.use(express.static(staticDir));
109
- app.use(express.static(path.join(__dirname, '../dist/frontend')));
110
- app.use('/assets/fonts', express.static(path.join(__dirname, '../node_modules/@excalidraw/excalidraw/dist/prod/fonts')));
111
- const clients = new Set();
112
- // Broadcast to all connected clients
113
- function broadcast(message) {
114
- const data = JSON.stringify(message);
115
- clients.forEach(client => {
116
- try {
117
- if (client.readyState === WebSocket.OPEN) {
118
- client.send(data);
119
- }
120
- }
121
- catch (err) {
122
- logger.warn('Failed to send to client, removing');
123
- clients.delete(client);
124
- }
125
- });
126
- }
127
- function normalizeLineBreakMarkup(text) {
128
- return text
129
- .replace(/<\s*b\s*r\s*\/?\s*>/gi, '\n')
130
- .replace(/\n{3,}/g, '\n\n');
131
- }
132
- // WebSocket connection handling
133
- wss.on('connection', (ws) => {
134
- clients.add(ws);
135
- logger.info('New WebSocket connection established');
136
- // Send current elements to new client
137
- const filesObj = {};
138
- files.forEach((f, id) => { filesObj[id] = f; });
139
- const initialMessage = {
140
- type: 'initial_elements',
141
- elements: Array.from(elements.values()),
142
- ...(files.size > 0 ? { files: filesObj } : {})
143
- };
144
- ws.send(JSON.stringify(initialMessage));
145
- // Send sync status to new client
146
- const syncMessage = {
147
- type: 'sync_status',
148
- elementCount: elements.size,
149
- timestamp: new Date().toISOString()
150
- };
151
- ws.send(JSON.stringify(syncMessage));
152
- ws.on('close', () => {
153
- clients.delete(ws);
154
- logger.info('WebSocket connection closed');
155
- });
156
- ws.on('error', (error) => {
157
- logger.error('WebSocket error:', error);
158
- clients.delete(ws);
159
- });
160
- });
161
- // Schema validation
162
- const CreateElementSchema = z.object({
163
- id: z.string().optional(), // Allow passing ID for MCP sync
164
- type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)),
165
- x: z.number(),
166
- y: z.number(),
167
- width: z.number().optional(),
168
- height: z.number().optional(),
169
- backgroundColor: z.string().optional(),
170
- strokeColor: z.string().optional(),
171
- strokeWidth: z.number().optional(),
172
- strokeStyle: z.string().optional(),
173
- roughness: z.number().optional(),
174
- opacity: z.number().optional(),
175
- text: z.string().optional(),
176
- label: z.object({
177
- text: z.string()
178
- }).optional(),
179
- fontSize: z.number().optional(),
180
- fontFamily: z.union([z.string(), z.number()]).optional(),
181
- groupIds: z.array(z.string()).optional(),
182
- locked: z.boolean().optional(),
183
- roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(),
184
- fillStyle: z.string().optional(),
185
- // Arrow-specific properties
186
- points: z.any().optional(),
187
- start: z.object({ id: z.string() }).optional(),
188
- end: z.object({ id: z.string() }).optional(),
189
- startArrowhead: z.string().nullable().optional(),
190
- endArrowhead: z.string().nullable().optional(),
191
- elbowed: z.boolean().optional(),
192
- // Arrow binding properties (preserved for Excalidraw frontend)
193
- startBinding: z.object({
194
- elementId: z.string(),
195
- focus: z.number().optional(),
196
- gap: z.number().optional(),
197
- fixedPoint: z.tuple([z.number(), z.number()]).nullable().optional(),
198
- mode: z.string().optional(),
199
- }).nullable().optional(),
200
- endBinding: z.object({
201
- elementId: z.string(),
202
- focus: z.number().optional(),
203
- gap: z.number().optional(),
204
- fixedPoint: z.tuple([z.number(), z.number()]).nullable().optional(),
205
- mode: z.string().optional(),
206
- }).nullable().optional(),
207
- boundElements: z.array(z.object({
208
- id: z.string(),
209
- type: z.enum(['arrow', 'text']),
210
- })).nullable().optional(),
211
- // Image-specific properties
212
- fileId: z.string().optional(),
213
- status: z.string().optional(),
214
- scale: z.tuple([z.number(), z.number()]).optional(),
215
- });
216
- const UpdateElementSchema = z.object({
217
- id: z.string(),
218
- type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)).optional(),
219
- x: z.number().optional(),
220
- y: z.number().optional(),
221
- width: z.number().optional(),
222
- height: z.number().optional(),
223
- backgroundColor: z.string().optional(),
224
- strokeColor: z.string().optional(),
225
- strokeWidth: z.number().optional(),
226
- strokeStyle: z.string().optional(),
227
- roughness: z.number().optional(),
228
- opacity: z.number().optional(),
229
- text: z.string().optional(),
230
- originalText: z.string().optional(),
231
- label: z.object({
232
- text: z.string()
233
- }).optional(),
234
- fontSize: z.number().optional(),
235
- fontFamily: z.union([z.string(), z.number()]).optional(),
236
- groupIds: z.array(z.string()).optional(),
237
- locked: z.boolean().optional(),
238
- roundness: z.object({ type: z.number(), value: z.number().optional() }).nullable().optional(),
239
- fillStyle: z.string().optional(),
240
- points: z.array(z.union([
241
- z.tuple([z.number(), z.number()]),
242
- z.object({ x: z.number(), y: z.number() })
243
- ])).optional(),
244
- start: z.object({ id: z.string() }).optional(),
245
- end: z.object({ id: z.string() }).optional(),
246
- startArrowhead: z.string().nullable().optional(),
247
- endArrowhead: z.string().nullable().optional(),
248
- elbowed: z.boolean().optional(),
249
- // Arrow binding properties (preserved for Excalidraw frontend)
250
- startBinding: z.object({
251
- elementId: z.string(),
252
- focus: z.number().optional(),
253
- gap: z.number().optional(),
254
- fixedPoint: z.tuple([z.number(), z.number()]).nullable().optional(),
255
- mode: z.string().optional(),
256
- }).nullable().optional(),
257
- endBinding: z.object({
258
- elementId: z.string(),
259
- focus: z.number().optional(),
260
- gap: z.number().optional(),
261
- fixedPoint: z.tuple([z.number(), z.number()]).nullable().optional(),
262
- mode: z.string().optional(),
263
- }).nullable().optional(),
264
- boundElements: z.array(z.object({
265
- id: z.string(),
266
- type: z.enum(['arrow', 'text']),
267
- })).nullable().optional(),
268
- // Image-specific properties
269
- fileId: z.string().optional(),
270
- status: z.string().optional(),
271
- scale: z.tuple([z.number(), z.number()]).optional(),
272
- });
273
- // API Routes
274
- // Get all elements
275
- app.get('/api/elements', (req, res) => {
276
- try {
277
- const elementsArray = Array.from(elements.values());
278
- res.json({
279
- success: true,
280
- elements: elementsArray,
281
- count: elementsArray.length
282
- });
283
- }
284
- catch (error) {
285
- logger.error('Error fetching elements:', error);
286
- res.status(500).json({
287
- success: false,
288
- error: error.message
289
- });
290
- }
291
- });
292
- // Create new element
293
- app.post('/api/elements', (req, res) => {
294
- try {
295
- const params = CreateElementSchema.parse(req.body);
296
- logger.info('Creating element via API', { type: params.type });
297
- // Prioritize passed ID (for MCP sync), otherwise generate new ID
298
- const id = params.id || generateId();
299
- const element = {
300
- id,
301
- ...params,
302
- fontFamily: normalizeFontFamily(params.fontFamily),
303
- createdAt: new Date().toISOString(),
304
- updatedAt: new Date().toISOString(),
305
- version: 1
306
- };
307
- // Resolve arrow bindings against existing elements
308
- if (element.type === 'arrow' || element.type === 'line') {
309
- resolveArrowBindings([element]);
310
- }
311
- elements.set(id, element);
312
- // Broadcast to all connected clients
313
- const message = {
314
- type: 'element_created',
315
- element: element
316
- };
317
- broadcast(message);
318
- persist();
319
- res.json({
320
- success: true,
321
- element: element
322
- });
323
- }
324
- catch (error) {
325
- logger.error('Error creating element:', error);
326
- res.status(400).json({
327
- success: false,
328
- error: error.message
329
- });
330
- }
331
- });
332
- // Update element
333
- app.put('/api/elements/:id', (req, res) => {
334
- try {
335
- const { id } = req.params;
336
- const updates = UpdateElementSchema.parse({ id, ...req.body });
337
- if (!id) {
338
- return res.status(400).json({
339
- success: false,
340
- error: 'Element ID is required'
341
- });
342
- }
343
- const existingElement = elements.get(id);
344
- if (!existingElement) {
345
- return res.status(404).json({
346
- success: false,
347
- error: `Element with ID ${id} not found`
348
- });
349
- }
350
- const updatedElement = {
351
- ...existingElement,
352
- ...updates,
353
- fontFamily: updates.fontFamily !== undefined ? normalizeFontFamily(updates.fontFamily) : existingElement.fontFamily,
354
- updatedAt: new Date().toISOString(),
355
- version: (existingElement.version || 0) + 1
356
- };
357
- // Keep Excalidraw text source in sync when clients update text via REST.
358
- // If originalText lags behind text, rendered wrapping/position can drift.
359
- const hasTextUpdate = Object.prototype.hasOwnProperty.call(req.body, 'text');
360
- const hasOriginalTextUpdate = Object.prototype.hasOwnProperty.call(req.body, 'originalText');
361
- if (updatedElement.type === EXCALIDRAW_ELEMENT_TYPES.TEXT && hasTextUpdate && !hasOriginalTextUpdate) {
362
- const incomingText = updates.text ?? '';
363
- const existingText = typeof existingElement.text === 'string' ? existingElement.text : '';
364
- const existingOriginalText = typeof existingElement.originalText === 'string'
365
- ? existingElement.originalText
366
- : '';
367
- const existingOriginalHasBr = /<\s*b\s*r\s*\/?\s*>/i.test(existingOriginalText);
368
- const normalizedExistingText = normalizeLineBreakMarkup(existingText);
369
- const normalizedExistingOriginalText = normalizeLineBreakMarkup(existingOriginalText);
370
- // Handle common cleanup flow: caller normalizes the rendered text value.
371
- // In this case, prefer normalized originalText so words aren't split by stale wraps.
372
- if (existingOriginalHasBr && incomingText === normalizedExistingText && normalizedExistingOriginalText) {
373
- updatedElement.text = normalizedExistingOriginalText;
374
- updatedElement.originalText = normalizedExistingOriginalText;
375
- }
376
- else {
377
- updatedElement.originalText = incomingText;
378
- }
379
- }
380
- elements.set(id, updatedElement);
381
- // Broadcast to all connected clients
382
- const message = {
383
- type: 'element_updated',
384
- element: updatedElement
385
- };
386
- broadcast(message);
387
- persist();
388
- res.json({
389
- success: true,
390
- element: updatedElement
391
- });
392
- }
393
- catch (error) {
394
- logger.error('Error updating element:', error);
395
- res.status(400).json({
396
- success: false,
397
- error: error.message
398
- });
399
- }
400
- });
401
- // Clear all elements (must be before /:id route)
402
- app.delete('/api/elements/clear', (req, res) => {
403
- try {
404
- const count = elements.size;
405
- elements.clear();
406
- broadcast({
407
- type: 'canvas_cleared',
408
- timestamp: new Date().toISOString()
409
- });
410
- logger.info(`Canvas cleared: ${count} elements removed`);
411
- res.json({
412
- success: true,
413
- message: `Cleared ${count} elements`,
414
- count
415
- });
416
- }
417
- catch (error) {
418
- logger.error('Error clearing canvas:', error);
419
- res.status(500).json({
420
- success: false,
421
- error: error.message
422
- });
423
- }
424
- });
425
- // Delete element
426
- app.delete('/api/elements/:id', (req, res) => {
427
- try {
428
- const { id } = req.params;
429
- if (!id) {
430
- return res.status(400).json({
431
- success: false,
432
- error: 'Element ID is required'
433
- });
434
- }
435
- if (!elements.has(id)) {
436
- return res.status(404).json({
437
- success: false,
438
- error: `Element with ID ${id} not found`
439
- });
440
- }
441
- elements.delete(id);
442
- // Broadcast to all connected clients
443
- const message = {
444
- type: 'element_deleted',
445
- elementId: id
446
- };
447
- broadcast(message);
448
- persist();
449
- res.json({
450
- success: true,
451
- message: `Element ${id} deleted successfully`
452
- });
453
- }
454
- catch (error) {
455
- logger.error('Error deleting element:', error);
456
- res.status(500).json({
457
- success: false,
458
- error: error.message
459
- });
460
- }
461
- });
462
- // Query elements with filters
463
- app.get('/api/elements/search', (req, res) => {
464
- try {
465
- const { type, x_min, x_max, y_min, y_max, ...filters } = req.query;
466
- let results = Array.from(elements.values());
467
- // Filter by type if specified
468
- if (type && typeof type === 'string') {
469
- results = results.filter(element => element.type === type);
470
- }
471
- // Filter by bounding box if specified
472
- if (x_min !== undefined || x_max !== undefined || y_min !== undefined || y_max !== undefined) {
473
- const xMin = x_min !== undefined ? Number(x_min) : -Infinity;
474
- const xMax = x_max !== undefined ? Number(x_max) : Infinity;
475
- const yMin = y_min !== undefined ? Number(y_min) : -Infinity;
476
- const yMax = y_max !== undefined ? Number(y_max) : Infinity;
477
- results = results.filter(el => el.x >= xMin &&
478
- el.x <= xMax &&
479
- el.y >= yMin &&
480
- el.y <= yMax);
481
- }
482
- // Apply additional exact-match filters
483
- if (Object.keys(filters).length > 0) {
484
- results = results.filter(element => {
485
- return Object.entries(filters).every(([key, value]) => {
486
- return element[key] === value;
487
- });
488
- });
489
- }
490
- res.json({
491
- success: true,
492
- elements: results,
493
- count: results.length
494
- });
495
- }
496
- catch (error) {
497
- logger.error('Error querying elements:', error);
498
- res.status(500).json({
499
- success: false,
500
- error: error.message
501
- });
502
- }
503
- });
504
- // Get element by ID
505
- app.get('/api/elements/:id', (req, res) => {
506
- try {
507
- const { id } = req.params;
508
- if (!id) {
509
- return res.status(400).json({
510
- success: false,
511
- error: 'Element ID is required'
512
- });
513
- }
514
- const element = elements.get(id);
515
- if (!element) {
516
- return res.status(404).json({
517
- success: false,
518
- error: `Element with ID ${id} not found`
519
- });
520
- }
521
- res.json({
522
- success: true,
523
- element: element
524
- });
525
- }
526
- catch (error) {
527
- logger.error('Error fetching element:', error);
528
- res.status(500).json({
529
- success: false,
530
- error: error.message
531
- });
532
- }
533
- });
534
- // Helper: compute edge point for an element given a direction toward a target
535
- function computeEdgePoint(el, targetCenterX, targetCenterY) {
536
- const cx = el.x + (el.width || 0) / 2;
537
- const cy = el.y + (el.height || 0) / 2;
538
- const dx = targetCenterX - cx;
539
- const dy = targetCenterY - cy;
540
- if (el.type === 'diamond') {
541
- // Diamond edge: use diamond geometry (rotated square)
542
- const hw = (el.width || 0) / 2;
543
- const hh = (el.height || 0) / 2;
544
- if (dx === 0 && dy === 0)
545
- return { x: cx, y: cy + hh };
546
- const absDx = Math.abs(dx);
547
- const absDy = Math.abs(dy);
548
- // Scale factor to reach diamond edge
549
- const scale = (absDx / hw + absDy / hh) > 0
550
- ? 1 / (absDx / hw + absDy / hh)
551
- : 1;
552
- return { x: cx + dx * scale, y: cy + dy * scale };
553
- }
554
- if (el.type === 'ellipse') {
555
- // Ellipse edge: parametric intersection
556
- const a = (el.width || 0) / 2;
557
- const b = (el.height || 0) / 2;
558
- if (dx === 0 && dy === 0)
559
- return { x: cx, y: cy + b };
560
- const angle = Math.atan2(dy, dx);
561
- return { x: cx + a * Math.cos(angle), y: cy + b * Math.sin(angle) };
562
- }
563
- // Rectangle: find intersection with edges
564
- const hw = (el.width || 0) / 2;
565
- const hh = (el.height || 0) / 2;
566
- if (dx === 0 && dy === 0)
567
- return { x: cx, y: cy + hh };
568
- const angle = Math.atan2(dy, dx);
569
- const tanA = Math.tan(angle);
570
- // Check if ray intersects top/bottom edge or left/right edge
571
- if (Math.abs(tanA * hw) <= hh) {
572
- // Intersects left or right edge
573
- const signX = dx >= 0 ? 1 : -1;
574
- return { x: cx + signX * hw, y: cy + signX * hw * tanA };
575
- }
576
- else {
577
- // Intersects top or bottom edge
578
- const signY = dy >= 0 ? 1 : -1;
579
- return { x: cx + signY * hh / tanA, y: cy + signY * hh };
580
- }
581
- }
582
- // Helper: resolve arrow bindings in a batch
583
- function resolveArrowBindings(batchElements) {
584
- const elementMap = new Map();
585
- batchElements.forEach(el => elementMap.set(el.id, el));
586
- // Also check existing elements for cross-batch references
587
- elements.forEach((el, id) => {
588
- if (!elementMap.has(id))
589
- elementMap.set(id, el);
590
- });
591
- for (const el of batchElements) {
592
- if (el.type !== 'arrow' && el.type !== 'line')
593
- continue;
594
- const startRef = el.start;
595
- const endRef = el.end;
596
- if (!startRef && !endRef)
597
- continue;
598
- const startEl = startRef ? elementMap.get(startRef.id) : undefined;
599
- const endEl = endRef ? elementMap.get(endRef.id) : undefined;
600
- // Calculate arrow path from edge to edge
601
- const startCenter = startEl
602
- ? { x: startEl.x + (startEl.width || 0) / 2, y: startEl.y + (startEl.height || 0) / 2 }
603
- : { x: el.x, y: el.y };
604
- const endCenter = endEl
605
- ? { x: endEl.x + (endEl.width || 0) / 2, y: endEl.y + (endEl.height || 0) / 2 }
606
- : { x: el.x + 100, y: el.y };
607
- const GAP = 8;
608
- const startPt = startEl
609
- ? computeEdgePoint(startEl, endCenter.x, endCenter.y)
610
- : startCenter;
611
- const endPt = endEl
612
- ? computeEdgePoint(endEl, startCenter.x, startCenter.y)
613
- : endCenter;
614
- // Apply gap: move start point slightly away from source, end point slightly away from target
615
- const startDx = endPt.x - startPt.x;
616
- const startDy = endPt.y - startPt.y;
617
- const startDist = Math.sqrt(startDx * startDx + startDy * startDy) || 1;
618
- const endDx = startPt.x - endPt.x;
619
- const endDy = startPt.y - endPt.y;
620
- const endDist = Math.sqrt(endDx * endDx + endDy * endDy) || 1;
621
- const finalStart = {
622
- x: startPt.x + (startDx / startDist) * GAP,
623
- y: startPt.y + (startDy / startDist) * GAP
624
- };
625
- const finalEnd = {
626
- x: endPt.x + (endDx / endDist) * GAP,
627
- y: endPt.y + (endDy / endDist) * GAP
628
- };
629
- // Set arrow position and points
630
- el.x = finalStart.x;
631
- el.y = finalStart.y;
632
- el.points = [[0, 0], [finalEnd.x - finalStart.x, finalEnd.y - finalStart.y]];
633
- // Do NOT delete `start` and `end` here.
634
- // Excalidraw's frontend `convertToExcalidrawElements` method looks for these exact properties
635
- // to calculate mathematically sound `startBinding`, `endBinding`, `focus`, `gap`, and `boundElements`.
636
- }
637
- }
638
- // Batch create elements
639
- app.post('/api/elements/batch', (req, res) => {
640
- try {
641
- const { elements: elementsToCreate } = req.body;
642
- if (!Array.isArray(elementsToCreate)) {
643
- return res.status(400).json({
644
- success: false,
645
- error: 'Expected an array of elements'
646
- });
647
- }
648
- const createdElements = [];
649
- elementsToCreate.forEach(elementData => {
650
- const params = CreateElementSchema.parse(elementData);
651
- // Prioritize passed ID (for MCP sync), otherwise generate new ID
652
- const id = params.id || generateId();
653
- const element = {
654
- id,
655
- ...params,
656
- fontFamily: normalizeFontFamily(params.fontFamily),
657
- createdAt: new Date().toISOString(),
658
- updatedAt: new Date().toISOString(),
659
- version: 1
660
- };
661
- createdElements.push(element);
662
- });
663
- // Resolve arrow bindings (computes positions, startBinding, endBinding, boundElements)
664
- resolveArrowBindings(createdElements);
665
- // Store all elements after binding resolution
666
- createdElements.forEach(el => elements.set(el.id, el));
667
- // Broadcast to all connected clients
668
- const message = {
669
- type: 'elements_batch_created',
670
- elements: createdElements
671
- };
672
- broadcast(message);
673
- persist();
674
- res.json({
675
- success: true,
676
- elements: createdElements,
677
- count: createdElements.length
678
- });
679
- }
680
- catch (error) {
681
- logger.error('Error batch creating elements:', error);
682
- res.status(400).json({
683
- success: false,
684
- error: error.message
685
- });
686
- }
687
- });
688
- // Convert Mermaid diagram to Excalidraw elements
689
- app.post('/api/elements/from-mermaid', (req, res) => {
690
- try {
691
- const { mermaidDiagram, config } = req.body;
692
- if (!mermaidDiagram || typeof mermaidDiagram !== 'string') {
693
- return res.status(400).json({
694
- success: false,
695
- error: 'Mermaid diagram definition is required'
696
- });
697
- }
698
- logger.info('Received Mermaid conversion request', {
699
- diagramLength: mermaidDiagram.length,
700
- hasConfig: !!config
701
- });
702
- // Broadcast to all WebSocket clients to process the Mermaid diagram
703
- broadcast({
704
- type: 'mermaid_convert',
705
- mermaidDiagram,
706
- config: config || {},
707
- timestamp: new Date().toISOString()
708
- });
709
- // Return the diagram for frontend processing
710
- res.json({
711
- success: true,
712
- mermaidDiagram,
713
- config: config || {},
714
- message: 'Mermaid diagram sent to frontend for conversion.'
715
- });
716
- }
717
- catch (error) {
718
- logger.error('Error processing Mermaid diagram:', error);
719
- res.status(400).json({
720
- success: false,
721
- error: error.message
722
- });
723
- }
724
- });
725
- // Sync elements from frontend (overwrite sync)
726
- app.post('/api/elements/sync', (req, res) => {
727
- try {
728
- const { elements: frontendElements, timestamp } = req.body;
729
- logger.info(`Sync request received: ${frontendElements.length} elements`, {
730
- timestamp,
731
- elementCount: frontendElements.length
732
- });
733
- // Validate input data
734
- if (!Array.isArray(frontendElements)) {
735
- return res.status(400).json({
736
- success: false,
737
- error: 'Expected elements to be an array'
738
- });
739
- }
740
- // Record element count before sync
741
- const beforeCount = elements.size;
742
- // 1. Clear existing memory storage
743
- elements.clear();
744
- logger.info(`Cleared existing elements: ${beforeCount} elements removed`);
745
- // 2. Batch write new data
746
- let successCount = 0;
747
- const processedElements = [];
748
- frontendElements.forEach((element, index) => {
749
- try {
750
- // Ensure element has ID, generate one if missing
751
- const elementId = element.id || generateId();
752
- // Add server metadata
753
- const processedElement = {
754
- ...element,
755
- id: elementId,
756
- syncedAt: new Date().toISOString(),
757
- source: 'frontend_sync',
758
- syncTimestamp: timestamp,
759
- version: 1
760
- };
761
- // Store to memory
762
- elements.set(elementId, processedElement);
763
- processedElements.push(processedElement);
764
- successCount++;
765
- }
766
- catch (elementError) {
767
- logger.warn(`Failed to process element ${index}:`, elementError);
768
- }
769
- });
770
- logger.info(`Sync completed: ${successCount}/${frontendElements.length} elements synced`);
771
- // 3. Broadcast sync event to all WebSocket clients
772
- persist();
773
- broadcast({
774
- type: 'elements_synced',
775
- count: successCount,
776
- timestamp: new Date().toISOString(),
777
- source: 'manual_sync'
778
- });
779
- // 4. Return sync results
780
- res.json({
781
- success: true,
782
- message: `Successfully synced ${successCount} elements`,
783
- count: successCount,
784
- syncedAt: new Date().toISOString(),
785
- beforeCount,
786
- afterCount: elements.size
787
- });
788
- }
789
- catch (error) {
790
- logger.error('Sync error:', error);
791
- res.status(500).json({
792
- success: false,
793
- error: error.message,
794
- details: 'Internal server error during sync operation'
795
- });
796
- }
797
- });
798
- // ─── Files API (for image elements) ───────────────────────────
799
- // GET all files
800
- app.get('/api/files', (_req, res) => {
801
- const filesObj = {};
802
- files.forEach((f, id) => { filesObj[id] = f; });
803
- res.json({ files: filesObj });
804
- });
805
- // POST add/update files (batch)
806
- app.post('/api/files', (req, res) => {
807
- const body = req.body;
808
- const fileList = Array.isArray(body) ? body : (body?.files || []);
809
- for (const f of fileList) {
810
- if (f.id && f.dataURL) {
811
- files.set(f.id, { id: f.id, dataURL: f.dataURL, mimeType: f.mimeType || 'image/png', created: f.created || Date.now() });
812
- }
813
- }
814
- // Broadcast files to connected clients
815
- persist();
816
- broadcast({ type: 'files_added', files: fileList });
817
- res.json({ success: true, count: fileList.length });
818
- });
819
- // DELETE a file
820
- app.delete('/api/files/:id', (req, res) => {
821
- const id = req.params.id;
822
- if (files.delete(id)) {
823
- broadcast({ type: 'file_deleted', fileId: id });
824
- res.json({ success: true });
825
- }
826
- else {
827
- res.status(404).json({ success: false, error: `File with ID ${id} not found` });
828
- }
829
- });
830
- const pendingExports = new Map();
831
- app.post('/api/export/image', (req, res) => {
832
- try {
833
- const { format, background } = req.body;
834
- if (!format || !['png', 'svg'].includes(format)) {
835
- return res.status(400).json({
836
- success: false,
837
- error: 'format must be "png" or "svg"'
838
- });
839
- }
840
- if (clients.size === 0) {
841
- return res.status(503).json({
842
- success: false,
843
- error: 'No frontend client connected. Open the canvas in a browser first.'
844
- });
845
- }
846
- const requestId = generateId();
847
- const exportPromise = new Promise((resolve, reject) => {
848
- const timeout = setTimeout(() => {
849
- const pending = pendingExports.get(requestId);
850
- pendingExports.delete(requestId);
851
- // If we collected any result during the window, use it
852
- if (pending?.bestResult) {
853
- resolve(pending.bestResult);
854
- }
855
- else {
856
- reject(new Error('Export timed out after 30 seconds'));
857
- }
858
- }, 30000);
859
- pendingExports.set(requestId, { resolve, reject, timeout, collectionTimeout: null, bestResult: null });
860
- });
861
- // Re-broadcast current elements so all connected clients (including stale ones)
862
- // sync to the canonical server state before exporting
863
- const filesObj = {};
864
- files.forEach((f, id) => { filesObj[id] = f; });
865
- broadcast({
866
- type: 'initial_elements',
867
- elements: Array.from(elements.values()),
868
- ...(files.size > 0 ? { files: filesObj } : {})
869
- });
870
- // Give browsers time to process the reload before requesting export
871
- setTimeout(() => {
872
- broadcast({
873
- type: 'export_image_request',
874
- requestId,
875
- format,
876
- background: background ?? true
877
- });
878
- }, 800);
879
- exportPromise
880
- .then(result => {
881
- res.json({
882
- success: true,
883
- format: result.format,
884
- data: result.data
885
- });
886
- })
887
- .catch(error => {
888
- res.status(500).json({
889
- success: false,
890
- error: error.message
891
- });
892
- });
893
- }
894
- catch (error) {
895
- logger.error('Error initiating image export:', error);
896
- res.status(500).json({
897
- success: false,
898
- error: error.message
899
- });
900
- }
901
- });
902
- // Image export: result (Frontend -> Express -> MCP)
903
- app.post('/api/export/image/result', (req, res) => {
904
- try {
905
- const { requestId, format, data, error } = req.body;
906
- if (!requestId) {
907
- return res.status(400).json({
908
- success: false,
909
- error: 'requestId is required'
910
- });
911
- }
912
- const pending = pendingExports.get(requestId);
913
- if (!pending) {
914
- // Already resolved by another client, or expired — ignore silently
915
- return res.json({ success: true });
916
- }
917
- if (error) {
918
- // Don't reject on error — another WebSocket client may still succeed.
919
- logger.warn(`Export error from one client (requestId=${requestId}): ${error}`);
920
- return res.json({ success: true });
921
- }
922
- // Keep the largest response (most complete canvas state wins)
923
- if (!pending.bestResult || data.length > pending.bestResult.data.length) {
924
- pending.bestResult = { format, data };
925
- }
926
- // Start a short collection window on the first response, then resolve with best
927
- if (!pending.collectionTimeout) {
928
- pending.collectionTimeout = setTimeout(() => {
929
- const p = pendingExports.get(requestId);
930
- if (p?.bestResult) {
931
- clearTimeout(p.timeout);
932
- pendingExports.delete(requestId);
933
- p.resolve(p.bestResult);
934
- }
935
- }, 3000);
936
- }
937
- res.json({ success: true });
938
- }
939
- catch (error) {
940
- logger.error('Error processing export result:', error);
941
- res.status(500).json({
942
- success: false,
943
- error: error.message
944
- });
945
- }
946
- });
947
- const pendingViewports = new Map();
948
- app.post('/api/viewport', (req, res) => {
949
- try {
950
- const { scrollToContent, scrollToElementId, zoom, offsetX, offsetY } = req.body;
951
- if (clients.size === 0) {
952
- return res.status(503).json({
953
- success: false,
954
- error: 'No frontend client connected. Open the canvas in a browser first.'
955
- });
956
- }
957
- const requestId = generateId();
958
- const viewportPromise = new Promise((resolve, reject) => {
959
- const timeout = setTimeout(() => {
960
- pendingViewports.delete(requestId);
961
- reject(new Error('Viewport request timed out after 10 seconds'));
962
- }, 10000);
963
- pendingViewports.set(requestId, { resolve, reject, timeout });
964
- });
965
- broadcast({
966
- type: 'set_viewport',
967
- requestId,
968
- scrollToContent,
969
- scrollToElementId,
970
- zoom,
971
- offsetX,
972
- offsetY
973
- });
974
- viewportPromise
975
- .then(result => {
976
- res.json(result);
977
- })
978
- .catch(error => {
979
- res.status(500).json({
980
- success: false,
981
- error: error.message
982
- });
983
- });
984
- }
985
- catch (error) {
986
- logger.error('Error initiating viewport change:', error);
987
- res.status(500).json({
988
- success: false,
989
- error: error.message
990
- });
991
- }
992
- });
993
- // Viewport control: result (Frontend -> Express -> MCP)
994
- app.post('/api/viewport/result', (req, res) => {
995
- try {
996
- const { requestId, success, message, error } = req.body;
997
- if (!requestId) {
998
- return res.status(400).json({
999
- success: false,
1000
- error: 'requestId is required'
1001
- });
1002
- }
1003
- const pending = pendingViewports.get(requestId);
1004
- if (!pending) {
1005
- return res.json({ success: true });
1006
- }
1007
- if (error) {
1008
- clearTimeout(pending.timeout);
1009
- pendingViewports.delete(requestId);
1010
- pending.resolve({ success: false, message: error });
1011
- return res.json({ success: true });
1012
- }
1013
- clearTimeout(pending.timeout);
1014
- pendingViewports.delete(requestId);
1015
- pending.resolve({ success: true, message: message || 'Viewport updated' });
1016
- res.json({ success: true });
1017
- }
1018
- catch (error) {
1019
- logger.error('Error processing viewport result:', error);
1020
- res.status(500).json({
1021
- success: false,
1022
- error: error.message
1023
- });
1024
- }
1025
- });
1026
- // Snapshots: save
1027
- app.post('/api/snapshots', (req, res) => {
1028
- try {
1029
- const { name } = req.body;
1030
- if (!name || typeof name !== 'string') {
1031
- return res.status(400).json({
1032
- success: false,
1033
- error: 'Snapshot name is required'
1034
- });
1035
- }
1036
- const snapshot = {
1037
- name,
1038
- elements: Array.from(elements.values()),
1039
- createdAt: new Date().toISOString()
1040
- };
1041
- snapshots.set(name, snapshot);
1042
- logger.info(`Snapshot saved: "${name}" with ${snapshot.elements.length} elements`);
1043
- res.json({
1044
- success: true,
1045
- name,
1046
- elementCount: snapshot.elements.length,
1047
- createdAt: snapshot.createdAt
1048
- });
1049
- }
1050
- catch (error) {
1051
- logger.error('Error saving snapshot:', error);
1052
- res.status(500).json({
1053
- success: false,
1054
- error: error.message
1055
- });
1056
- }
1057
- });
1058
- // Snapshots: list
1059
- app.get('/api/snapshots', (req, res) => {
1060
- try {
1061
- const list = Array.from(snapshots.values()).map(s => ({
1062
- name: s.name,
1063
- elementCount: s.elements.length,
1064
- createdAt: s.createdAt
1065
- }));
1066
- res.json({
1067
- success: true,
1068
- snapshots: list,
1069
- count: list.length
1070
- });
1071
- }
1072
- catch (error) {
1073
- logger.error('Error listing snapshots:', error);
1074
- res.status(500).json({
1075
- success: false,
1076
- error: error.message
1077
- });
1078
- }
1079
- });
1080
- // Snapshots: get by name
1081
- app.get('/api/snapshots/:name', (req, res) => {
1082
- try {
1083
- const { name } = req.params;
1084
- const snapshot = snapshots.get(name);
1085
- if (!snapshot) {
1086
- return res.status(404).json({
1087
- success: false,
1088
- error: `Snapshot "${name}" not found`
1089
- });
1090
- }
1091
- res.json({
1092
- success: true,
1093
- snapshot
1094
- });
1095
- }
1096
- catch (error) {
1097
- logger.error('Error fetching snapshot:', error);
1098
- res.status(500).json({
1099
- success: false,
1100
- error: error.message
1101
- });
1102
- }
1103
- });
1104
- // Serve the frontend
1105
- app.get('/', (req, res) => {
1106
- const htmlFile = path.join(__dirname, '../dist/frontend/index.html');
1107
- res.sendFile(htmlFile, (err) => {
1108
- if (err) {
1109
- logger.error('Error serving frontend:', err);
1110
- res.status(404).send('Frontend not found. Please run "npm run build" first.');
1111
- }
1112
- });
1113
- });
1114
- // Health check endpoint
1115
- app.get('/health', (req, res) => {
1116
- res.json({
1117
- status: 'healthy',
1118
- timestamp: new Date().toISOString(),
1119
- elements_count: elements.size,
1120
- websocket_clients: clients.size
1121
- });
1122
- });
1123
- // Sync status endpoint
1124
- app.get('/api/sync/status', (req, res) => {
1125
- res.json({
1126
- success: true,
1127
- elementCount: elements.size,
1128
- timestamp: new Date().toISOString(),
1129
- memoryUsage: {
1130
- heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), // MB
1131
- heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), // MB
1132
- },
1133
- websocketClients: clients.size
1134
- });
1135
- });
1136
- // Error handling middleware
1137
- app.use((err, req, res, next) => {
1138
- logger.error('Unhandled error:', err);
1139
- res.status(500).json({
1140
- success: false,
1141
- error: 'Internal server error'
1142
- });
1143
- });
1144
- // Start server
1145
- const PORT = parseInt(process.env.PORT || '3001', 10);
1146
- const HOST = process.env.HOST || '127.0.0.1';
1147
- const LOOPBACK_GUARD_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', '::']);
1148
- const LOOPBACK_ADDRESSES = ['127.0.0.1', '::1'];
1149
- function formatHostForUrl(host) {
1150
- return host.includes(':') ? `[${host}]` : host;
1151
- }
1152
- function canConnect(host, port) {
1153
- return new Promise(resolve => {
1154
- let settled = false;
1155
- const socket = net.createConnection({ host, port });
1156
- const finish = (isOpen) => {
1157
- if (settled)
1158
- return;
1159
- settled = true;
1160
- socket.destroy();
1161
- resolve(isOpen);
1162
- };
1163
- socket.setTimeout(250);
1164
- socket.once('connect', () => finish(true));
1165
- socket.once('timeout', () => finish(false));
1166
- socket.once('error', () => finish(false));
1167
- });
1168
- }
1169
- async function findExistingLoopbackListener(port) {
1170
- for (const host of LOOPBACK_ADDRESSES) {
1171
- if (await canConnect(host, port)) {
1172
- return host;
1173
- }
1174
- }
1175
- return null;
1176
- }
1177
- server.on('error', (error) => {
1178
- if (error.code === 'EADDRINUSE') {
1179
- const address = error.address || HOST;
1180
- logger.error(`Canvas server port ${PORT} is already in use on ${formatHostForUrl(address)}.`);
1181
- }
1182
- else if (error.code === 'EACCES') {
1183
- logger.error(`Canvas server cannot bind ${formatHostForUrl(HOST)}:${PORT}: permission denied.`);
1184
- }
1185
- else {
1186
- logger.error('Failed to start canvas server:', error);
1187
- }
1188
- process.exit(1);
1189
- });
1190
- async function startServer() {
1191
- if (LOOPBACK_GUARD_HOSTS.has(HOST)) {
1192
- const existingHost = await findExistingLoopbackListener(PORT);
1193
- if (existingHost) {
1194
- logger.error(`Refusing to start canvas server on ${formatHostForUrl(HOST)}:${PORT}: ` +
1195
- `${formatHostForUrl(existingHost)}:${PORT} is already listening. ` +
1196
- 'This prevents duplicate IPv4/IPv6 canvas servers from splitting state.');
1197
- process.exit(1);
1198
- }
1199
- }
1200
- server.listen(PORT, HOST, () => {
1201
- const hostForUrl = formatHostForUrl(HOST);
1202
- logger.info(`POC server running on http://${hostForUrl}:${PORT}`);
1203
- logger.info(`WebSocket server running on ws://${hostForUrl}:${PORT}`);
1204
- });
1205
- }
1206
- void startServer();
1207
- export default app;
1208
- //# sourceMappingURL=server.js.map