drafted 1.7.19 → 1.7.21
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 +11 -1
- package/mcp/server.mjs +178 -13
- package/package.json +4 -2
- package/server/lib/umami.mjs +156 -0
package/install-mcp.sh
CHANGED
|
@@ -215,8 +215,18 @@ export PATH="$HOME/.drafted/npm-global/bin:$PATH"
|
|
|
215
215
|
|
|
216
216
|
step "Installing Drafted"
|
|
217
217
|
|
|
218
|
-
npm install -g drafted@latest --force
|
|
218
|
+
npm install -g drafted@latest --force
|
|
219
219
|
hash -r 2>/dev/null || true
|
|
220
|
+
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
221
|
+
MCP_SERVER_MODULE="$NPM_ROOT/drafted/mcp/server.mjs"
|
|
222
|
+
if [ -n "$NPM_ROOT" ] && [ -f "$MCP_SERVER_MODULE" ]; then
|
|
223
|
+
node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$MCP_SERVER_MODULE"
|
|
224
|
+
elif [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
|
|
225
|
+
echo -e " ${DIM}Skipping package import check in headless installer test.${RESET}"
|
|
226
|
+
else
|
|
227
|
+
echo -e " ${RED}Could not find installed MCP server module at $MCP_SERVER_MODULE${RESET}"
|
|
228
|
+
exit 1
|
|
229
|
+
fi
|
|
220
230
|
ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
|
|
221
231
|
|
|
222
232
|
# ── 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
|
-
|
|
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
|
|
|
@@ -150,7 +164,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
150
164
|
|
|
151
165
|
// Frames — filesystem
|
|
152
166
|
ls: { title: 'List frames', readOnlyHint: true, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html' },
|
|
153
|
-
frame: { title: 'Frames', readOnlyHint: false, destructiveHint: true, openWorldHint:
|
|
167
|
+
frame: { title: 'Frames', readOnlyHint: false, destructiveHint: true, openWorldHint: true, widgetUri: 'ui://widget/drafted-frame-preview.html', description: 'Read, write, edit, move, anchor, search, restore frame versions, create Google Workspace frames, or write values into Google Sheet frames in the ACTIVE PROJECT. Dispatch by `action`. Use `ls` to browse, `rm` to delete.' },
|
|
154
168
|
rm: { title: 'Delete frame', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
155
169
|
// batch: { title: 'Batch operations', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|
|
156
170
|
|
|
@@ -1514,6 +1528,7 @@ async function getGoogleDriveAvailability() {
|
|
|
1514
1528
|
}
|
|
1515
1529
|
}
|
|
1516
1530
|
|
|
1531
|
+
|
|
1517
1532
|
tool('get_org', {
|
|
1518
1533
|
action: z.enum(['get', 'switch']).optional().describe('Default: "get" returns active org and member info. Use "switch" with orgId to change the active org without opening a project.'),
|
|
1519
1534
|
orgId: z.string().optional().describe('[switch] target org ID to switch to. Must be one of the orgs the user is a member of.'),
|
|
@@ -1570,18 +1585,41 @@ tool('get_org', {
|
|
|
1570
1585
|
|
|
1571
1586
|
// ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
|
|
1572
1587
|
|
|
1573
|
-
tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by path, frame URL, or UUID), write (new frame or overwrite), write_excalidraw (native editable Excalidraw diagram), edit (hashline ops), mv (rename/move), anchor (mark as required-read for the layer), search (match frame names). Use project(action="open") first. For listing use `ls`, for deletion use `rm`.\n\n**Write — content,
|
|
1574
|
-
action: z.enum(['read', 'write', 'write_excalidraw', 'edit', 'mv', 'anchor', 'search', 'versions', 'read_version', 'restore_version']).describe('Operation to perform.'),
|
|
1588
|
+
tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by path, frame URL, or UUID), write (new frame or overwrite), Google Sheet actions (`get_sheet`, `read_sheet_values`, `write_sheet_values`, `append_sheet_rows`, `clear_sheet_range`, `update_sheet`), Google Doc actions (`get_doc`, `read_doc_content`, `write_doc_content`, `append_doc_content`, `clear_doc_content`, `update_doc`), Google Slide actions (`get_slide`, `read_slide_content`, `write_slide_content`, `append_slides`, `clear_slides`, `update_slide`), write_excalidraw (native editable Excalidraw diagram), edit (hashline ops), mv (rename/move), anchor (mark as required-read for the layer), search (match frame names). Use project(action="open") first. For listing use `ls`, for deletion use `rm`.\n\n**Google Workspace native content:** Create or attach Google Docs/Sheets/Slides with `frame(action="write", googleType=...)`. After that, populate native Google Docs with `write_doc_content`/`append_doc_content` and native Google Slides with `write_slide_content`/`append_slides`; read them with `read_doc_content`/`read_slide_content`. Do NOT use inline `frame.write(content)` or hashline `frame.edit` to populate Google Doc/Slide frames — those actions are for Drafted inline frame files, not native Workspace document bodies.\n\n**Write — content, binary, or Google Workspace frame:** Provide exactly one of `content` (HTML/markdown/text), `file_path` (absolute local file), `base64` (base64-encoded binary with optional `content_type`), or `googleType` (`google-doc`, `google-sheet`, `google-slide`). Call get_org first; when `googleDrive.connected` is true, strongly prefer Google Workspace frames for docs, sheets, and slides in that org. For inline content, filename extension matters: use `.html` for complete HTML documents and `.md` for Markdown. Never place a full HTML document in a `.md` or extensionless frame. For a new Google file, pass `googleType` and optional `title`; for an existing Google file, pass `googleType` plus `url` or `googleId`. For binary frames (images, PDFs, videos), use `file_path` when the file is local to the MCP host, or `base64` when the caller already has binary bytes.\n\n**Write — dimensions:** By default, frames use the layer\'s default size (e.g. 1440×900 for designs, 1440×3000 for wireframes). Often too large for small content. Use `autoSize: true` to measure HTML content and size to fit, or pass explicit `width`/`height`.', {
|
|
1589
|
+
action: z.enum(['read', 'write', 'write_sheet_values', 'read_sheet_values', 'append_sheet_rows', 'clear_sheet_range', 'get_sheet', 'update_sheet', 'get_doc', 'read_doc_content', 'write_doc_content', 'append_doc_content', 'clear_doc_content', 'update_doc', 'get_slide', 'read_slide_content', 'write_slide_content', 'append_slides', 'clear_slides', 'update_slide', 'write_excalidraw', 'edit', 'mv', 'anchor', 'search', 'versions', 'read_version', 'restore_version']).describe('Operation to perform. Use native Doc/Slide actions for Google Docs/Slides; do not use inline write/edit for native Workspace content.'),
|
|
1575
1590
|
path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
|
|
1576
1591
|
lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
|
|
1577
|
-
content: z.string().optional().describe('[write] HTML/markdown/text
|
|
1592
|
+
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.'),
|
|
1578
1593
|
excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
|
|
1579
1594
|
mermaid: z.string().optional().describe('[write_excalidraw] Mermaid source to convert into an editable Excalidraw scene.'),
|
|
1580
|
-
file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content.'),
|
|
1595
|
+
file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
|
|
1596
|
+
base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
|
|
1597
|
+
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.'),
|
|
1581
1598
|
googleType: z.enum(['google-doc', 'google-sheet', 'google-slide']).optional().describe('[write] Create or attach a native Google Workspace frame. Use with title to create new, or url/googleId to attach existing.'),
|
|
1582
|
-
title: z.string().optional().describe('[write + googleType] Title for a new
|
|
1583
|
-
url: z.string().optional().describe('[write + googleType] Existing Google
|
|
1584
|
-
googleId: z.string().optional().describe('[write + googleType] Existing Google file ID to attach.'),
|
|
1599
|
+
title: z.string().optional().describe('[write + googleType] Title for a new native file. Defaults to filename from path.'),
|
|
1600
|
+
url: z.string().optional().describe('[write + googleType] Existing Google file URL to attach.'),
|
|
1601
|
+
googleId: z.string().optional().describe('[write + googleType] Existing Google file ID to attach. [Sheet/Doc/Slide actions] Native Google file ID to use when not resolving from path/frame.'),
|
|
1602
|
+
format: z.enum(['plain_text', 'markdown']).optional().describe('[write_doc_content|append_doc_content] Source format hint. Currently plain_text and minimal markdown are accepted as text.'),
|
|
1603
|
+
mode: z.enum(['replace', 'append', 'clear']).optional().describe('[Doc/Slide content actions] Optional mode hint for clients; prefer the explicit write/append/clear action names.'),
|
|
1604
|
+
slides: z.array(z.object({
|
|
1605
|
+
title: z.string().optional(),
|
|
1606
|
+
bullets: z.array(z.string()).optional(),
|
|
1607
|
+
speakerNotes: z.string().optional(),
|
|
1608
|
+
layout: z.string().optional(),
|
|
1609
|
+
}).passthrough()).optional().describe('[write_slide_content|append_slides] Structured slide spec: [{ title, bullets, speakerNotes?, layout? }].'),
|
|
1610
|
+
requests: z.array(z.any()).optional().describe('[update_doc|update_slide] Raw Google Docs/Slides batchUpdate requests for advanced updates only; common Doc/Slide population should use write_doc_content/append_doc_content/write_slide_content/append_slides.'),
|
|
1611
|
+
slideObjectIds: z.array(z.string()).optional().describe('[clear_slides] Optional slide object IDs to delete. Omit to clear all slides.'),
|
|
1612
|
+
range: z.string().optional().describe('[Sheet value actions] A1 range, e.g. Sheet1!A1 or Data!A:Z.'),
|
|
1613
|
+
values: z.array(z.array(z.any())).optional().describe('[write_sheet_values|append_sheet_rows] 2D array of row values.'),
|
|
1614
|
+
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().describe('[write_sheet_values|append_sheet_rows] Google Sheets value input option. Defaults to USER_ENTERED.'),
|
|
1615
|
+
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().describe('[Sheet value actions] Major dimension for values. Defaults to ROWS when writing/appending.'),
|
|
1616
|
+
valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']).optional().describe('[read_sheet_values] How values should be rendered. Defaults to Google Sheets API default.'),
|
|
1617
|
+
dateTimeRenderOption: z.enum(['SERIAL_NUMBER', 'FORMATTED_STRING']).optional().describe('[read_sheet_values] How dates/times should be rendered.'),
|
|
1618
|
+
insertDataOption: z.enum(['OVERWRITE', 'INSERT_ROWS']).optional().describe('[append_sheet_rows] How new rows are inserted. Defaults to INSERT_ROWS.'),
|
|
1619
|
+
operation: z.enum(['add_sheet', 'rename_sheet']).optional().describe('[update_sheet] Sheet tab operation.'),
|
|
1620
|
+
sheetTitle: z.string().optional().describe('[update_sheet add_sheet] Title for the new sheet tab.'),
|
|
1621
|
+
sheetId: z.number().optional().describe('[update_sheet rename_sheet] Numeric sheet/tab id.'),
|
|
1622
|
+
newTitle: z.string().optional().describe('[update_sheet rename_sheet] New title for the sheet tab.'),
|
|
1585
1623
|
autoSize: z.boolean().optional().describe('[write] measure HTML content and size frame to fit. Content only, not file_path.'),
|
|
1586
1624
|
width: z.number().optional().describe('[write] explicit width in pixels. Overrides layer default. Ignored if autoSize=true.'),
|
|
1587
1625
|
height: z.number().optional().describe('[write] explicit height in pixels. Overrides layer default. Ignored if autoSize=true.'),
|
|
@@ -1607,6 +1645,26 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1607
1645
|
// write landed before it can develop a wrong assumption.
|
|
1608
1646
|
const projectCtx = getCurrentProjectContext();
|
|
1609
1647
|
const withProject = (result) => ({ ...result, project: projectCtx });
|
|
1648
|
+
const workspaceBody = (kind) => {
|
|
1649
|
+
const { path, googleId } = args;
|
|
1650
|
+
const body = {};
|
|
1651
|
+
const frameUrlMatch = path?.match(/\/f\/([a-f0-9-]{36})/);
|
|
1652
|
+
const uuidMatch = path?.match(/^[a-f0-9-]{36}$/);
|
|
1653
|
+
if (googleId) {
|
|
1654
|
+
if (kind === 'sheet') body.spreadsheetId = googleId;
|
|
1655
|
+
else if (kind === 'doc') body.documentId = googleId;
|
|
1656
|
+
else body.presentationId = googleId;
|
|
1657
|
+
} else if (frameUrlMatch?.[1] || uuidMatch) {
|
|
1658
|
+
body.frameId = frameUrlMatch?.[1] || path;
|
|
1659
|
+
} else if (path) {
|
|
1660
|
+
body.path = path;
|
|
1661
|
+
body.projectId = getState().projectId;
|
|
1662
|
+
} else {
|
|
1663
|
+
const label = kind === 'sheet' ? 'Google Sheet' : kind === 'doc' ? 'Google Doc' : 'Google Slide';
|
|
1664
|
+
throw new Error(`Provide path to a ${label} frame, frame ID/URL, or googleId/native file ID`);
|
|
1665
|
+
}
|
|
1666
|
+
return body;
|
|
1667
|
+
};
|
|
1610
1668
|
switch (action) {
|
|
1611
1669
|
case 'read': {
|
|
1612
1670
|
const { path, lines } = args;
|
|
@@ -1646,12 +1704,117 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1646
1704
|
_meta: { frameHtml: result.content },
|
|
1647
1705
|
});
|
|
1648
1706
|
}
|
|
1707
|
+
case 'get_sheet':
|
|
1708
|
+
case 'read_sheet_values':
|
|
1709
|
+
case 'write_sheet_values':
|
|
1710
|
+
case 'append_sheet_rows':
|
|
1711
|
+
case 'clear_sheet_range':
|
|
1712
|
+
case 'update_sheet': {
|
|
1713
|
+
const { path, googleId, range, values, valueInputOption, majorDimension, valueRenderOption, dateTimeRenderOption, insertDataOption, operation, sheetTitle, sheetId, newTitle } = args;
|
|
1714
|
+
const body = workspaceBody('sheet');
|
|
1715
|
+
if (range) body.range = range;
|
|
1716
|
+
if (majorDimension) body.majorDimension = majorDimension;
|
|
1717
|
+
if (action === 'write_sheet_values' || action === 'append_sheet_rows') {
|
|
1718
|
+
if (!values || !Array.isArray(values)) throw new Error(`values required for action=${action}`);
|
|
1719
|
+
body.values = values;
|
|
1720
|
+
body.valueInputOption = valueInputOption || 'USER_ENTERED';
|
|
1721
|
+
body.majorDimension = majorDimension || 'ROWS';
|
|
1722
|
+
}
|
|
1723
|
+
if (action === 'read_sheet_values') {
|
|
1724
|
+
if (valueRenderOption) body.valueRenderOption = valueRenderOption;
|
|
1725
|
+
if (dateTimeRenderOption) body.dateTimeRenderOption = dateTimeRenderOption;
|
|
1726
|
+
}
|
|
1727
|
+
if (action === 'append_sheet_rows') body.insertDataOption = insertDataOption || 'INSERT_ROWS';
|
|
1728
|
+
if (action === 'update_sheet') {
|
|
1729
|
+
body.operation = operation;
|
|
1730
|
+
if (sheetTitle) body.sheetTitle = sheetTitle;
|
|
1731
|
+
if (sheetId != null) body.sheetId = sheetId;
|
|
1732
|
+
if (newTitle) body.newTitle = newTitle;
|
|
1733
|
+
}
|
|
1734
|
+
const endpoint = action === 'get_sheet'
|
|
1735
|
+
? '/api/google/workspace/sheet'
|
|
1736
|
+
: action === 'read_sheet_values'
|
|
1737
|
+
? '/api/google/workspace/sheet-values/read'
|
|
1738
|
+
: action === 'write_sheet_values'
|
|
1739
|
+
? '/api/google/workspace/sheet-values'
|
|
1740
|
+
: action === 'append_sheet_rows'
|
|
1741
|
+
? '/api/google/workspace/sheet-values/append'
|
|
1742
|
+
: action === 'clear_sheet_range'
|
|
1743
|
+
? '/api/google/workspace/sheet-values/clear'
|
|
1744
|
+
: '/api/google/workspace/sheet-update';
|
|
1745
|
+
const result = await api('POST', endpoint, body);
|
|
1746
|
+
return ok(withProject(result));
|
|
1747
|
+
}
|
|
1748
|
+
case 'get_doc':
|
|
1749
|
+
case 'read_doc_content':
|
|
1750
|
+
case 'write_doc_content':
|
|
1751
|
+
case 'append_doc_content':
|
|
1752
|
+
case 'clear_doc_content':
|
|
1753
|
+
case 'update_doc': {
|
|
1754
|
+
const { content, format, mode, requests } = args;
|
|
1755
|
+
const body = workspaceBody('doc');
|
|
1756
|
+
if (format) body.format = format;
|
|
1757
|
+
if (mode) body.mode = mode;
|
|
1758
|
+
if (action === 'write_doc_content' || action === 'append_doc_content') {
|
|
1759
|
+
if (typeof content !== 'string') throw new Error(`content string required for action=${action}`);
|
|
1760
|
+
body.content = content;
|
|
1761
|
+
}
|
|
1762
|
+
if (action === 'update_doc') {
|
|
1763
|
+
if (!Array.isArray(requests)) throw new Error('requests array required for action=update_doc');
|
|
1764
|
+
body.requests = requests;
|
|
1765
|
+
}
|
|
1766
|
+
const endpoint = action === 'get_doc'
|
|
1767
|
+
? '/api/google/workspace/doc'
|
|
1768
|
+
: action === 'read_doc_content'
|
|
1769
|
+
? '/api/google/workspace/doc-content/read'
|
|
1770
|
+
: action === 'write_doc_content'
|
|
1771
|
+
? '/api/google/workspace/doc-content'
|
|
1772
|
+
: action === 'append_doc_content'
|
|
1773
|
+
? '/api/google/workspace/doc-content/append'
|
|
1774
|
+
: action === 'clear_doc_content'
|
|
1775
|
+
? '/api/google/workspace/doc-content/clear'
|
|
1776
|
+
: '/api/google/workspace/doc-update';
|
|
1777
|
+
const result = await api('POST', endpoint, body);
|
|
1778
|
+
return ok(withProject(result));
|
|
1779
|
+
}
|
|
1780
|
+
case 'get_slide':
|
|
1781
|
+
case 'read_slide_content':
|
|
1782
|
+
case 'write_slide_content':
|
|
1783
|
+
case 'append_slides':
|
|
1784
|
+
case 'clear_slides':
|
|
1785
|
+
case 'update_slide': {
|
|
1786
|
+
const { slides, requests, slideObjectIds, mode } = args;
|
|
1787
|
+
const body = workspaceBody('slide');
|
|
1788
|
+
if (mode) body.mode = mode;
|
|
1789
|
+
if (action === 'write_slide_content' || action === 'append_slides') {
|
|
1790
|
+
if (!Array.isArray(slides)) throw new Error(`slides array required for action=${action}`);
|
|
1791
|
+
body.slides = slides;
|
|
1792
|
+
}
|
|
1793
|
+
if (action === 'clear_slides' && Array.isArray(slideObjectIds)) body.slideObjectIds = slideObjectIds;
|
|
1794
|
+
if (action === 'update_slide') {
|
|
1795
|
+
if (!Array.isArray(requests)) throw new Error('requests array required for action=update_slide');
|
|
1796
|
+
body.requests = requests;
|
|
1797
|
+
}
|
|
1798
|
+
const endpoint = action === 'get_slide'
|
|
1799
|
+
? '/api/google/workspace/slide'
|
|
1800
|
+
: action === 'read_slide_content'
|
|
1801
|
+
? '/api/google/workspace/slide-content/read'
|
|
1802
|
+
: action === 'write_slide_content'
|
|
1803
|
+
? '/api/google/workspace/slide-content'
|
|
1804
|
+
: action === 'append_slides'
|
|
1805
|
+
? '/api/google/workspace/slides/append'
|
|
1806
|
+
: action === 'clear_slides'
|
|
1807
|
+
? '/api/google/workspace/slides/clear'
|
|
1808
|
+
: '/api/google/workspace/slide-update';
|
|
1809
|
+
const result = await api('POST', endpoint, body);
|
|
1810
|
+
return ok(withProject(result));
|
|
1811
|
+
}
|
|
1649
1812
|
case 'write': {
|
|
1650
|
-
const { path, content, file_path, autoSize, width, height, color, googleType, title, url, googleId } = args;
|
|
1813
|
+
const { path, content, file_path, base64, content_type, autoSize, width, height, color, googleType, title, url, googleId } = args;
|
|
1651
1814
|
if (!path) throw new Error('path required for action=write');
|
|
1652
|
-
const writeSources = [content != null, !!file_path, !!googleType].filter(Boolean).length;
|
|
1653
|
-
if (writeSources > 1) throw new Error('Provide only one of content, file_path, or googleType');
|
|
1654
|
-
if (writeSources === 0) throw new Error('Provide content, file_path, or googleType');
|
|
1815
|
+
const writeSources = [content != null, !!file_path, base64 != null, !!googleType].filter(Boolean).length;
|
|
1816
|
+
if (writeSources > 1) throw new Error('Provide only one of content, file_path, base64, or googleType');
|
|
1817
|
+
if (writeSources === 0) throw new Error('Provide content, file_path, base64, or googleType');
|
|
1655
1818
|
const skillErr = await checkProjectSkills(getState().projectId);
|
|
1656
1819
|
if (skillErr) return err(new Error(skillErr));
|
|
1657
1820
|
const anchorErr = await checkAnchors(parseLayer(path));
|
|
@@ -1698,6 +1861,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1698
1861
|
const MIME = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', '.mp4': 'video/mp4', '.webm': 'video/webm', '.mov': 'video/quicktime', '.m4v': 'video/x-m4v' };
|
|
1699
1862
|
body = { base64: buffer.toString('base64'), contentType: MIME[ext] || 'application/octet-stream' };
|
|
1700
1863
|
}
|
|
1864
|
+
} else if (base64 != null) {
|
|
1865
|
+
body = { base64, contentType: content_type || mimeFromExt(extname(parts[2])) };
|
|
1701
1866
|
} else {
|
|
1702
1867
|
body = { content };
|
|
1703
1868
|
if (autoSize) body.autoSize = true;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.21",
|
|
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
|
+
}
|