drafted 1.7.20 → 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 CHANGED
@@ -217,8 +217,16 @@ step "Installing Drafted"
217
217
 
218
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
+ 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
222
230
  ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
223
231
 
224
232
  # ── Configure ─────────────────────────────────────────────────────
package/mcp/server.mjs CHANGED
@@ -164,7 +164,7 @@ const TOOL_ANNOTATIONS = {
164
164
 
165
165
  // Frames — filesystem
166
166
  ls: { title: 'List frames', readOnlyHint: true, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html' },
167
- frame: { title: 'Frames', readOnlyHint: false, destructiveHint: true, openWorldHint: false, widgetUri: 'ui://widget/drafted-frame-preview.html', description: 'Read, write, edit, move, anchor, search, or restore frame versions in the ACTIVE PROJECT. Dispatch by `action`. Use `ls` to browse, `rm` to delete.' },
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.' },
168
168
  rm: { title: 'Delete frame', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
169
169
  // batch: { title: 'Batch operations', readOnlyHint: false, destructiveHint: true, openWorldHint: false },
170
170
 
@@ -1528,6 +1528,7 @@ async function getGoogleDriveAvailability() {
1528
1528
  }
1529
1529
  }
1530
1530
 
1531
+
1531
1532
  tool('get_org', {
1532
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.'),
1533
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.'),
@@ -1584,18 +1585,41 @@ tool('get_org', {
1584
1585
 
1585
1586
  // ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
1586
1587
 
1587
- 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, file, or Google Workspace frame:** Provide `content` (HTML/markdown/text), `file_path` (absolute path to a local file), `googleType` (`google-doc`, `google-sheet`, `google-slide`), OR `write_excalidraw` with `excalidraw_data` or `mermaid`. Call get_org first; when `googleDrive.connected` is true, strongly prefer Google Workspace frames for business artifacts: use `google-doc` for memos/reports/briefs/SOPs/proposals, `google-sheet` for tables/trackers/budgets/research matrices/models, and `google-slide` for decks/presentation outlines. 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 files (images, PDFs), use `file_path` uploaded to storage and displayed as an asset frame.\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`.', {
1588
- 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.'),
1589
1590
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
1590
1591
  lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
1591
- content: z.string().optional().describe('[write] HTML/markdown/text. Mutually exclusive with file_path. Use a .html path for complete HTML documents and a .md path for Markdown.'),
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.'),
1592
1593
  excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
1593
1594
  mermaid: z.string().optional().describe('[write_excalidraw] Mermaid source to convert into an editable Excalidraw scene.'),
1594
- 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.'),
1595
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.'),
1596
- title: z.string().optional().describe('[write + googleType] Title for a new Google Doc/Sheet/Slide. Defaults to filename from path.'),
1597
- url: z.string().optional().describe('[write + googleType] Existing Google Doc/Sheet/Slide URL to attach.'),
1598
- 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.'),
1599
1623
  autoSize: z.boolean().optional().describe('[write] measure HTML content and size frame to fit. Content only, not file_path.'),
1600
1624
  width: z.number().optional().describe('[write] explicit width in pixels. Overrides layer default. Ignored if autoSize=true.'),
1601
1625
  height: z.number().optional().describe('[write] explicit height in pixels. Overrides layer default. Ignored if autoSize=true.'),
@@ -1621,6 +1645,26 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1621
1645
  // write landed before it can develop a wrong assumption.
1622
1646
  const projectCtx = getCurrentProjectContext();
1623
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
+ };
1624
1668
  switch (action) {
1625
1669
  case 'read': {
1626
1670
  const { path, lines } = args;
@@ -1660,12 +1704,117 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1660
1704
  _meta: { frameHtml: result.content },
1661
1705
  });
1662
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
+ }
1663
1812
  case 'write': {
1664
- 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;
1665
1814
  if (!path) throw new Error('path required for action=write');
1666
- const writeSources = [content != null, !!file_path, !!googleType].filter(Boolean).length;
1667
- if (writeSources > 1) throw new Error('Provide only one of content, file_path, or googleType');
1668
- 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');
1669
1818
  const skillErr = await checkProjectSkills(getState().projectId);
1670
1819
  if (skillErr) return err(new Error(skillErr));
1671
1820
  const anchorErr = await checkAnchors(parseLayer(path));
@@ -1712,6 +1861,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1712
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' };
1713
1862
  body = { base64: buffer.toString('base64'), contentType: MIME[ext] || 'application/octet-stream' };
1714
1863
  }
1864
+ } else if (base64 != null) {
1865
+ body = { base64, contentType: content_type || mimeFromExt(extname(parts[2])) };
1715
1866
  } else {
1716
1867
  body = { content };
1717
1868
  if (autoSize) body.autoSize = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.20",
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": [