drafted 1.13.0 → 1.14.0

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
@@ -227,7 +227,17 @@ if [ -n "$STALE_DRAFTED_BIN" ]; then
227
227
  fi
228
228
  fi
229
229
 
230
- npm config set prefix "$NPM_GLOBAL_PREFIX" >/dev/null
230
+ # Do NOT `npm config set prefix` that repoints the user's GLOBAL npm prefix, so every
231
+ # `npm install -g <pkg>` they ever run lands in our dir AND our uninstall (`rm -rf ~/.drafted`)
232
+ # would wipe all their other globals. Install drafted with an explicit per-command --prefix
233
+ # instead (below), and HEAL any global prefix pin an older version of this installer wrote so
234
+ # the user's default is restored. Preserve every other ~/.npmrc line (auth tokens, etc.).
235
+ NPMRC="${npm_config_userconfig:-$HOME/.npmrc}"
236
+ if [ -f "$NPMRC" ] && grep -Eq '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC"; then
237
+ tmp_npmrc="$(mktemp)"
238
+ grep -Ev '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC" > "$tmp_npmrc" && cat "$tmp_npmrc" > "$NPMRC"
239
+ rm -f "$tmp_npmrc"
240
+ fi
231
241
  export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
232
242
 
233
243
  # Persist the prefix's bin dir at the FRONT of PATH for future shells — without
