drafted 1.7.17 → 1.7.19

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
@@ -1,14 +1,56 @@
1
1
  #!/bin/bash
2
2
  set -e
3
3
 
4
- # Drafted — Design workspace for AI agents
4
+ # Drafted — Shared surface for AI-human collaboration
5
5
  # This script installs the Drafted MCP server and CLI globally via npm,
6
- # then registers it with Claude Desktop, Claude Code, and Cursor.
6
+ # then registers it with Claude Desktop, Claude Code, Codex, and Cursor.
7
7
  #
8
8
  # Run with:
9
9
  # curl -fsSL https://drafted.live/install.sh | bash
10
10
 
11
11
  SERVER="https://drafted.live"
12
+ INSTALLER_VERSION="1"
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ INSTALL_MODE="production"
15
+ INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted}"
16
+ INSTALL_SERVER="${DRAFTED_SERVER:-$SERVER}"
17
+ INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-}"
18
+
19
+ while [ $# -gt 0 ]; do
20
+ case "$1" in
21
+ --local)
22
+ INSTALL_MODE="local"
23
+ INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted-local}"
24
+ INSTALL_SERVER="${DRAFTED_SERVER:-http://localhost:3477}"
25
+ INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-$HOME/.drafted/auth.local.json}"
26
+ if [ -z "${DRAFTED_TELEMETRY+x}" ]; then export DRAFTED_TELEMETRY=0; fi
27
+ shift
28
+ ;;
29
+ --server)
30
+ INSTALL_SERVER="$2"
31
+ shift 2
32
+ ;;
33
+ --name)
34
+ INSTALL_NAME="$2"
35
+ shift 2
36
+ ;;
37
+ --auth-file)
38
+ INSTALL_AUTH_FILE="$2"
39
+ shift 2
40
+ ;;
41
+ --help|-h)
42
+ echo "Usage: install-mcp.sh [--local] [--server URL] [--name MCP_NAME] [--auth-file PATH]"
43
+ echo " default: installs production MCP named drafted -> https://drafted.live"
44
+ echo " --local: installs duplicate MCP named drafted-local -> http://localhost:3477"
45
+ exit 0
46
+ ;;
47
+ *)
48
+ echo "Unknown option: $1" >&2
49
+ exit 1
50
+ ;;
51
+ esac
52
+ done
53
+
12
54
 
13
55
  BOLD="\033[1m"
14
56
  DIM="\033[2m"
@@ -33,12 +75,117 @@ fail() {
33
75
  echo -e " ${RED}✗${RESET} $1"
34
76
  }
35
77
 
78
+ init_telemetry() {
79
+ INSTALL_ID=""
80
+ TELEMETRY_ENABLED=true
81
+ if [ "${DRAFTED_TELEMETRY:-}" = "0" ]; then
82
+ TELEMETRY_ENABLED=false
83
+ return 0
84
+ fi
85
+ mkdir -p "$HOME/.drafted"
86
+ INSTALL_ID="$(node - "$HOME/.drafted/install.json" <<'NODE'
87
+ const fs = require('fs');
88
+ const crypto = require('crypto');
89
+ const p = process.argv[2];
90
+ let data = {};
91
+ try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
92
+ if (data.telemetry === false) process.exit(2);
93
+ if (!/^[0-9a-f-]{36}$/i.test(String(data.installId || ''))) data.installId = crypto.randomUUID();
94
+ data.telemetry = data.telemetry !== false;
95
+ data.updatedAt = new Date().toISOString();
96
+ fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
97
+ process.stdout.write(data.installId);
98
+ NODE
99
+ )" || TELEMETRY_ENABLED=false
100
+ if [ "$TELEMETRY_ENABLED" = true ]; then
101
+ echo -e " ${DIM}Drafted sends anonymous install telemetry. Set DRAFTED_TELEMETRY=0 to opt out.${RESET}"
102
+ fi
103
+ }
104
+
105
+ report_telemetry() {
106
+ [ "${TELEMETRY_ENABLED:-false}" = true ] || return 0
107
+ [ -n "${INSTALL_ID:-}" ] || return 0
108
+ local event="$1"
109
+ local helper_status="${2:-installed}"
110
+ node - "$SERVER" "$INSTALL_ID" "$event" "$INSTALLER_VERSION" "$helper_status" \
111
+ "${CLIENT_CLAUDE_DESKTOP:-false}" "${CLIENT_CLAUDE_CODE:-false}" "${CLIENT_CODEX:-false}" "${CLIENT_CURSOR:-false}" <<'NODE' >/dev/null 2>&1 || true
112
+ const [server, installId, event, installerVersion, updateHelperStatus, claudeDesktop, claudeCode, codex, cursor] = process.argv.slice(2);
113
+ const os = require('os');
114
+ const cp = require('child_process');
115
+ function run(cmd) { try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().slice(0, 80); } catch { return undefined; } }
116
+ const platform = os.platform();
117
+ const osFamily = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : platform === 'linux' ? 'linux' : 'unknown';
118
+ fetch(`${server}/api/installations/report`, {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'Drafted Installer' },
121
+ body: JSON.stringify({
122
+ installId,
123
+ event,
124
+ schemaVersion: 1,
125
+ installerVersion,
126
+ cliVersion: run('drafted --version'),
127
+ osFamily,
128
+ osVersion: os.release(),
129
+ arch: os.arch(),
130
+ nodeVersion: process.version,
131
+ npmVersion: run('npm --version'),
132
+ clientsConfigured: {
133
+ claudeDesktop: claudeDesktop === 'true',
134
+ claudeCode: claudeCode === 'true',
135
+ codex: codex === 'true',
136
+ cursor: cursor === 'true'
137
+ },
138
+ updateHelperStatus,
139
+ mcpMode: 'stdio',
140
+ source: 'installer'
141
+ })
142
+ }).catch(() => {});
143
+ NODE
144
+ }
145
+
146
+ install_portable_node() {
147
+ local os arch platform filename url tmp node_dir
148
+ os="$(uname -s)"
149
+ arch="$(uname -m)"
150
+ case "$os" in
151
+ Darwin) platform="darwin" ;;
152
+ Linux) platform="linux" ;;
153
+ *) fail "Unsupported OS for automatic Node.js install: $os"; exit 1 ;;
154
+ esac
155
+ case "$arch" in
156
+ x86_64|amd64) arch="x64" ;;
157
+ arm64|aarch64) arch="arm64" ;;
158
+ *) fail "Unsupported CPU for automatic Node.js install: $arch"; exit 1 ;;
159
+ esac
160
+
161
+ mkdir -p "$HOME/.drafted"
162
+ node_dir="$HOME/.drafted/node"
163
+ tmp="$(mktemp -d)"
164
+ filename="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt | awk "/node-v.*-${platform}-${arch}\\.tar\\.xz/ {print \\$2; exit}")"
165
+ if [ -z "$filename" ]; then
166
+ fail "Could not find Node.js 22 download for $platform-$arch"
167
+ exit 1
168
+ fi
169
+ url="https://nodejs.org/dist/latest-v22.x/$filename"
170
+ echo -e " ${YELLOW}Downloading Node.js 22 for $platform-$arch...${RESET}"
171
+ curl -fsSL "$url" -o "$tmp/node.tar.xz"
172
+ rm -rf "$node_dir"
173
+ mkdir -p "$node_dir"
174
+ tar -xJf "$tmp/node.tar.xz" -C "$node_dir" --strip-components=1
175
+ rm -rf "$tmp"
176
+ export PATH="$node_dir/bin:$PATH"
177
+ ok "Installed portable Node.js $(node -v)"
178
+ }
179
+
36
180
  # ── Welcome ───────────────────────────────────────────────────────
