drafted 1.7.22 → 1.7.24

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/cli/drafted.mjs CHANGED
@@ -11,7 +11,7 @@ import { program } from 'commander';
11
11
  import { spawn, execSync } from 'child_process';
12
12
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
13
13
  import { join, dirname, basename, resolve } from 'path';
14
- import { homedir, tmpdir } from 'os';
14
+ import { homedir, tmpdir, platform } from 'os';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { DESIGN_SYSTEM_PROMPT, buildDesignPrompt } from './prompts.mjs';
17
17
 
@@ -140,6 +140,24 @@ function getServerUrl() {
140
140
  return `http://localhost:${process.env.DRAFTED_PORT || DEFAULT_PORT}`;
141
141
  }
142
142
 
143
+ function buildUpdateCommand() {
144
+ const server = getServerUrl().replace(/\/$/, '');
145
+ if (platform() === 'win32') {
146
+ const script = `$tmp = Join-Path $env:TEMP "drafted-install.ps1"; Invoke-WebRequest -UseBasicParsing "${server}/install.ps1" -OutFile $tmp; powershell -NoProfile -ExecutionPolicy Bypass -File $tmp`;
147
+ return {
148
+ shell: 'powershell.exe',
149
+ args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
150
+ manualCommand: script,
151
+ };
152
+ }
153
+ const script = `tmp=$(mktemp); curl -fsSL ${server}/install.sh -o "$tmp" && bash "$tmp"`;
154
+ return {
155
+ shell: 'sh',
156
+ args: ['-lc', script],
157
+ manualCommand: script,
158
+ };
159
+ }
160
+
143
161
  // --- Auth helpers ---
144
162
 
145
163
  function readAuth() {
@@ -347,6 +365,47 @@ program
347
365
  process.exit(1);
348
366
  });
349
367
 
368
+ // Command: update
369
+ program
370
+ .command('update')
371
+ .description('Update the npm-installed Drafted MCP daemon')
372
+ .option('--dry-run', 'Print update instructions without starting the updater')
373
+ .option('--yes', 'Start the updater out-of-process')
374
+ .action((options) => {
375
+ const update = buildUpdateCommand();
376
+ const data = {
377
+ started: false,
378
+ command: 'drafted update --yes',
379
+ dryRunCommand: 'drafted update --dry-run',
380
+ manualCommand: update.manualCommand,
381
+ restartRequired: true,
382
+ restartGuidance: 'Restart your agent/editor after updating so it starts the new drafted-mcp.',
383
+ };
384
+
385
+ if (options.yes && !options.dryRun) {
386
+ const child = spawn(update.shell, update.args, {
387
+ detached: true,
388
+ stdio: 'ignore',
389
+ });
390
+ child.unref();
391
+ data.started = true;
392
+ }
393
+
394
+ jsonOut(true, 'update', data);
395
+
396
+ if (data.started) {
397
+ console.log('Drafted updater started in the background.');
398
+ } else {
399
+ console.log('Drafted updater was not started.');
400
+ console.log('Run this command to update:');
401
+ console.log(` ${data.command}`);
402
+ console.log('');
403
+ console.log('Manual updater command:');
404
+ console.log(` ${data.manualCommand}`);
405
+ }
406
+ console.log(data.restartGuidance);
407
+ });
408
+
350
409
  // Command: logout
351
410
  program
352
411
  .command('logout')
