drafted 1.7.18 → 1.7.20

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/install-mcp.sh CHANGED
@@ -215,8 +215,10 @@ export PATH="$HOME/.drafted/npm-global/bin:$PATH"
215
215
 
216
216
  step "Installing Drafted"
217
217
 
218
- npm install -g drafted@latest --force --silent 2>/dev/null
218
+ npm install -g drafted@latest --force
219
219
  hash -r 2>/dev/null || true
220
+ NPM_ROOT="$(npm root -g)"
221
+ node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$NPM_ROOT/drafted/mcp/server.mjs"
220
222
  ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
221
223
 
222
224
  # ── Configure ─────────────────────────────────────────────────────
@@ -586,6 +588,7 @@ struct DraftedUpdaterApp: App {
586
588
  var body: some Scene {
587
589
  MenuBarExtra {
588
590
  Button("Update Drafted") { Actions.updateDrafted() }
591
+ Button("View Logs") { Actions.viewLogs() }
589
592
  Button("Open Drafted") { Actions.openDrafted() }
590
593
  Divider()
591
594
  Button("Quit") { NSApp.terminate(nil) }
@@ -735,6 +738,47 @@ struct Actions {
735
738
  }
736
739
  }
737
740
 
741
+ static func viewLogs() {
742
+ let fm = FileManager.default
743
+ let home = fm.homeDirectoryForCurrentUser
744
+ let fallback = home.appendingPathComponent(".drafted")
745
+ try? fm.createDirectory(at: fallback, withIntermediateDirectories: true)
746
+ let readme = fallback.appendingPathComponent("logs-readme.txt")
747
+ if !fm.fileExists(atPath: readme.path) {
748
+ let text = """
749
+ Drafted MCP logs are written by the host app that launched drafted-mcp.
750
+
751
+ Most useful locations:
752
+ - Drafted MCP client errors: ~/.drafted/mcp-client.log
753
+ - Claude Desktop: ~/Library/Logs/Claude/mcp-server-drafted.log
754
+ - Claude Code: ~/.claude/projects (session transcripts) or ~/.claude/logs when present
755
+ - Drafted installer/updater: ~/.drafted
756
+
757
+ If a tool returns `fetch failed`, open the Claude log or transcript from the same run and look for `mcp-server-drafted` or `Drafted MCP`.
758
+ """
759
+ try? text.write(to: readme, atomically: true, encoding: .utf8)
760
+ }
761
+
762
+ let candidates = [
763
+ home.appendingPathComponent(".drafted/mcp-client.log"),
764
+ home.appendingPathComponent("Library/Logs/Claude/mcp-server-drafted.log"),
765
+ home.appendingPathComponent("Library/Logs/Claude"),
766
+ home.appendingPathComponent(".claude/logs"),
767
+ home.appendingPathComponent(".claude/projects"),
768
+ fallback,
769
+ ]
770
+ for url in candidates where fm.fileExists(atPath: url.path) {
771
+ var isDirectory: ObjCBool = false
772
+ fm.fileExists(atPath: url.path, isDirectory: &isDirectory)
773
+ if isDirectory.boolValue {
774
+ NSWorkspace.shared.open(url)
775
+ } else {
776
+ NSWorkspace.shared.activateFileViewerSelecting([url])
777
+ }
778
+ return
779
+ }
780
+ }
781
+
738
782
  static func openDrafted() {
739
783
  NSWorkspace.shared.open(URL(string: "https://drafted.live")!)
740
784
  }
package/mcp/server.mjs CHANGED
@@ -10,9 +10,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
11
  import { execFile } from 'child_process';
12
12
  import { createHash } from 'node:crypto';
13
- import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
13
+ import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync, unlinkSync, appendFileSync } from 'fs';
14
14
  import { join, dirname, basename, extname, resolve } from 'path';
15
- import { homedir } from 'os';
15
+ import { homedir, platform, release as osRelease, arch as osArch } from 'os';
16
16
  import { fileURLToPath } from 'url';
17
17
  import { AsyncLocalStorage } from 'node:async_hooks';
18
18
  import { z } from 'zod';
@@ -20,7 +20,21 @@ import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/e
20
20
  import WebSocket from 'ws';
21
21
  import { LAYERS } from '../src/shared/constants.mjs';
22
22
  import { emptyExcalidrawScene, excalidrawSceneFromMermaid, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
23
- import { UMAMI_EVENTS, trackUmamiEvent } from '../server/lib/umami.mjs';
23
+ const { UMAMI_EVENTS, trackUmamiEvent } = await (async () => {
24
+ try {
25
+ return await import('../server/lib/umami.mjs');
26
+ } catch {
27
+ return {
28
+ UMAMI_EVENTS: Object.freeze({
29
+ MCP_CONNECTED: 'mcp_connected',
30
+ MCP_TOOL_CALLED: 'mcp_tool_called',
31
+ DRAFTED_MCP_REQUEST: 'drafted_mcp_request',
32
+ DRAFTED_MCP_ERROR: 'drafted_mcp_error',
33
+ }),
34
+ trackUmamiEvent: () => {},
35
+ };
36
+ }
37
+ })();
24
38
 
25
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
40
 
@@ -202,9 +216,15 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
202
216
  }
203
217
  return server.registerTool(name, config, async (...args) => {
204
218
  const state = getState();
219
+ const previousTool = state.currentTool;
220
+ state.currentTool = name;
205
221
  trackUmamiEvent(UMAMI_EVENTS.MCP_TOOL_CALLED, { tool: name, projectId: state.projectId || undefined, source: 'mcp' });
206
- reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST);
207
- return cb(...args);
222
+ reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST, { tool: name });
223
+ try {
224
+ return await cb(...args);
225
+ } finally {
226
+ state.currentTool = previousTool;
227
+ }
208
228
  });
