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 +35 -0
- package/install-mcp.sh +79 -11
- package/mcp/gws-status.mjs +31 -0
- package/mcp/server.mjs +6 -0
- package/package.json +1 -1
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
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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.
|
|
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": [
|