package/mcp/server.mjs CHANGED
@@ -186,6 +186,56 @@ const TOOL_ANNOTATIONS = {
186
186
  wiki: { title: 'Wiki', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Per-org wiki. Markdown pages with paths as hierarchy. Dispatch by `action`.' },
187
187
  };
188
188
 
189
+ function isMutatingToolCall(name, args = {}) {
190
+ const action = args?.action;
191
+ switch (name) {
192
+ case 'project':
193
+ return ['create', 'update', 'move'].includes(action);
194
+ case 'template':
195
+ return ['create', 'update', 'delete', 'fork'].includes(action);
196
+ case 'layer':
197
+ return ['add', 'update', 'remove', 'reorder'].includes(action);
198
+ case 'frame':
199
+ return ![
200
+ 'read', 'search', 'versions', 'read_version',
201
+ 'get_sheet', 'read_sheet_values',
202
+ 'get_doc', 'read_doc_content',
203
+ 'get_slide', 'read_slide_content',
204
+ 'get_excel', 'read_excel_range',
205
+ ].includes(action);
206
+ case 'asset':
207
+ return ['upload', 'rm'].includes(action);
208
+ case 'skill':
209
+ return ['add', 'update', 'remove', 'attach', 'detach', 'favorite', 'unfavorite', 'update_file'].includes(action);
210
+ case 'wiki':
211
+ return ['log', 'write', 'edit', 'mv', 'rm', 'source-register', 'bulk-write'].includes(action);
212
+ case 'rm':
213
+ case 'shape':
214
+ case 'group':
215
+ case 'connector':
216
+ case 'layout':
217
+ return true;
218
+ default:
219
+ return false;
220
+ }
221
+ }
222
+
223
+ async function getRequiredMcpUpdateError(name, args = {}) {
224
+ if (mcpMode() !== 'stdio') return null;
225
+ if (!isMutatingToolCall(name, args)) return null;
226
+ const updateMetadata = await getMcpUpdateMetadata();
227
+ if (!updateMetadata?.required) return null;
228
+ const instructions = buildInstalledMcpUpdateInstructions(updateMetadata);
229
+ return [
230
+ `Drafted MCP update required before running ${name}${args?.action ? `.${args.action}` : ''}.`,
231
+ `Current version: ${instructions.currentVersion || PACKAGE_VERSION}.`,
232
+ instructions.latestVersion ? `Latest version: ${instructions.latestVersion}.` : null,
233
+ instructions.minimumRequiredVersion ? `Minimum required version: ${instructions.minimumRequiredVersion}.` : null,
234
+ `Run: ${instructions.command}`,
235
+ `Then restart your agent/editor so it starts the updated drafted-mcp.`,
236
+ ].filter(Boolean).join(' ');
237
+ }
238
+
189
239
  function tool(name, descOrSchema, schemaOrHandler, handler) {
190
240
  const ann = TOOL_ANNOTATIONS[name];
191
241
  if (!ann) throw new Error(`MCP tool "${name}" missing entry in TOOL_ANNOTATIONS`);
@@ -221,6 +271,8 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
221
271
  trackUmamiEvent(UMAMI_EVENTS.MCP_TOOL_CALLED, { tool: name, projectId: state.projectId || undefined, source: 'mcp' });
222
272
  reportInstallationEvent(UMAMI_EVENTS.DRAFTED_MCP_REQUEST, { tool: name });
223
273
  try {
274
+ const requiredUpdateError = await getRequiredMcpUpdateError(name, args?.[0] || {});
275
+ if (requiredUpdateError) return err(new Error(requiredUpdateError));
224
276
  return await cb(...args);
225
277
  } finally {
226
278
  state.currentTool = previousTool;
@@ -1559,6 +1611,62 @@ function normalizeMcpUpdatePolicy(policy) {
1559
1611
  };
1560
1612
  }
1561
1613
 
1614
+ function buildInstalledMcpUpdateInstructions(updateMetadata = null) {
1615
+ const mode = updateMetadata?.mode || mcpMode();
1616
+ const restart = updateMetadata?.restart || {
1617
+ required: mode === 'stdio',
1618
+ guidance: 'Restart agents after updating the npm-installed Drafted MCP daemon.',
1619
+ };
1620
+
1621
+ if (mode !== 'stdio') {
1622
+ return {
1623
+ action: 'update_mcp',
1624
+ started: false,
1625
+ updateSupported: false,
1626
+ mode,
1627
+ currentVersion: updateMetadata?.currentVersion || PACKAGE_VERSION,
1628
+ latestVersion: updateMetadata?.latestVersion || null,
1629
+ updateAvailable: false,
1630
+ required: false,
1631
+ command: null,
1632
+ dryRunCommand: null,
1633
+ manualCommand: null,
1634
+ restart: {
1635
+ required: false,
1636
+ guidance: restart.guidance || 'Hosted HTTP MCP updates with the Drafted server deploy.',
1637
+ },
1638
+ note: 'This session is using hosted HTTP MCP, so there is no npm-installed stdio daemon to update on this machine.',
1639
+ };
1640
+ }
1641
+
1642
+ const server = getServerUrl().replace(/\/$/, '');
1643
+ const manualCommand = platform() === 'win32'
1644
+ ? `$tmp = Join-Path $env:TEMP "drafted-install.ps1"; Invoke-WebRequest -UseBasicParsing "${server}/install.ps1" -OutFile $tmp; powershell -NoProfile -ExecutionPolicy Bypass -File $tmp`
1645
+ : `tmp=$(mktemp); curl -fsSL ${server}/install.sh -o "$tmp" && bash "$tmp"`;
1646
+
1647
+ return {
1648
+ action: 'update_mcp',
1649
+ started: false,
1650
+ updateSupported: true,
1651
+ mode: 'stdio',
1652
+ currentVersion: updateMetadata?.currentVersion || PACKAGE_VERSION,
1653
+ latestVersion: updateMetadata?.latestVersion || null,
1654
+ recommendedVersion: updateMetadata?.recommendedVersion || null,
1655
+ minimumRequiredVersion: updateMetadata?.minimumRequiredVersion || null,
1656
+ updateAvailable: !!updateMetadata?.updateAvailable,
1657
+ required: !!updateMetadata?.required,
1658
+ command: 'drafted update --yes',
1659
+ dryRunCommand: 'drafted update --dry-run',
1660
+ manualCommand,
1661
+ restart: {
1662
+ required: true,
1663
+ guidance: restart.guidance || 'Restart agents after updating the npm-installed Drafted MCP daemon.',
1664
+ },
1665
+ note: 'This action is intentionally advisory: it does not replace the currently running MCP process. Run the command, then restart your agent/editor so it starts the updated drafted-mcp.',
1666
+ mcpUpdate: updateMetadata || null,
1667
+ };
1668
+ }
1669
+
1562
1670
  async function getMcpUpdateMetadata() {
1563
1671
  const mode = mcpMode();
1564
1672
  try {
@@ -1592,12 +1700,17 @@ async function getMcpUpdateMetadata() {
1592
1700
 
1593
1701
 
1594
1702
  tool('get_org', {
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.'),
1703
+ action: z.enum(['get', 'switch', 'update_mcp']).optional().describe('Default: "get" returns active org and member info. Use "switch" with orgId to change the active org without opening a project. Use "update_mcp" to get explicit installed stdio MCP update instructions.'),
1596
1704
  orgId: z.string().optional().describe('[switch] target org ID to switch to. Must be one of the orgs the user is a member of.'),
1597
1705
  }, async (args = {}) => {
1598
1706
  try {
1599
1707
  const action = args.action || 'get';
1600
1708
 
1709
+ if (action === 'update_mcp') {
1710
+ const mcpUpdate = await getMcpUpdateMetadata();
1711
+ return ok(buildInstalledMcpUpdateInstructions(mcpUpdate));
1712
+ }
1713
+
1601
1714
  if (action === 'switch') {
1602
1715
  if (!args.orgId) throw new Error('orgId is required for action=switch');
1603
1716
  await api('POST', '/auth/switch-org', { orgId: args.orgId });
@@ -1650,7 +1763,7 @@ tool('get_org', {
1650
1763
 
1651
1764
  // ── Filesystem tools (direct HTTP to /api/fs) ─────────────────────
1652
1765
 
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`.', {
1766
+ 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 creating, immediately populate the native file using the matching write action in the same tool — do NOT leave it empty and do NOT tell the user you cannot write to it. For Sheets: `write_sheet_values` or `append_sheet_rows` (pass `path` or `googleId` from the create response). For Docs: `write_doc_content`/`append_doc_content`. For Slides: `write_slide_content`/`append_slides`. Read with `read_sheet_values`/`read_doc_content`/`read_slide_content`. Do NOT use inline `frame.write(content)` or hashline `frame.edit` to populate Google Workspace frames.\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
1767
  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.'),
1655
1768
  path: z.string().optional().describe('[read] /{layer}/{lane}/{filename}, frame URL, or UUID. [write|edit|anchor] /{layer}/{lane}/{filename}.'),
1656
1769
  lines: z.string().optional().describe('[read] line range (e.g. "1-50"). Omit to read all.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.22",
3
+ "version": "1.7.24",
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": [