drafted 1.7.21 → 1.7.23

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;
@@ -1528,14 +1580,137 @@ async function getGoogleDriveAvailability() {
1528
1580
  }
1529
1581
  }
1530
1582
 
1583
+ function normalizeMcpUpdatePolicy(policy) {
1584
+ const severity = policy?.policy?.severity || 'unknown';
1585
+ const updateAvailable = !!policy?.policy?.updateAvailable;
1586
+ const required = !!policy?.policy?.required;
1587
+ return {
1588
+ status: required ? 'required' : updateAvailable ? 'stale' : severity === 'unknown' ? 'unknown' : 'current',
1589
+ currentVersion: policy?.versions?.client || PACKAGE_VERSION,
1590
+ latestVersion: policy?.versions?.latest || null,
1591
+ recommendedVersion: policy?.versions?.recommended || null,
1592
+ minimumRequiredVersion: policy?.versions?.minimumRequired || null,
1593
+ severity,
1594
+ updateAvailable,
1595
+ stale: updateAvailable,
1596
+ required,
1597
+ reason: policy?.policy?.reason || null,
1598
+ enabled: policy?.policy?.enabled !== false,
1599
+ mode: policy?.package?.mcpMode || mcpMode(),
1600
+ distribution: policy?.package?.distribution || (mcpMode() === 'stdio' ? 'npm-stdio' : 'hosted-http'),
1601
+ update: {
1602
+ command: policy?.update?.command || null,
1603
+ helper: policy?.update?.helper || null,
1604
+ packageManager: policy?.update?.packageManager || 'npm',
1605
+ },
1606
+ restart: {
1607
+ required: !!policy?.restart?.required,
1608
+ guidance: policy?.restart?.guidance || null,
1609
+ },
1610
+ checkedAt: policy?.serverTimestamp || null,
1611
+ };
1612
+ }
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
+
1670
+ async function getMcpUpdateMetadata() {
1671
+ const mode = mcpMode();
1672
+ try {
1673
+ const query = new URLSearchParams({
1674
+ cliVersion: PACKAGE_VERSION,
1675
+ mcpMode: mode,
1676
+ });
1677
+ const policy = await api('GET', `/api/installations/latest?${query.toString()}`);
1678
+ return normalizeMcpUpdatePolicy(policy);
1679
+ } catch (error) {
1680
+ return {
1681
+ status: 'unknown',
1682
+ currentVersion: PACKAGE_VERSION,
1683
+ latestVersion: null,
1684
+ recommendedVersion: null,
1685
+ minimumRequiredVersion: null,
1686
+ severity: 'unknown',
1687
+ updateAvailable: false,
1688
+ stale: false,
1689
+ required: false,
1690
+ reason: 'latest_check_failed',
1691
+ enabled: false,
1692
+ mode,
1693
+ distribution: mode === 'stdio' ? 'npm-stdio' : 'hosted-http',
1694
+ update: { command: null, helper: null, packageManager: 'npm' },
1695
+ restart: { required: false, guidance: 'Drafted MCP update status is unavailable; get_org still succeeded.' },
1696
+ checkedAt: null,
1697
+ };
1698
+ }
1699
+ }
1700
+
1531
1701
 
1532
1702
  tool('get_org', {
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.'),
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.'),
1534
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.'),
1535
1705
  }, async (args = {}) => {
1536
1706
  try {
1537
1707
  const action = args.action || 'get';
1538
1708
 
1709
+ if (action === 'update_mcp') {
1710
+ const mcpUpdate = await getMcpUpdateMetadata();
1711
+ return ok(buildInstalledMcpUpdateInstructions(mcpUpdate));
1712
+ }
1713
+
1539
1714
  if (action === 'switch') {
1540
1715
  if (!args.orgId) throw new Error('orgId is required for action=switch');
1541
1716
  await api('POST', '/auth/switch-org', { orgId: args.orgId });
@@ -1550,7 +1725,8 @@ tool('get_org', {
1550
1725
  const orgs = (await api('GET', '/api/orgs')).orgs || [];
1551
1726
  const activeOrg = (orgs || []).map(o => ({ id: o.orgId || o.id, name: o.orgName || o.name })).find(o => o.id === me?.orgId) || null;
1552
1727
  const googleDrive = await getGoogleDriveAvailability();
1553
- 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.' });
1728
+ const mcpUpdate = await getMcpUpdateMetadata();
1729
+ 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.' });
1554
1730
  }
1555
1731
 
1556
1732
  // Source of truth = THIS MCP session's bound org (what mutations will actually
@@ -1564,6 +1740,7 @@ tool('get_org', {
1564
1740
  const activeOrg = sessionOrgId ? (orgs.find(o => o.id === sessionOrgId) || null) : null;
1565
1741
 
1566
1742
  const googleDrive = await getGoogleDriveAvailability();
1743
+ const mcpUpdate = await getMcpUpdateMetadata();
1567
1744
 
1568
1745
  let members = [];
1569
1746
  if (sessionOrgId) {
@@ -1578,6 +1755,7 @@ tool('get_org', {
1578
1755
  members: members.map(m => ({ id: m.userId, name: m.username, email: m.email, role: m.role })),
1579
1756
  googleDrive,
1580
1757
  mcpVersion: PACKAGE_VERSION,
1758
+ mcpUpdate,
1581
1759
  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.",
1582
1760
  });
1583
1761
  } catch (error) { return err(error); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.7.21",
3
+ "version": "1.7.23",
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": [