drafted 1.7.20 → 1.7.22

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,69 @@ async function getGoogleDriveAvailability() {
1528
1528
  }
1529
1529
  }
1530
1530
 
1531
+ function normalizeMcpUpdatePolicy(policy) {
1532
+ const severity = policy?.policy?.severity || 'unknown';
1533
+ const updateAvailable = !!policy?.policy?.updateAvailable;
1534
+ const required = !!policy?.policy?.required;
1535
+ return {
1536
+ status: required ? 'required' : updateAvailable ? 'stale' : severity === 'unknown' ? 'unknown' : 'current',
1537
+ currentVersion: policy?.versions?.client || PACKAGE_VERSION,
1538
+ latestVersion: policy?.versions?.latest || null,
1539
+ recommendedVersion: policy?.versions?.recommended || null,
1540
+ minimumRequiredVersion: policy?.versions?.minimumRequired || null,
1541
+ severity,
1542
+ updateAvailable,
1543
+ stale: updateAvailable,
1544
+ required,
1545
+ reason: policy?.policy?.reason || null,
1546
+ enabled: policy?.policy?.enabled !== false,
1547
+ mode: policy?.package?.mcpMode || mcpMode(),
1548
+ distribution: policy?.package?.distribution || (mcpMode() === 'stdio' ? 'npm-stdio' : 'hosted-http'),
1549
+ update: {
1550
+ command: policy?.update?.command || null,
1551
+ helper: policy?.update?.helper || null,
1552
+ packageManager: policy?.update?.packageManager || 'npm',
1553
+ },
1554
+ restart: {
1555
+ required: !!policy?.restart?.required,
1556
+ guidance: policy?.restart?.guidance || null,
1557
+ },
1558
+ checkedAt: policy?.serverTimestamp || null,
1559
+ };
1560
+ }
1561
+
1562
+ async function getMcpUpdateMetadata() {
1563
+ const mode = mcpMode();
1564
+ try {
1565
+ const query = new URLSearchParams({
1566
+ cliVersion: PACKAGE_VERSION,
1567
+ mcpMode: mode,
1568
+ });
1569
+ const policy = await api('GET', `/api/installations/latest?${query.toString()}`);
1570
+ return normalizeMcpUpdatePolicy(policy);
1571
+ } catch (error) {
1572
+ return {
1573
+ status: 'unknown',
1574
+ currentVersion: PACKAGE_VERSION,
1575
+ latestVersion: null,
1576
+ recommendedVersion: null,
1577
+ minimumRequiredVersion: null,
1578
+ severity: 'unknown',
1579
+ updateAvailable: false,
1580
+ stale: false,
1581
+ required: false,
1582
+ reason: 'latest_check_failed',
1583
+ enabled: false,
1584
+ mode,
1585
+ distribution: mode === 'stdio' ? 'npm-stdio' : 'hosted-http',
1586
+ update: { command: null, helper: null, packageManager: 'npm' },
1587
+ restart: { required: false, guidance: 'Drafted MCP update status is unavailable; get_org still succeeded.' },
1588
+ checkedAt: null,
1589
+ };
1590
+ }
1591
+ }
1592
+
1593
+
1531
1594
  tool('get_org', {
1532
1595
  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
1596
  orgId: z.string().optional().describe('[switch] target org ID to switch to. Must be one of the orgs the user is a member of.'),
@@ -1549,7 +1612,8 @@ tool('get_org', {
1549
1612
  const orgs = (await api('GET', '/api/orgs')).orgs || [];
1550
1613
  const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
1551
1614
  const googleDrive = await getGoogleDriveAvailability();
1552
- return ok({ switched: true, activeOrg, googleDrive, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
1615
+ const mcpUpdate = await getMcpUpdateMetadata();
1616
+ return ok({ switched: true, activeOrg, googleDrive, mcpVersion: PACKAGE_VERSION, mcpUpdate, note: 'Active org switched. Wiki and skill calls now target this org. Active project cleared — open a project (or stay org-scoped for wiki/skill). If googleDrive.connected is true, prefer Google Workspace frames for docs, sheets, and slides.' });
1553
1617
  }
1554
1618
 
1555
1619
  // Source of truth = THIS MCP session's bound org (what mutations will actually
@@ -1563,6 +1627,7 @@ tool('get_org', {
1563
1627
  const activeOrg = sessionOrgId ? (orgs.find(o => o.id === sessionOrgId) || null) : null;
1564
1628
 
1565
1629
  const googleDrive = await getGoogleDriveAvailability();
1630
+ const mcpUpdate = await getMcpUpdateMetadata();
1566
1631
 
1567
1632
  let members = [];
1568
1633
  if (sessionOrgId) {
@@ -1577,6 +1642,7 @@ tool('get_org', {
1577
1642
  members: members.map(m => ({ id: m.userId, name: m.username, email: m.email, role: m.role })),
1578
1643
  googleDrive,
1579
1644
  mcpVersion: PACKAGE_VERSION,
1645
+ mcpUpdate,
1580
1646
  note: "activeOrg is the org bound to THIS MCP session — what mutations will actually target. To switch orgs without creating a project, call get_org(action=\"switch\", orgId=\"...\"). Browser tabs and other MCP sessions for the same user can be on different orgs. If googleDrive.connected is true, strongly prefer Google Workspace frames for docs, sheets, and slides.",
1581
1647
  });
1582
1648
  } catch (error) { return err(error); }
@@ -1584,18 +1650,41 @@ tool('get_org', {
1584
1650
 
1585
1651
  // ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
1586
1652
 
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.'),
1653
+ 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`.', {
1654
+ 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
1655
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
1590
1656
  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.'),
1657
+ 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
1658
  excalidraw_data: z.any().optional().describe('[write_excalidraw] Excalidraw scene JSON object or JSON string. Defaults to an empty scene.'),
1593
1659
  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.'),
1660
+ file_path: z.string().optional().describe('[write] absolute path to a local file to upload. Mutually exclusive with content/base64/googleType.'),
1661
+ base64: z.string().optional().describe('[write] base64-encoded binary content. Mutually exclusive with content/file_path/googleType. Use with content_type when known.'),
1662
+ 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
1663
  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.'),
1664
+ title: z.string().optional().describe('[write + googleType] Title for a new native file. Defaults to filename from path.'),
1665
+ url: z.string().optional().describe('[write + googleType] Existing Google file URL to attach.'),
1666
+ 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.'),
1667
+ 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.'),
1668
+ 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.'),
1669
+ slides: z.array(z.object({
1670
+ title: z.string().optional(),
1671
+ bullets: z.array(z.string()).optional(),
1672
+ speakerNotes: z.string().optional(),
1673
+ layout: z.string().optional(),
1674
+ }).passthrough()).optional().describe('[write_slide_content|append_slides] Structured slide spec: [{ title, bullets, speakerNotes?, layout? }].'),
1675
+ 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.'),
1676
+ slideObjectIds: z.array(z.string()).optional().describe('[clear_slides] Optional slide object IDs to delete. Omit to clear all slides.'),
1677
+ range: z.string().optional().describe('[Sheet value actions] A1 range, e.g. Sheet1!A1 or Data!A:Z.'),
1678
+ values: z.array(z.array(z.any())).optional().describe('[write_sheet_values|append_sheet_rows] 2D array of row values.'),
1679
+ valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().describe('[write_sheet_values|append_sheet_rows] Google Sheets value input option. Defaults to USER_ENTERED.'),
1680
+ majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().describe('[Sheet value actions] Major dimension for values. Defaults to ROWS when writing/appending.'),
1681
+ valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']).optional().describe('[read_sheet_values] How values should be rendered. Defaults to Google Sheets API default.'),
1682
+ dateTimeRenderOption: z.enum(['SERIAL_NUMBER', 'FORMATTED_STRING']).optional().describe('[read_sheet_values] How dates/times should be rendered.'),
1683
+ insertDataOption: z.enum(['OVERWRITE', 'INSERT_ROWS']).optional().describe('[append_sheet_rows] How new rows are inserted. Defaults to INSERT_ROWS.'),
1684
+ operation: z.enum(['add_sheet', 'rename_sheet']).optional().describe('[update_sheet] Sheet tab operation.'),
1685
+ sheetTitle: z.string().optional().describe('[update_sheet add_sheet] Title for the new sheet tab.'),
1686
+ sheetId: z.number().optional().describe('[update_sheet rename_sheet] Numeric sheet/tab id.'),
1687
+ newTitle: z.string().optional().describe('[update_sheet rename_sheet] New title for the sheet tab.'),
1599
1688
  autoSize: z.boolean().optional().describe('[write] measure HTML content and size frame to fit. Content only, not file_path.'),
1600
1689
  width: z.number().optional().describe('[write] explicit width in pixels. Overrides layer default. Ignored if autoSize=true.'),
1601
1690
  height: z.number().optional().describe('[write] explicit height in pixels. Overrides layer default. Ignored if autoSize=true.'),
@@ -1621,6 +1710,26 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1621
1710
  // write landed before it can develop a wrong assumption.
1622
1711
  const projectCtx = getCurrentProjectContext();
1623
1712
  const withProject = (result) => ({ ...result, project: projectCtx });
1713
+ const workspaceBody = (kind) => {
1714
+ const { path, googleId } = args;
1715
+ const body = {};
1716
+ const frameUrlMatch = path?.match(/\/f\/([a-f0-9-]{36})/);
1717
+ const uuidMatch = path?.match(/^[a-f0-9-]{36}$/);
1718
+ if (googleId) {
1719
+ if (kind === 'sheet') body.spreadsheetId = googleId;
1720
+ else if (kind === 'doc') body.documentId = googleId;
1721
+ else body.presentationId = googleId;
1722
+ } else if (frameUrlMatch?.[1] || uuidMatch) {
1723
+ body.frameId = frameUrlMatch?.[1] || path;
1724
+ } else if (path) {
1725
+ body.path = path;
1726
+ body.projectId = getState().projectId;
1727
+ } else {
1728
+ const label = kind === 'sheet' ? 'Google Sheet' : kind === 'doc' ? 'Google Doc' : 'Google Slide';
1729
+ throw new Error(`Provide path to a ${label} frame, frame ID/URL, or googleId/native file ID`);
1730
+ }
1731
+ return body;
1732
+ };
1624
1733
  switch (action) {
1625
1734
  case 'read': {
1626
1735
  const { path, lines } = args;
@@ -1660,12 +1769,117 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1660
1769
  _meta: { frameHtml: result.content },
1661
1770
  });
1662
1771
  }
1772
+ case 'get_sheet':
1773
+ case 'read_sheet_values':
1774
+ case 'write_sheet_values':
1775
+ case 'append_sheet_rows':
1776
+ case 'clear_sheet_range':
1777
+ case 'update_sheet': {
1778
+ const { path, googleId, range, values, valueInputOption, majorDimension, valueRenderOption, dateTimeRenderOption, insertDataOption, operation, sheetTitle, sheetId, newTitle } = args;
1779
+ const body = workspaceBody('sheet');
1780
+ if (range) body.range = range;
1781
+ if (majorDimension) body.majorDimension = majorDimension;
1782
+ if (action === 'write_sheet_values' || action === 'append_sheet_rows') {
1783
+ if (!values || !Array.isArray(values)) throw new Error(`values required for action=${action}`);
1784
+ body.values = values;
1785
+ body.valueInputOption = valueInputOption || 'USER_ENTERED';
1786
+ body.majorDimension = majorDimension || 'ROWS';
1787
+ }
1788
+ if (action === 'read_sheet_values') {
1789
+ if (valueRenderOption) body.valueRenderOption = valueRenderOption;
1790
+ if (dateTimeRenderOption) body.dateTimeRenderOption = dateTimeRenderOption;
1791
+ }
1792
+ if (action === 'append_sheet_rows') body.insertDataOption = insertDataOption || 'INSERT_ROWS';
1793
+ if (action === 'update_sheet') {
1794
+ body.operation = operation;
1795
+ if (sheetTitle) body.sheetTitle = sheetTitle;
1796
+ if (sheetId != null) body.sheetId = sheetId;
1797
+ if (newTitle) body.newTitle = newTitle;
1798
+ }
1799
+ const endpoint = action === 'get_sheet'
1800
+ ? '/api/google/workspace/sheet'
1801
+ : action === 'read_sheet_values'
1802
+ ? '/api/google/workspace/sheet-values/read'
1803
+ : action === 'write_sheet_values'
1804
+ ? '/api/google/workspace/sheet-values'
1805
+ : action === 'append_sheet_rows'
1806
+ ? '/api/google/workspace/sheet-values/append'
1807
+ : action === 'clear_sheet_range'
1808
+ ? '/api/google/workspace/sheet-values/clear'
1809
+ : '/api/google/workspace/sheet-update';
1810
+ const result = await api('POST', endpoint, body);
1811
+ return ok(withProject(result));
1812
+ }
1813
+ case 'get_doc':
1814
+ case 'read_doc_content':
1815
+ case 'write_doc_content':
1816
+ case 'append_doc_content':
1817
+ case 'clear_doc_content':
1818
+ case 'update_doc': {
1819
+ const { content, format, mode, requests } = args;
1820
+ const body = workspaceBody('doc');
1821
+ if (format) body.format = format;
1822
+ if (mode) body.mode = mode;
1823
+ if (action === 'write_doc_content' || action === 'append_doc_content') {
1824
+ if (typeof content !== 'string') throw new Error(`content string required for action=${action}`);
1825
+ body.content = content;
1826
+ }
1827
+ if (action === 'update_doc') {
1828
+ if (!Array.isArray(requests)) throw new Error('requests array required for action=update_doc');
1829
+ body.requests = requests;
1830
+ }
1831
+ const endpoint = action === 'get_doc'
1832
+ ? '/api/google/workspace/doc'
1833
+ : action === 'read_doc_content'
1834
+ ? '/api/google/workspace/doc-content/read'
1835
+ : action === 'write_doc_content'
1836
+ ? '/api/google/workspace/doc-content'
1837
+ : action === 'append_doc_content'
1838
+ ? '/api/google/workspace/doc-content/append'
1839
+ : action === 'clear_doc_content'
1840
+ ? '/api/google/workspace/doc-content/clear'
1841
+ : '/api/google/workspace/doc-update';
1842
+ const result = await api('POST', endpoint, body);
1843
+ return ok(withProject(result));
1844
+ }
1845
+ case 'get_slide':
1846
+ case 'read_slide_content':
1847
+ case 'write_slide_content':
1848
+ case 'append_slides':
1849
+ case 'clear_slides':
1850
+ case 'update_slide': {
1851
+ const { slides, requests, slideObjectIds, mode } = args;
1852
+ const body = workspaceBody('slide');
1853
+ if (mode) body.mode = mode;
1854
+ if (action === 'write_slide_content' || action === 'append_slides') {
1855
+ if (!Array.isArray(slides)) throw new Error(`slides array required for action=${action}`);
1856
+ body.slides = slides;
1857
+ }
1858
+ if (action === 'clear_slides' && Array.isArray(slideObjectIds)) body.slideObjectIds = slideObjectIds;
1859
+ if (action === 'update_slide') {
1860
+ if (!Array.isArray(requests)) throw new Error('requests array required for action=update_slide');
1861
+ body.requests = requests;
1862
+ }
1863
+ const endpoint = action === 'get_slide'
1864
+ ? '/api/google/workspace/slide'
1865
+ : action === 'read_slide_content'
1866
+ ? '/api/google/workspace/slide-content/read'
1867
+ : action === 'write_slide_content'
1868
+ ? '/api/google/workspace/slide-content'
1869
+ : action === 'append_slides'
1870
+ ? '/api/google/workspace/slides/append'
1871
+ : action === 'clear_slides'
1872
+ ? '/api/google/workspace/slides/clear'
1873
+ : '/api/google/workspace/slide-update';
1874
+ const result = await api('POST', endpoint, body);
1875
+ return ok(withProject(result));
1876
+ }
1663
1877
  case 'write': {
1664
- const { path, content, file_path, autoSize, width, height, color, googleType, title, url, googleId } = args;
1878
+ const { path, content, file_path, base64, content_type, autoSize, width, height, color, googleType, title, url, googleId } = args;
1665
1879
  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');
1880
+ const writeSources = [content != null, !!file_path, base64 != null, !!googleType].filter(Boolean).length;
1881
+ if (writeSources > 1) throw new Error('Provide only one of content, file_path, base64, or googleType');
1882
+ if (writeSources === 0) throw new Error('Provide content, file_path, base64, or googleType');
1669
1883
  const skillErr = await checkProjectSkills(getState().projectId);
1670
1884
  if (skillErr) return err(new Error(skillErr));
1671
1885
  const anchorErr = await checkAnchors(parseLayer(path));
@@ -1712,6 +1926,8 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
1712
1926
  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
1927
  body = { base64: buffer.toString('base64'), contentType: MIME[ext] || 'application/octet-stream' };
1714
1928
  }
1929
+ } else if (base64 != null) {
1930
+ body = { base64, contentType: content_type || mimeFromExt(extname(parts[2])) };
1715
1931
  } else {
1716
1932
  body = { content };
1717
1933
  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.22",
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": [