@@ -262,7 +272,7 @@ step "Installing Drafted"
262
272
  install_drafted_pkg() {
263
273
  attempts=5; delay=4; n=1
264
274
  while :; do
265
- if npm install -g drafted@latest --force; then return 0; fi
275
+ if npm install -g drafted@latest --force --prefix "$NPM_GLOBAL_PREFIX"; then return 0; fi
266
276
  if [ "$n" -ge "$attempts" ]; then return 1; fi
267
277
  echo -e " ${YELLOW}npm install failed (attempt $n/$attempts) — retrying in ${delay}s (a new release may still be propagating to the npm CDN)...${RESET}"
268
278
  sleep "$delay"
@@ -274,7 +284,7 @@ if ! install_drafted_pkg; then
274
284
  exit 1
275
285
  fi
276
286
  hash -r 2>/dev/null || true
277
- NPM_ROOT="$(npm root -g 2>/dev/null || true)"
287
+ NPM_ROOT="$(npm root -g --prefix "$NPM_GLOBAL_PREFIX" 2>/dev/null || true)"
278
288
  MCP_SERVER_MODULE="$NPM_ROOT/drafted/mcp/server.mjs"
279
289
  if [ -n "$NPM_ROOT" ] && [ -f "$MCP_SERVER_MODULE" ]; then
280
290
  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"
@@ -1288,7 +1298,7 @@ echo ""
1288
1298
  echo -e " ${DIM}MCP name:${RESET} ${BOLD}$INSTALL_NAME${RESET}"
1289
1299
  echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
1290
1300
  echo -e " ${DIM}To update production:${RESET} rerun curl -fsSL https://drafted.live/install.sh | bash"
1291
- echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted && rm -rf ~/.drafted"
1301
+ echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
1292
1302
  echo ""
1293
1303
  echo -e "${YELLOW}${BOLD}"
1294
1304
  echo " ┌─────────────────────────────────────────────────────────┐"
package/mcp/server.mjs CHANGED
@@ -1462,10 +1462,85 @@ async function consumePendingDeviceCode() {
1462
1462
  // meaningless and disruptive — it returns spurious sign-in URLs and, on login,
1463
1463
  // spawns a server-side browser-open and blocks polling until timeout. Register
1464
1464
  // it only on stdio.
1465
- if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL immediately (use for SSH/headless/tmux where a browser may not open) and starts background polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval. If get_link was called first, login reuses that pending code instead of opening a new browser.', {
1465
+ // --- Local-install auth surface: the desktop app, never a device link ---
1466
+ // The stdio installer ALWAYS installs the Drafted desktop app alongside the MCP, so on a
1467
+ // local install the app IS the sign-in surface. Spawning it hands off to the always-running
1468
+ // instance (single-instance plugin), which opens the sign-in window and starts the native
1469
+ // cookie->auth.json capture that this MCP reads via getBootstrapSessionId(). Only the web MCP
1470
+ // is app-less, and that path authenticates via OAuth2 in the browser — never this tool. The
1471
+ // device-code flow below is kept ONLY as a fallback for platforms without the desktop app.
1472
+ function desktopAppBinary() {
1473
+ try {
1474
+ if (process.platform === 'darwin') {
1475
+ const p = '/Applications/Drafted.app/Contents/MacOS/drafted-desktop';
1476
+ return existsSync(p) ? p : null;
1477
+ }
1478
+ if (process.platform === 'win32') {
1479
+ const p = join(process.env.LOCALAPPDATA || '', 'Programs', 'Drafted', 'Drafted.exe');
1480
+ return existsSync(p) ? p : null;
1481
+ }
1482
+ } catch { /* fall through to no-app */ }
1483
+ return null; // e.g. Linux — no desktop app → device-code fallback
1484
+ }
1485
+
1486
+ async function launchDesktopSignin() {
1487
+ const bin = desktopAppBinary();
1488
+ if (!bin) return false;
1489
+ try {
1490
+ const { spawn } = await import('child_process');
1491
+ // If the app is already running (macOS KeepAlive normally guarantees it), the single-instance
1492
+ // handler opens the sign-in window and DRAFTED_OPEN_LOGIN is ignored. If it isn't running,
1493
+ // the fresh primary instance honors DRAFTED_OPEN_LOGIN=1 and opens sign-in on boot.
1494
+ const child = spawn(bin, [], {
1495
+ detached: true,
1496
+ stdio: 'ignore',
1497
+ env: { ...process.env, DRAFTED_OPEN_LOGIN: '1' },
1498
+ });
1499
+ child.unref();
1500
+ return true;
1501
+ } catch { return false; }
1502
+ }
1503
+
1504
+ // Poll auth.json (written by the desktop app's cookie->auth.json bridge on in-app sign-in)
1505
+ // until a valid session id lands or the deadline passes.
1506
+ async function waitForBootstrapAuth(deadline) {
1507
+ while (Date.now() < deadline) {
1508
+ const sid = getBootstrapSessionId();
1509
+ if (sid) return sid;
1510
+ await new Promise(r => setTimeout(r, 1500));
1511
+ }
1512
+ return null;
1513
+ }
1514
+
1515
+ if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP APP is the sign-in surface: both actions open the Drafted app to its sign-in window — no device link is shown. `action=login` opens the app and waits for the in-app sign-in to complete; `action=get_link` opens the app and returns immediately (the next Drafted tool call picks up the captured session). A device-code link is used ONLY as a fallback on platforms without the desktop app (the web MCP uses OAuth2, not this tool).', {
1466
1516
  action: z.enum(['get_link', 'login']).describe('Operation to perform.'),
1467
1517
  }, async ({ action }) => {
1468
1518
  try {
1519
+ // Local install → open the desktop app's sign-in window (no link). Falls through to the
1520
+ // device-code flow below only when no desktop app is installed on this platform.
1521
+ {
1522
+ const existing = getState().sessionId || getBootstrapSessionId();
1523
+ if (existing) {
1524
+ try {
1525
+ const meRes = await serverFetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${existing}` } });
1526
+ if (meRes.ok) {
1527
+ const me = await meRes.json();
1528
+ return ok({ status: 'already_authenticated', userId: me.userId, email: me.userEmail, org: me.currentOrg?.name });
1529
+ }
1530
+ } catch { /* stale session — continue to sign-in */ }
1531
+ }
1532
+ if (await launchDesktopSignin()) {
1533
+ if (action === 'get_link') {
1534
+ return ok('Opening the Drafted app to sign in — approve in the app window, then retry your request.');
1535
+ }
1536
+ const sid = await waitForBootstrapAuth(Date.now() + 180000);
1537
+ if (!sid) throw new Error('Timed out waiting for sign-in. Complete sign-in in the Drafted app window, then retry.');
1538
+ getState().sessionId = null;
1539
+ await cloneSession();
1540
+ connectAgentWs();
1541
+ return ok({ status: 'logged_in', via: 'desktop-app' });
1542
+ }
1543
+ }
1469
1544
  if (action === 'get_link') {
1470
1545
  const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1471
1546
  if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
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": [