excalidrop 0.1.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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +533 -0
  6. package/dist/cli.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-bKUmsig-.js +1 -0
  14. package/dist/frontend/assets/ar-SA-G6X2FPQ2-C7K_UR3P.js +10 -0
  15. package/dist/frontend/assets/arc-BI_FJWE7.js +1 -0
  16. package/dist/frontend/assets/array-BKyUJesY.js +1 -0
  17. package/dist/frontend/assets/az-AZ-76LH7QW2-PkhuRuSr.js +1 -0
  18. package/dist/frontend/assets/band-dPffDWoQ.js +1 -0
  19. package/dist/frontend/assets/bg-BG-XCXSNQG7-CgLRSYTl.js +5 -0
  20. package/dist/frontend/assets/blockDiagram-38ab4fdb-BFdBRYSn.js +118 -0
  21. package/dist/frontend/assets/blockDiagram-68f4deed-Ynbqr7bQ.js +118 -0
  22. package/dist/frontend/assets/bn-BD-2XOGV67Q-CEvkU0mz.js +5 -0
  23. package/dist/frontend/assets/c4Diagram-15b5d702-Dm7F1Pn_.js +10 -0
  24. package/dist/frontend/assets/c4Diagram-3d4e48cf-COkK-e67.js +10 -0
  25. package/dist/frontend/assets/ca-ES-6MX7JW3Y-DzP6q9bE.js +8 -0
  26. package/dist/frontend/assets/channel-BqCiG5n3.js +1 -0
  27. package/dist/frontend/assets/classDiagram-70f12bd4-CRzulwvH.js +2 -0
  28. package/dist/frontend/assets/classDiagram-d40c83e7-DNbA-n-a.js +2 -0
  29. package/dist/frontend/assets/classDiagram-v2-d5a6b087-l-GglywH.js +2 -0
  30. package/dist/frontend/assets/classDiagram-v2-f2320105-B_TY1zoc.js +2 -0
  31. package/dist/frontend/assets/clone-B6Jqcrku.js +1 -0
  32. package/dist/frontend/assets/createText-2e5e7dd3-DB_k52Pn.js +5 -0
  33. package/dist/frontend/assets/createText-d213de94-D-G49giw.js +5 -0
  34. package/dist/frontend/assets/cs-CZ-2BRQDIVT-Bf4TOzpR.js +11 -0
  35. package/dist/frontend/assets/cytoscape-cose-bilkent-DQYypj0P.js +331 -0
  36. package/dist/frontend/assets/da-DK-5WZEPLOC-jO-5kyXb.js +5 -0
  37. package/dist/frontend/assets/de-DE-XR44H4JA-BkNXWOVh.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-CVvMtkTa.js +4 -0
  41. package/dist/frontend/assets/edges-e0da2a9e-DQBAvC8-.js +4 -0
  42. package/dist/frontend/assets/el-GR-BZB4AONW-Dv7Y9hMk.js +10 -0
  43. package/dist/frontend/assets/elk.bundled-YVS3PsMH.js +26 -0
  44. package/dist/frontend/assets/erDiagram-880f2ed8-BjBeR38f.js +51 -0
  45. package/dist/frontend/assets/erDiagram-9861fffd-B2Bi3R45.js +51 -0
  46. package/dist/frontend/assets/es-ES-U4NZUMDT-CWhs0SQY.js +9 -0
  47. package/dist/frontend/assets/eu-ES-A7QVB2H4-BfkFwc3e.js +11 -0
  48. package/dist/frontend/assets/fa-IR-HGAKTJCU-BgWzSj46.js +8 -0
  49. package/dist/frontend/assets/fi-FI-Z5N7JZ37-CUyvkum-.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-BSZyyVBf.js +10 -0
  55. package/dist/frontend/assets/flowDb-956e92f1-BC3-CLkF.js +10 -0
  56. package/dist/frontend/assets/flowDiagram-66a62f08-H57DAvmM.js +4 -0
  57. package/dist/frontend/assets/flowDiagram-cbd28bf7-Bw4Z2nBD.js +4 -0
  58. package/dist/frontend/assets/flowDiagram-v2-96b9c2cf-CBjbm_oB.js +1 -0
  59. package/dist/frontend/assets/flowDiagram-v2-ffc7f31a-CoQufPp2.js +1 -0
  60. package/dist/frontend/assets/flowchart-elk-definition-36e2d292-1kZNdf3X.js +114 -0
  61. package/dist/frontend/assets/flowchart-elk-definition-4a651766-DDhG2Esi.js +114 -0
  62. package/dist/frontend/assets/fr-FR-RHASNOE6-7xzoRwbd.js +9 -0
  63. package/dist/frontend/assets/ganttDiagram-04f9e578-CtUSsXCz.js +257 -0
  64. package/dist/frontend/assets/ganttDiagram-c361ad54-K42AklFY.js +257 -0
  65. package/dist/frontend/assets/gitGraphDiagram-21fc4d3e-uW6XA6Y6.js +70 -0
  66. package/dist/frontend/assets/gitGraphDiagram-72cf32ee-CCNbhnBg.js +70 -0
  67. package/dist/frontend/assets/gl-ES-HMX3MZ6V-CXuDDwNP.js +10 -0
  68. package/dist/frontend/assets/graph-CYOmcSYN.js +1 -0
  69. package/dist/frontend/assets/graph-o42dOfI7.js +1 -0
  70. package/dist/frontend/assets/he-IL-6SHJWFNN-O9dzh9EM.js +10 -0
  71. package/dist/frontend/assets/hi-IN-IWLTKZ5I-DlrOXhWs.js +4 -0
  72. package/dist/frontend/assets/hu-HU-A5ZG7DT2-C3eNg-uI.js +7 -0
  73. package/dist/frontend/assets/id-ID-SAP4L64H-6iM4pI5c.js +10 -0
  74. package/dist/frontend/assets/image-blob-reduce.esm-B6b2_-a4.js +7 -0
  75. package/dist/frontend/assets/index-3862675e-DrvLhypC.js +1 -0
  76. package/dist/frontend/assets/index-6079d271-BgVTRqg2.js +1 -0
  77. package/dist/frontend/assets/index-BRA5OktX.css +1 -0
  78. package/dist/frontend/assets/index-D-cr7jpJ.js +349 -0
  79. package/dist/frontend/assets/index-DGmpr33w.js +3 -0
  80. package/dist/frontend/assets/index-HkUXslJy.js +87 -0
  81. package/dist/frontend/assets/infoDiagram-4a4f5b27-ZxmAcaW1.js +7 -0
  82. package/dist/frontend/assets/infoDiagram-f8f76790-Bn-PhiFG.js +7 -0
  83. package/dist/frontend/assets/init-Gi6I4Gst.js +1 -0
  84. package/dist/frontend/assets/it-IT-JPQ66NNP-B26NQzGp.js +11 -0
  85. package/dist/frontend/assets/ja-JP-DBVTYXUO-CB0fvnYJ.js +8 -0
  86. package/dist/frontend/assets/journeyDiagram-29694f62-ggDKs06f.js +139 -0
  87. package/dist/frontend/assets/journeyDiagram-49397b02-pdDS-n-z.js +139 -0
  88. package/dist/frontend/assets/kaa-6HZHGXH3-YNdBdcwJ.js +1 -0
  89. package/dist/frontend/assets/kab-KAB-ZGHBKWFO-D86pwZO0.js +8 -0
  90. package/dist/frontend/assets/katex-ChWnQ-fc.js +261 -0
  91. package/dist/frontend/assets/kk-KZ-P5N5QNE5-CpG5oheM.js +1 -0
  92. package/dist/frontend/assets/km-KH-HSX4SM5Z-DYOZ3Azw.js +11 -0
  93. package/dist/frontend/assets/ko-KR-MTYHY66A-BHorm6lC.js +9 -0
  94. package/dist/frontend/assets/ku-TR-6OUDTVRD-CM9RgEHv.js +9 -0
  95. package/dist/frontend/assets/layout-CCtkxrAf.js +1 -0
  96. package/dist/frontend/assets/layout-DW9Qf-At.js +1 -0
  97. package/dist/frontend/assets/line-DvZfgg9P.js +1 -0
  98. package/dist/frontend/assets/linear-FwFrGRfa.js +1 -0
  99. package/dist/frontend/assets/lt-LT-XHIRWOB4-DvriKXBj.js +3 -0
  100. package/dist/frontend/assets/lv-LV-5QDEKY6T-DzQpwk87.js +7 -0
  101. package/dist/frontend/assets/mindmap-definition-ac74a2e8-DsRo8f2w.js +95 -0
  102. package/dist/frontend/assets/mindmap-definition-fc14e90a-dQ9reluI.js +95 -0
  103. package/dist/frontend/assets/mr-IN-CRQNXWMA-C7RzxxP5.js +13 -0
  104. package/dist/frontend/assets/my-MM-5M5IBNSE-B-RRuy29.js +1 -0
  105. package/dist/frontend/assets/nb-NO-T6EIAALU-B47x16Ik.js +10 -0
  106. package/dist/frontend/assets/nl-NL-IS3SIHDZ-BxAKoQne.js +8 -0
  107. package/dist/frontend/assets/nn-NO-6E72VCQL-dfpbEDSZ.js +8 -0
  108. package/dist/frontend/assets/oc-FR-POXYY2M6-gv6geBjV.js +8 -0
  109. package/dist/frontend/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/dist/frontend/assets/pa-IN-N4M65BXN-CY30-qH7.js +4 -0
  111. package/dist/frontend/assets/path-CbwjOpE9.js +1 -0
  112. package/dist/frontend/assets/pica-DWYGHLQV.js +7 -0
  113. package/dist/frontend/assets/pie-CDiWh1rj.js +1 -0
  114. package/dist/frontend/assets/pieDiagram-421022e6-Bgy-duyf.js +35 -0
  115. package/dist/frontend/assets/pieDiagram-8a3498a8-DH7FsbPK.js +35 -0
  116. package/dist/frontend/assets/pl-PL-T2D74RX3-TrHugsIA.js +9 -0
  117. package/dist/frontend/assets/pt-BR-5N22H2LF-CJxdFp9c.js +9 -0
  118. package/dist/frontend/assets/pt-PT-UZXXM6DQ-CB-1q6YJ.js +9 -0
  119. package/dist/frontend/assets/quadrantDiagram-0957ecba-DcwYI1oi.js +7 -0
  120. package/dist/frontend/assets/quadrantDiagram-120e2f19-CvHU5a9v.js +7 -0
  121. package/dist/frontend/assets/requirementDiagram-23d650b8-tdagN8HP.js +52 -0
  122. package/dist/frontend/assets/requirementDiagram-deff3bca-BMGM-fSI.js +52 -0
  123. package/dist/frontend/assets/ro-RO-JPDTUUEW-DQq9DBOd.js +11 -0
  124. package/dist/frontend/assets/roundRect-0PYZxl1G.js +1 -0
  125. package/dist/frontend/assets/ru-RU-B4JR7IUQ-CWX32rfh.js +9 -0
  126. package/dist/frontend/assets/sankeyDiagram-04a897e0-3i6zWpY2.js +8 -0
  127. package/dist/frontend/assets/sankeyDiagram-23345273-ncj1Ol7S.js +8 -0
  128. package/dist/frontend/assets/sankeyLinkHorizontal-DgqkLiUE.js +1 -0
  129. package/dist/frontend/assets/selectAll-DJwesbwT.js +1 -0
  130. package/dist/frontend/assets/sequenceDiagram-17ac3bff-ChyKKRZA.js +122 -0
  131. package/dist/frontend/assets/sequenceDiagram-704730f1-D57_wP9k.js +122 -0
  132. package/dist/frontend/assets/si-LK-N5RQ5JYF-Lkc5ZxyG.js +1 -0
  133. package/dist/frontend/assets/sk-SK-C5VTKIMK-DyvYXjqZ.js +6 -0
  134. package/dist/frontend/assets/sl-SI-NN7IZMDC-D_MvP9Rh.js +6 -0
  135. package/dist/frontend/assets/stateDiagram-587899a1-OjFTnFLS.js +1 -0
  136. package/dist/frontend/assets/stateDiagram-9c5f0230-BzU36S5C.js +1 -0
  137. package/dist/frontend/assets/stateDiagram-v2-51a3dcff-CfX1qo7U.js +1 -0
  138. package/dist/frontend/assets/stateDiagram-v2-d93cdb3a-BH7Ud4sU.js +1 -0
  139. package/dist/frontend/assets/styles-2ab5d517-CA8x9Nh5.js +116 -0
  140. package/dist/frontend/assets/styles-5f03d8d2-BnO2pLhV.js +160 -0
  141. package/dist/frontend/assets/styles-6aaf32cf-COnot1ea.js +207 -0
  142. package/dist/frontend/assets/styles-9a916d00-C3QkBgTF.js +160 -0
  143. package/dist/frontend/assets/styles-c10674c1-CPAG2_rr.js +116 -0
  144. package/dist/frontend/assets/styles-edf9a4b0-B8Q9fyu0.js +207 -0
  145. package/dist/frontend/assets/subset-shared.chunk.js +84 -0
  146. package/dist/frontend/assets/subset-worker.chunk.js +1 -0
  147. package/dist/frontend/assets/sv-SE-XGPEYMSR-t6voGQSI.js +10 -0
  148. package/dist/frontend/assets/svgDrawCommon-08f97a94-CdT21LWf.js +1 -0
  149. package/dist/frontend/assets/svgDrawCommon-3ba9043b-D1MU7jAE.js +1 -0
  150. package/dist/frontend/assets/ta-IN-2NMHFXQM-7VmfDDyV.js +9 -0
  151. package/dist/frontend/assets/th-TH-HPSO5L25-B5rLEY9P.js +2 -0
  152. package/dist/frontend/assets/timeline-definition-7e6b55e7-BIcL44O_.js +61 -0
  153. package/dist/frontend/assets/timeline-definition-85554ec2-TgrYYwpU.js +61 -0
  154. package/dist/frontend/assets/tr-TR-DEFEU3FU-0CgOHXTn.js +7 -0
  155. package/dist/frontend/assets/uk-UA-QMV73CPH-CzSFsIsx.js +6 -0
  156. package/dist/frontend/assets/union-CnQAJu0l.js +1 -0
  157. package/dist/frontend/assets/vi-VN-M7AON7JQ-Bhx3rYU9.js +5 -0
  158. package/dist/frontend/assets/xml-BOsq7VnW.js +1 -0
  159. package/dist/frontend/assets/xychartDiagram-b6496bcd-BMKDop6v.js +7 -0
  160. package/dist/frontend/assets/xychartDiagram-e933f94c-DAWithrN.js +7 -0
  161. package/dist/frontend/assets/zh-CN-LNUGB5OW-Br52_wH4.js +10 -0
  162. package/dist/frontend/assets/zh-HK-E62DVLB3-Cj6mnIQ9.js +1 -0
  163. package/dist/frontend/assets/zh-TW-RAJ6MFWO-yLEsVBbC.js +9 -0
  164. package/dist/frontend/assets/zipObject-BeXj5PVd.js +1 -0
  165. package/dist/frontend/index.html +339 -0
  166. package/dist/index.d.ts +4 -0
  167. package/dist/index.d.ts.map +1 -0
  168. package/dist/index.js +2305 -0
  169. package/dist/index.js.map +1 -0
  170. package/dist/remote.d.ts +4 -0
  171. package/dist/remote.d.ts.map +1 -0
  172. package/dist/remote.js +133 -0
  173. package/dist/remote.js.map +1 -0
  174. package/dist/server.d.ts +4 -0
  175. package/dist/server.d.ts.map +1 -0
  176. package/dist/server.js +1217 -0
  177. package/dist/server.js.map +1 -0
  178. package/dist/target.d.ts +41 -0
  179. package/dist/target.d.ts.map +1 -0
  180. package/dist/target.js +244 -0
  181. package/dist/target.js.map +1 -0
  182. package/dist/types.d.ts +228 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +53 -0
  185. package/dist/types.js.map +1 -0
  186. package/dist/utils/auth.d.ts +15 -0
  187. package/dist/utils/auth.d.ts.map +1 -0
  188. package/dist/utils/auth.js +68 -0
  189. package/dist/utils/auth.js.map +1 -0
  190. package/dist/utils/github.d.ts +26 -0
  191. package/dist/utils/github.d.ts.map +1 -0
  192. package/dist/utils/github.js +111 -0
  193. package/dist/utils/github.js.map +1 -0
  194. package/dist/utils/logger.d.ts +4 -0
  195. package/dist/utils/logger.d.ts.map +1 -0
  196. package/dist/utils/logger.js +25 -0
  197. package/dist/utils/logger.js.map +1 -0
  198. package/dist/utils/r2store.d.ts +19 -0
  199. package/dist/utils/r2store.d.ts.map +1 -0
  200. package/dist/utils/r2store.js +75 -0
  201. package/dist/utils/r2store.js.map +1 -0
  202. package/frontend/index.html +338 -0
  203. package/frontend/src/App.tsx +1091 -0
  204. package/frontend/src/main.tsx +15 -0
  205. package/frontend/src/utils/ghSync.ts +171 -0
  206. package/frontend/src/utils/mermaidConverter.ts +49 -0
  207. package/frontend/src/utils/webmcp.ts +285 -0
  208. package/package.json +101 -0
  209. package/scripts/check-local-bind.mjs +164 -0
  210. package/scripts/postinstall.mjs +23 -0
  211. package/scripts/publish-pages.sh +39 -0
  212. package/scripts/seed-r2.mjs +9 -0
  213. package/skills/excalidraw-skill/SKILL.md +303 -0
  214. package/skills/excalidraw-skill/evals/evals.json +23 -0
  215. package/skills/excalidraw-skill/references/cheatsheet.md +142 -0
  216. package/skills/excalidraw-skill/scripts/clear-canvas.cjs +38 -0
  217. package/skills/excalidraw-skill/scripts/create-element.cjs +68 -0
  218. package/skills/excalidraw-skill/scripts/delete-element.cjs +48 -0
  219. package/skills/excalidraw-skill/scripts/export-elements.cjs +53 -0
  220. package/skills/excalidraw-skill/scripts/healthcheck.cjs +35 -0
  221. package/skills/excalidraw-skill/scripts/import-elements.cjs +81 -0
  222. package/skills/excalidraw-skill/scripts/update-element.cjs +70 -0
  223. package/src/cli.ts +559 -0
  224. package/src/index.ts +2557 -0
  225. package/src/remote.ts +96 -0
  226. package/src/server.ts +1349 -0
  227. package/src/target.ts +208 -0
  228. package/src/types.ts +338 -0
  229. package/src/utils/auth.ts +61 -0
  230. package/src/utils/github.ts +93 -0
  231. package/src/utils/logger.ts +35 -0
  232. package/src/utils/r2store.ts +76 -0
  233. package/tsconfig.json +50 -0
  234. package/vite.config.js +37 -0
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import '@excalidraw/excalidraw/index.css'
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error('Root element not found');
9
+ }
10
+
11
+ ReactDOM.createRoot(rootElement).render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>,
15
+ )
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Static-viewer GitHub sync. Zero config on *.github.io:
3
+ * owner/repo are derived from the URL, token comes from
4
+ * localStorage (pasted PAT or agent handoff via #token=...).
5
+ * Device-flow login needs VITE_GITHUB_CLIENT_ID (one-time OAuth App setup).
6
+ */
7
+
8
+ export interface RepoRef { owner: string; repo: string; branch: string }
9
+
10
+ export function detectRepo(): RepoRef | null {
11
+ const explicit = (import.meta as any).env?.VITE_REPO_SLUG as string | undefined;
12
+ if (explicit?.includes('/')) {
13
+ const [owner, repo] = explicit.split('/');
14
+ return { owner, repo, branch: 'gh-pages' };
15
+ }
16
+ const host = window.location.hostname; // <owner>.github.io
17
+ const parts = window.location.pathname.split('/').filter(Boolean);
18
+ if (host.endsWith('.github.io') && parts.length > 0) {
19
+ return { owner: host.slice(0, -'.github.io'.length), repo: parts[0], branch: 'gh-pages' };
20
+ }
21
+ return null;
22
+ }
23
+
24
+ const TOKEN_KEY = 'excalidrop_gh_token';
25
+
26
+ export function getToken(): string | null {
27
+ // Agent handoff: https://<site>/#token=gho_...
28
+ if (window.location.hash.startsWith('#token=')) {
29
+ const t = decodeURIComponent(window.location.hash.slice('#token='.length));
30
+ if (t) {
31
+ localStorage.setItem(TOKEN_KEY, t);
32
+ history.replaceState(null, '', window.location.pathname + window.location.search);
33
+ return t;
34
+ }
35
+ }
36
+ return localStorage.getItem(TOKEN_KEY);
37
+ }
38
+
39
+ export function setToken(t: string | null): void {
40
+ if (t) localStorage.setItem(TOKEN_KEY, t);
41
+ else localStorage.removeItem(TOKEN_KEY);
42
+ }
43
+
44
+ async function gh(path: string, token: string, init?: RequestInit): Promise<any> {
45
+ const r = await fetch(`https://api.github.com${path}`, {
46
+ ...init,
47
+ headers: {
48
+ Authorization: `Bearer ${token}`,
49
+ Accept: 'application/vnd.github+json',
50
+ 'Content-Type': 'application/json',
51
+ ...(init?.headers || {}),
52
+ },
53
+ });
54
+ if (!r.ok) throw new Error(`GitHub ${r.status}: ${(await r.text()).slice(0, 200)}`);
55
+ return r.json();
56
+ }
57
+
58
+ /** Push scene JSON straight to the branch (viewer copy = source of truth for static mode). */
59
+ export async function pushScene(
60
+ ref: RepoRef, token: string, scene: { elements: unknown[]; files?: Record<string, unknown> },
61
+ ): Promise<string> {
62
+ const path = 'canvas.excalidraw';
63
+ let sha: string | undefined;
64
+ try {
65
+ const cur = await gh(`/repos/${ref.owner}/${ref.repo}/contents/${path}?ref=${ref.branch}`, token);
66
+ sha = cur.sha;
67
+ } catch { /* new file */ }
68
+ const body: any = {
69
+ message: `excalidrop: autosync ${scene.elements.length} elements`,
70
+ content: btoa(unescape(encodeURIComponent(JSON.stringify(
71
+ { type: 'excalidraw', version: 2, source: 'excalidrop', elements: scene.elements },
72
+ null, 2,
73
+ )))),
74
+ branch: ref.branch,
75
+ };
76
+ if (sha) body.sha = sha;
77
+ const out = await gh(`/repos/${ref.owner}/${ref.repo}/contents/${path}`, token, {
78
+ method: 'PUT',
79
+ body: JSON.stringify(body),
80
+ });
81
+ return out.content.sha as string;
82
+ }
83
+
84
+ /** Load the static scene copy (no auth needed for public repos). */
85
+ export async function loadStaticScene(): Promise<{ elements: any[]; files?: Record<string, unknown> }> {
86
+ const r = await fetch('./canvas.excalidraw', { cache: 'no-cache' });
87
+ if (!r.ok) throw new Error(`scene fetch ${r.status}`);
88
+ const j = await r.json();
89
+ return { elements: j.elements || [], files: j.files || {} };
90
+ }
91
+
92
+ /** Device-flow login. Returns {userCode, verificationUri} — the token
93
+ * polling step may be CORS-blocked on static hosting, so the agent can
94
+ * complete it instead (github_login tool) and hand back a #token= link.
95
+ * NOTE: no `scope` — GitHub App user-tokens use fine-grained permissions
96
+ * (user ∩ app), never scopes. */
97
+ export async function startDeviceFlow(clientId: string): Promise<{ user_code: string; verification_uri: string }> {
98
+ const r = await fetch('https://github.com/login/device/code', {
99
+ method: 'POST',
100
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
101
+ body: JSON.stringify({ client_id: clientId }),
102
+ });
103
+ if (!r.ok) throw new Error('device flow start failed');
104
+ const j = await r.json();
105
+ return { user_code: j.user_code, verification_uri: j.verification_uri };
106
+ }
107
+
108
+ export type Access = 'editor' | 'viewer' | 'denied';
109
+
110
+ export type AccessDetail =
111
+ | 'app-not-installed' // ghu_ token, but the user has no app installation reaching this repo
112
+ | 'repo-not-covered' // installation(s) exist, but none of them include this repo
113
+ | 'no-token-permissions' // classic token with no read access to the repo
114
+ | null; // editor/viewer — no problem to report
115
+
116
+ /** What can this token do on this repo? Reads the user's app installations —
117
+ * the token only reaches repos both the user AND the app can access, so this
118
+ * mirrors GitHub's own enforcement (repo access == canvas access).
119
+ * `detail` tells the UI *why* access was denied so the login modal can
120
+ * point at the right fix (install the app vs. add this repo to the install). */
121
+ export async function checkAccess(ref: RepoRef, token: string): Promise<{ access: Access; login: string; detail: AccessDetail }> {
122
+ const me = await gh('/user', token);
123
+ const login = (me.login || '') as string;
124
+ const wanted = `${ref.owner}/${ref.repo}`.toLowerCase();
125
+ // Path 1: GitHub App user-token (ghu_) — installations list the repos it reaches.
126
+ // Classic PATs/OAuth tokens get 403 here ("must authenticate with an access
127
+ // token authorized to a GitHub App") and fall through to Path 2.
128
+ // per_page=100: without it a user with many repos can miss the hit (p.30 default).
129
+ const inst = await gh('/user/installations?per_page=100', token).catch(() => null);
130
+ if (inst?.installations) {
131
+ if ((inst.installations as unknown[]).length === 0) {
132
+ return { access: 'denied', login, detail: 'app-not-installed' };
133
+ }
134
+ for (const i of inst.installations || []) {
135
+ const repos = await gh(`/user/installations/${(i as any).id}/repositories?per_page=100`, token).catch(() => null);
136
+ const hit = (repos?.repositories || []).find((r: any) => r.full_name.toLowerCase() === wanted);
137
+ if (hit) {
138
+ // Token reaches the repo. Resolve read vs write via the collaborator
139
+ // permission endpoint; if that probe fails, start optimistic-editor —
140
+ // the Contents API is the real enforcement and a 403 push demotes to
141
+ // viewer (fail-open UI, fail-closed API).
142
+ const perms = await gh(`/repos/${ref.owner}/${ref.repo}/collaborators/${login}/permission`, token).catch(() => null);
143
+ const p = (perms?.permission || '') as string;
144
+ if (p === 'read' || p === 'triage') return { access: 'viewer', login, detail: null };
145
+ return { access: 'editor', login, detail: null };
146
+ }
147
+ }
148
+ return { access: 'denied', login, detail: 'repo-not-covered' };
149
+ }
150
+ // Path 2: classic token — the repo endpoint echoes *this token's* permissions.
151
+ const repo = await gh(`/repos/${ref.owner}/${ref.repo}`, token).catch(() => null);
152
+ if (!repo?.permissions) return { access: 'denied', login, detail: 'no-token-permissions' };
153
+ if (repo.permissions.push || repo.permissions.admin || repo.permissions.maintain) {
154
+ return { access: 'editor', login, detail: null };
155
+ }
156
+ return { access: 'viewer', login, detail: null };
157
+ }
158
+
159
+ /**
160
+ * Shared GitHub App (one app, many installs — any user installs it on their
161
+ * own repo; the Client ID is public, not a secret). Overridable at publish
162
+ * time via VITE_GITHUB_CLIENT_ID / VITE_GITHUB_APP_SLUG.
163
+ */
164
+ export const CLIENT_ID =
165
+ ((import.meta as any).env?.VITE_GITHUB_CLIENT_ID as string | undefined) ||
166
+ 'Iv23liuS2fx3QOEIoDmx'; // shared Excalidrop GitHub App (public ID, not a secret)
167
+ export const APP_SLUG =
168
+ ((import.meta as any).env?.VITE_GITHUB_APP_SLUG as string | undefined) || 'excalidrop';
169
+ export const APP_INSTALL_URL = `https://github.com/apps/${APP_SLUG}/installations/new`;
170
+ /** True when a real Client ID is baked in (not the publish-time placeholder). */
171
+ export const HAS_CLIENT_ID = !!CLIENT_ID && !CLIENT_ID.includes('EXCALIDROP_APP');
@@ -0,0 +1,49 @@
1
+ import { parseMermaidToExcalidraw, MermaidConfig } from '@excalidraw/mermaid-to-excalidraw';
2
+ import type { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
3
+ import type { BinaryFiles } from '@excalidraw/excalidraw/types/types';
4
+
5
+ export interface MermaidConversionResult {
6
+ elements: readonly ExcalidrawElement[];
7
+ files?: BinaryFiles;
8
+ error?: string;
9
+ }
10
+
11
+ /**
12
+ * Converts a Mermaid diagram definition to Excalidraw elements
13
+ * This function needs to run in the browser context as it requires DOM access
14
+ */
15
+ export const convertMermaidToExcalidraw = async (
16
+ mermaidDefinition: string,
17
+ config?: MermaidConfig
18
+ ): Promise<MermaidConversionResult> => {
19
+ try {
20
+ // Parse the Mermaid diagram to Excalidraw elements
21
+ const result = await parseMermaidToExcalidraw(mermaidDefinition, config);
22
+
23
+ return {
24
+ elements: result.elements,
25
+ files: result.files,
26
+ };
27
+ } catch (error) {
28
+ console.error('Error converting Mermaid to Excalidraw:', error);
29
+ return {
30
+ elements: [],
31
+ error: error instanceof Error ? error.message : String(error),
32
+ };
33
+ }
34
+ };
35
+
36
+ /**
37
+ * Default Mermaid configuration for Excalidraw conversion
38
+ */
39
+ export const DEFAULT_MERMAID_CONFIG: MermaidConfig = {
40
+ startOnLoad: false,
41
+ flowchart: {
42
+ curve: 'linear',
43
+ },
44
+ themeVariables: {
45
+ fontSize: '20px',
46
+ },
47
+ maxEdges: 500,
48
+ maxTextSize: 50000,
49
+ };
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Thin WebMCP layer for the Excalidrop canvas.
3
+ *
4
+ * Exposes the live in-tab Excalidraw scene as browser-native tools via
5
+ * `document.modelContext.registerTool()` (W3C WebML CG draft, Chromium 146+
6
+ * behind `#enable-webmcp-testing`). No-ops where the API is absent.
7
+ *
8
+ * Tools mirror the local MCP names so agents recognise them:
9
+ * - describe_scene (read-only)
10
+ * - create_elements (write)
11
+ * - update_element (write)
12
+ * - delete_element (write)
13
+ * - clear_canvas (consequential)
14
+ *
15
+ * The page itself is the tool registry — no HTTP/SSE/stdio involved.
16
+ * Auth/session comes for free: tools run inside the user's tab.
17
+ */
18
+
19
+ interface MinimalCanvasApi {
20
+ getSceneElements(): Array<Record<string, any>>;
21
+ updateScene(scene: { elements: Array<Record<string, any>> }): void;
22
+ resetScene(): void;
23
+ }
24
+
25
+ export type CanvasApiGetter = () => MinimalCanvasApi | null | undefined;
26
+
27
+ export interface WebMCPHandle {
28
+ available: boolean;
29
+ count: number;
30
+ cleanup: () => void;
31
+ }
32
+
33
+ interface ModelContextLike {
34
+ registerTool(
35
+ tool: {
36
+ name: string;
37
+ title?: string;
38
+ description: string;
39
+ inputSchema: Record<string, unknown>;
40
+ annotations?: Record<string, unknown>;
41
+ execute: (args: any, opts?: { signal?: AbortSignal }) => unknown | Promise<unknown>;
42
+ },
43
+ options?: { signal?: AbortSignal },
44
+ ): Promise<void>;
45
+ }
46
+
47
+ function getModelContext(): ModelContextLike | null {
48
+ try {
49
+ const doc = document as unknown as Record<string, unknown>;
50
+ const nav = navigator as unknown as Record<string, unknown>;
51
+ const mc =
52
+ (doc['modelContext'] as ModelContextLike | undefined) ??
53
+ (nav['modelContext'] as ModelContextLike | undefined);
54
+ if (mc && typeof mc.registerTool === 'function') return mc;
55
+ } catch {
56
+ /* non-DOM / restricted context */
57
+ }
58
+ return null;
59
+ }
60
+
61
+ function newId(): string {
62
+ try {
63
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
64
+ return (crypto as Crypto).randomUUID().slice(0, 12);
65
+ }
66
+ } catch {
67
+ /* fall through */
68
+ }
69
+ return `el_${Math.random().toString(36).slice(2, 10)}`;
70
+ }
71
+
72
+ function summarize(el: Record<string, any>): Record<string, unknown> {
73
+ const out: Record<string, unknown> = {
74
+ id: el['id'],
75
+ type: el['type'],
76
+ x: Math.round(Number(el['x'] ?? 0)),
77
+ y: Math.round(Number(el['y'] ?? 0)),
78
+ };
79
+ if (el['width'] !== undefined) out['width'] = el['width'];
80
+ if (el['height'] !== undefined) out['height'] = el['height'];
81
+ const text =
82
+ (typeof el['text'] === 'string' && el['text']) ||
83
+ (el['label'] as { text?: string } | undefined)?.text;
84
+ if (text) out['text'] = String(text).slice(0, 120);
85
+ if (el['locked']) out['locked'] = true;
86
+ return out;
87
+ }
88
+
89
+ const ELEMENT_SCHEMA: Record<string, unknown> = {
90
+ type: 'object',
91
+ properties: {
92
+ id: { type: 'string', description: 'Custom element ID (auto-generated if omitted)' },
93
+ type: { type: 'string', description: 'Excalidraw element type: rectangle, ellipse, diamond, text, arrow, line, freedraw, image' },
94
+ x: { type: 'number' },
95
+ y: { type: 'number' },
96
+ width: { type: 'number' },
97
+ height: { type: 'number' },
98
+ text: { type: 'string', description: 'Label text for shapes / content for text elements' },
99
+ backgroundColor: { type: 'string' },
100
+ strokeColor: { type: 'string' },
101
+ strokeWidth: { type: 'number' },
102
+ fontSize: { type: 'number' },
103
+ startElementId: { type: 'string', description: 'For arrows: bind start to this element ID' },
104
+ endElementId: { type: 'string', description: 'For arrows: bind end to this element ID' },
105
+ },
106
+ required: ['type', 'x', 'y'],
107
+ additionalProperties: true,
108
+ };
109
+
110
+ /**
111
+ * Register the thin WebMCP toolset against the live canvas.
112
+ * Safe to call anywhere: feature-detects `document.modelContext` first.
113
+ * Returns a handle whose `cleanup()` unregisters all tools (AbortSignal).
114
+ */
115
+ export function registerCanvasWebMCP(getApi: CanvasApiGetter): WebMCPHandle {
116
+ const noop = () => {};
117
+ const mc = getModelContext();
118
+ if (!mc) return { available: false, count: 0, cleanup: noop };
119
+
120
+ const controller = new AbortController();
121
+ const signal = controller.signal;
122
+ let count = 0;
123
+
124
+ const requireApi = (): MinimalCanvasApi => {
125
+ const api = getApi();
126
+ if (!api) throw new Error('Canvas not ready yet');
127
+ return api;
128
+ };
129
+
130
+ const safeRegister = (tool: Parameters<ModelContextLike['registerTool']>[0]): void => {
131
+ try {
132
+ const p = mc.registerTool(tool, { signal });
133
+ if (p && typeof (p as Promise<void>).catch === 'function') {
134
+ (p as Promise<void>).catch(() => {
135
+ /* duplicate / invalid — ignore, keeps thin layer best-effort */
136
+ });
137
+ }
138
+ count += 1;
139
+ } catch {
140
+ /* API present but rejected synchronously — ignore */
141
+ }
142
+ };
143
+
144
+ safeRegister({
145
+ name: 'describe_scene',
146
+ title: 'Describe canvas scene',
147
+ description:
148
+ 'Read-only summary of the live Excalidraw canvas: element counts by type plus id/type/position/label per element (capped at 200). Call before editing.',
149
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
150
+ annotations: { readOnlyHint: true },
151
+ execute: () => {
152
+ const api = requireApi();
153
+ const els = api.getSceneElements().filter((e) => !e['isDeleted']);
154
+ const byType: Record<string, number> = {};
155
+ for (const e of els) {
156
+ const t = String(e['type'] ?? 'unknown');
157
+ byType[t] = (byType[t] ?? 0) + 1;
158
+ }
159
+ return {
160
+ count: els.length,
161
+ byType,
162
+ elements: els.slice(0, 200).map(summarize),
163
+ };
164
+ },
165
+ });
166
+
167
+ safeRegister({
168
+ name: 'create_elements',
169
+ title: 'Create canvas elements',
170
+ description:
171
+ 'Add new elements to the live canvas in one batch. Assign custom ids to shapes so arrows can reference them via startElementId/endElementId.',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: { elements: { type: 'array', items: ELEMENT_SCHEMA, minItems: 1 } },
175
+ required: ['elements'],
176
+ additionalProperties: false,
177
+ },
178
+ annotations: { destructiveHint: false },
179
+ execute: (args: { elements?: Array<Record<string, any>> }) => {
180
+ const api = requireApi();
181
+ const input = Array.isArray(args?.elements) ? args.elements : [];
182
+ if (input.length === 0) throw new Error('elements must be a non-empty array');
183
+ if (input.length > 200) throw new Error('max 200 elements per call');
184
+ const created = input.map((raw) => {
185
+ if (!raw || typeof raw !== 'object') throw new Error('each element must be an object');
186
+ if (typeof raw['type'] !== 'string') throw new Error('each element needs a type');
187
+ if (typeof raw['x'] !== 'number' || typeof raw['y'] !== 'number') {
188
+ throw new Error('each element needs numeric x and y');
189
+ }
190
+ const { startElementId, endElementId, ...rest } = raw;
191
+ const el: Record<string, any> = { id: newId(), ...rest };
192
+ if (typeof startElementId === 'string') el['start'] = { id: startElementId };
193
+ if (typeof endElementId === 'string') el['end'] = { id: endElementId };
194
+ if ((el['type'] === 'arrow' || el['type'] === 'line') && !Array.isArray(el['points'])) {
195
+ el['points'] = [
196
+ [0, 0],
197
+ [100, 0],
198
+ ];
199
+ }
200
+ return el;
201
+ });
202
+ const current = api.getSceneElements();
203
+ api.updateScene({ elements: [...current, ...created] });
204
+ return { ok: true, count: created.length, ids: created.map((e) => e['id']) };
205
+ },
206
+ });
207
+
208
+ safeRegister({
209
+ name: 'update_element',
210
+ title: 'Update canvas element',
211
+ description: 'Patch position, size, colors, or text of one element by id on the live canvas.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ id: { type: 'string', description: 'Element ID from describe_scene' },
216
+ patch: {
217
+ type: 'object',
218
+ description: 'Fields to merge (x, y, width, height, backgroundColor, strokeColor, text, ...)',
219
+ },
220
+ },
221
+ required: ['id', 'patch'],
222
+ additionalProperties: false,
223
+ },
224
+ annotations: { destructiveHint: false },
225
+ execute: (args: { id?: string; patch?: Record<string, unknown> }) => {
226
+ const api = requireApi();
227
+ if (!args?.id) throw new Error('id is required');
228
+ if (!args?.patch || typeof args.patch !== 'object') throw new Error('patch must be an object');
229
+ const current = api.getSceneElements();
230
+ const idx = current.findIndex((e) => e['id'] === args.id);
231
+ if (idx === -1) throw new Error(`Element ${args.id} not found`);
232
+ const next = current.slice();
233
+ next[idx] = { ...current[idx], ...args.patch, id: args.id };
234
+ api.updateScene({ elements: next });
235
+ return { ok: true, id: args.id };
236
+ },
237
+ });
238
+
239
+ safeRegister({
240
+ name: 'delete_element',
241
+ title: 'Delete canvas element',
242
+ description: 'Remove one element by id from the live canvas.',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: { id: { type: 'string' } },
246
+ required: ['id'],
247
+ additionalProperties: false,
248
+ },
249
+ annotations: { destructiveHint: true },
250
+ execute: (args: { id?: string }) => {
251
+ const api = requireApi();
252
+ if (!args?.id) throw new Error('id is required');
253
+ const current = api.getSceneElements();
254
+ if (!current.some((e) => e['id'] === args.id)) throw new Error(`Element ${args.id} not found`);
255
+ api.updateScene({ elements: current.filter((e) => e['id'] !== args.id) });
256
+ return { ok: true, deleted: args.id };
257
+ },
258
+ });
259
+
260
+ safeRegister({
261
+ name: 'clear_canvas',
262
+ title: 'Clear canvas',
263
+ description: 'Remove ALL elements from the live canvas. Ask the user to confirm first.',
264
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
265
+ annotations: { destructiveHint: true, consequentialHint: true },
266
+ execute: () => {
267
+ const api = requireApi();
268
+ const n = api.getSceneElements().length;
269
+ api.resetScene();
270
+ return { ok: true, cleared: n };
271
+ },
272
+ });
273
+
274
+ return {
275
+ available: true,
276
+ count,
277
+ cleanup: () => {
278
+ try {
279
+ controller.abort();
280
+ } catch {
281
+ /* ignore */
282
+ }
283
+ },
284
+ };
285
+ }
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "excalidrop",
3
+ "version": "0.1.1",
4
+ "description": "Drop an Excalidraw canvas + MCP server into any project — per-project ports, one-command AI agent setup",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "excalidrop": "dist/cli.js",
9
+ "mcp-excalidraw-server": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "start": "npm run build:server && node dist/index.js",
13
+ "canvas": "npm run build:server && node dist/server.js",
14
+ "build": "npm run build:frontend && npm run build:server",
15
+ "build:frontend": "vite build",
16
+ "build:server": "npx tsc",
17
+ "build:types": "npx tsc --emitDeclarationOnly",
18
+ "dev": "concurrently \"npm run dev:server\" \"vite\"",
19
+ "dev:server": "npx tsc --watch",
20
+ "production": "npm run build && npm run canvas",
21
+ "postinstall": "node scripts/postinstall.mjs",
22
+ "excalidrop": "node dist/cli.js",
23
+ "prepublishOnly": "npm run build",
24
+ "test:bind": "npm run build:server && node scripts/check-local-bind.mjs",
25
+ "type-check": "npx tsc --noEmit"
26
+ },
27
+ "dependencies": {
28
+ "@excalidraw/excalidraw": "^0.18.0",
29
+ "@excalidraw/mermaid-to-excalidraw": "^1.1.3",
30
+ "@modelcontextprotocol/sdk": "latest",
31
+ "cors": "^2.8.5",
32
+ "dotenv": "^16.3.1",
33
+ "express": "^4.18.2",
34
+ "mermaid": "^11.12.1",
35
+ "node-fetch": "^3.3.2",
36
+ "react": "^18.3.1",
37
+ "react-dom": "^18.3.1",
38
+ "winston": "^3.11.0",
39
+ "ws": "^8.14.2",
40
+ "zod": "^3.22.4",
41
+ "zod-to-json-schema": "^3.22.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/cors": "^2.8.17",
45
+ "@types/express": "^4.17.21",
46
+ "@types/node": "^20.19.7",
47
+ "@types/react": "^18.3.3",
48
+ "@types/react-dom": "^18.3.0",
49
+ "@types/ws": "^8.5.10",
50
+ "@vitejs/plugin-react": "^4.6.0",
51
+ "concurrently": "^9.2.0",
52
+ "typescript": "^5.8.3",
53
+ "vite": "^6.3.5"
54
+ },
55
+ "keywords": [
56
+ "excalidrop",
57
+ "excalidraw",
58
+ "model-context-protocol",
59
+ "ai",
60
+ "drawing",
61
+ "diagrams",
62
+ "canvas",
63
+ "real-time",
64
+ "websocket",
65
+ "visualization",
66
+ "claude",
67
+ "ai-tools"
68
+ ],
69
+ "author": {
70
+ "name": "yctimlin",
71
+ "email": "c22647809@gmail.com"
72
+ },
73
+ "license": "MIT",
74
+ "repository": {
75
+ "type": "git",
76
+ "url": "https://github.com/wtf403/excalidrop.git"
77
+ },
78
+ "homepage": "https://github.com/wtf403/excalidrop#readme",
79
+ "bugs": {
80
+ "url": "https://github.com/wtf403/excalidrop/issues"
81
+ },
82
+ "engines": {
83
+ "node": ">=18.0.0"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public",
87
+ "registry": "https://registry.npmjs.org/"
88
+ },
89
+ "files": [
90
+ "src/**/*",
91
+ "dist/**/*",
92
+ "frontend/**/*",
93
+ "scripts/**/*",
94
+ "skills/**/*",
95
+ "*.d.ts",
96
+ "README.md",
97
+ "LICENSE",
98
+ "tsconfig.json",
99
+ "vite.config.js"
100
+ ]
101
+ }