209
229
  }
210
230
 
@@ -301,7 +321,25 @@ function getInstallInfo() {
301
321
  }
302
322
  }
303
323
 
304
- function reportInstallationEvent(event) {
324
+
325
+ function osFamily() {
326
+ const p = platform();
327
+ if (p === 'darwin') return 'macos';
328
+ if (p === 'win32') return 'windows';
329
+ if (p === 'linux') return 'linux';
330
+ return 'unknown';
331
+ }
332
+
333
+ function normalizedArch() {
334
+ const a = osArch();
335
+ return ['x64', 'arm64', 'arm', 'ia32'].includes(a) ? a : 'unknown';
336
+ }
337
+
338
+ function mcpMode() {
339
+ return process.argv.includes('--http') ? 'http' : 'stdio';
340
+ }
341
+
342
+ function reportInstallationEvent(event, extra = {}) {
305
343
  const info = getInstallInfo();
306
344
  if (!info) return;
307
345
  fetch(`${getServerUrl()}/api/installations/report`, {
@@ -312,12 +350,78 @@ function reportInstallationEvent(event) {
312
350
  event,
313
351
  schemaVersion: 1,
314
352
  cliVersion: PACKAGE_VERSION,
315
- mcpMode: process.argv.includes('--http') ? 'http' : 'stdio',
353
+ osFamily: osFamily(),
354
+ osVersion: osRelease().slice(0, 60),
355
+ arch: normalizedArch(),
356
+ nodeVersion: process.version,
357
+ mcpMode: mcpMode(),
316
358
  source: 'mcp',
359
+ ...extra,
317
360
  }),
318
361
  }).catch(() => {});
319
362
  }
320
363
 
