drafted 1.19.55 → 1.20.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/cli/drafted.mjs CHANGED
@@ -394,11 +394,46 @@ program
394
394
  .command('login')
395
395
  .description('Authenticate with Drafted')
396
396
  .option('--server <url>', 'Server URL (or set DRAFTED_SERVER env var)')
397
+ .option('--link <code>', 'Link this computer with the one-time code from the Get set up page (no browser step)')
397
398
  .action(async (options) => {
398
399
  if (options.server) {
399
400
  process.env.DRAFTED_SERVER = options.server;
400
401
  }
401
402
 
403
+ // A link code names the account explicitly, so it REPLACES any existing
404
+ // login rather than deferring to it: the user just asked for this account.
405
+ if (options.link) {
406
+ const serverUrl = getServerUrl();
407
+ let data = null;
408
+ for (let attempt = 1; attempt <= 3 && !data; attempt++) {
409
+ try {
410
+ const res = await fetch(`${serverUrl}/auth/device/token`, {
411
+ method: 'POST',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ body: JSON.stringify({ deviceCode: String(options.link).trim() }),
414
+ });
415
+ if (res.ok) data = await res.json();
416
+ } catch { /* network blip: retry */ }
417
+ if (!data && attempt < 3) await new Promise((r) => setTimeout(r, 2000));
418
+ }
419
+ if (data?.status !== 'approved') {
420
+ jsonOut(false, 'login', 'Link code expired or already used');
421
+ console.error('❌ That link code has expired or was already used.');
422
+ console.error(' Copy a fresh install command from the Get set up page on drafted.live.');
423
+ process.exit(1);
424
+ }
425
+ writeAuth({
426
+ sessionId: data.sessionId,
427
+ userId: data.userId || null,
428
+ orgId: data.orgId || null,
429
+ server: serverUrl,
430
+ updatedAt: new Date().toISOString(),
431
+ });
432
+ jsonOut(true, 'login', { sessionId: data.sessionId, userId: data.userId || null });
433
+ console.log('✅ This computer is linked to your Drafted account.');
434
+ return;
435
+ }
436
+
402
437
  const existing = readAuth();
403
438
  if (existing && existing.sessionId) {
404
439
  // Validate session is still valid server-side
package/install-mcp.sh CHANGED
@@ -16,6 +16,7 @@ INSTALL_MODE="production"
16
16
  INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted}"
17
17
  INSTALL_SERVER="${DRAFTED_SERVER:-$SERVER}"
18
18
  INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-}"
19
+ LINK_CODE=""
19
20
 
20
21
  while [ $# -gt 0 ]; do
21
22
  case "$1" in
@@ -39,8 +40,13 @@ while [ $# -gt 0 ]; do
39
40
  INSTALL_AUTH_FILE="$2"
40
41
  shift 2
41
42
  ;;
43
+ --link)
44
+ LINK_CODE="$2"
45
+ shift 2
46
+ ;;
42
47
  --help|-h)
43
- echo "Usage: install-mcp.sh [--local] [--server URL] [--name MCP_NAME] [--auth-file PATH]"
48
+ echo "Usage: install-mcp.sh [--local] [--server URL] [--name MCP_NAME] [--auth-file PATH] [--link CODE]"
49
+ echo " --link: one-time code from the Get set up page; links this computer to that account"
44
50
  echo " default: installs production MCP named drafted -> https://drafted.live"
45
51
  echo " --local: installs duplicate MCP named drafted-local -> http://localhost:3477"
46
52
  exit 0
@@ -297,6 +303,19 @@ else
297
303
  fi
298
304
  ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
299
305
 
306
+ # The Get set up page's command carries a one-time link code for the account that
307
+ # showed it, so the computer links without a browser hop (which could land in a
308
+ # different account's browser). Single use, 15 minutes; failure is not fatal, the
309
+ # agent can still sign in on first use.
310
+ if [ -n "$LINK_CODE" ]; then
311
+ step "Linking this computer to your account"
312
+ if DRAFTED_SERVER="$INSTALL_SERVER" drafted login --link "$LINK_CODE"; then
313
+ ok "Linked"
314
+ else
315
+ echo -e " ${YELLOW}Not linked. Your agent will ask you to sign in the first time it uses Drafted.${RESET}"
316
+ fi
317
+ fi
318
+
300
319
  # ── Configure ─────────────────────────────────────────────────────
301
320
 
302
321
  step "Connecting to your tools"
@@ -1318,13 +1337,62 @@ echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
1318
1337
  echo -e " ${DIM}To update production:${RESET} rerun the installer from drafted.live/install (curl -fsSL https://drafted.live/install.sh -o /tmp/install-drafted.sh && bash /tmp/install-drafted.sh)"
1319
1338
  echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
1320
1339
  echo ""
1321
- echo -e "${YELLOW}${BOLD}"
1322
- echo " ┌─────────────────────────────────────────────────────────┐"
1323
- echo " │ │"
1324
- echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
1325
- echo " │ │"
1326
- echo " │ Close and reopen Claude Desktop, Claude Code, │"
1327
- echo " │ Codex, or Cursor so it picks up the new MCP server. │"
1328
- echo " │ │"
1329
- echo " └─────────────────────────────────────────────────────────┘"
1330
- echo -e "${RESET}"
1340
+ # ── Hand-off: start the first session ───────────────────────────
1341
+ # Interactive production installs only. A fresh agent process loads the new MCP,
1342
+ # so no restart is needed, and onboard-drafted is what turns the install into a
1343
+ # first project. Without this the user is left at a banner, not knowing what to
1344
+ # type next. Anything unusual (no agent CLI, no terminal, a "no") falls back to it.
1345
+ handoff_to_agent() {
1346
+ # DRAFTED_TTY lets tests/install-handoff.test.ts play the terminal.
1347
+ local tty="${DRAFTED_TTY:-/dev/tty}"
1348
+ [ "$INSTALL_MODE" = "production" ] && [ "${DRAFTED_HEADLESS:-}" != "1" ] || return 1
1349
+ [ -n "${DRAFTED_TTY:-}" ] || [ -t 1 ] || return 1
1350
+ # One descriptor for every answer, so a scripted terminal is read line by line.
1351
+ exec 4<"$tty" 2>/dev/null || return 1
1352
+ local agents=() agent="" reply
1353
+ command -v claude >/dev/null 2>&1 && agents+=(claude)
1354
+ command -v codex >/dev/null 2>&1 && agents+=(codex)
1355
+ [ ${#agents[@]} -gt 0 ] || return 1
1356
+ step "Start your first project"
1357
+ read -r -p " Start it now in your agent? [Y/n] " reply <&4 || reply="n"
1358
+ case "$reply" in [nN]*) return 1 ;; esac
1359
+ if [ ${#agents[@]} -eq 1 ]; then
1360
+ agent="${agents[0]}"
1361
+ else
1362
+ read -r -p " Which agent do you use? 1) Claude Code 2) Codex [1] " reply <&4 || reply=""
1363
+ case "$reply" in 2*) agent="codex" ;; *) agent="claude" ;; esac
1364
+ fi
1365
+ if [ ! -s "$HOME/.drafted/auth.json" ]; then
1366
+ echo -e " Signing in to Drafted (a browser tab opens; approve it there)"
1367
+ DRAFTED_SERVER="$INSTALL_SERVER" drafted login <"$tty" || return 1
1368
+ fi
1369
+ local url="$INSTALL_SERVER/?setup=1"
1370
+ echo -e " Opening your surface: ${BOLD}$url${RESET}"
1371
+ echo -e " ${DIM}Signed up in a different browser or a private window? Open that link there.${RESET}"
1372
+ if command -v open >/dev/null 2>&1; then open "$url" >/dev/null 2>&1 || true
1373
+ elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 || true
1374
+ fi
1375
+ if [ "$agent" = "claude" ]; then
1376
+ echo -e " Starting Claude Code with ${BOLD}/drafted:onboard-drafted${RESET}..."
1377
+ exec claude "/drafted:onboard-drafted" <"$tty"
1378
+ fi
1379
+ # Codex has no namespaced slash command for it: pass the command's own text.
1380
+ local cmd_file="${PLUGIN_SRC:-}/commands/onboard-drafted.md" prompt="Run the Drafted onboarding (the onboard-drafted command)."
1381
+ if [ -f "$cmd_file" ]; then
1382
+ prompt="$(awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2' "$cmd_file" | sed 's/\$ARGUMENTS//g')"
1383
+ fi
1384
+ echo -e " Starting Codex with the Drafted onboarding..."
1385
+ exec codex "$prompt" <"$tty"
1386
+ }
1387
+ handoff_to_agent || {
1388
+ echo -e "${YELLOW}${BOLD}"
1389
+ echo " ┌─────────────────────────────────────────────────────────┐"
1390
+ echo " │ │"
1391
+ echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
1392
+ echo " │ │"
1393
+ echo " │ Close and reopen Claude Desktop, Claude Code, │"
1394
+ echo " │ Codex, or Cursor so it picks up the new MCP server. │"
1395
+ echo " │ │"
1396
+ echo " └─────────────────────────────────────────────────────────┘"
1397
+ echo -e "${RESET}"
1398
+ }
@@ -0,0 +1,31 @@
1
+ // Local-capability probe for the Get set up page (see mcp/server.mjs, agent-capabilities).
2
+ // Its own module so tests can import it without opening the MCP WebSocket.
3
+ import { execFile } from 'child_process';
4
+ import { homedir } from 'os';
5
+ import { join } from 'path';
6
+
7
+ /**
8
+ * Is Google's Workspace CLI signed in for this user, with Gmail readable?
9
+ * 'ready' | 'signed_out' | 'absent'. Checks the Drafted npm prefix first (where
10
+ * `drafted google-setup` installs it, and which a GUI-launched agent's PATH may
11
+ * lack), then PATH. Anything that is not Google's gws (Homebrew's `gws` is an
12
+ * unrelated tool) prints non-JSON and reads as 'absent'.
13
+ */
14
+ export function gwsStatusFromJson(stdout) {
15
+ try {
16
+ const j = JSON.parse(stdout);
17
+ return j.has_refresh_token && (j.scopes || []).some((x) => String(x).includes('/auth/gmail')) ? 'ready' : 'signed_out';
18
+ } catch {
19
+ return 'absent';
20
+ }
21
+ }
22
+ export function detectGwsStatus({ candidates = [join(homedir(), '.drafted', 'npm-global', 'bin', 'gws'), 'gws'] } = {}) {
23
+ const tryOne = (i) => new Promise((resolve) => {
24
+ if (i >= candidates.length) return resolve('absent');
25
+ execFile(candidates[i], ['auth', 'status'], { timeout: 5000, windowsHide: true }, (err, stdout) => {
26
+ if (err && (err.code === 'ENOENT' || err.code === 'EACCES')) return resolve(tryOne(i + 1));
27
+ resolve(gwsStatusFromJson(String(stdout || '')));
28
+ });
29
+ });
30
+ return tryOne(0).catch(() => 'absent');
31
+ }
package/mcp/server.mjs CHANGED
@@ -13,6 +13,7 @@ import { createHash } from 'node:crypto';
13
13
  import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync, unlinkSync, appendFileSync, readdirSync, statSync } from 'fs';
14
14
  import { join, dirname, basename, extname, resolve } from 'path';
15
15
  import { homedir, platform, release as osRelease, arch as osArch } from 'os';
16
+ import { detectGwsStatus } from './gws-status.mjs';
16
17
  import { fileURLToPath } from 'url';
17
18
  import { AsyncLocalStorage } from 'node:async_hooks';
18
19
  import { z } from 'zod';
@@ -1801,6 +1802,11 @@ async function connectAgentWs() {
1801
1802
  // suggestedName (basename of the working directory) lets the server offer it in the
1802
1803
  // name-before-work gate message and as the tab placeholder.
1803
1804
  try { agentWs.send(JSON.stringify({ type: 'agent-hello', agentLabel: getAgentLabel(), suggestedName: getSuggestedSessionName() })); } catch {}
1805
+ // Local capabilities for the Get set up page: only a status word, never the
1806
+ // Google account, scopes or client. Same open-race guard as the hello.
1807
+ if (!isRemote) detectGwsStatus().then((gws) => {
1808
+ try { if (agentWs?.readyState === 1) agentWs.send(JSON.stringify({ type: 'agent-capabilities', gws })); } catch {}
1809
+ });
1804
1810
  // Same open-race guard as the hello above: the socket can leave 'open' before
1805
1811
  // this second send lands, and an unguarded throw here kills the stdio child
1806
1812
  // (taking the agent's whole MCP connection with it) for a presence message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.55",
3
+ "version": "1.20.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": [