@drawpro/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +89 -0
  2. package/dist/server.js +1545 -0
  3. package/package.json +43 -0
package/dist/server.js ADDED
@@ -0,0 +1,1545 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/server.ts
27
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
+ var import_zod = require("zod");
30
+
31
+ // ../client/src/crypto.ts
32
+ var import_node_module = require("node:module");
33
+ var import_node_crypto = require("node:crypto");
34
+ var import_node = __toESM(require("@phi-ag/argon2/node"));
35
+ var import_argon2 = require("@phi-ag/argon2");
36
+ var HKDF_SALT = new TextEncoder().encode("drawpro-e2ee-salt");
37
+ var HKDF_INFO = new TextEncoder().encode("drawpro-e2ee-key");
38
+ var AAD_MESSAGE = new TextEncoder().encode("drawpro-e2ee-message");
39
+ var AAD_PRIVATE_KEY = new TextEncoder().encode("drawpro-e2ee-private-key");
40
+ var ARGON2_PARAMS = {
41
+ memoryCost: 128 * 1024,
42
+ // 128 MB
43
+ timeCost: 4,
44
+ parallelism: 2,
45
+ hashLength: 32
46
+ };
47
+ var argon2Instance = null;
48
+ async function getArgon2() {
49
+ if (!argon2Instance) {
50
+ const require2 = (0, import_node_module.createRequire)(__filename);
51
+ argon2Instance = await (0, import_node.default)(require2.resolve("@phi-ag/argon2/argon2.wasm"));
52
+ }
53
+ return argon2Instance;
54
+ }
55
+ async function deriveHKDFKey(sharedSecret) {
56
+ const material = await import_node_crypto.webcrypto.subtle.importKey("raw", sharedSecret, { name: "HKDF" }, false, [
57
+ "deriveBits"
58
+ ]);
59
+ return import_node_crypto.webcrypto.subtle.deriveBits(
60
+ { name: "HKDF", hash: "SHA-512", salt: HKDF_SALT, info: HKDF_INFO },
61
+ material,
62
+ 256
63
+ );
64
+ }
65
+ async function encryptMessage(message, publicKeyBase64) {
66
+ const ephemeral = await import_node_crypto.webcrypto.subtle.generateKey({ name: "X25519" }, true, [
67
+ "deriveBits"
68
+ ]);
69
+ const recipient = await import_node_crypto.webcrypto.subtle.importKey(
70
+ "raw",
71
+ Buffer.from(publicKeyBase64, "base64"),
72
+ { name: "X25519" },
73
+ false,
74
+ []
75
+ );
76
+ const shared = new Uint8Array(
77
+ await import_node_crypto.webcrypto.subtle.deriveBits(
78
+ { name: "X25519", public: recipient },
79
+ ephemeral.privateKey,
80
+ 256
81
+ )
82
+ );
83
+ const aesKey = await import_node_crypto.webcrypto.subtle.importKey("raw", await deriveHKDFKey(shared), {
84
+ name: "AES-GCM"
85
+ }, false, ["encrypt"]);
86
+ const iv = import_node_crypto.webcrypto.getRandomValues(new Uint8Array(16));
87
+ const sealed = new Uint8Array(
88
+ await import_node_crypto.webcrypto.subtle.encrypt(
89
+ { name: "AES-GCM", iv, additionalData: AAD_MESSAGE },
90
+ aesKey,
91
+ new TextEncoder().encode(message)
92
+ )
93
+ );
94
+ const ciphertext = sealed.slice(0, -16);
95
+ const authTag = sealed.slice(-16);
96
+ const ephPub = new Uint8Array(await import_node_crypto.webcrypto.subtle.exportKey("raw", ephemeral.publicKey));
97
+ return Buffer.concat([
98
+ Buffer.from(ephPub),
99
+ Buffer.from(iv),
100
+ Buffer.from(authTag),
101
+ Buffer.from(ciphertext)
102
+ ]).toString("base64");
103
+ }
104
+ async function decryptMessage(encryptedBase64, privateKeyBytes) {
105
+ const blob = Buffer.from(encryptedBase64, "base64");
106
+ const ephPub = blob.subarray(0, 32);
107
+ const iv = blob.subarray(32, 48);
108
+ const authTag = blob.subarray(48, 64);
109
+ const ciphertext = blob.subarray(64);
110
+ const privateKey = await importX25519Private(privateKeyBytes);
111
+ const publicKey = await import_node_crypto.webcrypto.subtle.importKey(
112
+ "raw",
113
+ ephPub,
114
+ { name: "X25519" },
115
+ false,
116
+ []
117
+ );
118
+ const shared = new Uint8Array(
119
+ await import_node_crypto.webcrypto.subtle.deriveBits(
120
+ { name: "X25519", public: publicKey },
121
+ privateKey,
122
+ 256
123
+ )
124
+ );
125
+ const aesKey = await import_node_crypto.webcrypto.subtle.importKey("raw", await deriveHKDFKey(shared), {
126
+ name: "AES-GCM"
127
+ }, false, ["decrypt"]);
128
+ const plain = await import_node_crypto.webcrypto.subtle.decrypt(
129
+ { name: "AES-GCM", iv, additionalData: AAD_MESSAGE },
130
+ aesKey,
131
+ Buffer.concat([ciphertext, authTag])
132
+ );
133
+ return new TextDecoder().decode(plain);
134
+ }
135
+ var PKCS8_X25519_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
136
+ async function importX25519Private(raw) {
137
+ return import_node_crypto.webcrypto.subtle.importKey(
138
+ "pkcs8",
139
+ Buffer.concat([PKCS8_X25519_PREFIX, Buffer.from(raw)]),
140
+ { name: "X25519" },
141
+ false,
142
+ ["deriveBits"]
143
+ );
144
+ }
145
+ function hexToBytes(hex) {
146
+ return new Uint8Array(Buffer.from(hex, "hex"));
147
+ }
148
+ function pemToPrivateKeyBytes(pem) {
149
+ const b64 = pem.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
150
+ const decoded = new Uint8Array(Buffer.from(b64, "base64"));
151
+ if (decoded.length > 32) {
152
+ for (let i = 0; i <= decoded.length - 34; i++) {
153
+ if (decoded[i] === 4 && decoded[i + 1] === 32) {
154
+ const raw = decoded.slice(i + 2, i + 34);
155
+ if (raw.length === 32) return raw;
156
+ }
157
+ }
158
+ return decoded.slice(-32);
159
+ }
160
+ return decoded;
161
+ }
162
+ async function decryptPrivateKey(encryptedBase64, passcode, salt) {
163
+ const argon2 = await getArgon2();
164
+ const derived = argon2.hash(passcode, {
165
+ salt: Buffer.from(hexToBytes(salt)),
166
+ type: import_argon2.Argon2Type.Argon2id,
167
+ ...ARGON2_PARAMS
168
+ });
169
+ const combined = Buffer.from(encryptedBase64, "base64");
170
+ const iv = combined.subarray(0, 16);
171
+ const encrypted = combined.subarray(16);
172
+ const aesKey = await import_node_crypto.webcrypto.subtle.importKey(
173
+ "raw",
174
+ new Uint8Array(derived.hash),
175
+ { name: "AES-GCM" },
176
+ false,
177
+ ["decrypt"]
178
+ );
179
+ const pem = await import_node_crypto.webcrypto.subtle.decrypt(
180
+ { name: "AES-GCM", iv, additionalData: AAD_PRIVATE_KEY },
181
+ aesKey,
182
+ encrypted
183
+ );
184
+ return pemToPrivateKeyBytes(new TextDecoder().decode(pem));
185
+ }
186
+ async function decryptSheet(encryptedData, privateKeyBytes) {
187
+ return JSON.parse(await decryptMessage(encryptedData, privateKeyBytes));
188
+ }
189
+
190
+ // ../client/src/api.ts
191
+ var DrawProClient = class {
192
+ constructor(baseUrl, token) {
193
+ this.baseUrl = baseUrl;
194
+ this.token = token;
195
+ }
196
+ async request(path, init) {
197
+ const res = await fetch(`${this.baseUrl}${path}`, {
198
+ ...init,
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ Authorization: `Bearer ${this.token}`,
202
+ ...init?.headers ?? {}
203
+ }
204
+ });
205
+ const body = await res.json().catch(() => ({}));
206
+ if (!res.ok) {
207
+ throw new Error(`${path} -> ${res.status} ${body.error ?? JSON.stringify(body)}`);
208
+ }
209
+ return body.data;
210
+ }
211
+ me() {
212
+ return this.request("/auth/me");
213
+ }
214
+ listWorkspaces() {
215
+ return this.request("/workspaces");
216
+ }
217
+ listSheets(workspaceId) {
218
+ return this.request(`/workspaces/${workspaceId}/sheets`);
219
+ }
220
+ getSheet(workspaceId, sheetId) {
221
+ return this.request(`/workspaces/${workspaceId}/sheets/${sheetId}`);
222
+ }
223
+ /**
224
+ * Read a sheet's real contents. Requires the private key, so the caller must
225
+ * have unlocked it with the passcode first.
226
+ */
227
+ async readSheet(workspaceId, sheetId, privateKey) {
228
+ const sheet = await this.getSheet(workspaceId, sheetId);
229
+ if (!sheet.encryptedData) {
230
+ const raw = sheet;
231
+ return { name: sheet.name, elements: raw.elements ?? [], appState: raw.appState ?? {} };
232
+ }
233
+ return decryptSheet(sheet.encryptedData, privateKey);
234
+ }
235
+ /** Decrypt a workspace or sheet name for display. Returns null if unreadable. */
236
+ async readName(encrypted, privateKey) {
237
+ if (!encrypted) return null;
238
+ try {
239
+ const opened = await decryptMessage(encrypted, privateKey);
240
+ try {
241
+ return JSON.parse(opened).name;
242
+ } catch {
243
+ return opened;
244
+ }
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+ /**
250
+ * Create a sheet. POST requires a name field, which the server replaces with
251
+ * the '[encrypted]' sentinel; the real name travels inside the blob.
252
+ */
253
+ async createSheet(workspaceId, payload, publicKey) {
254
+ const body = publicKey ? { name: "[encrypted]", encryptedData: await encryptMessage(JSON.stringify(payload), publicKey) } : { name: payload.name };
255
+ return this.request(`/workspaces/${workspaceId}/sheets`, {
256
+ method: "POST",
257
+ body: JSON.stringify(body)
258
+ });
259
+ }
260
+ /**
261
+ * Replace a sheet's contents. Unlike POST, PUT rejects a plaintext name
262
+ * outright for an encrypted account — the blob is the whole update.
263
+ */
264
+ async updateSheet(workspaceId, sheetId, payload, publicKey) {
265
+ const body = publicKey ? { encryptedData: await encryptMessage(JSON.stringify(payload), publicKey) } : { name: payload.name, elements: payload.elements, appState: payload.appState };
266
+ return this.request(`/workspaces/${workspaceId}/sheets/${sheetId}`, {
267
+ method: "PUT",
268
+ body: JSON.stringify(body)
269
+ });
270
+ }
271
+ };
272
+
273
+ // ../client/src/keystore.ts
274
+ var import_node_child_process = require("node:child_process");
275
+ var import_node_fs = require("node:fs");
276
+ var import_node_os = require("node:os");
277
+ var import_node_path = require("node:path");
278
+ var import_node_crypto2 = require("node:crypto");
279
+ var SERVICE = "drawpro-mcp";
280
+ function accountId(email) {
281
+ return (0, import_node_crypto2.createHash)("sha256").update(email).digest("hex").slice(0, 16);
282
+ }
283
+ function filePath(email) {
284
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), ".drawpro", `key-${accountId(email)}`);
285
+ }
286
+ function macOS() {
287
+ return process.platform === "darwin";
288
+ }
289
+ function storeKey(email, key) {
290
+ const encoded = Buffer.from(key).toString("base64");
291
+ if (macOS()) {
292
+ try {
293
+ (0, import_node_child_process.execFileSync)(
294
+ "security",
295
+ ["add-generic-password", "-a", accountId(email), "-s", SERVICE, "-w", encoded, "-U"],
296
+ { stdio: "ignore" }
297
+ );
298
+ return { location: "macOS keychain" };
299
+ } catch {
300
+ }
301
+ }
302
+ const path = filePath(email);
303
+ (0, import_node_fs.mkdirSync)((0, import_node_path.join)((0, import_node_os.homedir)(), ".drawpro"), { recursive: true, mode: 448 });
304
+ (0, import_node_fs.writeFileSync)(path, encoded, { mode: 384 });
305
+ (0, import_node_fs.chmodSync)(path, 384);
306
+ return { location: path };
307
+ }
308
+ function loadKey(email) {
309
+ if (macOS()) {
310
+ try {
311
+ const out = (0, import_node_child_process.execFileSync)(
312
+ "security",
313
+ ["find-generic-password", "-a", accountId(email), "-s", SERVICE, "-w"],
314
+ { stdio: ["ignore", "pipe", "ignore"] }
315
+ ).toString().trim();
316
+ if (out) return new Uint8Array(Buffer.from(out, "base64"));
317
+ } catch {
318
+ }
319
+ }
320
+ const path = filePath(email);
321
+ if (!(0, import_node_fs.existsSync)(path)) return null;
322
+ return new Uint8Array(Buffer.from((0, import_node_fs.readFileSync)(path, "utf8").trim(), "base64"));
323
+ }
324
+ function forgetKey(email) {
325
+ if (macOS()) {
326
+ try {
327
+ (0, import_node_child_process.execFileSync)(
328
+ "security",
329
+ ["delete-generic-password", "-a", accountId(email), "-s", SERVICE],
330
+ { stdio: "ignore" }
331
+ );
332
+ } catch {
333
+ }
334
+ }
335
+ const path = filePath(email);
336
+ if ((0, import_node_fs.existsSync)(path)) (0, import_node_fs.rmSync)(path);
337
+ }
338
+
339
+ // ../client/src/prompt.ts
340
+ var import_node_readline = require("node:readline");
341
+ var CLEAR_LINE = "\x1B[2K\x1B[200D";
342
+ function askHidden(question) {
343
+ return new Promise((resolve) => {
344
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout, terminal: true });
345
+ process.stdout.write(question);
346
+ const onData = () => {
347
+ const typed = rl.line ?? "";
348
+ process.stdout.write(CLEAR_LINE + question + "*".repeat(typed.length));
349
+ };
350
+ process.stdin.on("data", onData);
351
+ rl.question("", (answer) => {
352
+ process.stdin.removeListener("data", onData);
353
+ rl.close();
354
+ process.stdout.write("\n");
355
+ resolve(answer);
356
+ });
357
+ });
358
+ }
359
+
360
+ // ../diagram/src/ids.ts
361
+ var import_fractional_indexing = require("fractional-indexing");
362
+ var ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
363
+ function elementId() {
364
+ let out = "";
365
+ for (let i = 0; i < 21; i++) {
366
+ out += ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
367
+ }
368
+ return out;
369
+ }
370
+ function seed() {
371
+ return Math.floor(Math.random() * 2 ** 31);
372
+ }
373
+ function elementMeta() {
374
+ return {
375
+ seed: seed(),
376
+ version: 1,
377
+ versionNonce: seed(),
378
+ isDeleted: false,
379
+ updated: Date.now()
380
+ };
381
+ }
382
+ function assignIndices(elements) {
383
+ const keys = (0, import_fractional_indexing.generateNKeysBetween)(null, null, elements.length);
384
+ elements.forEach((el, i) => {
385
+ el.index = keys[i];
386
+ });
387
+ return elements;
388
+ }
389
+
390
+ // ../diagram/src/font-metrics.ts
391
+ var EXCALIFONT_ADVANCE = {
392
+ " ": 0.4,
393
+ "!": 0.314,
394
+ '"': 0.371,
395
+ "#": 0.783,
396
+ "$": 0.721,
397
+ "%": 0.928,
398
+ "&": 0.718,
399
+ "'": 0.218,
400
+ "(": 0.441,
401
+ ")": 0.402,
402
+ "*": 0.525,
403
+ "+": 0.55,
404
+ ",": 0.257,
405
+ "-": 0.411,
406
+ ".": 0.274,
407
+ "/": 0.561,
408
+ "0": 0.664,
409
+ "1": 0.427,
410
+ "2": 0.7,
411
+ "3": 0.608,
412
+ "4": 0.585,
413
+ "5": 0.618,
414
+ "6": 0.64,
415
+ "7": 0.558,
416
+ "8": 0.636,
417
+ "9": 0.629,
418
+ ":": 0.264,
419
+ ";": 0.298,
420
+ "<": 0.55,
421
+ "=": 0.55,
422
+ ">": 0.55,
423
+ "?": 0.466,
424
+ "@": 0.829,
425
+ "A": 0.676,
426
+ "B": 0.761,
427
+ "C": 0.629,
428
+ "D": 0.78,
429
+ "E": 0.707,
430
+ "F": 0.661,
431
+ "G": 0.78,
432
+ "H": 0.573,
433
+ "I": 0.545,
434
+ "J": 0.569,
435
+ "K": 0.613,
436
+ "L": 0.543,
437
+ "M": 0.766,
438
+ "N": 0.632,
439
+ "O": 0.767,
440
+ "P": 0.698,
441
+ "Q": 0.768,
442
+ "R": 0.736,
443
+ "S": 0.622,
444
+ "T": 0.857,
445
+ "U": 0.73,
446
+ "V": 0.592,
447
+ "W": 0.786,
448
+ "X": 0.628,
449
+ "Y": 0.564,
450
+ "Z": 0.832,
451
+ "[": 0.472,
452
+ "\\": 0.589,
453
+ "]": 0.497,
454
+ "^": 0.51,
455
+ "_": 0.67,
456
+ "`": 0.6,
457
+ "a": 0.576,
458
+ "b": 0.555,
459
+ "c": 0.504,
460
+ "d": 0.605,
461
+ "e": 0.537,
462
+ "f": 0.497,
463
+ "g": 0.555,
464
+ "h": 0.567,
465
+ "i": 0.244,
466
+ "j": 0.328,
467
+ "k": 0.533,
468
+ "l": 0.225,
469
+ "m": 0.663,
470
+ "n": 0.526,
471
+ "o": 0.6,
472
+ "p": 0.537,
473
+ "q": 0.539,
474
+ "r": 0.412,
475
+ "s": 0.543,
476
+ "t": 0.553,
477
+ "u": 0.548,
478
+ "v": 0.525,
479
+ "w": 0.693,
480
+ "x": 0.591,
481
+ "y": 0.53,
482
+ "z": 0.572,
483
+ "{": 0.504,
484
+ "|": 0.299,
485
+ "}": 0.544,
486
+ "~": 0.669,
487
+ "\xA0": 0.4,
488
+ "\xA1": 0.29,
489
+ "\xA2": 0.489,
490
+ "\xA3": 0.71,
491
+ "\xA5": 0.656,
492
+ "\xA6": 0.299,
493
+ "\xA8": 0.6,
494
+ "\xA9": 0.858,
495
+ "\xAA": 0.422,
496
+ "\xAB": 0.649,
497
+ "\xAD": 0.692,
498
+ "\xAE": 0.65,
499
+ "\xAF": 0.6,
500
+ "\xB0": 0.412,
501
+ "\xB1": 0.55,
502
+ "\xB4": 0.6,
503
+ "\xB6": 0.643,
504
+ "\xB7": 0.2,
505
+ "\xB8": 0.6,
506
+ "\xBA": 0.422,
507
+ "\xBB": 0.668,
508
+ "\xBC": 0.762,
509
+ "\xBD": 0.762,
510
+ "\xBE": 0.868,
511
+ "\xBF": 0.455,
512
+ "\xC0": 0.676,
513
+ "\xC1": 0.676,
514
+ "\xC2": 0.676,
515
+ "\xC3": 0.676,
516
+ "\xC4": 0.676,
517
+ "\xC5": 0.676,
518
+ "\xC6": 1.028,
519
+ "\xC7": 0.629,
520
+ "\xC8": 0.707,
521
+ "\xC9": 0.707,
522
+ "\xCA": 0.707,
523
+ "\xCB": 0.707,
524
+ "\xCC": 0.545,
525
+ "\xCD": 0.545,
526
+ "\xCE": 0.545,
527
+ "\xCF": 0.545,
528
+ "\xD0": 0.815,
529
+ "\xD1": 0.632,
530
+ "\xD2": 0.767,
531
+ "\xD3": 0.767,
532
+ "\xD4": 0.767,
533
+ "\xD5": 0.767,
534
+ "\xD6": 0.767,
535
+ "\xD7": 0.55,
536
+ "\xD8": 0.772,
537
+ "\xD9": 0.73,
538
+ "\xDA": 0.73,
539
+ "\xDB": 0.73,
540
+ "\xDC": 0.73,
541
+ "\xDD": 0.564,
542
+ "\xDE": 0.678,
543
+ "\xDF": 0.573,
544
+ "\xE0": 0.542,
545
+ "\xE1": 0.542,
546
+ "\xE2": 0.542,
547
+ "\xE3": 0.542,
548
+ "\xE4": 0.542,
549
+ "\xE5": 0.542,
550
+ "\xE6": 0.909,
551
+ "\xE7": 0.504,
552
+ "\xE8": 0.551,
553
+ "\xE9": 0.551,
554
+ "\xEA": 0.551,
555
+ "\xEB": 0.551,
556
+ "\xEC": 0.244,
557
+ "\xED": 0.244,
558
+ "\xEE": 0.244,
559
+ "\xEF": 0.244,
560
+ "\xF0": 0.514,
561
+ "\xF1": 0.526,
562
+ "\xF2": 0.561,
563
+ "\xF3": 0.561,
564
+ "\xF4": 0.561,
565
+ "\xF5": 0.561,
566
+ "\xF6": 0.561,
567
+ "\xF7": 0.55,
568
+ "\xF8": 0.617,
569
+ "\xF9": 0.508,
570
+ "\xFA": 0.508,
571
+ "\xFB": 0.508,
572
+ "\xFC": 0.508,
573
+ "\xFD": 0.522,
574
+ "\xFE": 0.568,
575
+ "\xFF": 0.522,
576
+ "\u2013": 0.702,
577
+ "\u2014": 0.935,
578
+ "\u2018": 0.267,
579
+ "\u2019": 0.304,
580
+ "\u201A": 0.239,
581
+ "\u201C": 0.412,
582
+ "\u201D": 0.428,
583
+ "\u201E": 0.428,
584
+ "\u2020": 0.549,
585
+ "\u2022": 0.522,
586
+ "\u2024": 0.274,
587
+ "\u2025": 0.548,
588
+ "\u2026": 0.709,
589
+ "\u2030": 1.298,
590
+ "\u2039": 0.507,
591
+ "\u203A": 0.38,
592
+ "\u2212": 0.55
593
+ };
594
+ var FALLBACK_ADVANCE = 0.5773;
595
+
596
+ // ../diagram/src/theme.ts
597
+ var STROKE = "#1e1e1e";
598
+ var ACCENT_FILL = {
599
+ blue: "#a5d8ff",
600
+ green: "#b2f2bb",
601
+ yellow: "#ffec99",
602
+ red: "#ffc9c9",
603
+ violet: "#d0bfff",
604
+ grey: "#e9ecef",
605
+ none: "transparent"
606
+ };
607
+ function fillFor(accent) {
608
+ return ACCENT_FILL[accent ?? "none"];
609
+ }
610
+ var FONT_SIZE = 20;
611
+ var EDGE_LABEL_FONT_SIZE = 16;
612
+ var FONT_FAMILY = 5;
613
+ var LINE_HEIGHT = 1.25;
614
+ var CONTAINER_PADDING = 16;
615
+ var MAX_LABEL_WIDTH = 220;
616
+ var MIN_NODE_WIDTH = 120;
617
+ var MIN_NODE_HEIGHT = 60;
618
+ var GRID = 20;
619
+ var BASE_STYLE = {
620
+ angle: 0,
621
+ strokeColor: STROKE,
622
+ fillStyle: "solid",
623
+ strokeWidth: 2,
624
+ strokeStyle: "solid",
625
+ roughness: 1,
626
+ opacity: 100,
627
+ groupIds: [],
628
+ frameId: null,
629
+ locked: false,
630
+ link: null
631
+ };
632
+ function snap(n) {
633
+ return Math.ceil(n / GRID) * GRID;
634
+ }
635
+
636
+ // ../diagram/src/text.ts
637
+ function charWidth(ch) {
638
+ return EXCALIFONT_ADVANCE[ch] ?? FALLBACK_ADVANCE;
639
+ }
640
+ function measureLine(line, fontSize) {
641
+ let w = 0;
642
+ for (const ch of line) w += charWidth(ch);
643
+ return w * fontSize;
644
+ }
645
+ function wrapText(text2, fontSize, maxWidth) {
646
+ const paragraphs = text2.split("\n");
647
+ const lines = [];
648
+ for (const paragraph of paragraphs) {
649
+ const words = paragraph.split(/\s+/).filter(Boolean);
650
+ if (words.length === 0) {
651
+ lines.push("");
652
+ continue;
653
+ }
654
+ let current = words[0];
655
+ for (let i = 1; i < words.length; i++) {
656
+ const candidate = `${current} ${words[i]}`;
657
+ if (measureLine(candidate, fontSize) <= maxWidth) {
658
+ current = candidate;
659
+ } else {
660
+ lines.push(current);
661
+ current = words[i];
662
+ }
663
+ }
664
+ lines.push(current);
665
+ }
666
+ return lines;
667
+ }
668
+ function measureText(text2, fontSize, maxWidth) {
669
+ const lines = wrapText(text2, fontSize, maxWidth);
670
+ const width = Math.max(...lines.map((l) => measureLine(l, fontSize)), 0);
671
+ const height = Math.ceil(lines.length * fontSize * LINE_HEIGHT);
672
+ return { lines, width: Math.ceil(width), height };
673
+ }
674
+
675
+ // ../diagram/src/geometry.ts
676
+ function centerOf(b) {
677
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
678
+ }
679
+ function boundaryPoint(box, kind, target) {
680
+ const c = centerOf(box);
681
+ const dx = target.x - c.x;
682
+ const dy = target.y - c.y;
683
+ if (dx === 0 && dy === 0) return c;
684
+ const hw = box.width / 2;
685
+ const hh = box.height / 2;
686
+ if (kind === "ellipse") {
687
+ const denom = Math.hypot(dx / hw, dy / hh);
688
+ return { x: c.x + dx / denom, y: c.y + dy / denom };
689
+ }
690
+ if (kind === "diamond") {
691
+ const t2 = 1 / (Math.abs(dx) / hw + Math.abs(dy) / hh);
692
+ return { x: c.x + dx * t2, y: c.y + dy * t2 };
693
+ }
694
+ const t = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
695
+ return { x: c.x + dx * t, y: c.y + dy * t };
696
+ }
697
+ function retract(from, toward, gap) {
698
+ const dx = from.x - toward.x;
699
+ const dy = from.y - toward.y;
700
+ const len = Math.hypot(dx, dy);
701
+ if (len === 0) return from;
702
+ return { x: from.x + dx / len * gap, y: from.y + dy / len * gap };
703
+ }
704
+ function boxesOverlap(a, b, tolerance = 0) {
705
+ return a.x < b.x + b.width - tolerance && a.x + a.width - tolerance > b.x && a.y < b.y + b.height - tolerance && a.y + a.height - tolerance > b.y;
706
+ }
707
+ function roundPoint(p) {
708
+ return { x: Math.round(p.x), y: Math.round(p.y) };
709
+ }
710
+ function polylineMidpoint(points) {
711
+ if (points.length === 0) return { x: 0, y: 0 };
712
+ if (points.length === 1) return points[0];
713
+ const segments = points.slice(1).map((p, i) => Math.hypot(p.x - points[i].x, p.y - points[i].y));
714
+ const total = segments.reduce((a, b) => a + b, 0);
715
+ if (total === 0) return points[0];
716
+ let travelled = 0;
717
+ for (let i = 0; i < segments.length; i++) {
718
+ if (travelled + segments[i] >= total / 2) {
719
+ const t = (total / 2 - travelled) / segments[i];
720
+ return {
721
+ x: points[i].x + (points[i + 1].x - points[i].x) * t,
722
+ y: points[i].y + (points[i + 1].y - points[i].y) * t
723
+ };
724
+ }
725
+ travelled += segments[i];
726
+ }
727
+ return points[points.length - 1];
728
+ }
729
+ function distanceToSegment(p, a, b) {
730
+ const dx = b.x - a.x;
731
+ const dy = b.y - a.y;
732
+ if (dx === 0 && dy === 0) return Math.hypot(p.x - a.x, p.y - a.y);
733
+ const t = Math.max(
734
+ 0,
735
+ Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy))
736
+ );
737
+ return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy));
738
+ }
739
+ function distanceToPolyline(p, points) {
740
+ if (points.length < 2) return points.length ? Math.hypot(p.x - points[0].x, p.y - points[0].y) : 0;
741
+ let best = Infinity;
742
+ for (let i = 0; i < points.length - 1; i++) {
743
+ best = Math.min(best, distanceToSegment(p, points[i], points[i + 1]));
744
+ }
745
+ return best;
746
+ }
747
+
748
+ // ../diagram/src/elements.ts
749
+ var ROUNDNESS = {
750
+ rectangle: { type: 3 },
751
+ diamond: { type: 2 },
752
+ ellipse: null
753
+ };
754
+ var STROKE_STYLE = {
755
+ solid: "solid",
756
+ dashed: "dashed",
757
+ dotted: "dotted"
758
+ };
759
+ function createLabeledShape(opts) {
760
+ const shapeId = elementId();
761
+ const textId = elementId();
762
+ const metrics = measureText(opts.label, FONT_SIZE, Math.max(opts.width - 16, 40));
763
+ const shape = {
764
+ id: shapeId,
765
+ type: opts.kind,
766
+ x: opts.x,
767
+ y: opts.y,
768
+ width: opts.width,
769
+ height: opts.height,
770
+ ...BASE_STYLE,
771
+ backgroundColor: fillFor(opts.accent),
772
+ roundness: ROUNDNESS[opts.kind],
773
+ boundElements: [{ type: "text", id: textId }],
774
+ ...elementMeta()
775
+ };
776
+ const text2 = {
777
+ id: textId,
778
+ type: "text",
779
+ // Centred within the container; Excalidraw recomputes this on first render.
780
+ x: Math.round(opts.x + (opts.width - metrics.width) / 2),
781
+ y: Math.round(opts.y + (opts.height - metrics.height) / 2),
782
+ width: metrics.width,
783
+ height: metrics.height,
784
+ ...BASE_STYLE,
785
+ backgroundColor: "transparent",
786
+ roundness: null,
787
+ boundElements: null,
788
+ text: metrics.lines.join("\n"),
789
+ originalText: opts.label,
790
+ fontSize: FONT_SIZE,
791
+ fontFamily: FONT_FAMILY,
792
+ textAlign: "center",
793
+ verticalAlign: "middle",
794
+ containerId: shapeId,
795
+ lineHeight: LINE_HEIGHT,
796
+ autoResize: true,
797
+ ...elementMeta()
798
+ };
799
+ return { shape, text: text2 };
800
+ }
801
+ function createArrow(opts) {
802
+ const pts = opts.points.map(roundPoint);
803
+ const [origin, ...rest] = pts;
804
+ const relative = [[0, 0], ...rest.map((p) => [p.x - origin.x, p.y - origin.y])];
805
+ const xs = relative.map((p) => p[0]);
806
+ const ys = relative.map((p) => p[1]);
807
+ return {
808
+ id: elementId(),
809
+ type: "arrow",
810
+ x: origin.x,
811
+ y: origin.y,
812
+ width: Math.max(...xs) - Math.min(...xs),
813
+ height: Math.max(...ys) - Math.min(...ys),
814
+ ...BASE_STYLE,
815
+ backgroundColor: "transparent",
816
+ strokeStyle: STROKE_STYLE[opts.style ?? "solid"],
817
+ roundness: { type: 2 },
818
+ points: relative,
819
+ lastCommittedPoint: null,
820
+ startBinding: { elementId: opts.startElementId, focus: 0, gap: opts.gap ?? 4 },
821
+ endBinding: { elementId: opts.endElementId, focus: 0, gap: opts.gap ?? 4 },
822
+ startArrowhead: null,
823
+ endArrowhead: opts.arrowhead === false ? null : "arrow",
824
+ elbowed: false,
825
+ boundElements: null,
826
+ ...elementMeta()
827
+ };
828
+ }
829
+ function attachArrow(shape, arrowId) {
830
+ const bound = shape.boundElements ?? [];
831
+ bound.push({ type: "arrow", id: arrowId });
832
+ shape.boundElements = bound;
833
+ }
834
+ function createArrowLabel(arrow, label) {
835
+ const metrics = measureText(label, EDGE_LABEL_FONT_SIZE, MAX_LABEL_WIDTH);
836
+ const textId = elementId();
837
+ const bound = arrow.boundElements ?? [];
838
+ bound.push({ type: "text", id: textId });
839
+ arrow.boundElements = bound;
840
+ const relative = arrow.points;
841
+ const absolute = relative.map(([dx, dy]) => ({
842
+ x: arrow.x + dx,
843
+ y: arrow.y + dy
844
+ }));
845
+ const mid = polylineMidpoint(absolute);
846
+ const labelX = Math.round(mid.x - metrics.width / 2);
847
+ const labelY = Math.round(mid.y - metrics.height / 2);
848
+ return {
849
+ id: textId,
850
+ type: "text",
851
+ x: labelX,
852
+ y: labelY,
853
+ width: metrics.width,
854
+ height: metrics.height,
855
+ ...BASE_STYLE,
856
+ strokeColor: STROKE,
857
+ backgroundColor: "transparent",
858
+ roundness: null,
859
+ boundElements: null,
860
+ text: metrics.lines.join("\n"),
861
+ originalText: label,
862
+ fontSize: EDGE_LABEL_FONT_SIZE,
863
+ fontFamily: FONT_FAMILY,
864
+ textAlign: "center",
865
+ verticalAlign: "middle",
866
+ containerId: arrow.id,
867
+ lineHeight: LINE_HEIGHT,
868
+ autoResize: true,
869
+ ...elementMeta()
870
+ };
871
+ }
872
+
873
+ // ../diagram/src/layout.ts
874
+ var import_dagre = __toESM(require("@dagrejs/dagre"));
875
+ function layoutDiagram(spec, sizes, edgeLabels) {
876
+ const g = new import_dagre.default.graphlib.Graph({ multigraph: true });
877
+ g.setGraph({
878
+ rankdir: spec.direction ?? "TB",
879
+ nodesep: spec.spacing?.node ?? 60,
880
+ ranksep: spec.spacing?.rank ?? 90,
881
+ marginx: 40,
882
+ marginy: 40
883
+ });
884
+ g.setDefaultEdgeLabel(() => ({}));
885
+ for (const s of sizes) {
886
+ g.setNode(s.id, { width: s.width, height: s.height });
887
+ }
888
+ spec.edges.forEach((edge, i) => {
889
+ const label = edgeLabels[i];
890
+ g.setEdge(
891
+ edge.from,
892
+ edge.to,
893
+ label ? { width: label.width, height: label.height, labelpos: "c" } : {},
894
+ `e${i}`
895
+ );
896
+ });
897
+ import_dagre.default.layout(g);
898
+ const nodes = /* @__PURE__ */ new Map();
899
+ for (const s of sizes) {
900
+ const n = g.node(s.id);
901
+ nodes.set(s.id, {
902
+ id: s.id,
903
+ x: Math.round(n.x - n.width / 2),
904
+ y: Math.round(n.y - n.height / 2),
905
+ width: n.width,
906
+ height: n.height
907
+ });
908
+ }
909
+ const edges = spec.edges.map((edge, i) => {
910
+ const e = g.edge({ v: edge.from, w: edge.to, name: `e${i}` });
911
+ return {
912
+ from: edge.from,
913
+ to: edge.to,
914
+ points: (e?.points ?? []).map((p) => ({ x: Math.round(p.x), y: Math.round(p.y) }))
915
+ };
916
+ });
917
+ return { nodes, edges };
918
+ }
919
+
920
+ // ../diagram/src/validate.ts
921
+ function validateSpec(spec) {
922
+ const issues = [];
923
+ const ids = /* @__PURE__ */ new Set();
924
+ if (spec.nodes.length === 0) {
925
+ issues.push({ level: "error", message: "Diagram has no nodes." });
926
+ }
927
+ for (const node of spec.nodes) {
928
+ if (ids.has(node.id)) {
929
+ issues.push({ level: "error", message: `Duplicate node id '${node.id}'.` });
930
+ }
931
+ ids.add(node.id);
932
+ if (!node.label?.trim()) {
933
+ issues.push({ level: "warning", message: `Node '${node.id}' has an empty label.` });
934
+ }
935
+ }
936
+ spec.edges.forEach((edge, i) => {
937
+ if (!ids.has(edge.from)) {
938
+ issues.push({ level: "error", message: `Edge ${i} references unknown node '${edge.from}'.` });
939
+ }
940
+ if (!ids.has(edge.to)) {
941
+ issues.push({ level: "error", message: `Edge ${i} references unknown node '${edge.to}'.` });
942
+ }
943
+ });
944
+ const connected = /* @__PURE__ */ new Set();
945
+ for (const e of spec.edges) {
946
+ connected.add(e.from);
947
+ connected.add(e.to);
948
+ }
949
+ for (const node of spec.nodes) {
950
+ if (spec.nodes.length > 1 && !connected.has(node.id)) {
951
+ issues.push({
952
+ level: "warning",
953
+ message: `Node '${node.id}' is not connected to anything \u2014 it will float alone.`
954
+ });
955
+ }
956
+ }
957
+ return issues;
958
+ }
959
+ function validateScene(elements) {
960
+ const issues = [];
961
+ const byId = new Map(elements.map((el) => [el.id, el]));
962
+ if (byId.size !== elements.length) {
963
+ issues.push({ level: "error", message: "Duplicate element ids in scene." });
964
+ }
965
+ for (const el of elements) {
966
+ for (const key of ["startBinding", "endBinding"]) {
967
+ const binding = el[key];
968
+ if (binding && !byId.has(binding.elementId)) {
969
+ issues.push({
970
+ level: "error",
971
+ message: `Arrow ${el.id} ${key} points at missing element ${binding.elementId}.`
972
+ });
973
+ }
974
+ }
975
+ const bound = el.boundElements;
976
+ for (const ref of bound ?? []) {
977
+ if (!byId.has(ref.id)) {
978
+ issues.push({
979
+ level: "error",
980
+ message: `Element ${el.id} boundElements references missing ${ref.id}.`
981
+ });
982
+ }
983
+ }
984
+ const containerId = el.containerId;
985
+ if (containerId) {
986
+ const container = byId.get(containerId);
987
+ if (!container) {
988
+ issues.push({
989
+ level: "error",
990
+ message: `Text ${el.id} has containerId ${containerId}, which does not exist.`
991
+ });
992
+ } else {
993
+ const back = container.boundElements ?? [];
994
+ if (!back.some((r) => r.id === el.id)) {
995
+ issues.push({
996
+ level: "error",
997
+ message: `Text ${el.id} names container ${containerId}, but the container does not list it back.`
998
+ });
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ for (const el of elements) {
1004
+ const containerId = el.containerId;
1005
+ if (el.type !== "text" || !containerId) continue;
1006
+ const container = byId.get(containerId);
1007
+ if (!container) continue;
1008
+ if (["rectangle", "ellipse", "diamond"].includes(container.type)) {
1009
+ if (el.width > container.width - 8) {
1010
+ issues.push({
1011
+ level: "error",
1012
+ message: `Label on ${container.id} is ${Math.round(el.width)}px wide inside a ${Math.round(container.width)}px shape \u2014 it will overflow.`
1013
+ });
1014
+ }
1015
+ }
1016
+ if (container.type === "arrow") {
1017
+ const pts = container.points.map(([dx, dy]) => ({
1018
+ x: container.x + dx,
1019
+ y: container.y + dy
1020
+ }));
1021
+ const centre2 = { x: el.x + el.width / 2, y: el.y + el.height / 2 };
1022
+ const drift = distanceToPolyline(centre2, pts);
1023
+ if (drift > 12) {
1024
+ issues.push({
1025
+ level: "error",
1026
+ message: `Label on arrow ${container.id} sits ${Math.round(drift)}px off the line.`
1027
+ });
1028
+ }
1029
+ }
1030
+ }
1031
+ let previousIndex = "";
1032
+ for (const el of elements) {
1033
+ const index = el.index;
1034
+ if (typeof index !== "string" || index.length === 0) {
1035
+ issues.push({ level: "error", message: `Element ${el.id} is missing a z-order index.` });
1036
+ } else if (index <= previousIndex) {
1037
+ issues.push({
1038
+ level: "error",
1039
+ message: `Element ${el.id} index '${index}' does not follow '${previousIndex}'.`
1040
+ });
1041
+ } else {
1042
+ previousIndex = index;
1043
+ }
1044
+ }
1045
+ const shapes = elements.filter((el) => ["rectangle", "ellipse", "diamond"].includes(el.type));
1046
+ for (let i = 0; i < shapes.length; i++) {
1047
+ for (let j = i + 1; j < shapes.length; j++) {
1048
+ if (boxesOverlap(shapes[i], shapes[j], 2)) {
1049
+ issues.push({
1050
+ level: "error",
1051
+ message: `Shapes ${shapes[i].id} and ${shapes[j].id} overlap.`
1052
+ });
1053
+ }
1054
+ }
1055
+ }
1056
+ return issues;
1057
+ }
1058
+
1059
+ // ../diagram/src/describe.ts
1060
+ var SHAPES = /* @__PURE__ */ new Set(["rectangle", "ellipse", "diamond"]);
1061
+ var ARROW_LABEL_RADIUS = 60;
1062
+ function centre(b) {
1063
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
1064
+ }
1065
+ function contains(box, p) {
1066
+ return p.x >= box.x && p.x <= box.x + box.width && p.y >= box.y && p.y <= box.y + box.height;
1067
+ }
1068
+ function distanceToPath(p, pts) {
1069
+ let best = Infinity;
1070
+ for (let i = 0; i < pts.length - 1; i++) {
1071
+ const a = pts[i];
1072
+ const b = pts[i + 1];
1073
+ const dx = b.x - a.x;
1074
+ const dy = b.y - a.y;
1075
+ const t = dx === 0 && dy === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy)));
1076
+ best = Math.min(best, Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy)));
1077
+ }
1078
+ return best;
1079
+ }
1080
+ function describeScene(elements) {
1081
+ const byId = new Map(elements.map((el) => [el.id, el]));
1082
+ const textOf = (el) => (el?.originalText ?? el?.text)?.replace(/\n/g, " ").trim() ?? "";
1083
+ const boundLabel = (id) => {
1084
+ const owner = byId.get(id);
1085
+ const bound = owner?.boundElements ?? [];
1086
+ const ref = bound.find((b) => b.type === "text");
1087
+ return ref ? textOf(byId.get(ref.id)) : "";
1088
+ };
1089
+ const ownedText = new Set(
1090
+ elements.flatMap(
1091
+ (el) => (el.boundElements ?? []).filter((b) => b.type === "text").map((b) => b.id)
1092
+ )
1093
+ );
1094
+ const floating = elements.filter((el) => el.type === "text" && !ownedText.has(el.id));
1095
+ const adopted = /* @__PURE__ */ new Set();
1096
+ const labelFor = (id) => {
1097
+ const bound = boundLabel(id);
1098
+ if (bound) return bound;
1099
+ const owner = byId.get(id);
1100
+ if (!owner) return "";
1101
+ if (SHAPES.has(owner.type)) {
1102
+ const hit = floating.find((t) => !adopted.has(t.id) && contains(owner, centre(t)));
1103
+ if (hit) {
1104
+ adopted.add(hit.id);
1105
+ return textOf(hit);
1106
+ }
1107
+ return "";
1108
+ }
1109
+ if (owner.type === "arrow") {
1110
+ const pts = (owner.points ?? []).map(([dx, dy]) => ({
1111
+ x: owner.x + dx,
1112
+ y: owner.y + dy
1113
+ }));
1114
+ if (pts.length < 2) return "";
1115
+ let best = null;
1116
+ for (const t of floating) {
1117
+ if (adopted.has(t.id)) continue;
1118
+ const d = distanceToPath(centre(t), pts);
1119
+ if (d <= ARROW_LABEL_RADIUS && (!best || d < best.d)) {
1120
+ best = { id: t.id, text: textOf(t), d };
1121
+ }
1122
+ }
1123
+ if (best) {
1124
+ adopted.add(best.id);
1125
+ return best.text;
1126
+ }
1127
+ }
1128
+ return "";
1129
+ };
1130
+ const counts = {};
1131
+ for (const el of elements) counts[el.type] = (counts[el.type] ?? 0) + 1;
1132
+ const shapes = elements.filter((el) => SHAPES.has(el.type)).map((el) => ({ id: el.id, type: el.type, label: labelFor(el.id) }));
1133
+ const shapeLabel = new Map(shapes.map((s) => [s.id, s.label]));
1134
+ const edges = elements.filter((el) => el.type === "arrow").map((el) => {
1135
+ const start = el.startBinding;
1136
+ const end = el.endBinding;
1137
+ const name = (id) => id ? shapeLabel.get(id) || labelFor(id) || `<${id.slice(0, 6)}>` : "(unbound)";
1138
+ return {
1139
+ from: name(start?.elementId),
1140
+ to: name(end?.elementId),
1141
+ label: labelFor(el.id)
1142
+ };
1143
+ });
1144
+ const looseText = floating.filter((el) => !adopted.has(el.id)).map((el) => textOf(el));
1145
+ return { shapes, edges, looseText, counts };
1146
+ }
1147
+ function formatOutline(outline) {
1148
+ const lines = [];
1149
+ for (const t of outline.looseText) lines.push(`# ${t}`);
1150
+ if (outline.looseText.length) lines.push("");
1151
+ lines.push(`shapes (${outline.shapes.length}):`);
1152
+ for (const s of outline.shapes) lines.push(` [${s.type}] ${s.label || "(no label)"}`);
1153
+ lines.push("");
1154
+ lines.push(`edges (${outline.edges.length}):`);
1155
+ for (const e of outline.edges) {
1156
+ lines.push(` ${e.from} -> ${e.to}${e.label ? ` "${e.label}"` : ""}`);
1157
+ }
1158
+ lines.push("");
1159
+ lines.push(
1160
+ "element counts: " + Object.entries(outline.counts).map(([k, v]) => `${k}=${v}`).join(" ")
1161
+ );
1162
+ return lines.join("\n");
1163
+ }
1164
+
1165
+ // ../diagram/src/index.ts
1166
+ var ARROW_GAP = 4;
1167
+ var TITLE_FONT_SIZE = 28;
1168
+ function shapePadding(kind) {
1169
+ if (kind === "ellipse") return CONTAINER_PADDING * 2.2;
1170
+ if (kind === "diamond") return CONTAINER_PADDING * 2.6;
1171
+ return CONTAINER_PADDING;
1172
+ }
1173
+ var WIDTH_SAFETY = 1.02;
1174
+ function sizeNode(label, kind) {
1175
+ const metrics = measureText(label, FONT_SIZE, MAX_LABEL_WIDTH);
1176
+ const pad = shapePadding(kind);
1177
+ return {
1178
+ width: Math.max(snap(metrics.width * WIDTH_SAFETY + pad * 2), MIN_NODE_WIDTH),
1179
+ height: Math.max(snap(metrics.height + pad * 2), MIN_NODE_HEIGHT)
1180
+ };
1181
+ }
1182
+ function buildDiagram(spec) {
1183
+ const specIssues = validateSpec(spec);
1184
+ if (specIssues.some((i) => i.level === "error")) {
1185
+ return { scene: emptyScene(), issues: specIssues };
1186
+ }
1187
+ const kinds = /* @__PURE__ */ new Map();
1188
+ const sizes = spec.nodes.map((node) => {
1189
+ const kind = node.shape ?? "rectangle";
1190
+ kinds.set(node.id, kind);
1191
+ return { id: node.id, ...sizeNode(node.label, kind) };
1192
+ });
1193
+ const edgeLabels = spec.edges.map((edge) => {
1194
+ if (!edge.label) return null;
1195
+ const m = measureText(edge.label, EDGE_LABEL_FONT_SIZE, MAX_LABEL_WIDTH);
1196
+ return { width: m.width + 12, height: m.height + 8 };
1197
+ });
1198
+ const layout = layoutDiagram(spec, sizes, edgeLabels);
1199
+ const elements = [];
1200
+ const shapesById = /* @__PURE__ */ new Map();
1201
+ for (const node of spec.nodes) {
1202
+ const placed = layout.nodes.get(node.id);
1203
+ const { shape, text: text2 } = createLabeledShape({
1204
+ kind: kinds.get(node.id),
1205
+ accent: node.accent,
1206
+ label: node.label,
1207
+ x: placed.x,
1208
+ y: placed.y,
1209
+ width: placed.width,
1210
+ height: placed.height
1211
+ });
1212
+ shapesById.set(node.id, shape);
1213
+ elements.push(shape, text2);
1214
+ }
1215
+ const arrowElements = [];
1216
+ spec.edges.forEach((edge, i) => {
1217
+ const fromShape = shapesById.get(edge.from);
1218
+ const toShape = shapesById.get(edge.to);
1219
+ const fromBox = layout.nodes.get(edge.from);
1220
+ const toBox = layout.nodes.get(edge.to);
1221
+ const routed = layout.edges[i].points;
1222
+ const interior = routed.length > 2 ? routed.slice(1, -1) : [];
1223
+ const towardTarget = interior[0] ?? centerOf(toBox);
1224
+ const towardSource = interior[interior.length - 1] ?? centerOf(fromBox);
1225
+ const rawStart = boundaryPoint(fromBox, kinds.get(edge.from), towardTarget);
1226
+ const rawEnd = boundaryPoint(toBox, kinds.get(edge.to), towardSource);
1227
+ const start = retract(rawStart, centerOf(fromBox), ARROW_GAP);
1228
+ const end = retract(rawEnd, centerOf(toBox), ARROW_GAP);
1229
+ const arrow = createArrow({
1230
+ points: [start, ...interior, end],
1231
+ startElementId: fromShape.id,
1232
+ endElementId: toShape.id,
1233
+ style: edge.style,
1234
+ arrowhead: edge.arrowhead,
1235
+ gap: ARROW_GAP
1236
+ });
1237
+ attachArrow(fromShape, arrow.id);
1238
+ attachArrow(toShape, arrow.id);
1239
+ arrowElements.push(arrow);
1240
+ if (edge.label) {
1241
+ arrowElements.push(createArrowLabel(arrow, edge.label));
1242
+ }
1243
+ });
1244
+ elements.push(...arrowElements);
1245
+ if (spec.title) {
1246
+ elements.unshift(createTitle(spec.title, elements));
1247
+ }
1248
+ return { scene: toScene(elements), issues: [...specIssues, ...validateScene(elements)] };
1249
+ }
1250
+ function createTitle(title, elements) {
1251
+ const minX = Math.min(...elements.map((e) => e.x));
1252
+ const minY = Math.min(...elements.map((e) => e.y));
1253
+ const metrics = measureText(title, TITLE_FONT_SIZE, 600);
1254
+ return {
1255
+ id: elementId(),
1256
+ type: "text",
1257
+ x: minX,
1258
+ y: Math.round(minY - metrics.height - 32),
1259
+ width: metrics.width,
1260
+ height: metrics.height,
1261
+ ...BASE_STYLE,
1262
+ backgroundColor: "transparent",
1263
+ roundness: null,
1264
+ boundElements: null,
1265
+ text: metrics.lines.join("\n"),
1266
+ originalText: title,
1267
+ fontSize: TITLE_FONT_SIZE,
1268
+ fontFamily: FONT_FAMILY,
1269
+ textAlign: "left",
1270
+ verticalAlign: "top",
1271
+ containerId: null,
1272
+ lineHeight: LINE_HEIGHT,
1273
+ autoResize: true,
1274
+ ...elementMeta()
1275
+ };
1276
+ }
1277
+ function toScene(elements) {
1278
+ assignIndices(elements);
1279
+ return {
1280
+ type: "excalidraw",
1281
+ version: 2,
1282
+ source: "https://drawpro.kithly.app",
1283
+ elements,
1284
+ appState: { viewBackgroundColor: "#ffffff", gridSize: null },
1285
+ files: {}
1286
+ };
1287
+ }
1288
+ function emptyScene() {
1289
+ return toScene([]);
1290
+ }
1291
+
1292
+ // src/server.ts
1293
+ var BASE_URL = process.env.DRAWPRO_URL ?? "https://drawpro.kithly.app/api";
1294
+ var APP_URL = BASE_URL.replace(/\/api\/?$/, "");
1295
+ var ConfigError = class extends Error {
1296
+ };
1297
+ var clientInstance = null;
1298
+ function api() {
1299
+ if (!clientInstance) {
1300
+ const token = process.env.DRAWPRO_TOKEN;
1301
+ if (!token) {
1302
+ throw new ConfigError(
1303
+ 'DRAWPRO_TOKEN is not set. Create a token in DrawPro under "Connect to Claude Code", then:\n claude mcp add drawpro -e DRAWPRO_TOKEN="dp_live_..." -- npx -y @drawpro/mcp'
1304
+ );
1305
+ }
1306
+ clientInstance = new DrawProClient(BASE_URL, token);
1307
+ }
1308
+ return clientInstance;
1309
+ }
1310
+ var cachedUser = null;
1311
+ async function currentUser() {
1312
+ if (!cachedUser) cachedUser = await api().me();
1313
+ return cachedUser;
1314
+ }
1315
+ async function unlockedKey() {
1316
+ const user = await currentUser();
1317
+ if (!user.encryptedPrivateKey) {
1318
+ return { error: "This account has no encryption keys set up." };
1319
+ }
1320
+ const key = loadKey(user.email);
1321
+ if (!key) {
1322
+ return {
1323
+ error: "This account is locked, so names and contents cannot be read. Ask the user to run `DRAWPRO_TOKEN=... npx -y @drawpro/mcp login` in a terminal. It prompts for their passcode and stores the derived key in the OS keychain. Never ask the user for their passcode here \u2014 it must not pass through this conversation."
1324
+ };
1325
+ }
1326
+ return { key };
1327
+ }
1328
+ function text(body) {
1329
+ return { content: [{ type: "text", text: body }] };
1330
+ }
1331
+ function sheetUrl(workspaceId, sheetId) {
1332
+ return `${APP_URL}/workspace/${workspaceId}/sheet/${sheetId}`;
1333
+ }
1334
+ var server = new import_mcp.McpServer({ name: "drawpro", version: "0.0.1" });
1335
+ server.tool(
1336
+ "list_workspaces",
1337
+ "List the DrawPro workspaces this account can access. Workspace names are encrypted at rest and are only readable once the account is unlocked.",
1338
+ {},
1339
+ async () => {
1340
+ const workspaces = await api().listWorkspaces();
1341
+ const unlocked = await unlockedKey();
1342
+ const rows = await Promise.all(
1343
+ workspaces.map(async (ws) => {
1344
+ const name = "key" in unlocked ? await api().readName(ws.encryptedName, unlocked.key) : null;
1345
+ return `${ws.id} ${name ?? ws.name} (${ws.sheetsCount ?? "?"} sheets)`;
1346
+ })
1347
+ );
1348
+ const note = "error" in unlocked ? `
1349
+
1350
+ Names are encrypted. ${unlocked.error}` : "";
1351
+ return text(rows.join("\n") + note);
1352
+ }
1353
+ );
1354
+ server.tool(
1355
+ "list_sheets",
1356
+ "List the sheets in a DrawPro workspace, with their decrypted names.",
1357
+ { workspace_id: import_zod.z.string().describe("Workspace id, from list_workspaces") },
1358
+ async ({ workspace_id }) => {
1359
+ const sheets = await api().listSheets(workspace_id);
1360
+ const unlocked = await unlockedKey();
1361
+ const rows = await Promise.all(
1362
+ sheets.map(async (s) => {
1363
+ const name = "key" in unlocked ? await api().readName(s.encryptedData, unlocked.key) : null;
1364
+ return `${s.id} ${name ?? s.name} updated ${s.updatedAt}`;
1365
+ })
1366
+ );
1367
+ const note = "error" in unlocked ? `
1368
+
1369
+ Names are encrypted. ${unlocked.error}` : "";
1370
+ return text((rows.join("\n") || "(no sheets)") + note);
1371
+ }
1372
+ );
1373
+ server.tool(
1374
+ "read_sheet",
1375
+ "Read what a DrawPro sheet contains: its shapes, and which arrows connect what. Returns a readable outline rather than raw Excalidraw JSON, which is mostly coordinates and style.",
1376
+ {
1377
+ workspace_id: import_zod.z.string(),
1378
+ sheet_id: import_zod.z.string()
1379
+ },
1380
+ async ({ workspace_id, sheet_id }) => {
1381
+ const unlocked = await unlockedKey();
1382
+ if ("error" in unlocked) return text(unlocked.error);
1383
+ const scene = await api().readSheet(workspace_id, sheet_id, unlocked.key);
1384
+ const outline = describeScene(scene.elements);
1385
+ return text(
1386
+ `sheet: ${scene.name}
1387
+ elements: ${scene.elements.length}
1388
+
1389
+ ${formatOutline(outline)}`
1390
+ );
1391
+ }
1392
+ );
1393
+ var specShape = {
1394
+ spec: import_zod.z.object({
1395
+ title: import_zod.z.string().optional(),
1396
+ direction: import_zod.z.enum(["TB", "BT", "LR", "RL"]).optional(),
1397
+ nodes: import_zod.z.array(
1398
+ import_zod.z.object({
1399
+ id: import_zod.z.string(),
1400
+ label: import_zod.z.string(),
1401
+ shape: import_zod.z.enum(["rectangle", "ellipse", "diamond"]).optional(),
1402
+ accent: import_zod.z.enum(["blue", "green", "yellow", "red", "violet", "grey", "none"]).optional()
1403
+ })
1404
+ ),
1405
+ edges: import_zod.z.array(
1406
+ import_zod.z.object({
1407
+ from: import_zod.z.string(),
1408
+ to: import_zod.z.string(),
1409
+ label: import_zod.z.string().optional(),
1410
+ style: import_zod.z.enum(["solid", "dashed", "dotted"]).optional(),
1411
+ arrowhead: import_zod.z.boolean().optional()
1412
+ })
1413
+ )
1414
+ }).describe(
1415
+ "What connects to what. Layout, sizing, text wrapping, and arrow binding are derived \u2014 never supply coordinates."
1416
+ )
1417
+ };
1418
+ server.tool(
1419
+ "validate_spec",
1420
+ "Check a diagram spec without creating anything. Use this before writing if the diagram is large or the spec was assembled programmatically.",
1421
+ specShape,
1422
+ async ({ spec }) => {
1423
+ const issues = validateSpec(spec);
1424
+ if (issues.length === 0) return text("Spec is valid.");
1425
+ return text(issues.map((i) => `${i.level}: ${i.message}`).join("\n"));
1426
+ }
1427
+ );
1428
+ function buildOrExplain(spec) {
1429
+ const { scene, issues } = buildDiagram(spec);
1430
+ const errors = issues.filter((i) => i.level === "error");
1431
+ if (errors.length > 0) {
1432
+ return {
1433
+ ok: false,
1434
+ error: "The diagram was not created. Fix the spec and try again:\n" + errors.map((i) => ` error: ${i.message}`).join("\n")
1435
+ };
1436
+ }
1437
+ const warnings = issues.filter((i) => i.level === "warning");
1438
+ return { ok: true, scene, warnings: warnings.map((w) => w.message) };
1439
+ }
1440
+ server.tool(
1441
+ "create_diagram",
1442
+ "Create a new sheet in DrawPro from a diagram spec. Returns a link to open it.",
1443
+ {
1444
+ workspace_id: import_zod.z.string().describe("Workspace id, from list_workspaces"),
1445
+ name: import_zod.z.string().describe("Sheet name, shown in the dashboard"),
1446
+ ...specShape
1447
+ },
1448
+ async ({ workspace_id, name, spec }) => {
1449
+ const built = buildOrExplain(spec);
1450
+ if (!built.ok) return text(built.error);
1451
+ const user = await currentUser();
1452
+ const sheet = await api().createSheet(
1453
+ workspace_id,
1454
+ { name, elements: built.scene.elements, appState: built.scene.appState },
1455
+ user.publicKey
1456
+ );
1457
+ const warned = built.warnings.length ? `
1458
+
1459
+ warnings:
1460
+ ${built.warnings.map((w) => ` ${w}`).join("\n")}` : "";
1461
+ return text(
1462
+ `Created "${name}" with ${built.scene.elements.length} elements.
1463
+ ${sheetUrl(workspace_id, sheet.id)}${warned}`
1464
+ );
1465
+ }
1466
+ );
1467
+ server.tool(
1468
+ "update_diagram",
1469
+ "Replace a sheet\u2019s contents with a new diagram. This overwrites the whole sheet, so read_sheet first if you intend to preserve anything already there.",
1470
+ {
1471
+ workspace_id: import_zod.z.string(),
1472
+ sheet_id: import_zod.z.string(),
1473
+ name: import_zod.z.string().describe("Sheet name; the existing name is inside the encrypted blob"),
1474
+ ...specShape
1475
+ },
1476
+ async ({ workspace_id, sheet_id, name, spec }) => {
1477
+ const built = buildOrExplain(spec);
1478
+ if (!built.ok) return text(built.error);
1479
+ const user = await currentUser();
1480
+ await api().updateSheet(
1481
+ workspace_id,
1482
+ sheet_id,
1483
+ { name, elements: built.scene.elements, appState: built.scene.appState },
1484
+ user.publicKey
1485
+ );
1486
+ const warned = built.warnings.length ? `
1487
+
1488
+ warnings:
1489
+ ${built.warnings.map((w) => ` ${w}`).join("\n")}` : "";
1490
+ return text(
1491
+ `Updated "${name}" to ${built.scene.elements.length} elements.
1492
+ ${sheetUrl(workspace_id, sheet_id)}${warned}`
1493
+ );
1494
+ }
1495
+ );
1496
+ async function login(forget) {
1497
+ const user = await currentUser();
1498
+ if (forget) {
1499
+ forgetKey(user.email);
1500
+ console.log(`Forgot the stored key for ${user.email}.`);
1501
+ return;
1502
+ }
1503
+ if (!user.encryptedPrivateKey || !user.salt) {
1504
+ console.log(`${user.email} has no encryption keys \u2014 nothing to unlock.`);
1505
+ return;
1506
+ }
1507
+ if (loadKey(user.email)) {
1508
+ console.log(`${user.email} is already unlocked. Use \`login --forget\` to clear it.`);
1509
+ return;
1510
+ }
1511
+ const passcode = await askHidden("passcode: ");
1512
+ process.stdout.write("deriving key (argon2id, 128 MB)... ");
1513
+ const started = Date.now();
1514
+ let key;
1515
+ try {
1516
+ key = await decryptPrivateKey(user.encryptedPrivateKey, passcode, user.salt);
1517
+ } catch {
1518
+ console.log("");
1519
+ throw new ConfigError("Incorrect passcode.");
1520
+ }
1521
+ console.log(`${Date.now() - started} ms`);
1522
+ const { location } = storeKey(user.email, key);
1523
+ console.log(`Unlocked ${user.email}. Key stored in the ${location}.`);
1524
+ console.log("Claude can now read your sheets. The passcode was not stored or sent anywhere.");
1525
+ }
1526
+ async function main() {
1527
+ const command = process.argv[2];
1528
+ if (command === "login") {
1529
+ await login(process.argv.includes("--forget"));
1530
+ return;
1531
+ }
1532
+ if (command && command !== "serve") {
1533
+ console.error(`Unknown command "${command}". Use: drawpro-mcp [serve|login] [--forget]`);
1534
+ process.exit(2);
1535
+ }
1536
+ await server.connect(new import_stdio.StdioServerTransport());
1537
+ }
1538
+ main().catch((err) => {
1539
+ if (err instanceof ConfigError) {
1540
+ console.error(err.message);
1541
+ process.exit(2);
1542
+ }
1543
+ console.error("[drawpro-mcp]", err.message);
1544
+ process.exit(1);
1545
+ });