364
+ function classifyMcpError(error) {
365
+ const msg = String(error?.message || error || '').toLowerCase();
366
+ const causeCode = String(error?.cause?.code || error?.code || '').toLowerCase();
367
+ if (causeCode) return causeCode.slice(0, 80);
368
+ if (msg.includes('fetch failed')) return 'fetch_failed';
369
+ if (msg.includes('failed to fetch')) return 'fetch_failed';
370
+ if (msg.includes('network')) return 'network_error';
371
+ if (msg.includes('timeout') || msg.includes('timed out')) return 'timeout';
372
+ const httpMatch = msg.match(/http\s+(\d{3})/);
373
+ if (httpMatch) return `http_${httpMatch[1]}`;
374
+ if (msg.includes('unauthorized') || msg.includes('401')) return 'auth_401';
375
+ if (msg.includes('forbidden') || msg.includes('403')) return 'auth_403';
376
+ return 'tool_error';
377
+ }
378
+
379
+ function scrubLogValue(value) {
380
+ if (typeof value !== 'string') return value;
381
+ return value
382
+ .replace(/([?&](?:token|code|dci|session|auth|password|secret)=)[^&\s]+/gi, '$1[Filtered]')
383
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi, '$1[Filtered]')
384
+ .replace(/(gc_session=)[^;\s]+/gi, '$1[Filtered]')
385
+ .slice(0, 500);
386
+ }
387
+
388
+ function serverOriginForLog() {
389
+ try { return new URL(getServerUrl()).origin; } catch { return 'invalid_server_url'; }
390
+ }
391
+
392
+ function writeMcpClientLog(tool, error) {
393
+ const entry = {
394
+ ts: new Date().toISOString(),
395
+ level: 'error',
396
+ component: 'drafted-mcp-client',
397
+ version: PACKAGE_VERSION,
398
+ tool: String(tool || 'unknown').slice(0, 80),
399
+ errorCode: classifyMcpError(error),
400
+ message: scrubLogValue(error?.message || String(error)),
401
+ causeCode: scrubLogValue(error?.cause?.code || error?.code || ''),
402
+ causeMessage: scrubLogValue(error?.cause?.message || ''),
403
+ server: serverOriginForLog(),
404
+ osFamily: osFamily(),
405
+ nodeVersion: process.version,
406
+ mcpMode: mcpMode(),
407
+ };
408
+ const line = `[Drafted MCP] ${JSON.stringify(entry)}\n`;
409
+ try { console.error(line.trimEnd()); } catch { /* ignore */ }
410
+ try {
411
+ const logPath = join(homedir(), '.drafted', 'mcp-client.log');
412
+ mkdirSync(dirname(logPath), { recursive: true });
413
+ appendFileSync(logPath, line, { mode: 0o600 });
414
+ } catch { /* best effort */ }
415
+ }
416
+
417
+ function reportMcpToolError(tool, error) {
418
+ writeMcpClientLog(tool, error);
419
+ reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_ERROR, {
420
+ tool: String(tool || 'unknown').slice(0, 80),
421
+ errorCode: classifyMcpError(error),
422
+ });
423
+ }
424
+
321
425
  function wikiBrowserUrl(path = '') {
322
426
  const normalized = normalizeWikiPath(path || '');
323
427
  if (!normalized) return `${getServerUrl()}/wiki`;
@@ -702,6 +806,7 @@ if (!globalThis.__draftedAgentWsBootstrapped) {
702
806
  }
703
807
 
704
808
  function err(error) {
809
+ reportMcpToolError(getState().currentTool, error);
705
810
  return { content: [{ type: 'text', text: error.message || String(error) }], isError: true };
706
811
  }
707
812
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.18",
3
+ "version": "1.7.20",
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": [
7
7
  "cli/",
8
8
  "mcp/",
9
+ "server/lib/umami.mjs",
9
10
  "src/shared/",
10
11
  "agent-instructions/",
11
12
  "install-mcp.sh"
@@ -31,7 +32,8 @@
31
32
  "version:check": "node scripts/sync-versions.mjs",
32
33
  "postpublish": "bash scripts/sync-plugin.sh \"chore: sync plugin to v$npm_package_version\"",
33
34
  "deploy:check:google": "node scripts/check-google-drive-deploy.mjs",
34
- "build:excalidraw": "node scripts/build-excalidraw-editor.mjs"
35
+ "build:excalidraw": "node scripts/build-excalidraw-editor.mjs",
36
+ "prepublishOnly": "node scripts/check-npm-package.mjs"
35
37
  },