37
181
 
38
182
  echo ""
39
183
  echo -e "${BOLD}Welcome to Drafted${RESET}"
40
- echo -e "This will set up Drafted so Claude can create designs for you."
184
+ echo -e "This will set up Drafted so your agents can create work on a shared surface."
41
185
  echo -e "It only takes a minute."
186
+ if [ "$INSTALL_MODE" = "local" ]; then
187
+ echo -e "${DIM}Local mode: installing MCP ${BOLD}$INSTALL_NAME${RESET}${DIM} -> $INSTALL_SERVER without touching production drafted.${RESET}"
188
+ fi
42
189
 
43
190
  # ── Prerequisites ─────────────────────────────────────────────────
44
191
 
@@ -48,30 +195,28 @@ step "Checking your system"
48
195
  if command -v node &>/dev/null; then
49
196
  NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
50
197
  if [ "$NODE_VERSION" -lt 22 ]; then
51
- fail "Node.js is too old (found $(node -v), need 22+)."
52
- echo ""
53
- echo -e " Update it by running: ${BOLD}brew upgrade node${RESET}"
54
- echo -e " Then re-run this installer."
55
- exit 1
56
- fi
57
- ok "Node.js $(node -v)"
58
- else
59
- fail "Node.js is not installed."
60
- echo ""
61
- if command -v brew &>/dev/null; then
62
- echo -e " Install it by running: ${BOLD}brew install node${RESET}"
198
+ echo -e " ${YELLOW}Node.js $(node -v) is too old; installing Node.js 22 for Drafted.${RESET}"
199
+ install_portable_node
63
200
  else
64
- echo -e " Download it from: ${BOLD}https://nodejs.org${RESET}"
201
+ ok "Node.js $(node -v)"
65
202
  fi
66
- echo -e " Then re-run this installer."
67
- exit 1
203
+ else
204
+ echo -e " ${YELLOW}Node.js is not installed; installing Node.js 22 for Drafted.${RESET}"
205
+ install_portable_node
68
206
  fi
69
207
 
208
+ init_telemetry
209
+
210
+ mkdir -p "$HOME/.drafted/npm-global"
211
+ npm config set prefix "$HOME/.drafted/npm-global" >/dev/null
212
+ export PATH="$HOME/.drafted/npm-global/bin:$PATH"
213
+
70
214
  # ── Install ───────────────────────────────────────────────────────
71
215
 
72
216
  step "Installing Drafted"
73
217
 
74
- npm install -g drafted@latest --silent 2>/dev/null
218
+ npm install -g drafted@latest --force --silent 2>/dev/null
219
+ hash -r 2>/dev/null || true
75
220
  ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
76
221
 
77
222
  # ── Configure ─────────────────────────────────────────────────────
@@ -80,11 +225,21 @@ step "Connecting to your tools"
80
225
 
81
226
  # Write server URL config
82
227
  mkdir -p "$HOME/.drafted"
83
- echo "{\"server\":\"$SERVER\"}" > "$HOME/.drafted/config.json"
84
- ok "Server: $SERVER"
228
+ if [ "$INSTALL_MODE" = "production" ]; then
229
+ echo "{\"server\":\"$INSTALL_SERVER\"}" > "$HOME/.drafted/config.json"
230
+ ok "Server: $INSTALL_SERVER"
231
+ else
232
+ ok "Local server: $INSTALL_SERVER"
233
+ fi
85
234
 
