drafted 1.19.54 → 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
@@ -1372,6 +1407,20 @@ program
1372
1407
  console.log(JSON.stringify(config, null, 2));
1373
1408
  });
1374
1409
 
1410
+ // Command: google-setup — walks the user through connecting their OWN Google
1411
+ // account to the local agent via Google's `gws` CLI (cli/google-setup.sh).
1412
+ program
1413
+ .command('google-setup')
1414
+ .description('Let your local agent read your Gmail, Drive and Calendar (read-only, via Google\'s gws CLI and your own Cloud project)')
1415
+ .action(() => {
1416
+ if (platform() === 'win32') {
1417
+ console.error('Not yet available on Windows. Set up gws by hand: https://github.com/googleworkspace/cli#authentication');
1418
+ process.exit(1);
1419
+ }
1420
+ const child = spawn('bash', [join(__dirname, 'google-setup.sh')], { stdio: 'inherit' });
1421
+ child.on('exit', (code) => process.exit(code ?? 1));
1422
+ });
1423
+
1375
1424
  // Command: setup
1376
1425
  program
1377
1426
  .command('setup')
@@ -0,0 +1,173 @@
1
+ #!/bin/bash
2
+ # Connect the user's OWN Google account to their local agent, through Google's
3
+ # Workspace CLI (`gws`, npm @googleworkspace/cli) and a Cloud project THEY own.
4
+ #
5
+ # Why not Drafted's Google connection: full-inbox read needs a restricted Gmail
6
+ # scope, and a restricted scope on Drafted's consent screen means an annual,
7
+ # paid CASA assessment (see GOOGLE_SCOPES in server/lib/google-auth.mjs). On the
8
+ # user's own project the scope is theirs, the token stays on this machine
9
+ # (encrypted, OS keyring), and Drafted's servers never see a message. The agent
10
+ # reads source material with `gws`; Drafted holds the work it produces.
11
+ #
12
+ # Run by install-mcp.sh (interactive installs only) and by `drafted google-setup`.
13
+ # --from-installer remember a "no" so the auto-updater's re-runs never re-ask
14
+ set -e
15
+
16
+ FROM_INSTALLER=0
17
+ [ "${1:-}" = "--from-installer" ] && FROM_INSTALLER=1
18
+
19
+ BOLD="\033[1m"; DIM="\033[2m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"; RESET="\033[0m"
20
+ say() { echo -e " $1"; }
21
+ ok() { echo -e " ${GREEN}✓${RESET} $1"; }
22
+ warn() { echo -e " ${YELLOW}!${RESET} $1"; }
23
+ # Prompts read the terminal, not stdin (stdin may be a pipe). DRAFTED_TTY lets
24
+ # the test feed answers from a file.
25
+ TTY="${DRAFTED_TTY:-/dev/tty}"
26
+ exec 3<"$TTY" 2>/dev/null || exec 3</dev/null
27
+ ask() { local reply; read -r -p " $1 " reply <&3 || reply=""; echo "$reply"; }
28
+ pause() { read -r -p " $1 — press Enter when done " _ <&3 || true; }
29
+ open_url() {
30
+ say "${DIM}$1${RESET}"
31
+ if command -v open >/dev/null 2>&1; then open "$1" >/dev/null 2>&1 || true
32
+ elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$1" >/dev/null 2>&1 || true
33
+ fi
34
+ }
35
+
36
+ DRAFTED_HOME="$HOME/.drafted"
37
+ DECLINED_MARKER="$DRAFTED_HOME/google-setup.declined"
38
+ NPM_GLOBAL_PREFIX="${NPM_GLOBAL_PREFIX:-$DRAFTED_HOME/npm-global}"
39
+ GWS_CONFIG_DIR="${GOOGLE_WORKSPACE_CLI_CONFIG_DIR:-$HOME/.config/gws}"
40
+ # Read-only, deliberately: the agent reads with gws, and EVERY send goes through
41
+ # Drafted's send_email action, which the user approves in the Drafted UI. A
42
+ # send-capable gws token would be a way around that approval.
43
+ GWS_SERVICES="gmail,drive,calendar,docs,sheets"
44
+ APIS="gmail.googleapis.com,drive.googleapis.com,calendar-json.googleapis.com,docs.googleapis.com,sheets.googleapis.com"
45
+
46
+ # `gws auth status` prints JSON; read ONE field without echoing the rest (it
47
+ # carries the account's client id and scopes, which don't belong in a log).
48
+ gws_status_field() {
49
+ "$GWS" auth status 2>/dev/null | node -e '
50
+ let s = ""; process.stdin.on("data", (d) => (s += d)).on("end", () => {
51
+ let j = {}; try { j = JSON.parse(s); } catch {}
52
+ const f = process.argv[1];
53
+ if (f === "gmail") return console.log((j.has_refresh_token && (j.scopes || []).some((x) => x.includes("/auth/gmail"))) ? "yes" : "no");
54
+ console.log(j[f] === undefined || j[f] === null ? "" : String(j[f]));
55
+ });' "$1"
56
+ }
57
+
58
+ # ── The gws CLI ─────────────────────────────────────────────────
59
+ # Resolve Google's gws specifically: Homebrew's `gws` formula is an unrelated
60
+ # git-workspace tool that installs a binary of the same name.
61
+ GWS=""
62
+ for candidate in "$NPM_GLOBAL_PREFIX/bin/gws" "$(command -v gws 2>/dev/null || true)"; do
63
+ if [ -n "$candidate" ] && [ -x "$candidate" ] && "$candidate" --help 2>&1 | grep -q "Google Workspace"; then
64
+ GWS="$candidate"; break
65
+ fi
66
+ done
67
+ # Already connected: nothing to ask. Checked BEFORE the consent prompt so an
68
+ # interactive installer re-run doesn't re-pitch a finished setup.
69
+ if [ -n "$GWS" ] && [ "$(gws_status_field gmail)" = "yes" ]; then
70
+ ok "Google is connected for your agent (gws)"
71
+ exit 0
72
+ fi
73
+
74
+ # ── Consent ───────────────────────────────────────────────────────
75
+ echo ""
76
+ echo -e " ${BOLD}Let your agent read your Gmail, Drive and Calendar${RESET} ${DIM}(optional, ~5 minutes)${RESET}"
77
+ say "Your agent (Claude or Codex) can then find the work already sitting in your inbox"
78
+ say "and files, and build it out on your Drafted surface."
79
+ say "${DIM}• Uses a Google Cloud project that YOU own — not a Drafted app.${RESET}"
80
+ say "${DIM}• Read-only. The login token is stored encrypted on this machine only.${RESET}"
81
+ say "${DIM}• Drafted's servers never receive your mail. Only what you ask your agent to${RESET}"
82
+ say "${DIM} put on a surface is uploaded.${RESET}"
83
+ answer="$(ask "Set this up now? [y/N]")"
84
+ case "$answer" in
85
+ [yY]*) /bin/rm -f "$DECLINED_MARKER" ;;
86
+ *)
87
+ [ "$FROM_INSTALLER" = 1 ] && { mkdir -p "$DRAFTED_HOME"; : > "$DECLINED_MARKER"; }
88
+ say "${DIM}Skipped. Run ${BOLD}drafted google-setup${RESET}${DIM} any time.${RESET}"
89
+ exit 0
90
+ ;;
91
+ esac
92
+
93
+ if [ -z "$GWS" ]; then
94
+ say "Installing Google's Workspace CLI (gws)..."
95
+ npm install -g @googleworkspace/cli --prefix "$NPM_GLOBAL_PREFIX" >/dev/null
96
+ GWS="$NPM_GLOBAL_PREFIX/bin/gws"
97
+ fi
98
+ ok "gws $("$GWS" --version 2>/dev/null | head -1 | sed 's/^gws //')"
99
+
100
+ # ── Your Cloud project + OAuth client ───────────────────────────
101
+ account_type="$(ask "Is this a Google Workspace (work/school) account, or a personal @gmail.com? [w/p]")"
102
+
103
+ if [ "$(gws_status_field client_config_exists)" != "true" ]; then
104
+ if command -v gcloud >/dev/null 2>&1; then
105
+ say "gcloud found — ${BOLD}gws auth setup${RESET} will sign you in, create a project and enable the APIs."
106
+ "$GWS" auth setup || warn "gws auth setup did not finish — continuing with the manual steps."
107
+ else
108
+ say "${DIM}gcloud not installed — doing the same steps in the Cloud Console instead.${RESET}"
109
+ fi
110
+ fi
111
+
112
+ PROJECT_ID="$(gws_status_field project_id)"
113
+ [ -z "$PROJECT_ID" ] && command -v gcloud >/dev/null 2>&1 && PROJECT_ID="$(gcloud config get-value project 2>/dev/null || true)"
114
+ if [ -z "$PROJECT_ID" ]; then
115
+ say "Create a Cloud project (any name, e.g. ${BOLD}my-agent${RESET}):"
116
+ open_url "https://console.cloud.google.com/projectcreate"
117
+ PROJECT_ID="$(ask "Paste the project ID:")"
118
+ [ -z "$PROJECT_ID" ] && { warn "No project ID — stopping. Run ${BOLD}drafted google-setup${RESET} to resume."; exit 1; }
119
+ fi
120
+ ok "Project $PROJECT_ID"
121
+
122
+ if [ "$(gws_status_field client_config_exists)" != "true" ]; then
123
+ say ""
124
+ say "${BOLD}a.${RESET} Enable the Gmail, Drive, Calendar, Docs and Sheets APIs (click Enable):"
125
+ open_url "https://console.cloud.google.com/flows/enableapi?apiid=$APIS&project=$PROJECT_ID"
126
+ pause "APIs enabled"
127
+ say "${BOLD}b.${RESET} Name the app (e.g. ${BOLD}My agent${RESET}) and pick your own email as support contact:"
128
+ open_url "https://console.cloud.google.com/auth/branding?project=$PROJECT_ID"
129
+ case "$account_type" in
130
+ [wW]*) say " Audience: choose ${BOLD}Internal${RESET} — only your organisation, no review needed." ;;
131
+ *) say " Audience: choose ${BOLD}External${RESET}." ;;
132
+ esac
133
+ pause "Branding saved"
134
+ say "${BOLD}c.${RESET} Create an OAuth client: type ${BOLD}Desktop app${RESET} → Create → ${BOLD}Download JSON${RESET}:"
135
+ open_url "https://console.cloud.google.com/auth/clients/create?project=$PROJECT_ID"
136
+ pause "JSON downloaded"
137
+ secret="$(ls -t "$HOME"/Downloads/client_secret_*.json 2>/dev/null | head -1 || true)"
138
+ if [ -z "$secret" ]; then
139
+ secret="$(ask "Couldn't find client_secret_*.json in Downloads. Path to the file:")"
140
+ fi
141
+ [ -f "$secret" ] || { warn "No client file — stopping. Run ${BOLD}drafted google-setup${RESET} to resume."; exit 1; }
142
+ mkdir -p "$GWS_CONFIG_DIR"
143
+ # Moved, not copied: a client secret shouldn't be left lying in Downloads.
144
+ /bin/mv -f "$secret" "$GWS_CONFIG_DIR/client_secret.json"
145
+ chmod 600 "$GWS_CONFIG_DIR/client_secret.json"
146
+ ok "OAuth client saved"
147
+ fi
148
+
149
+ # External apps left in "Testing" get refresh tokens Google expires after
150
+ # 7 days. Publishing the user's OWN unverified app is allowed for personal use
151
+ # (Google caps unverified apps at 100 users) and makes the token long-lived.
152
+ case "$account_type" in
153
+ [wW]*) ;;
154
+ *)
155
+ say ""
156
+ say "${BOLD}d.${RESET} Click ${BOLD}Publish app${RESET} → Confirm. Without this Google logs you out every 7 days."
157
+ open_url "https://console.cloud.google.com/auth/audience?project=$PROJECT_ID"
158
+ pause "App published"
159
+ ;;
160
+ esac
161
+
162
+ # ── Sign in ─────────────────────────────────────────────────────
163
+ say ""
164
+ say "Signing in. Google will say ${BOLD}\"Google hasn't verified this app\"${RESET} — it's your own app:"
165
+ say "click ${BOLD}Advanced → Go to <your app> (unsafe)${RESET}, then allow read access."
166
+ "$GWS" auth login --readonly -s "$GWS_SERVICES" || true
167
+
168
+ if [ "$(gws_status_field gmail)" = "yes" ]; then
169
+ ok "Google connected — your agent can now read Gmail, Drive and Calendar via ${BOLD}gws${RESET}"
170
+ exit 0
171
+ fi
172
+ echo -e " ${RED}✗${RESET} Sign-in didn't complete. Run ${BOLD}drafted google-setup${RESET} to try again."
173
+ exit 1
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"
@@ -1293,6 +1312,20 @@ verify_no_legacy_http_config
1293
1312
  report_telemetry "drafted_mcp_configured" "installed"