36
38
  "dependencies": {
37
39
  "@aws-sdk/client-s3": "^3.1007.0",
@@ -0,0 +1,156 @@
1
+ export const UMAMI_EVENTS = Object.freeze({
2
+ SIGNUP_STARTED: 'signup_started',
3
+ SIGNUP_COMPLETED: 'signup_completed',
4
+ ORG_CREATED: 'org_created',
5
+ PROJECT_CREATED: 'project_created',
6
+ PROJECT_OPENED: 'project_opened',
7
+ TEMPLATE_USED: 'template_used',
8
+ FRAME_CREATED: 'frame_created',
9
+ FRAME_UPDATED: 'frame_updated',
10
+ FRAME_DELETED: 'frame_deleted',
11
+ SHARE_CREATED: 'share_created',
12
+ SHARE_ACCEPTED: 'share_accepted',
13
+ INVITE_SENT: 'invite_sent',
14
+ MCP_CONNECTED: 'mcp_connected',
15
+ MCP_TOOL_CALLED: 'mcp_tool_called',
16
+ WIKI_PAGE_CREATED: 'wiki_page_created',
17
+ SKILL_ATTACHED: 'skill_attached',
18
+ DRIVE_CONNECTED: 'drive_connected',
19
+ DRIVE_SYNC_COMPLETED: 'drive_sync_completed',
20
+ DRAFTED_INSTALL: 'drafted_install',
21
+ DRAFTED_UPDATE: 'drafted_update',
22
+ DRAFTED_HEARTBEAT: 'drafted_heartbeat',
23
+ DRAFTED_MCP_CONFIGURED: 'drafted_mcp_configured',
24
+ DRAFTED_MCP_REQUEST: 'drafted_mcp_request',
25
+ DRAFTED_MCP_ERROR: 'drafted_mcp_error',
26
+ DRAFTED_UPDATE_HELPER_STARTED: 'drafted_update_helper_started',
27
+ DRAFTED_UPDATE_HELPER_FAILED: 'drafted_update_helper_failed',
28
+ CTA_CLICKED: 'cta_clicked',
29
+ TEMPLATE_SELECTED: 'template_selected',
30
+ PROJECT_NAVIGATED: 'project_navigated',
31
+ SHARE_MODAL_OPENED: 'share_modal_opened',
32
+ SHARE_MODAL_ACTION: 'share_modal_action',
33
+ SURFACE_LOADED: 'surface_loaded',
34
+ SIDEBAR_USED: 'sidebar_used',
35
+ TOOL_PANEL_USED: 'tool_panel_used',
36
+ });
37
+
38
+ const BLOCKED_KEYS = new Set([
39
+ 'email', 'userEmail', 'name', 'userName', 'orgName', 'projectName', 'label', 'title',
40
+ 'content', 'html', 'markdown', 'body', 'prompt', 'token', 'cookie', 'authorization',
41
+ 'password', 'secret', 'apiKey', 'request', 'headers', 'file', 'buffer', 'dataUrl',
42
+ ]);
43
+ const ID_KEYS = new Set(['userId', 'orgId', 'projectId', 'projectSlug', 'frameId', 'shareId', 'skillId', 'templateId', 'templateSlug', 'userRole', 'role', 'buildId', 'pageType', 'tool', 'action', 'source', 'layer', 'lane', 'mode', 'installId', 'schemaVersion', 'installerVersion', 'cliVersion', 'osFamily', 'osVersion', 'arch', 'nodeVersion', 'npmVersion', 'claudeDesktop', 'claudeCode', 'codex', 'cursor', 'updateHelperStatus', 'mcpMode', 'errorCode']);
44
+
45
+ export function getUmamiConfig(config = {}) {
46
+ const hostUrl = (config.umamiHostUrl || process.env.UMAMI_HOST_URL || '').replace(/\/$/, '');
47
+ const websiteId = config.umamiWebsiteId || process.env.UMAMI_WEBSITE_ID || '';
48
+ return { hostUrl, websiteId, enabled: Boolean(hostUrl && websiteId) };
49
+ }
50
+
51
+ export function sanitizeUmamiEventData(data = {}) {
52
+ const output = {};
53
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return output;
54
+ for (const [key, value] of Object.entries(data)) {
55
+ if (BLOCKED_KEYS.has(key)) continue;
56
+ if (value === undefined || value === null) continue;
57
+ if (typeof value === 'object') continue;
58
+ if (!ID_KEYS.has(key) && /email|name|content|html|markdown|body|prompt|token|cookie|auth|secret|password|key|file/i.test(key)) continue;
59
+ if (typeof value === 'string') {
60
+ if (value.length > 120) continue;
61
+ if (/@/.test(value)) continue;
62
+ output[key] = value;
63
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
64
+ output[key] = value;
65
+ }
66
+ }
67
+ return output;
68
+ }
69
+
70
+ export function getUmamiHead(config = {}) {
71
+ const umami = getUmamiConfig(config);
72
+ if (!umami.enabled) return '';
73
+ const ctx = sanitizeUmamiEventData({
74
+ userId: config.userId,
75
+ orgId: config.orgId,
76
+ projectId: config.projectId,
77
+ projectSlug: config.projectSlug,
78
+ userRole: config.userRole,
79
+ buildId: config.buildId,
80
+ pageType: config.pageType,
81
+ });
82
+ return `<script defer src="${umami.hostUrl}/script.js" data-website-id="${umami.websiteId}"></script>
83
+ <script>
84
+ window.__DRAFTED_ANALYTICS__ = ${JSON.stringify(ctx)};
85
+ window.draftedTrack = function(name, data) {
86
+ var base = window.__DRAFTED_ANALYTICS__ || {};
87
+ var payload = Object.assign({}, base, data || {});
88
+ if (window.umami && typeof window.umami.track === 'function') window.umami.track(name, payload);
89
+ };
90
+ window.addEventListener('load', function() {
91
+ var c = window.__DRAFTED_ANALYTICS__ || {};
92
+ if (c.userId && window.umami && typeof window.umami.identify === 'function') window.umami.identify(c.userId, { orgId: c.orgId, role: c.userRole });
93
+ });
94
+ </script>`;
95
+ }
96
+
97
+ export function getUmamiInteractionScript(pageType) {
98
+ return `<script>
99
+ (function() {
100
+ function track(name, data) { if (window.draftedTrack) window.draftedTrack(name, Object.assign({ pageType: ${JSON.stringify(pageType)} }, data || {})); }
101
+ window.addEventListener('load', function() {
102
+ if (${JSON.stringify(pageType)} === 'surface') track('surface_loaded');
103
+ });
104
+ document.addEventListener('click', function(event) {
105
+ var el = event.target && event.target.closest && event.target.closest('a,button,[data-analytics-event],[data-template-slug],[data-project-id]');
106
+ if (!el) return;
107
+ var eventName = el.getAttribute('data-analytics-event');
108
+ if (eventName) return track(eventName, { action: el.getAttribute('data-analytics-action') || undefined });
109
+ var href = el.getAttribute('href') || '';
110
+ var cls = el.className || '';
111
+ if (el.matches('.nav-cta,.btn-primary,.btn-secondary')) track('cta_clicked', { action: href || (el.textContent || '').trim().slice(0, 40) });
112
+ if (el.getAttribute('data-template-slug')) track('template_selected', { templateSlug: el.getAttribute('data-template-slug') });
113
+ if (el.getAttribute('data-project-id')) track('project_navigated', { projectId: el.getAttribute('data-project-id') });
114
+ if (/share/i.test(String(cls)) || /share/i.test(el.textContent || '')) track('share_modal_action');
115
+ if (/sidebar/i.test(String(cls))) track('sidebar_used');
116
+ if (/tool|panel/i.test(String(cls))) track('tool_panel_used');
117
+ }, true);
118
+ })();
119
+ </script>`;
120
+ }
121
+
122
+ function getRequestUrl(req) {
123
+ if (!req) return undefined;
124
+ const host = typeof req.get === 'function' ? req.get('host') : req.headers?.host;
125
+ const protocol = req.protocol || 'https';
126
+ return host ? `${protocol}://${host}${req.originalUrl || req.url || ''}` : undefined;
127
+ }
128
+
129
+ export function trackUmamiEvent(name, data = {}, req = null) {
130
+ const umami = getUmamiConfig();
131
+ if (!umami.enabled || !name || name.length > 50) return;
132
+ const payload = {
133
+ type: 'event',
134
+ payload: {
135
+ website: umami.websiteId,
136
+ name,
137
+ url: getRequestUrl(req) || process.env.BASE_URL || 'https://drafted.live',
138
+ hostname: req?.hostname || 'drafted.live',
139
+ language: req?.headers?.['accept-language'],
140
+ referrer: req?.headers?.referer,
141
+ data: sanitizeUmamiEventData(data),
142
+ },
143
+ };
144
+ const controller = new AbortController();
145
+ const timeout = setTimeout(() => controller.abort(), 1500);
146
+ fetch(`${umami.hostUrl}/api/send`, {
147
+ method: 'POST',
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ 'User-Agent': req?.headers?.['user-agent'] || 'Drafted Server Analytics',
151
+ 'X-Forwarded-For': req?.ip || req?.headers?.['x-forwarded-for'] || '',
152
+ },
153
+ body: JSON.stringify(payload),
154
+ signal: controller.signal,
155
+ }).catch(() => {}).finally(() => clearTimeout(timeout));
156
+ }