86
- # Resolve the installed binary path for MCP configs
87
- DRAFTED_MCP_BIN="$(which drafted-mcp 2>/dev/null || echo "drafted-mcp")"
235
+ # Local stdio MCP command. Production uses the installed package; --local uses this checkout when available.
236
+ if [ "$INSTALL_MODE" = "local" ] && [ -f "$SCRIPT_DIR/mcp/server.mjs" ]; then
237
+ DRAFTED_MCP_COMMAND="$(command -v node)"
238
+ DRAFTED_MCP_ARGS_JSON="$(node -e 'console.log(JSON.stringify([process.argv[1]]))' "$SCRIPT_DIR/mcp/server.mjs")"
239
+ else
240
+ DRAFTED_MCP_COMMAND="$(command -v drafted-mcp)"
241
+ DRAFTED_MCP_ARGS_JSON="[]"
242
+ fi
88
243
 
89
244
  configure_mcp() {
90
245
  local config_path="$1"
@@ -96,39 +251,130 @@ configure_mcp() {
96
251
  node -e "
97
252
  const fs = require('fs');
98
253
  const p = process.argv[1];
254
+ const name = process.argv[2];
255
+ const command = process.argv[3];
256
+ const args = JSON.parse(process.argv[4]);
257
+ const server = process.argv[5];
258
+ const authFile = process.argv[6];
259
+ const mode = process.argv[7];
99
260
  let c = {};
100
261
  try { c = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
101
262
  if (!c.mcpServers) c.mcpServers = {};
102
- c.mcpServers.drafted = {
103
- command: 'drafted-mcp',
104
- args: []
105
- };
263
+ const env = { DRAFTED_SERVER: server };
264
+ if (authFile) env.DRAFTED_AUTH_FILE = authFile;
265
+ if (mode === 'local') env.DRAFTED_TELEMETRY = '0';
266
+ c.mcpServers[name] = { command, args, env };
106
267
  fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
107
- " "$config_path"
268
+ " "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE"
269
+ ok "$label"
270
+ }
271
+
272
+
273
+ verify_no_legacy_http_config() {
274
+ local found=false
275
+ for file in "$CLAUDE_DESKTOP_CONFIG" "$CLAUDE_CODE_CONFIG" "$CURSOR_CONFIG" "$CODEX_CONFIG"; do
276
+ [ -f "$file" ] || continue
277
+ if grep -q "https://drafted.live/mcp" "$file" 2>/dev/null; then
278
+ found=true
279
+ echo -e " ${RED}✗${RESET} Legacy HTTP Drafted MCP still present in $file"
280
+ fi
281
+ done
282
+ if [ "$found" = true ]; then
283
+ fail "Installer migration incomplete. Remove legacy https://drafted.live/mcp entries and rerun."
284
+ exit 1
285
+ fi
286
+ }
287
+
288
+ configure_codex() {
289
+ local config_path="$1"
290
+ local label="$2"
291
+ local config_dir
292
+ config_dir="$(dirname "$config_path")"
293
+ mkdir -p "$config_dir"
294
+
295
+ node - "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE" <<'NODE'
296
+ const fs = require('fs');
297
+ const p = process.argv[2];
298
+ const mcpName = process.argv[3];
299
+ const command = process.argv[4];
300
+ const args = JSON.parse(process.argv[5]);
301
+ const server = process.argv[6];
302
+ const authFile = process.argv[7];
303
+ const mode = process.argv[8];
304
+
305
+ let text = '';
306
+ try { text = fs.readFileSync(p, 'utf8'); } catch {}
307
+
308
+ const lines = text.split(/\r?\n/);
309
+ const out = [];
310
+ let skip = false;
311
+
312
+ for (const line of lines) {
313
+ const section = line.match(/^\[(.+)\]$/);
314
+ if (section) {
315
+ const name = section[1].trim();
316
+ if (name === `mcp_servers.${mcpName}` || name === `mcp_servers.${mcpName}.env`) {
317
+ skip = true;
318
+ continue;
319
+ }
320
+ if (skip) skip = false;
321
+ }
322
+ if (!skip) out.push(line);
323
+ }
324
+
325
+ let next = out.join('\n').replace(/\s+$/, '');
326
+ if (next) next += '\n\n';
327
+ next += `[mcp_servers.${mcpName}]\n`;
328
+ next += 'command = ' + JSON.stringify(command) + '\n';
329
+ next += 'args = [' + args.map(a => JSON.stringify(a)).join(', ') + ']\n';
330
+ next += `\n[mcp_servers.${mcpName}.env]\n`;
331
+ next += 'DRAFTED_SERVER = ' + JSON.stringify(server) + '\n';
332
+ if (authFile) next += 'DRAFTED_AUTH_FILE = ' + JSON.stringify(authFile) + '\n';
333
+ if (mode === 'local') next += 'DRAFTED_TELEMETRY = "0"\n';
334
+ next += '\n';
335
+
336
+ fs.writeFileSync(p, next);
337
+ NODE
338
+
108
339
  ok "$label"
109
340
  }
110
341
 
111
342
  CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
112
343
  CLAUDE_CODE_CONFIG="$HOME/.claude.json"
344
+ CODEX_CONFIG="$HOME/.codex/config.toml"
113
345
  CURSOR_CONFIG="$HOME/.cursor/mcp.json"
114
346
 
115
347
  CONFIGURED=false
348
+ CLIENT_CLAUDE_DESKTOP=false
349
+ CLIENT_CLAUDE_CODE=false
350
+ CLIENT_CODEX=false
351
+ CLIENT_CURSOR=false
116
352
 
117
353
  # Claude Desktop
118
- if [ -d "/Applications/Claude.app" ] || [ -d "$HOME/Applications/Claude.app" ]; then
354
+ if [ -d "/Applications/Claude.app" ] || [ -d "$HOME/Applications/Claude.app" ] || [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
119
355
  configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop"
356
+ CLIENT_CLAUDE_DESKTOP=true
120
357
  CONFIGURED=true
121
358
  fi
122
359
 
123
360
  # Claude Code
124
- if command -v claude &>/dev/null; then
361
+ if command -v claude &>/dev/null || [ -f "$CLAUDE_CODE_CONFIG" ] || [ -d "$HOME/.claude" ]; then
125
362
  configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code"
363
+ CLIENT_CLAUDE_CODE=true
364
+ CONFIGURED=true
365
+ fi
366
+
367
+ # Codex
368
+ if command -v codex &>/dev/null || [ -d "$HOME/.codex" ]; then
369
+ configure_codex "$CODEX_CONFIG" "Codex"
370
+ CLIENT_CODEX=true
126
371
  CONFIGURED=true
127
372
  fi
128
373
 
129
374
  # Cursor
130
- if [ -d "/Applications/Cursor.app" ] || [ -d "$HOME/Applications/Cursor.app" ] || command -v cursor &>/dev/null; then
375
+ if [ -d "/Applications/Cursor.app" ] || [ -d "$HOME/Applications/Cursor.app" ] || command -v cursor &>/dev/null || [ -f "$CURSOR_CONFIG" ]; then
131
376
  configure_mcp "$CURSOR_CONFIG" "Cursor"
377
+ CLIENT_CURSOR=true
132
378
  CONFIGURED=true
133
379
  fi
134
380
 
@@ -137,9 +383,120 @@ if [ "$CONFIGURED" = false ]; then
137
383
  echo -e " ${YELLOW}No supported editors detected — pre-configuring all.${RESET}"
138
384
  configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop (pre-configured)"
139
385
  configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code (pre-configured)"
386
+ configure_codex "$CODEX_CONFIG" "Codex (pre-configured)"
140
387
  configure_mcp "$CURSOR_CONFIG" "Cursor (pre-configured)"
388
+ CLIENT_CLAUDE_DESKTOP=true
389
+ CLIENT_CLAUDE_CODE=true
390
+ CLIENT_CODEX=true
391
+ CLIENT_CURSOR=true
141
392
  fi
142
393
 
394
+ # ── Agent instructions ────────────────────────────────────────────
395
+
396
+ step "Installing global agent instructions"
397
+
398
+ load_global_agent_instructions() {
399
+ local local_path="$SCRIPT_DIR/agent-instructions/global.md"
400
+ if [ -f "$local_path" ]; then
401
+ cat "$local_path"
402
+ return 0
403
+ fi
404
+ cat <<'DRAFTED_GLOBAL_INSTRUCTIONS'
405
+ <drafted>
406
+ Drafted is the shared workspace for agent-produced consulting artifacts. Claude Code and Codex agents should use it proactively when the task benefits from persistent visual work, client-facing documents, reusable skills, or organization knowledge.
407
+
408
+ Drafted terminology:
409
+ - Wiki: durable organization knowledge. It may also be called the knowledge base or database. Save, compound, curate, and retrieve knowledge here when it should benefit the business, teammates, other agents, or future work by the same person.
410
+ - Skills: reusable operating knowledge. Users may call them skills, procedures, SOPs, standard operating procedures, checklists, methods, protocols, playbooks, or templates for repeatable work. Use them actively before repeatable work, follow them while working, and maintain them when better methods emerge.
411
+ - Projects: client-facing or reviewable work products, organized as frames on a shared surface. Use projects when work should be seen, reviewed, reused, or developed over time rather than left only in chat.
412
+
413
+ Operating principle:
414
+ - Do not treat Drafted as passive storage. Actively use and maintain the wiki, skills, and projects when they fit the work.
415
+ - Default to creating or updating Drafted project frames for substantive outputs instead of leaving artifacts only in chat. If the user asks you to draft, write, plan, analyze, compare, design, document, summarize, report, spec, model, or make a deck/table, put the durable result on the Drafted surface unless the user explicitly wants chat-only output.
416
+ - Put durable knowledge in the wiki, reusable methods in skills, and producible artifacts in projects.
417
+ - Do not save reusable procedures, SOPs, checklists, methods, protocols, or repeatable work instructions to the wiki/knowledge base. Save them as Drafted skills.
418
+
419
+ Before working:
420
+ - Check whether Drafted MCP tools are available. If not authenticated, use auth(action="get_link") or auth(action="login") and give the user the sign-in link.
421
+ - Select the correct organization first. Use get_org(action="get") and project(action="list") to identify the target org/project. Opening a project switches org context automatically; for wiki/skill-only work, use get_org(action="switch", orgId=...).
422
+ - Check get_org(action="get") for googleDrive.connected. When Google Drive is connected for the active org, Google Workspace frames are available and should be the strong default for documents, spreadsheets, and presentations.
423
+ - Never assume the current active Drafted project or organization is correct. Verify the returned project/org before writing.
424
+
425
+ Wiki rules:
426
+ - Search the org wiki before substantive work: wiki(action="search") with relevant keywords.
427
+ - Read relevant pages before creating or changing artifacts.
428
+ - Maintain the wiki when you discover durable knowledge, decisions, client constraints, project context, research notes, or facts that should compound for the business or future agents.
429
+ - When creating or editing wiki pages, return a browser link the user can click.
430
+
431
+ Skill rules:
432
+ - Treat skill, procedure, SOP, checklist, method, protocol, playbook, and repeatable-work template as the same user intent.
433
+ - Search skills before starting repeatable work: skill(action="search") or skill(action="list").
434
+ - Load and follow relevant skills with skill(action="load"). Read supporting skill files when needed.
435
+ - When the user asks to record, distill, create, save, install, or update a skill/procedure/SOP/checklist/method/protocol/playbook, create or update a Drafted skill for the org with skill tools.
436
+ - Improve skills when you find a better checklist, standard, or operating method.
437
+
438
+ Project/producible rules:
439
+ - For client-facing, durable, or reviewable artifacts, produce the work inside the appropriate Drafted project instead of leaving it only in chat.
440
+ - Prefer one visible frame per artifact or artifact section so the user can review, compare, and refine work on the surface.
441
+ - Use project(action="open") before frame/asset/connector/layout changes. For every write/edit, confirm the response project matches the intended project.
442
+ - Read anchored frames and existing neighboring frames before editing a layer.
443
+ - After creating frames, documents, diagrams, or other producibles, use focus when available and return the clickable Drafted frame or project link.
444
+
445
+ Google Workspace rules:
446
+ - If get_org reports googleDrive.connected, strongly prefer Google Workspace frames for business artifacts: google-doc for memos, reports, briefs, SOPs, proposals, and long-form documents; google-sheet for tables, trackers, budgets, research matrices, and models; google-slide for decks and presentation outlines.
447
+ - Create Google Workspace frames with frame(action="write", googleType="google-doc" | "google-sheet" | "google-slide", path="/{layer}/{lane}/{filename}", title="...") after opening the project.
448
+ - For flowcharts, process maps, architecture diagrams, system diagrams, data-flow diagrams, visual maps, or other editable diagrams, load and follow the system skill `excalidraw-drafted` (Drafted Excalidraw Diagram) and create native Drafted Excalidraw frames. Use normal Drafted HTML/markdown frames for web/UI mockups, rich visual layouts, or non-editable presentation artifacts.
449
+ - Use breadcrumbs in code or docs where useful: drafted:<frameId> for frames and drafted-project:<projectId> for projects.
450
+
451
+ Collaboration rules:
452
+ - Parallel agents must each open and verify their own Drafted project/org context.
453
+ - If there is any doubt about the correct org, project, wiki location, or skill, ask a short clarifying question before writing.
454
+ </drafted>
455
+ DRAFTED_GLOBAL_INSTRUCTIONS
456
+ }
457
+
458
+ install_agent_instructions() {
459
+ local instructions_path="$1"
460
+ local label="$2"
461
+ local instructions_dir
462
+ local body
463
+ instructions_dir="$(dirname "$instructions_path")"
464
+ mkdir -p "$instructions_dir"
465
+
466
+ if ! body="$(load_global_agent_instructions)"; then
467
+ fail "Could not load Drafted global instructions"
468
+ return 1
469
+ fi
470
+
471
+ DRAFTED_GLOBAL_INSTRUCTIONS_BODY="$body" node - "$instructions_path" <<'NODE'
472
+ const fs = require('fs');
473
+ const p = process.argv[2];
474
+ const begin = '<!-- BEGIN drafted-global-instructions -->';
475
+ const end = '<!-- END drafted-global-instructions -->';
476
+ const body = process.env.DRAFTED_GLOBAL_INSTRUCTIONS_BODY || '';
477
+ if (!body.trim()) throw new Error('Drafted global instructions are empty');
478
+ const block = `${begin}\n${body.replace(/\s+$/, '')}\n${end}`;
479
+ let text = '';
480
+ try { text = fs.readFileSync(p, 'utf8'); } catch {}
481
+ const escapeRe = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
482
+ const re = new RegExp(`${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}`);
483
+ let next;
484
+ if (re.test(text)) {
485
+ next = text.replace(re, block);
486
+ } else {
487
+ next = text.replace(/\s+$/, '');
488
+ if (next) next += '\n\n';
489
+ next += block + '\n';
490
+ }
491
+ fs.writeFileSync(p, next.endsWith('\n') ? next : next + '\n');
492
+ NODE
493
+
494
+ ok "$label"
495
+ }
496
+
497
+ install_agent_instructions "$HOME/.claude/CLAUDE.md" "Claude global instructions"
498
+ install_agent_instructions "$HOME/.codex/CODEX.md" "Codex global instructions"
499
+
143
500
  # ── Skills ───────────────────────────────────────────────────────
144
501
 
145
502
  step "Installing skills"
@@ -170,14 +527,384 @@ else
170
527
  echo -e " ${YELLOW}Skills directory not found in package — skipping.${RESET}"
171
528
  fi
172
529
 
530
+
531
+ # ── Update menu bar helper ───────────────────────────────────────
532
+
533
+ install_update_menu_icon() {
534
+ if [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
535
+ echo -e " ${YELLOW}Headless mode: skipping update menu icon.${RESET}"
536
+ return 0
537
+ fi
538
+ if [ "$(uname -s)" != "Darwin" ]; then
539
+ echo -e " ${YELLOW}Update menu icon is only installed on macOS by this script.${RESET}"
540
+ return 0
541
+ fi
542
+ if ! command -v swiftc >/dev/null 2>&1; then
543
+ echo -e " ${YELLOW}Swift compiler not found; skipping macOS menu bar updater.${RESET}"
544
+ return 0
545
+ fi
546
+
547
+ local work_dir install_dir app_bundle app_macos app_resources swift_file exe_path plist_dir plist uid logo_path legacy_bundle
548
+ work_dir="$HOME/.drafted/updater"
549
+ install_dir="/Applications"
550
+ if [ ! -w "$install_dir" ]; then
551
+ install_dir="$HOME/Applications"
552
+ fi
553
+ app_bundle="$install_dir/Drafted Updater.app"
554
+ app_macos="$app_bundle/Contents/MacOS"
555
+ app_resources="$app_bundle/Contents/Resources"
556
+ swift_file="$work_dir/DraftedUpdater.swift"
557
+ exe_path="$app_macos/DraftedUpdater"
558
+ plist_dir="$HOME/Library/LaunchAgents"
559
+ plist="$plist_dir/live.drafted.updater.plist"
560
+ uid="$(id -u)"
561
+ legacy_bundle="$work_dir/DraftedUpdater.app"
562
+ mkdir -p "$work_dir" "$install_dir" "$app_macos" "$app_resources" "$plist_dir"
563
+ rm -rf "$legacy_bundle"
564
+
565
+ logo_path="$app_resources/logo.svg"
566
+ if [ -f "$SCRIPT_DIR/server/vendor/logo.svg" ]; then
567
+ cp "$SCRIPT_DIR/server/vendor/logo.svg" "$logo_path"
568
+ else
569
+ curl -fsSL "$INSTALL_SERVER/vendor/logo.svg" -o "$logo_path" >/dev/null 2>&1 || true
570
+ fi
571
+ if [ -f "$logo_path" ]; then
572
+ perl -0pi -e 's/fill="currentColor"/fill="#FFFFFF"/g; s/fill="var\(--logo-letter, #[^)]+\)"/fill="#0A2540"/g' "$logo_path" >/dev/null 2>&1 || true
573
+ fi
574
+
575
+ cat > "$swift_file" <<'SWIFT'
576
+ import SwiftUI
577
+ import AppKit
578
+
579
+ @main
580
+ struct DraftedUpdaterApp: App {
581
+ init() {
582
+ Telemetry.report(event: "drafted_update_helper_started", updateHelperStatus: "running")
583
+ Telemetry.report(event: "drafted_heartbeat", updateHelperStatus: "running")
584
+ }
585
+
586
+ var body: some Scene {
587
+ MenuBarExtra {
588
+ Button("Update Drafted") { Actions.updateDrafted() }
589
+ Button("View Logs") { Actions.viewLogs() }
590
+ Button("Open Drafted") { Actions.openDrafted() }
591
+ Divider()
592
+ Button("Quit") { NSApp.terminate(nil) }
593
+ } label: {
594
+ if let image = Actions.logoImage() {
595
+ Image(nsImage: image)
596
+ .resizable()
597
+ .frame(width: 16, height: 18)
598
+ .accessibilityLabel("Drafted")
599
+ } else {
600
+ Text("Drafted")
601
+ }
602
+ }
603
+ .menuBarExtraStyle(.menu)
604
+ }
605
+ }
606
+
607
+
608
+ final class UpdateStatusWindow: NSWindowController {
609
+ private let stack = NSStackView()
610
+ private let spinner = NSProgressIndicator()
611
+ private let titleField = NSTextField(labelWithString: "Updating Drafted…")
612
+ private let bodyField = NSTextField(labelWithString: "Downloading and running the installer. This can take a minute.")
613
+ private let closeButton = NSButton(title: "OK", target: nil, action: nil)
614
+
615
+ init() {
616
+ let panel = NSPanel(
617
+ contentRect: NSRect(x: 0, y: 0, width: 380, height: 168),
618
+ styleMask: [.titled, .closable],
619
+ backing: .buffered,
620
+ defer: false
621
+ )
622
+ panel.title = "Drafted Update"
623
+ panel.isReleasedWhenClosed = false
624
+ panel.level = .floating
625
+ super.init(window: panel)
626
+
627
+ stack.orientation = .vertical
628
+ stack.alignment = .centerX
629
+ stack.spacing = 12
630
+ stack.translatesAutoresizingMaskIntoConstraints = false
631
+
632
+ spinner.style = .spinning
633
+ spinner.controlSize = .regular
634
+
635
+ titleField.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
636
+ titleField.alignment = .center
637
+
638
+ bodyField.font = NSFont.systemFont(ofSize: 13)
639
+ bodyField.textColor = .secondaryLabelColor
640
+ bodyField.alignment = .center
641
+ bodyField.maximumNumberOfLines = 3
642
+ bodyField.lineBreakMode = .byWordWrapping
643
+
644
+ closeButton.target = self
645
+ closeButton.action = #selector(closeClicked)
646
+ closeButton.isHidden = true
647
+
648
+ stack.addArrangedSubview(spinner)
649
+ stack.addArrangedSubview(titleField)
650
+ stack.addArrangedSubview(bodyField)
651
+ stack.addArrangedSubview(closeButton)
652
+
653
+ panel.contentView = NSView()
654
+ panel.contentView?.addSubview(stack)
655
+ NSLayoutConstraint.activate([
656
+ stack.leadingAnchor.constraint(equalTo: panel.contentView!.leadingAnchor, constant: 28),
657
+ stack.trailingAnchor.constraint(equalTo: panel.contentView!.trailingAnchor, constant: -28),
658
+ stack.centerYAnchor.constraint(equalTo: panel.contentView!.centerYAnchor)
659
+ ])
660
+ }
661
+
662
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
663
+
664
+ func showUpdating() {
665
+ titleField.stringValue = "Updating Drafted…"
666
+ bodyField.stringValue = "Downloading and running the installer. This can take a minute."
667
+ closeButton.isHidden = true
668
+ spinner.isHidden = false
669
+ spinner.startAnimation(nil)
670
+ NSApp.activate(ignoringOtherApps: true)
671
+ showWindow(nil)
672
+ window?.center()
673
+ }
674
+
675
+ func showCompleted(success: Bool, message: String) {
676
+ spinner.stopAnimation(nil)
677
+ spinner.isHidden = true
678
+ titleField.stringValue = success ? "Update completed" : "Update failed"
679
+ bodyField.stringValue = message
680
+ closeButton.isHidden = false
681
+ NSApp.activate(ignoringOtherApps: true)
682
+ showWindow(nil)
683
+ window?.center()
684
+ }
685
+
686
+ @objc private func closeClicked() { window?.close() }
687
+ }
688
+
689
+ struct Actions {
690
+ static func logoImage() -> NSImage? {
691
+ guard
692
+ let url = Bundle.main.url(forResource: "logo", withExtension: "svg"),
693
+ let image = NSImage(contentsOf: url)
694
+ else { return nil }
695
+ image.size = NSSize(width: 16, height: 18)
696
+ image.isTemplate = false
697
+ return image
698
+ }
699
+
700
+ private static var isUpdating = false
701
+ private static var updateWindow: UpdateStatusWindow?
702
+
703
+ static func updateDrafted() {
704
+ if isUpdating {
705
+ updateWindow?.showUpdating()
706
+ return
707
+ }
708
+
709
+ isUpdating = true
710
+ let statusWindow = UpdateStatusWindow()
711
+ updateWindow = statusWindow
712
+ statusWindow.showUpdating()
713
+
714
+ let command = "tmp=$(mktemp); curl -fsSL https://drafted.live/install.sh -o \"$tmp\" && bash \"$tmp\""
715
+ let process = Process()
716
+ process.executableURL = URL(fileURLWithPath: "/bin/bash")
717
+ process.arguments = ["-lc", command]
718
+ process.terminationHandler = { proc in
719
+ if proc.terminationStatus != 0 {
720
+ Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "installer_exit_\(proc.terminationStatus)")
721
+ }
722
+ DispatchQueue.main.async {
723
+ isUpdating = false
724
+ let success = proc.terminationStatus == 0
725
+ statusWindow.showCompleted(
726
+ success: success,
727
+ message: success ? "Restart your editor to use the latest MCP tools." : "Run the Drafted installer again from drafted.live/install."
728
+ )
729
+ }
730
+ }
731
+ do { try process.run() } catch {
732
+ isUpdating = false
733
+ Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "process_run_failed")
734
+ statusWindow.showCompleted(success: false, message: error.localizedDescription)
735
+ return
736
+ }
737
+ }
738
+
739
+ static func viewLogs() {
740
+ let fm = FileManager.default
741
+ let home = fm.homeDirectoryForCurrentUser
742
+ let fallback = home.appendingPathComponent(".drafted")
743
+ try? fm.createDirectory(at: fallback, withIntermediateDirectories: true)
744
+ let readme = fallback.appendingPathComponent("logs-readme.txt")
745
+ if !fm.fileExists(atPath: readme.path) {
746
+ let text = """
747
+ Drafted MCP logs are written by the host app that launched drafted-mcp.
748
+
749
+ Most useful locations:
750
+ - Drafted MCP client errors: ~/.drafted/mcp-client.log
751
+ - Claude Desktop: ~/Library/Logs/Claude/mcp-server-drafted.log
752
+ - Claude Code: ~/.claude/projects (session transcripts) or ~/.claude/logs when present
753
+ - Drafted installer/updater: ~/.drafted
754
+
755
+ If a tool returns `fetch failed`, open the Claude log or transcript from the same run and look for `mcp-server-drafted` or `Drafted MCP`.
756
+ """
757
+ try? text.write(to: readme, atomically: true, encoding: .utf8)
758
+ }
759
+
760
+ let candidates = [
761
+ home.appendingPathComponent(".drafted/mcp-client.log"),
762
+ home.appendingPathComponent("Library/Logs/Claude/mcp-server-drafted.log"),
763
+ home.appendingPathComponent("Library/Logs/Claude"),
764
+ home.appendingPathComponent(".claude/logs"),
765
+ home.appendingPathComponent(".claude/projects"),
766
+ fallback,
767
+ ]
768
+ for url in candidates where fm.fileExists(atPath: url.path) {
769
+ var isDirectory: ObjCBool = false
770
+ fm.fileExists(atPath: url.path, isDirectory: &isDirectory)
771
+ if isDirectory.boolValue {
772
+ NSWorkspace.shared.open(url)
773
+ } else {
774
+ NSWorkspace.shared.activateFileViewerSelecting([url])
775
+ }
776
+ return
777
+ }
778
+ }
779
+
780
+ static func openDrafted() {
781
+ NSWorkspace.shared.open(URL(string: "https://drafted.live")!)
782
+ }
783
+ }
784
+
785
+ struct Telemetry {
786
+ static func report(event: String, updateHelperStatus: String, errorCode: String? = nil) {
787
+ guard ProcessInfo.processInfo.environment["DRAFTED_TELEMETRY"] != "0" else { return }
788
+ let path = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".drafted/install.json")
789
+ guard
790
+ let data = try? Data(contentsOf: path),
791
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
792
+ json["telemetry"] as? Bool != false,
793
+ let installId = json["installId"] as? String
794
+ else { return }
795
+ var body: [String: Any] = [
796
+ "installId": installId,
797
+ "event": event,
798
+ "schemaVersion": 1,
799
+ "osFamily": "macos",
800
+ "osVersion": ProcessInfo.processInfo.operatingSystemVersionString,
801
+ "arch": SystemVersion.machine,
802
+ "updateHelperStatus": updateHelperStatus,
803
+ "source": "macos-helper"
804
+ ]
805
+ if let errorCode = errorCode { body["errorCode"] = errorCode }
806
+ guard
807
+ let url = URL(string: "https://drafted.live/api/installations/report"),
808
+ let payload = try? JSONSerialization.data(withJSONObject: body)
809
+ else { return }
810
+ var request = URLRequest(url: url)
811
+ request.httpMethod = "POST"
812
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
813
+ request.httpBody = payload
814
+ URLSession.shared.dataTask(with: request).resume()
815
+ }
816
+ }
817
+
818
+ enum SystemVersion {
819
+ static var machine: String {
820
+ #if arch(arm64)
821
+ return "arm64"
822
+ #elseif arch(x86_64)
823
+ return "x64"
824
+ #else
825
+ return "unknown"
826
+ #endif
827
+ }
828
+ }
829
+ SWIFT
830
+
831
+ cat > "$app_bundle/Contents/Info.plist" <<APPPLIST
832
+ <?xml version="1.0" encoding="UTF-8"?>
833
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
834
+ <plist version="1.0">
835
+ <dict>
836
+ <key>CFBundleExecutable</key>
837
+ <string>DraftedUpdater</string>
838
+ <key>CFBundleIdentifier</key>
839
+ <string>live.drafted.updater</string>
840
+ <key>CFBundleName</key>
841
+ <string>Drafted Updater</string>
842
+ <key>CFBundleDisplayName</key>
843
+ <string>Drafted Updater</string>
844
+ <key>CFBundlePackageType</key>
845
+ <string>APPL</string>
846
+ <key>CFBundleShortVersionString</key>
847
+ <string>1.0</string>
848
+ <key>LSMinimumSystemVersion</key>
849
+ <string>13.0</string>
850
+ <key>LSUIElement</key>
851
+ <true/>
852
+ <key>NSHighResolutionCapable</key>
853
+ <true/>
854
+ </dict>
855
+ </plist>
856
+ APPPLIST
857
+
858
+ if ! swiftc -parse-as-library "$swift_file" -o "$exe_path" -framework SwiftUI -framework AppKit >/dev/null 2>&1; then
859
+ echo -e " ${YELLOW}Could not build macOS menu bar updater; skipping.${RESET}"
860
+ return 0
861
+ fi
862
+ codesign -s - -f "$exe_path" >/dev/null 2>&1 || true
863
+
864
+ cat > "$plist" <<PLIST
865
+ <?xml version="1.0" encoding="UTF-8"?>
866
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
867
+ <plist version="1.0">
868
+ <dict>
869
+ <key>Label</key>
870
+ <string>live.drafted.updater</string>
871
+ <key>ProgramArguments</key>
872
+ <array>
873
+ <string>open</string>
874
+ <string>-a</string>
875
+ <string>$app_bundle</string>
876
+ </array>
877
+ <key>RunAtLoad</key>
878
+ <true/>
879
+ <key>KeepAlive</key>
880
+ <false/>
881
+ </dict>
882
+ </plist>
883
+ PLIST
884
+
885
+ launchctl bootout "gui/$uid" "$plist" >/dev/null 2>&1 || true
886
+ pkill -x DraftedUpdater >/dev/null 2>&1 || true
887
+ mdimport "$app_bundle" >/dev/null 2>&1 || true
888
+ open "$app_bundle" >/dev/null 2>&1 || true
889
+ ok "macOS menu bar updater"
890
+ }
891
+ if [ "$INSTALL_MODE" = "production" ]; then
892
+ step "Installing update helper"
893
+ install_update_menu_icon
894
+ fi
895
+ verify_no_legacy_http_config
896
+ report_telemetry "drafted_mcp_configured" "installed"
897
+ report_telemetry "drafted_install" "installed"
898
+
173
899
  # ── Done ─────────────────────────────────────────────────────────
174
900
 
175
901
  echo ""
176
902
  echo ""
177
903
  echo -e "${GREEN}${BOLD}You're all set!${RESET}"
178
904
  echo ""
179
- echo -e " ${DIM}View your designs at:${RESET} ${BOLD}https://drafted.live${RESET}"
180
- echo -e " ${DIM}To update:${RESET} npm install -g drafted@latest"
905
+ echo -e " ${DIM}MCP name:${RESET} ${BOLD}$INSTALL_NAME${RESET}"
906
+ echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
907
+ echo -e " ${DIM}To update production:${RESET} rerun curl -fsSL https://drafted.live/install.sh | bash"
181
908
  echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted && rm -rf ~/.drafted"
182
909
  echo ""
183
910
  echo -e "${YELLOW}${BOLD}"
@@ -186,7 +913,7 @@ echo " │ │"
186
913
  echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
187
914
  echo " │ │"
188
915
  echo " │ Close and reopen Claude Desktop, Claude Code, │"
189
- echo " │ or Cursor so it picks up the new MCP server. │"
916
+ echo " │ Codex, or Cursor so it picks up the new MCP server. │"
190
917
  echo " │ │"
191
918
  echo " └─────────────────────────────────────────────────────────┘"
192
919
  echo -e "${RESET}"