drafted 1.11.36 → 1.11.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp/server.mjs CHANGED
@@ -19,7 +19,7 @@ import { z } from 'zod';
19
19
  import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
20
20
  import WebSocket from 'ws';
21
21
  import { LAYERS } from '../src/shared/constants.mjs';
22
- import { emptyExcalidrawScene, excalidrawSceneFromMermaid, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
22
+ import { emptyExcalidrawScene, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
23
23
  import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
24
24
  import { loadPersistedProject, savePersistedProject } from './active-project-store.mjs';
25
25
 
@@ -515,26 +515,89 @@ function mcpMode() {
515
515
  return process.argv.includes('--http') ? 'http' : 'stdio';
516
516
  }
517
517
 
518
+ // ponytail: reports that fail to send (e.g. server unreachable) queue here on disk
519
+ // instead of dropping silently, and get retried on the next tool call.
520
+ const PENDING_REPORTS_PATH = () => join(homedir(), '.drafted', 'pending-reports.jsonl');
521
+ const MAX_PENDING_REPORTS = 50;
522
+
523
+ function readPendingReports() {
524
+ try {
525
+ return readFileSync(PENDING_REPORTS_PATH(), 'utf8')
526
+ .split('\n')
527
+ .filter(Boolean)
528
+ .map((line) => { try { return JSON.parse(line); } catch { return null; } })
529
+ .filter(Boolean);
530
+ } catch {
531
+ return [];
532
+ }
533
+ }
534
+
535
+ function writePendingReports(reports) {
536
+ const reportsPath = PENDING_REPORTS_PATH();
537
+ if (!reports.length) {
538
+ try { unlinkSync(reportsPath); } catch { /* nothing to remove */ }
539
+ return;
540
+ }
541
+ try {
542
+ mkdirSync(dirname(reportsPath), { recursive: true });
543
+ writeFileSync(reportsPath, reports.slice(-MAX_PENDING_REPORTS).map((r) => JSON.stringify(r)).join('\n') + '\n', { mode: 0o600 });
544
+ } catch { /* best effort */ }
545
+ }
546
+
547
+ async function flushPendingReports(serverUrl) {
548
+ const pending = readPendingReports();
549
+ if (!pending.length) return;
550
+ const stillPending = [];
551
+ for (const body of pending) {
552
+ try {
553
+ await fetch(`${serverUrl}/api/installations/report`, {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
556
+ body: JSON.stringify(body),
557
+ });
558
+ } catch {
559
+ stillPending.push(body);
560
+ }
561
+ }
562
+ writePendingReports(stillPending);
563
+ }
564
+
565
+ // Serializes all report sends/flushes so rapid back-to-back tool calls (the common
566
+ // case during an outage, when a client retries repeatedly) don't race on a
567
+ // read-modify-write of the same pending-reports file and drop queued events.
568
+ let installReportChain = Promise.resolve();
569
+
570
+ async function sendInstallationReport(serverUrl, body) {
571
+ await flushPendingReports(serverUrl);
572
+ try {
573
+ await fetch(`${serverUrl}/api/installations/report`, {
574
+ method: 'POST',
575
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
576
+ body: JSON.stringify(body),
577
+ });
578
+ } catch {
579
+ writePendingReports([...readPendingReports(), body]);
580
+ }
581
+ }
582
+
518
583
  function reportInstallationEvent(event, extra = {}) {
519
584
  const info = getInstallInfo();
520
585
  if (!info) return;
521
- fetch(`${getServerUrl()}/api/installations/report`, {
522
- method: 'POST',
523
- headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
524
- body: JSON.stringify({
525
- installId: info.installId,
526
- event,
527
- schemaVersion: 1,
528
- cliVersion: PACKAGE_VERSION,
529
- osFamily: osFamily(),
530
- osVersion: osRelease().slice(0, 60),
531
- arch: normalizedArch(),
532
- nodeVersion: process.version,
533
- mcpMode: mcpMode(),
534
- source: 'mcp',
535
- ...extra,
536
- }),
537
- }).catch(() => {});
586
+ const serverUrl = getServerUrl();
587
+ const body = {
588
+ installId: info.installId,
589
+ event,
590
+ schemaVersion: 1,
591
+ cliVersion: PACKAGE_VERSION,
592
+ osFamily: osFamily(),
593
+ osVersion: osRelease().slice(0, 60),
594
+ arch: normalizedArch(),
595
+ nodeVersion: process.version,
596
+ mcpMode: mcpMode(),
597
+ source: 'mcp',
598
+ ...extra,
599
+ };
600
+ installReportChain = installReportChain.then(() => sendInstallationReport(serverUrl, body)).catch(() => {});
538
601
  }
539
602
 
540
603
  function classifyMcpError(error) {
@@ -2046,7 +2109,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
2046
2109
  content: z.string().optional().describe('[write] HTML/markdown/text for Drafted inline frames. [write_doc_content|append_doc_content] native Google Doc body text. Do not use action=write content to populate Google Doc/Slide frames.'),
2047
2110
  excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
2048
2111
  state: z.any().optional().describe('[set_state] App-frame state object (JSON) to persist for a deployed windowType:"app" frame — e.g. {specText:"..."} for the AS/NZS electrical app. The canvas hydrates the app from this on load (the host posts a "hydrate" message with it when the frame mounts), so you can deploy a generic app frame once and drive it with data afterwards. Max 64KB. Frame must be an app frame.'),
2049
- mermaid: z.string().optional().describe('[write_excalidraw] Mermaid source to convert into an editable Excalidraw scene.'),
2050
2112
  file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
2051
2113
  base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
2052
2114
  content_type: z.string().optional().describe('[write + base64] MIME type for base64 binary content, e.g. image/png, application/pdf. Defaults from the path extension or application/octet-stream.'),
@@ -2438,13 +2500,12 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
2438
2500
  });
2439
2501
  }
