drafted 1.7.19 → 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 ─────────────────────────────────────────────────────
package/mcp/server.mjs CHANGED
@@ -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
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.19",
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
+ }