1294
1313
  report_telemetry "drafted_install" "installed"
1295
1314
 
1315
+ # ── Optional: Google for the local agent ─────────────────────────
1316
+ # Interactive installs only. The auto-updater re-runs this script with no
1317
+ # terminal, so it must never reach a prompt; a "no" is remembered in
1318
+ # ~/.drafted/google-setup.declined so interactive re-runs don't re-ask either.
1319
+ GOOGLE_SETUP_SCRIPT="${DRAFTED_PKG_DIR:+$DRAFTED_PKG_DIR/cli/google-setup.sh}"
1320
+ [ -f "$SCRIPT_DIR/cli/google-setup.sh" ] && GOOGLE_SETUP_SCRIPT="$SCRIPT_DIR/cli/google-setup.sh"
1321
+ if [ "$INSTALL_MODE" = "production" ] && [ "${DRAFTED_HEADLESS:-}" != "1" ] \
1322
+ && [ -t 1 ] && { : </dev/tty; } 2>/dev/null \
1323
+ && [ ! -f "$HOME/.drafted/google-setup.declined" ] \
1324
+ && [ -n "$GOOGLE_SETUP_SCRIPT" ] && [ -f "$GOOGLE_SETUP_SCRIPT" ]; then
1325
+ step "Connecting Google (optional)"
1326
+ NPM_GLOBAL_PREFIX="$NPM_GLOBAL_PREFIX" bash "$GOOGLE_SETUP_SCRIPT" --from-installer || true
1327
+ fi
1328
+
1296
1329
  # ── Done ─────────────────────────────────────────────────────────