2440
2502
  case 'write_excalidraw': {
2441
- const { path, excalidraw_data, mermaid, width, height, color } = args;
2503
+ const { path, excalidraw_data, width, height, color } = args;
2442
2504
  if (!path) throw new Error('path required for action=write_excalidraw');
2443
- if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
2444
2505
  const parts = path.replace(/^\/+/, '').split('/');
2445
2506
  if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
2446
2507
  const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
2447
- const scene = mermaid ? await excalidrawSceneFromMermaid(mermaid) : (excalidraw_data ?? emptyExcalidrawScene());
2508
+ const scene = excalidraw_data ?? emptyExcalidrawScene();
2448
2509
  const body = { content: stringifyExcalidrawScene(scene) };
2449
2510
  if (width) body.width = width;
2450
2511
  if (height) body.height = height;
@@ -3462,13 +3523,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3462
3523
  // `LINE+ID|content`). The server applies the ops via the same
3463
3524
  // hashline algorithm — no client-side re-hashing, no algorithm drift.
3464
3525
  case 'write_excalidraw': {
3465
- const { path, excalidraw_data, mermaid, width, height, color } = args;
3526
+ const { path, excalidraw_data, width, height, color } = args;
3466
3527
  if (!path) throw new Error('path required for action=write_excalidraw');
3467
- if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
3468
3528
  const parts = path.replace(/^\/+/, '').split('/');
3469
3529
  if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
3470
3530
  const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
3471
- const scene = mermaid ? await excalidrawSceneFromMermaid(mermaid) : (excalidraw_data ?? emptyExcalidrawScene());
3531
+ const scene = excalidraw_data ?? emptyExcalidrawScene();
3472
3532
  const body = { content: stringifyExcalidrawScene(scene) };
3473
3533
  if (width) body.width = width;
3474
3534
  if (height) body.height = height;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.36",
3
+ "version": "1.11.38",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [
@@ -45,7 +45,6 @@
45
45
  "@clerk/express": "^2.1.4",
46
46
  "@clerk/mcp-tools": "^0.3.1",
47
47
  "@excalidraw/excalidraw": "^0.18.1",
48
- "@excalidraw/mermaid-to-excalidraw": "^2.2.2",
49
48
  "@modelcontextprotocol/ext-apps": "^1.6.0",
50
49
  "@modelcontextprotocol/sdk": "^1.27.1",
51
50
  "@sentry/browser": "^10.53.1",
@@ -59,7 +58,6 @@
59
58
  "esbuild": "^0.28.0",
60
59
  "exceljs": "^4.4.0",
61
60
  "express": "^4.18.2",
62
- "jsdom": "^27.0.1",
63
61
  "jszip": "^3.10.1",
64
62
  "lucide": "^1.16.0",
65
63
  "mdast-util-from-markdown": "^2.0.3",
@@ -38,64 +38,3 @@ export function normalizeExcalidrawScene(input) {
38
38
  export function stringifyExcalidrawScene(input) {
39
39
  return JSON.stringify(normalizeExcalidrawScene(input), null, 2);
40
40
  }
41
-
42
- function restoreMermaidDom(previous, dom) {
43
- for (const [key, value] of Object.entries(previous)) {
44
- if (value === undefined) {
45
- try { delete globalThis[key]; } catch {}
46
- } else if (key === 'navigator') {
47
- Object.defineProperty(globalThis, 'navigator', { value, configurable: true });
48
- } else {
49
- globalThis[key] = value;
50
- }
51
- }
52
- if (dom) dom.window.close();
53
- }
54
-
55
- export async function excalidrawSceneFromMermaid(mermaid) {
56
- let dom;
57
- const previous = {
58
- window: globalThis.window,
59
- document: globalThis.document,
60
- DOMParser: globalThis.DOMParser,
61
- XMLSerializer: globalThis.XMLSerializer,
62
- Element: globalThis.Element,
63
- SVGElement: globalThis.SVGElement,
64
- navigator: globalThis.navigator,
65
- CSSStyleSheet: globalThis.CSSStyleSheet,
66
- };
67
- try {
68
- if (typeof document === 'undefined') {
69
- const { JSDOM } = await import('jsdom');
70
- dom = new JSDOM('<!doctype html><html><body></body></html>');
71
- globalThis.window = dom.window;
72
- globalThis.document = dom.window.document;
73
- globalThis.DOMParser = dom.window.DOMParser;
74
- globalThis.XMLSerializer = dom.window.XMLSerializer;
75
- globalThis.Element = dom.window.Element;
76
- globalThis.SVGElement = dom.window.SVGElement;
77
- if (globalThis.SVGElement && !globalThis.SVGElement.prototype.getBBox) {
78
- globalThis.SVGElement.prototype.getBBox = function () {
79
- const text = this.textContent || '';
80
- return { x: 0, y: 0, width: Math.max(24, text.length * 8), height: 20 };
81
- };
82
- }
83
- Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true });
84
- globalThis.CSSStyleSheet = class {
85
- constructor() { this.cssRules = []; }
86
- replaceSync() { this.cssRules = []; }
87
- insertRule(rule) { this.cssRules.push({ cssText: rule }); }
88
- };
89
- }
90
- const { parseMermaidToExcalidraw } = await import('@excalidraw/mermaid-to-excalidraw');
91
- const result = await parseMermaidToExcalidraw(mermaid);
92
- return {
93
- ...emptyExcalidrawScene(),
94
- elements: result.elements || [],
95
- files: result.files || {},
96
- appState: { viewBackgroundColor: '#ffffff' },
97
- };
98
- } finally {
99
- if (dom) restoreMermaidDom(previous, dom);
100
- }
101
- }