1297
1330
 
1298
1331
  echo ""
@@ -1304,13 +1337,62 @@ echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
1304
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)"
1305
1338
  echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
1306
1339
  echo ""
1307
- echo -e "${YELLOW}${BOLD}"
1308
- echo " ┌─────────────────────────────────────────────────────────┐"
1309
- echo " │ │"
1310
- echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
1311
- echo " │ │"
1312
- echo " │ Close and reopen Claude Desktop, Claude Code, │"
1313
- echo " │ Codex, or Cursor so it picks up the new MCP server. │"
1314
- echo " │ │"
1315
- echo " └─────────────────────────────────────────────────────────┘"
1316
- 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';
@@ -430,7 +431,9 @@ LINKING: link the user to the narrowest thing you touched, never the project as
430
431
 
431
432
  DUAL REGISTRATION IS NORMAL: a separate "Drafted" remote connector (https://drafted.live/mcp, managed by claude.ai) may appear alongside this local stdio server in MCP listings. It is NOT a duplicate and NOT broken — it serves claude.ai web/mobile/Cowork, where a local stdio process cannot run. Never advise removing it; a "needs authentication" state on it is fixed by signing in from claude.ai and does not affect this stdio session.${isRemote ? `
432
433
 
433
- PLUGIN vs BARE CONNECTOR: Drafted ships as a plugin that bundles these tools WITH the Drafted skill and eight slash commands (/drafted:onboard-drafted, /drafted:create-project, /drafted:create-skill, /drafted:ingest, /drafted:extract, /drafted:improve-wiki, /drafted:improve-skill, /drafted:improve-project-harness). If the user added only the connector URL, they have the tools but none of that — the guidance for HOW to use the surface well is in the skill and commands they are missing. If the user asks where the slash commands are, or seems to be missing Drafted guidance, tell them: Settings → Customize plugins → Add → Add marketplace → ddfourtwo/drafted-web → install Drafted, then sign in when prompted. Mention it once, do not nag.` : ''}`,
434
+ PLUGIN vs BARE CONNECTOR: Drafted ships as a plugin that bundles these tools WITH the Drafted skill and eight slash commands (/drafted:onboard-drafted, /drafted:create-project, /drafted:create-skill, /drafted:ingest, /drafted:extract, /drafted:improve-wiki, /drafted:improve-skill, /drafted:improve-project-harness). If the user added only the connector URL, they have the tools but none of that — the guidance for HOW to use the surface well is in the skill and commands they are missing. If the user asks where the slash commands are, or seems to be missing Drafted guidance, tell them: Settings → Customize plugins → Add → Add marketplace → ddfourtwo/drafted-web → install Drafted, then sign in when prompted. Mention it once, do not nag.` : `
435
+
436
+ GOOGLE SOURCE MATERIAL: if the local \`gws\` CLI (Google Workspace CLI) is signed in, it is the user's OWN read-only access to their Gmail, Drive and Calendar. Use it to find source material and build the result in Drafted. Ask before the first read, read headers before bodies, and link back to sources rather than pasting email into frames. gws is read-only: EVERY email send goes through action(propose, type="send_email"), which the user must approve in the Drafted UI; never send by any other route. Not set up? The user can run \`drafted google-setup\`. Full rules: the drafted skill's "Source material from Google" section.`}`,
434
437
  });
435
438
 
436
439
  const layerKeys = Object.keys(LAYERS);
@@ -1799,6 +1802,11 @@ async function connectAgentWs() {
1799
1802
  // suggestedName (basename of the working directory) lets the server offer it in the
1800
1803
  // name-before-work gate message and as the tab placeholder.
1801
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
+ });
1802
1810
  // Same open-race guard as the hello above: the socket can leave 'open' before
1803
1811
  // this second send lands, and an unguarded throw here kills the stdio child
1804
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.54",
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": [
@@ -9,9 +9,10 @@ This is the over-arching prime command: orient, then seed all three stores so ev
9
9
 
10
10
  1. **Orient** — in 3-4 lines explain the loop: you *prime* from the harness (the system makes you search the wiki, and auto-loads the project's attached skills, anchors, and layer rules before you work), you *build*, then you *compound* (deposit what you learned). The more it's used, the less searching and the more stable the work. Drafted is navigated like a filesystem — one `fs` tool, three roots: `/wiki`, `/skills`, `/projects`.
11
11
  2. **Name the org** — the org is an address, not a cursor: address wiki/skills with `org=...` on the call, and projects self-derive their org from the path. Never rely on an "active" org: a project-less create by a multi-org user is refused rather than guessed.
12
- 3. **Seed knowledge** — run the `/drafted:ingest` flow: help the user point at existing business materials (folders, docs, past research) or interrogate them for tacit knowledge, and land durable pages in the wiki.
13
- 4. **Seed procedures** — from those materials and the conversation, surface 1-3 candidate SOPs. For the most valuable, run the `/drafted:create-skill` flow.
14
- 5. **Seed the surface** — run the `/drafted:create-project` flow for the user's immediate piece of work (or a reusable template).
15
- 6. Close by showing what now exists (wiki pages, skills, project) and restate the one-line loop: prime → build → compound.
12
+ 3. **Find the work (ask first).** Make your first question permission, offering three scopes, narrowest first: (a) "tell me what you're working on this week" (nothing is read), (b) one folder they point you to, (c) recent files plus their inbox, Drive and calendar. Option (c) needs `gws` signed in; check it as the `drafted` skill's *Source material from Google* section describes, and if it isn't, offer `! drafted google-setup`. Say plainly what stays on their machine (everything you read) and what reaches Drafted (only what they pick). Read headers before bodies. Then write one frame, **Your work, mapped**: the 3-7 strongest candidates for a Drafted project, each with why it fits (many artifacts, several people, a recurring process, a decision to make), its source links, and the project it would become. Let the user pick.
13
+ 4. **Seed knowledge** — run the `/drafted:ingest` flow: help the user point at existing business materials (folders, docs, past research, the sources behind the candidate they picked) or interrogate them for tacit knowledge, and land durable pages in the wiki.
14
+ 5. **Seed procedures** — from those materials and the conversation, surface 1-3 candidate SOPs. For the most valuable, run the `/drafted:create-skill` flow.
15
+ 6. **Seed the surface** — run the `/drafted:create-project` flow for the candidate the user picked, or their immediate piece of work (or a reusable template).
16
+ 7. Close by showing what now exists (wiki pages, skills, project) and restate the one-line loop: prime → build → compound.
16
17
 
17
18
  Keep it guided and conversational — one step at a time, confirming before each deposit. Don't dump everything at once.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: drafted
3
- description: Use the Drafted producibles harness — a compounding workspace that uplifts any AI across three primitives: knowledge (the org wiki), procedures (skills), and the project surface (frames on a shared real-time canvas). Drafted is navigated like a filesystem — one fs tool, one path grammar, org first: `fs(ls, path="/")` lists your orgs, then `/o/<org>/<root>/...`. Prime from the harness before working, build durable artifacts as frames instead of burying output in chat, and deposit what you learned back so the next session starts smarter. When Google Drive is connected, prefer Google Workspace frames for docs, sheets, and slides.
3
+ description: Use the Drafted producibles harness — a compounding workspace that uplifts any AI across three primitives: knowledge (the org wiki), procedures (skills), and the project surface (frames on a shared real-time canvas). Drafted is navigated like a filesystem — one fs tool, one path grammar, org first: `fs(ls, path="/")` lists your orgs, then `/o/<org>/<root>/...`. Prime from the harness before working, build durable artifacts as frames instead of burying output in chat, and deposit what you learned back so the next session starts smarter. When Google Drive is connected, prefer Google Workspace frames for docs, sheets, and slides. When the local `gws` CLI is signed in, read the user's Gmail, Drive and Calendar with it as source material and build the result in Drafted.
4
4
  ---
5
5
 
6
6
  # Drafted — a producibles harness that compounds
@@ -69,6 +69,16 @@ These bookend the loop. Prime/feed at the start, deposit at the end.
69
69
  - **`fs(mv, from="/o/<org>/projects/<p>/<layer>/<lane>/<file>", to="...")`** renames or moves (cross-project too). **`fs(rm, path="/o/<org>/projects/<project>")` archives** — agents never hard-delete; the archive is in the web UI.
70
70
  - **Return a clickable link** for what you touched — the `frameUrl`/`projectUrl` in the fs response is the URL the user opens.
71
71
 
72
+ ## Source material from Google (`gws`) — Drafted holds the work
73
+
74
+ Local sessions may have Google's Workspace CLI, `gws`, signed in **read-only** to the user's **own** Google account (set up by the installer or `drafted google-setup`). Split the jobs: **`gws` reads what already exists** (mail, Drive files, calendar) on the user's machine with the user's token. **Drafted is where what you make from it lives, and the only way anything is sent.** Don't use Drafted as a mail store, and don't leave the result in chat.
75
+
76
+ - **Check first.** `gws auth status` is ready when `has_refresh_token` is `true` and a scope contains `/auth/gmail`. Read those two fields; don't echo the whole output. If it isn't ready, suggest `! drafted google-setup` once (macOS/Linux; it's interactive, so the user runs it). Remote sessions (claude.ai, `/mcp`) have no `gws`, so skip this section there.
77
+ - **Ask before you read.** The first inbox or Drive read in a session needs the user's yes and a scope ("last 30 days", "threads with Acme", "this folder"). Offer the narrowest useful one.
78
+ - **Headers before bodies.** List with `gws gmail users threads list --params '{"userId":"me","q":"newer_than:30d -category:promotions -category:social","maxResults":50}'`. Get headers with `gws gmail users threads get --params '{"userId":"me","id":"<threadId>","format":"metadata","metadataHeaders":["From","Subject","Date"]}'` and triage on subject, participants and date. Open a body (`gws gmail +read --id <messageId>`) only for threads you will actually use. Drive: `gws drive files list --params '{"q":"trashed = false","orderBy":"modifiedTime desc","pageSize":50,"fields":"files(id,name,mimeType,modifiedTime,webViewLink)"}'`. When unsure of a method's shape, run `gws schema <service.resource.method>`.
79
+ - **Synthesize, never copy.** A frame or wiki page carries your synthesis plus a link back to the source (`https://mail.google.com/mail/u/0/#all/<threadId>`, or the Drive file's `webViewLink`). It never carries pasted email bodies or other people's addresses. The surface is shared with collaborators; the inbox is not. Quote only what the user asks for, attributed.
80
+ - **Every send goes through Drafted, and the user approves it.** `gws` is signed in read-only, and it stays that way. To send, draft the email, then `action(action="propose", type="send_email", payload='{"from":"<the user\'s own connected Gmail address>","to":"<one address>","subject":"<one line>","body":"<plain text>"}', reason="<one line: why>")`. The user reads it and approves it in the Drafted UI. Nothing is sent before that, and you cannot approve it yourself. It goes out AS the user, from their Gmail connected in Drafted Settings; if the proposal is refused because none is connected, tell them to connect it there. Never send by any other route (widening the `gws` sign-in, SMTP, another tool), even when asked to "just send it": propose, and point the user to the approval.
81
+
72
82
  ## Quality conventions
73
83
 
74
84
  - **Match format to layer intent.** Research/strategy/copy are usually markdown; visual work (wireframes, designs, dashboards) is HTML.