drafted 1.24.2 → 1.24.4
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/README.md +15 -34
- package/cli/drafted.mjs +1 -1
- package/mcp/chunked-write.mjs +178 -0
- package/mcp/server.mjs +189 -12
- package/package.json +21 -27
- package/skills/import-website-to-drafted.md +329 -0
- package/agent-instructions/global.md +0 -3
- package/cli/google-setup.sh +0 -186
- package/cli/mail-watch.mjs +0 -237
- package/cli/open-url.mjs +0 -17
- package/cli/repo-scan.mjs +0 -240
- package/cli/skill-setup-trust.mjs +0 -17
- package/install-mcp.sh +0 -1440
- package/mcp/active-project-store.mjs +0 -81
- package/mcp/gates.mjs +0 -206
- package/mcp/gws-status.mjs +0 -31
- package/mcp/repo-address.mjs +0 -215
- package/mcp/test-file-path-text.mjs +0 -58
- package/mcp/test-org-guards.mjs +0 -234
- package/mcp/test-project-index.mjs +0 -88
- package/mcp/test-search-terms.mjs +0 -79
- package/mcp/test-skill-paths.mjs +0 -29
- package/mcp/widgets/canvas-overview.html +0 -229
- package/mcp/widgets/frame-preview.html +0 -162
- package/plugin/commands/create-project.md +0 -20
- package/plugin/commands/create-skill.md +0 -18
- package/plugin/commands/extract.md +0 -16
- package/plugin/commands/improve-project-harness.md +0 -16
- package/plugin/commands/improve-skill.md +0 -14
- package/plugin/commands/improve-wiki.md +0 -14
- package/plugin/commands/ingest.md +0 -20
- package/plugin/commands/onboard-drafted.md +0 -18
- package/plugin/skills/drafted/SKILL.md +0 -102
- package/server/lib/umami.mjs +0 -164
- package/src/shared/anchor-scope.mjs +0 -98
- package/src/shared/excalidraw.mjs +0 -94
- package/src/shared/folder-path.mjs +0 -176
- package/src/shared/gate-budget.mjs +0 -30
- package/src/shared/minion-presets.mjs +0 -67
- package/src/shared/okf-log.mjs +0 -62
- package/src/shared/record-conformance.mjs +0 -167
- package/src/shared/root-route.mjs +0 -45
- package/src/shared/task-id.mjs +0 -54
- package/src/shared/test-excalidraw-merge.mjs +0 -52
- package/src/shared/wiki-excalidraw.mjs +0 -90
- /package/{src/shared → shared}/constants.mjs +0 -0
package/install-mcp.sh
DELETED
|
@@ -1,1440 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
|
|
4
|
-
# Drafted — Shared surface for AI-human collaboration
|
|
5
|
-
# This script installs the Drafted MCP server and CLI globally via npm,
|
|
6
|
-
# then registers it with Claude Desktop, Claude Code, Codex, and Cursor.
|
|
7
|
-
#
|
|
8
|
-
# Run with (download first, then run the local file):
|
|
9
|
-
# curl -fsSL https://drafted.live/install.sh -o ~/Downloads/install-drafted.sh
|
|
10
|
-
# bash ~/Downloads/install-drafted.sh
|
|
11
|
-
|
|
12
|
-
SERVER="https://drafted.live"
|
|
13
|
-
INSTALLER_VERSION="1"
|
|
14
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
-
INSTALL_MODE="production"
|
|
16
|
-
INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted}"
|
|
17
|
-
INSTALL_SERVER="${DRAFTED_SERVER:-$SERVER}"
|
|
18
|
-
INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-}"
|
|
19
|
-
LINK_CODE=""
|
|
20
|
-
|
|
21
|
-
while [ $# -gt 0 ]; do
|
|
22
|
-
case "$1" in
|
|
23
|
-
--local)
|
|
24
|
-
INSTALL_MODE="local"
|
|
25
|
-
INSTALL_NAME="${DRAFTED_MCP_NAME:-drafted-local}"
|
|
26
|
-
INSTALL_SERVER="${DRAFTED_SERVER:-http://localhost:3477}"
|
|
27
|
-
INSTALL_AUTH_FILE="${DRAFTED_AUTH_FILE:-$HOME/.drafted/auth.local.json}"
|
|
28
|
-
if [ -z "${DRAFTED_TELEMETRY+x}" ]; then export DRAFTED_TELEMETRY=0; fi
|
|
29
|
-
shift
|
|
30
|
-
;;
|
|
31
|
-
--server)
|
|
32
|
-
INSTALL_SERVER="$2"
|
|
33
|
-
shift 2
|
|
34
|
-
;;
|
|
35
|
-
--name)
|
|
36
|
-
INSTALL_NAME="$2"
|
|
37
|
-
shift 2
|
|
38
|
-
;;
|
|
39
|
-
--auth-file)
|
|
40
|
-
INSTALL_AUTH_FILE="$2"
|
|
41
|
-
shift 2
|
|
42
|
-
;;
|
|
43
|
-
--link)
|
|
44
|
-
LINK_CODE="$2"
|
|
45
|
-
shift 2
|
|
46
|
-
;;
|
|
47
|
-
--help|-h)
|
|
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"
|
|
50
|
-
echo " default: installs production MCP named drafted -> https://drafted.live"
|
|
51
|
-
echo " --local: installs duplicate MCP named drafted-local -> http://localhost:3477"
|
|
52
|
-
exit 0
|
|
53
|
-
;;
|
|
54
|
-
*)
|
|
55
|
-
echo "Unknown option: $1" >&2
|
|
56
|
-
exit 1
|
|
57
|
-
;;
|
|
58
|
-
esac
|
|
59
|
-
done
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
BOLD="\033[1m"
|
|
63
|
-
DIM="\033[2m"
|
|
64
|
-
GREEN="\033[32m"
|
|
65
|
-
YELLOW="\033[33m"
|
|
66
|
-
RED="\033[31m"
|
|
67
|
-
CYAN="\033[36m"
|
|
68
|
-
RESET="\033[0m"
|
|
69
|
-
|
|
70
|
-
step=0
|
|
71
|
-
step() {
|
|
72
|
-
step=$((step + 1))
|
|
73
|
-
echo ""
|
|
74
|
-
echo -e "${CYAN}[$step]${RESET} ${BOLD}$1${RESET}"
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
ok() {
|
|
78
|
-
echo -e " ${GREEN}✓${RESET} $1"
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
fail() {
|
|
82
|
-
echo -e " ${RED}✗${RESET} $1"
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
init_telemetry() {
|
|
86
|
-
INSTALL_ID=""
|
|
87
|
-
TELEMETRY_ENABLED=true
|
|
88
|
-
if [ "${DRAFTED_TELEMETRY:-}" = "0" ]; then
|
|
89
|
-
TELEMETRY_ENABLED=false
|
|
90
|
-
return 0
|
|
91
|
-
fi
|
|
92
|
-
mkdir -p "$HOME/.drafted"
|
|
93
|
-
INSTALL_ID="$(node - "$HOME/.drafted/install.json" <<'NODE'
|
|
94
|
-
const fs = require('fs');
|
|
95
|
-
const crypto = require('crypto');
|
|
96
|
-
const p = process.argv[2];
|
|
97
|
-
let data = {};
|
|
98
|
-
try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
|
|
99
|
-
if (data.telemetry === false) process.exit(2);
|
|
100
|
-
if (!/^[0-9a-f-]{36}$/i.test(String(data.installId || ''))) data.installId = crypto.randomUUID();
|
|
101
|
-
data.telemetry = data.telemetry !== false;
|
|
102
|
-
data.updatedAt = new Date().toISOString();
|
|
103
|
-
fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
104
|
-
process.stdout.write(data.installId);
|
|
105
|
-
NODE
|
|
106
|
-
)" || TELEMETRY_ENABLED=false
|
|
107
|
-
if [ "$TELEMETRY_ENABLED" = true ]; then
|
|
108
|
-
echo -e " ${DIM}Drafted sends anonymous install telemetry. Set DRAFTED_TELEMETRY=0 to opt out.${RESET}"
|
|
109
|
-
fi
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
report_telemetry() {
|
|
113
|
-
[ "${TELEMETRY_ENABLED:-false}" = true ] || return 0
|
|
114
|
-
[ -n "${INSTALL_ID:-}" ] || return 0
|
|
115
|
-
local event="$1"
|
|
116
|
-
local helper_status="${2:-installed}"
|
|
117
|
-
node - "$SERVER" "$INSTALL_ID" "$event" "$INSTALLER_VERSION" "$helper_status" \
|
|
118
|
-
"${CLIENT_CLAUDE_DESKTOP:-false}" "${CLIENT_CLAUDE_CODE:-false}" "${CLIENT_CODEX:-false}" "${CLIENT_CURSOR:-false}" <<'NODE' >/dev/null 2>&1 || true
|
|
119
|
-
const [server, installId, event, installerVersion, updateHelperStatus, claudeDesktop, claudeCode, codex, cursor] = process.argv.slice(2);
|
|
120
|
-
const os = require('os');
|
|
121
|
-
const cp = require('child_process');
|
|
122
|
-
function run(cmd) { try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().slice(0, 80); } catch { return undefined; } }
|
|
123
|
-
const platform = os.platform();
|
|
124
|
-
const osFamily = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : platform === 'linux' ? 'linux' : 'unknown';
|
|
125
|
-
fetch(`${server}/api/installations/report`, {
|
|
126
|
-
method: 'POST',
|
|
127
|
-
headers: { 'Content-Type': 'application/json', 'User-Agent': 'Drafted Installer' },
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
installId,
|
|
130
|
-
event,
|
|
131
|
-
schemaVersion: 1,
|
|
132
|
-
installerVersion,
|
|
133
|
-
cliVersion: run('drafted --version'),
|
|
134
|
-
osFamily,
|
|
135
|
-
osVersion: os.release(),
|
|
136
|
-
arch: os.arch(),
|
|
137
|
-
nodeVersion: process.version,
|
|
138
|
-
npmVersion: run('npm --version'),
|
|
139
|
-
clientsConfigured: {
|
|
140
|
-
claudeDesktop: claudeDesktop === 'true',
|
|
141
|
-
claudeCode: claudeCode === 'true',
|
|
142
|
-
codex: codex === 'true',
|
|
143
|
-
cursor: cursor === 'true'
|
|
144
|
-
},
|
|
145
|
-
updateHelperStatus,
|
|
146
|
-
mcpMode: 'stdio',
|
|
147
|
-
source: 'installer'
|
|
148
|
-
})
|
|
149
|
-
}).catch(() => {});
|
|
150
|
-
NODE
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
# sha256 of a file equals an expected hex digest (macOS: shasum, Linux: sha256sum).
|
|
154
|
-
verify_sha256() {
|
|
155
|
-
local actual=""
|
|
156
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
157
|
-
actual="$(sha256sum "$1" | awk '{print $1}')"
|
|
158
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
159
|
-
actual="$(shasum -a 256 "$1" | awk '{print $1}')"
|
|
160
|
-
else
|
|
161
|
-
return 1
|
|
162
|
-
fi
|
|
163
|
-
[ -n "$2" ] && [ "$actual" = "$2" ]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
install_portable_node() {
|
|
167
|
-
local os arch platform filename url tmp node_dir
|
|
168
|
-
os="$(uname -s)"
|
|
169
|
-
arch="$(uname -m)"
|
|
170
|
-
case "$os" in
|
|
171
|
-
Darwin) platform="darwin" ;;
|
|
172
|
-
Linux) platform="linux" ;;
|
|
173
|
-
*) fail "Unsupported OS for automatic Node.js install: $os"; exit 1 ;;
|
|
174
|
-
esac
|
|
175
|
-
case "$arch" in
|
|
176
|
-
x86_64|amd64) arch="x64" ;;
|
|
177
|
-
arm64|aarch64) arch="arm64" ;;
|
|
178
|
-
*) fail "Unsupported CPU for automatic Node.js install: $arch"; exit 1 ;;
|
|
179
|
-
esac
|
|
180
|
-
|
|
181
|
-
mkdir -p "$HOME/.drafted"
|
|
182
|
-
node_dir="$HOME/.drafted/node"
|
|
183
|
-
tmp="$(mktemp -d)"
|
|
184
|
-
sums="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt)"
|
|
185
|
-
filename="$(printf '%s\n' "$sums" | awk "/node-v.*-${platform}-${arch}\\.tar\\.xz/ {print \$2; exit}")"
|
|
186
|
-
expected="$(printf '%s\n' "$sums" | awk "/node-v.*-${platform}-${arch}\\.tar\\.xz/ {print \$1; exit}")"
|
|
187
|
-
if [ -z "$filename" ] || [ -z "$expected" ]; then
|
|
188
|
-
fail "Could not find Node.js 22 download for $platform-$arch"
|
|
189
|
-
exit 1
|
|
190
|
-
fi
|
|
191
|
-
url="https://nodejs.org/dist/latest-v22.x/$filename"
|
|
192
|
-
echo -e " ${YELLOW}Downloading Node.js 22 for $platform-$arch...${RESET}"
|
|
193
|
-
curl -fsSL "$url" -o "$tmp/node.tar.xz"
|
|
194
|
-
# The checksum list was downloaded and then ignored. A corrupted, truncated or
|
|
195
|
-
# swapped tarball must not be unpacked and put on PATH (audit 2026-10-13).
|
|
196
|
-
if ! verify_sha256 "$tmp/node.tar.xz" "$expected"; then
|
|
197
|
-
rm -rf "$tmp"
|
|
198
|
-
fail "Node.js download failed its checksum; not installing it"
|
|
199
|
-
exit 1
|
|
200
|
-
fi
|
|
201
|
-
rm -rf "$node_dir"
|
|
202
|
-
mkdir -p "$node_dir"
|
|
203
|
-
tar -xJf "$tmp/node.tar.xz" -C "$node_dir" --strip-components=1
|
|
204
|
-
rm -rf "$tmp"
|
|
205
|
-
export PATH="$node_dir/bin:$PATH"
|
|
206
|
-
ok "Installed portable Node.js $(node -v)"
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# ── Welcome ───────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
echo ""
|
|
212
|
-
echo -e "${BOLD}Welcome to Drafted${RESET}"
|
|
213
|
-
echo -e "This will set up Drafted so your agents can create work on a shared surface."
|
|
214
|
-
echo -e "It only takes a minute."
|
|
215
|
-
if [ "$INSTALL_MODE" = "local" ]; then
|
|
216
|
-
echo -e "${DIM}Local mode: installing MCP ${BOLD}$INSTALL_NAME${RESET}${DIM} -> $INSTALL_SERVER without touching production drafted.${RESET}"
|
|
217
|
-
fi
|
|
218
|
-
|
|
219
|
-
# ── Prerequisites ─────────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
step "Checking your system"
|
|
222
|
-
|
|
223
|
-
# Node.js
|
|
224
|
-
if command -v node &>/dev/null; then
|
|
225
|
-
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
|
226
|
-
if [ "$NODE_VERSION" -lt 22 ]; then
|
|
227
|
-
echo -e " ${YELLOW}Node.js $(node -v) is too old; installing Node.js 22 for Drafted.${RESET}"
|
|
228
|
-
install_portable_node
|
|
229
|
-
else
|
|
230
|
-
ok "Node.js $(node -v)"
|
|
231
|
-
fi
|
|
232
|
-
else
|
|
233
|
-
echo -e " ${YELLOW}Node.js is not installed; installing Node.js 22 for Drafted.${RESET}"
|
|
234
|
-
install_portable_node
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
init_telemetry
|
|
238
|
-
|
|
239
|
-
NPM_GLOBAL_PREFIX="$HOME/.drafted/npm-global"
|
|
240
|
-
mkdir -p "$NPM_GLOBAL_PREFIX"
|
|
241
|
-
|
|
242
|
-
# A `drafted` resolved elsewhere on PATH (a leftover install from before this
|
|
243
|
-
# fixed-prefix scheme, or a manual `npm i -g` under the default prefix) would
|
|
244
|
-
# permanently shadow the fixed-prefix copy: the updater keeps this prefix
|
|
245
|
-
# current, but the shell keeps running the stale one. Remove it at its source.
|
|
246
|
-
STALE_DRAFTED_BIN="$(command -v drafted 2>/dev/null || true)"
|
|
247
|
-
if [ -n "$STALE_DRAFTED_BIN" ]; then
|
|
248
|
-
# Derived from where the resolved binary actually sits (bin's grandparent),
|
|
249
|
-
# not `npm config get prefix` — that reflects our own target prefix once
|
|
250
|
-
# it's already been set on a prior run, which would make this a no-op on
|
|
251
|
-
# every machine that's already installed once (i.e. every real machine).
|
|
252
|
-
CANDIDATE_PREFIX="$(dirname "$(dirname "$STALE_DRAFTED_BIN")")"
|
|
253
|
-
if [ "$CANDIDATE_PREFIX" != "$NPM_GLOBAL_PREFIX" ] && [ -d "$CANDIDATE_PREFIX/lib/node_modules/drafted" ]; then
|
|
254
|
-
echo -e " ${YELLOW}Found a stale Drafted install at $CANDIDATE_PREFIX — removing it so it can't shadow the current one.${RESET}"
|
|
255
|
-
npm uninstall -g drafted --prefix "$CANDIDATE_PREFIX" >/dev/null 2>&1 || true
|
|
256
|
-
fi
|
|
257
|
-
fi
|
|
258
|
-
|
|
259
|
-
# Do NOT `npm config set prefix` — that repoints the user's GLOBAL npm prefix, so every
|
|
260
|
-
# `npm install -g <pkg>` they ever run lands in our dir AND our uninstall (`rm -rf ~/.drafted`)
|
|
261
|
-
# would wipe all their other globals. Install drafted with an explicit per-command --prefix
|
|
262
|
-
# instead (below), and HEAL any global prefix pin an older version of this installer wrote so
|
|
263
|
-
# the user's default is restored. Preserve every other ~/.npmrc line (auth tokens, etc.).
|
|
264
|
-
NPMRC="${npm_config_userconfig:-$HOME/.npmrc}"
|
|
265
|
-
if [ -f "$NPMRC" ] && grep -Eq '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC"; then
|
|
266
|
-
tmp_npmrc="$(mktemp)"
|
|
267
|
-
grep -Ev '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC" > "$tmp_npmrc" && cat "$tmp_npmrc" > "$NPMRC"
|
|
268
|
-
rm -f "$tmp_npmrc"
|
|
269
|
-
fi
|
|
270
|
-
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
|
|
271
|
-
|
|
272
|
-
# Persist the prefix's bin dir at the FRONT of PATH for future shells — without
|
|
273
|
-
# this, npm's prefix config makes `npm install -g` land in the right place, but
|
|
274
|
-
# a bare `drafted` in a new terminal keeps resolving through whatever was
|
|
275
|
-
# already on PATH (see the installedVersion() comment on the login-shell PATH
|
|
276
|
-
# gap below). Idempotent guarded block, same pattern as the agent-instructions
|
|
277
|
-
# markers further down.
|
|
278
|
-
persist_npm_global_path() {
|
|
279
|
-
local marker_begin="# BEGIN drafted-path"
|
|
280
|
-
local marker_end="# END drafted-path"
|
|
281
|
-
local line="export PATH=\"$NPM_GLOBAL_PREFIX/bin:\$PATH\""
|
|
282
|
-
local rc
|
|
283
|
-
case "$(basename "${SHELL:-}")" in
|
|
284
|
-
zsh) rc="$HOME/.zshrc" ;;
|
|
285
|
-
bash) rc="$HOME/.bash_profile" ;;
|
|
286
|
-
*) rc="$HOME/.profile" ;;
|
|
287
|
-
esac
|
|
288
|
-
[ -f "$rc" ] || : > "$rc"
|
|
289
|
-
grep -qF "$marker_begin" "$rc" 2>/dev/null && return 0
|
|
290
|
-
{ echo ""; echo "$marker_begin"; echo "$line"; echo "$marker_end"; } >> "$rc"
|
|
291
|
-
}
|
|
292
|
-
persist_npm_global_path
|
|
293
|
-
|
|
294
|
-
# ── Install ───────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
step "Installing Drafted"
|
|
297
|
-
|
|
298
|
-
# Retry with back-off: a fresh `npm publish` can briefly 404/ETARGET on the CDN
|
|
299
|
-
# edge before the tarball propagates, so an updater that fires the instant a new
|
|
300
|
-
# version lands would otherwise report a spurious "update failed". Ride it out.
|
|
301
|
-
install_drafted_pkg() {
|
|
302
|
-
attempts=5; delay=4; n=1
|
|
303
|
-
while :; do
|
|
304
|
-
if npm install -g drafted@latest --force --prefix "$NPM_GLOBAL_PREFIX"; then return 0; fi
|
|
305
|
-
if [ "$n" -ge "$attempts" ]; then return 1; fi
|
|
306
|
-
echo -e " ${YELLOW}npm install failed (attempt $n/$attempts) — retrying in ${delay}s (a new release may still be propagating to the npm CDN)...${RESET}"
|
|
307
|
-
sleep "$delay"
|
|
308
|
-
n=$((n + 1)); delay=$((delay * 2))
|
|
309
|
-
done
|
|
310
|
-
}
|
|
311
|
-
if ! install_drafted_pkg; then
|
|
312
|
-
echo -e " ${RED}npm install -g drafted@latest failed after multiple attempts.${RESET}"
|
|
313
|
-
exit 1
|
|
314
|
-
fi
|
|
315
|
-
hash -r 2>/dev/null || true
|
|
316
|
-
NPM_ROOT="$(npm root -g --prefix "$NPM_GLOBAL_PREFIX" 2>/dev/null || true)"
|
|
317
|
-
MCP_SERVER_MODULE="$NPM_ROOT/drafted/mcp/server.mjs"
|
|
318
|
-
if [ -n "$NPM_ROOT" ] && [ -f "$MCP_SERVER_MODULE" ]; then
|
|
319
|
-
node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$MCP_SERVER_MODULE"
|
|
320
|
-
elif [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
|
|
321
|
-
echo -e " ${DIM}Skipping package import check in headless installer test.${RESET}"
|
|
322
|
-
else
|
|
323
|
-
echo -e " ${RED}Could not find installed MCP server module at $MCP_SERVER_MODULE${RESET}"
|
|
324
|
-
exit 1
|
|
325
|
-
fi
|
|
326
|
-
ok "Installed $(drafted --version 2>/dev/null || echo 'drafted') via npm"
|
|
327
|
-
|
|
328
|
-
# The Get set up page's command carries a one-time link code for the account that
|
|
329
|
-
# showed it, so the computer links without a browser hop (which could land in a
|
|
330
|
-
# different account's browser). Single use, 15 minutes; failure is not fatal, the
|
|
331
|
-
# agent can still sign in on first use.
|
|
332
|
-
if [ -n "$LINK_CODE" ]; then
|
|
333
|
-
step "Linking this computer to your account"
|
|
334
|
-
if DRAFTED_SERVER="$INSTALL_SERVER" drafted login --link "$LINK_CODE"; then
|
|
335
|
-
ok "Linked"
|
|
336
|
-
else
|
|
337
|
-
echo -e " ${YELLOW}Not linked. Your agent will ask you to sign in the first time it uses Drafted.${RESET}"
|
|
338
|
-
fi
|
|
339
|
-
fi
|
|
340
|
-
|
|
341
|
-
# ── Configure ─────────────────────────────────────────────────────
|
|
342
|
-
|
|
343
|
-
step "Connecting to your tools"
|
|
344
|
-
|
|
345
|
-
# Write server URL config
|
|
346
|
-
mkdir -p "$HOME/.drafted"
|
|
347
|
-
if [ "$INSTALL_MODE" = "production" ]; then
|
|
348
|
-
echo "{\"server\":\"$INSTALL_SERVER\"}" > "$HOME/.drafted/config.json"
|
|
349
|
-
ok "Server: $INSTALL_SERVER"
|
|
350
|
-
else
|
|
351
|
-
ok "Local server: $INSTALL_SERVER"
|
|
352
|
-
fi
|
|
353
|
-
|
|
354
|
-
# Local stdio MCP command. Production uses the installed package; --local uses this checkout when available.
|
|
355
|
-
if [ "$INSTALL_MODE" = "local" ] && [ -f "$SCRIPT_DIR/mcp/server.mjs" ]; then
|
|
356
|
-
DRAFTED_MCP_COMMAND="$(command -v node)"
|
|
357
|
-
DRAFTED_MCP_ARGS_JSON="$(node -e 'console.log(JSON.stringify([process.argv[1]]))' "$SCRIPT_DIR/mcp/server.mjs")"
|
|
358
|
-
elif [ -n "${NPM_ROOT:-}" ] && [ -f "$MCP_SERVER_MODULE" ]; then
|
|
359
|
-
# Absolute node + server file, NOT the drafted-mcp shebang (#!/usr/bin/env node):
|
|
360
|
-
# GUI clients build their own PATH, and Claude Desktop put an old nvm Node (v16,
|
|
361
|
-
# no fetch) first, so every tool call failed as "unreachable" (2026-10-06). This
|
|
362
|
-
# is the node checked >= 22 above; the auto-updater re-runs this script, so the
|
|
363
|
-
# registration follows it if it moves.
|
|
364
|
-
DRAFTED_MCP_COMMAND="$(command -v node)"
|
|
365
|
-
DRAFTED_MCP_ARGS_JSON="$(node -e 'console.log(JSON.stringify([process.argv[1]]))' "$MCP_SERVER_MODULE")"
|
|
366
|
-
else
|
|
367
|
-
DRAFTED_MCP_COMMAND="$(command -v drafted-mcp)"
|
|
368
|
-
DRAFTED_MCP_ARGS_JSON="[]"
|
|
369
|
-
fi
|
|
370
|
-
|
|
371
|
-
configure_mcp() {
|
|
372
|
-
local config_path="$1"
|
|
373
|
-
local label="$2"
|
|
374
|
-
local config_dir
|
|
375
|
-
config_dir="$(dirname "$config_path")"
|
|
376
|
-
mkdir -p "$config_dir"
|
|
377
|
-
|
|
378
|
-
node -e "
|
|
379
|
-
const fs = require('fs');
|
|
380
|
-
const p = process.argv[1];
|
|
381
|
-
const name = process.argv[2];
|
|
382
|
-
const command = process.argv[3];
|
|
383
|
-
const args = JSON.parse(process.argv[4]);
|
|
384
|
-
const server = process.argv[5];
|
|
385
|
-
const authFile = process.argv[6];
|
|
386
|
-
const mode = process.argv[7];
|
|
387
|
-
let c = {};
|
|
388
|
-
try { c = JSON.parse(fs.readFileSync(p, 'utf8')); } catch {}
|
|
389
|
-
if (!c.mcpServers) c.mcpServers = {};
|
|
390
|
-
const env = { DRAFTED_SERVER: server };
|
|
391
|
-
if (authFile) env.DRAFTED_AUTH_FILE = authFile;
|
|
392
|
-
if (mode === 'local') env.DRAFTED_TELEMETRY = '0';
|
|
393
|
-
c.mcpServers[name] = { command, args, env };
|
|
394
|
-
fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');
|
|
395
|
-
" "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE"
|
|
396
|
-
ok "$label"
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
verify_no_legacy_http_config() {
|
|
401
|
-
local found=false
|
|
402
|
-
for file in "$CLAUDE_DESKTOP_CONFIG" "$CLAUDE_CODE_CONFIG" "$CURSOR_CONFIG" "$CODEX_CONFIG"; do
|
|
403
|
-
[ -f "$file" ] || continue
|
|
404
|
-
if grep -q "https://drafted.live/mcp" "$file" 2>/dev/null; then
|
|
405
|
-
found=true
|
|
406
|
-
echo -e " ${RED}✗${RESET} Legacy HTTP Drafted MCP still present in $file"
|
|
407
|
-
fi
|
|
408
|
-
done
|
|
409
|
-
if [ "$found" = true ]; then
|
|
410
|
-
fail "Installer migration incomplete. Remove legacy https://drafted.live/mcp entries and rerun."
|
|
411
|
-
exit 1
|
|
412
|
-
fi
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
configure_codex() {
|
|
416
|
-
local config_path="$1"
|
|
417
|
-
local label="$2"
|
|
418
|
-
local config_dir
|
|
419
|
-
config_dir="$(dirname "$config_path")"
|
|
420
|
-
mkdir -p "$config_dir"
|
|
421
|
-
|
|
422
|
-
node - "$config_path" "$INSTALL_NAME" "$DRAFTED_MCP_COMMAND" "$DRAFTED_MCP_ARGS_JSON" "$INSTALL_SERVER" "$INSTALL_AUTH_FILE" "$INSTALL_MODE" <<'NODE'
|
|
423
|
-
const fs = require('fs');
|
|
424
|
-
const p = process.argv[2];
|
|
425
|
-
const mcpName = process.argv[3];
|
|
426
|
-
const command = process.argv[4];
|
|
427
|
-
const args = JSON.parse(process.argv[5]);
|
|
428
|
-
const server = process.argv[6];
|
|
429
|
-
const authFile = process.argv[7];
|
|
430
|
-
const mode = process.argv[8];
|
|
431
|
-
|
|
432
|
-
let text = '';
|
|
433
|
-
try { text = fs.readFileSync(p, 'utf8'); } catch {}
|
|
434
|
-
|
|
435
|
-
const lines = text.split(/\r?\n/);
|
|
436
|
-
const out = [];
|
|
437
|
-
let skip = false;
|
|
438
|
-
|
|
439
|
-
for (const line of lines) {
|
|
440
|
-
const section = line.match(/^\[(.+)\]$/);
|
|
441
|
-
if (section) {
|
|
442
|
-
const name = section[1].trim();
|
|
443
|
-
if (name === `mcp_servers.${mcpName}` || name === `mcp_servers.${mcpName}.env`) {
|
|
444
|
-
skip = true;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (skip) skip = false;
|
|
448
|
-
}
|
|
449
|
-
if (!skip) out.push(line);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
let next = out.join('\n').replace(/\s+$/, '');
|
|
453
|
-
if (next) next += '\n\n';
|
|
454
|
-
next += `[mcp_servers.${mcpName}]\n`;
|
|
455
|
-
next += 'command = ' + JSON.stringify(command) + '\n';
|
|
456
|
-
next += 'args = [' + args.map(a => JSON.stringify(a)).join(', ') + ']\n';
|
|
457
|
-
next += `\n[mcp_servers.${mcpName}.env]\n`;
|
|
458
|
-
next += 'DRAFTED_SERVER = ' + JSON.stringify(server) + '\n';
|
|
459
|
-
if (authFile) next += 'DRAFTED_AUTH_FILE = ' + JSON.stringify(authFile) + '\n';
|
|
460
|
-
if (mode === 'local') next += 'DRAFTED_TELEMETRY = "0"\n';
|
|
461
|
-
next += '\n';
|
|
462
|
-
|
|
463
|
-
fs.writeFileSync(p, next);
|
|
464
|
-
NODE
|
|
465
|
-
|
|
466
|
-
ok "$label"
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
470
|
-
CLAUDE_CODE_CONFIG="$HOME/.claude.json"
|
|
471
|
-
CODEX_CONFIG="$HOME/.codex/config.toml"
|
|
472
|
-
CURSOR_CONFIG="$HOME/.cursor/mcp.json"
|
|
473
|
-
|
|
474
|
-
CONFIGURED=false
|
|
475
|
-
CLIENT_CLAUDE_DESKTOP=false
|
|
476
|
-
CLIENT_CLAUDE_CODE=false
|
|
477
|
-
CLIENT_CODEX=false
|
|
478
|
-
CLIENT_CURSOR=false
|
|
479
|
-
|
|
480
|
-
# Claude Desktop
|
|
481
|
-
if [ -d "/Applications/Claude.app" ] || [ -d "$HOME/Applications/Claude.app" ] || [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
|
|
482
|
-
configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop"
|
|
483
|
-
CLIENT_CLAUDE_DESKTOP=true
|
|
484
|
-
CONFIGURED=true
|
|
485
|
-
fi
|
|
486
|
-
|
|
487
|
-
# Claude Code
|
|
488
|
-
if command -v claude &>/dev/null || [ -f "$CLAUDE_CODE_CONFIG" ] || [ -d "$HOME/.claude" ]; then
|
|
489
|
-
configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code"
|
|
490
|
-
CLIENT_CLAUDE_CODE=true
|
|
491
|
-
CONFIGURED=true
|
|
492
|
-
fi
|
|
493
|
-
|
|
494
|
-
# Codex
|
|
495
|
-
if command -v codex &>/dev/null || [ -d "$HOME/.codex" ]; then
|
|
496
|
-
configure_codex "$CODEX_CONFIG" "Codex"
|
|
497
|
-
CLIENT_CODEX=true
|
|
498
|
-
CONFIGURED=true
|
|
499
|
-
fi
|
|
500
|
-
|
|
501
|
-
# Cursor
|
|
502
|
-
if [ -d "/Applications/Cursor.app" ] || [ -d "$HOME/Applications/Cursor.app" ] || command -v cursor &>/dev/null || [ -f "$CURSOR_CONFIG" ]; then
|
|
503
|
-
configure_mcp "$CURSOR_CONFIG" "Cursor"
|
|
504
|
-
CLIENT_CURSOR=true
|
|
505
|
-
CONFIGURED=true
|
|
506
|
-
fi
|
|
507
|
-
|
|
508
|
-
# If nothing detected, pre-configure all
|
|
509
|
-
if [ "$CONFIGURED" = false ]; then
|
|
510
|
-
echo -e " ${YELLOW}No supported editors detected — pre-configuring all.${RESET}"
|
|
511
|
-
configure_mcp "$CLAUDE_DESKTOP_CONFIG" "Claude Desktop (pre-configured)"
|
|
512
|
-
configure_mcp "$CLAUDE_CODE_CONFIG" "Claude Code (pre-configured)"
|
|
513
|
-
configure_codex "$CODEX_CONFIG" "Codex (pre-configured)"
|
|
514
|
-
configure_mcp "$CURSOR_CONFIG" "Cursor (pre-configured)"
|
|
515
|
-
CLIENT_CLAUDE_DESKTOP=true
|
|
516
|
-
CLIENT_CLAUDE_CODE=true
|
|
517
|
-
CLIENT_CODEX=true
|
|
518
|
-
CLIENT_CURSOR=true
|
|
519
|
-
fi
|
|
520
|
-
|
|
521
|
-
# ── Agent instructions ────────────────────────────────────────────
|
|
522
|
-
|
|
523
|
-
step "Installing global agent instructions"
|
|
524
|
-
|
|
525
|
-
load_global_agent_instructions() {
|
|
526
|
-
local local_path="$SCRIPT_DIR/agent-instructions/global.md"
|
|
527
|
-
if [ -f "$local_path" ]; then
|
|
528
|
-
cat "$local_path"
|
|
529
|
-
return 0
|
|
530
|
-
fi
|
|
531
|
-
cat <<'DRAFTED_GLOBAL_INSTRUCTIONS'
|
|
532
|
-
<drafted>
|
|
533
|
-
You have Drafted MCP tools — a shared surface for durable, reviewable work: produce substantive output as frames on the surface (not only in chat), put knowledge in the org wiki, and encode repeatable methods as skills. The full operating manual is the `drafted` skill installed with the plugin — follow it when working with Drafted. Address orgs by path: `fs(ls, path="/")` lists them, and `/o/<org>/<root>/...` (root ∈ wiki, skills, projects) is the canonical path form — the org is part of the path, never a separate switch. Before writing, verify the org/project echoed in the response is the one you intend.
|
|
534
|
-
</drafted>
|
|
535
|
-
DRAFTED_GLOBAL_INSTRUCTIONS
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
install_agent_instructions() {
|
|
539
|
-
local instructions_path="$1"
|
|
540
|
-
local label="$2"
|
|
541
|
-
local instructions_dir
|
|
542
|
-
local body
|
|
543
|
-
instructions_dir="$(dirname "$instructions_path")"
|
|
544
|
-
mkdir -p "$instructions_dir"
|
|
545
|
-
|
|
546
|
-
if ! body="$(load_global_agent_instructions)"; then
|
|
547
|
-
fail "Could not load Drafted global instructions"
|
|
548
|
-
return 1
|
|
549
|
-
fi
|
|
550
|
-
|
|
551
|
-
DRAFTED_GLOBAL_INSTRUCTIONS_BODY="$body" node - "$instructions_path" <<'NODE'
|
|
552
|
-
const fs = require('fs');
|
|
553
|
-
const p = process.argv[2];
|
|
554
|
-
const begin = '<!-- BEGIN drafted-global-instructions -->';
|
|
555
|
-
const end = '<!-- END drafted-global-instructions -->';
|
|
556
|
-
const body = process.env.DRAFTED_GLOBAL_INSTRUCTIONS_BODY || '';
|
|
557
|
-
if (!body.trim()) throw new Error('Drafted global instructions are empty');
|
|
558
|
-
const block = `${begin}\n${body.replace(/\s+$/, '')}\n${end}`;
|
|
559
|
-
let text = '';
|
|
560
|
-
try { text = fs.readFileSync(p, 'utf8'); } catch {}
|
|
561
|
-
const escapeRe = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
562
|
-
const re = new RegExp(`${escapeRe(begin)}[\\s\\S]*?${escapeRe(end)}`);
|
|
563
|
-
let next;
|
|
564
|
-
if (re.test(text)) {
|
|
565
|
-
next = text.replace(re, block);
|
|
566
|
-
} else {
|
|
567
|
-
next = text.replace(/\s+$/, '');
|
|
568
|
-
if (next) next += '\n\n';
|
|
569
|
-
next += block + '\n';
|
|
570
|
-
}
|
|
571
|
-
fs.writeFileSync(p, next.endsWith('\n') ? next : next + '\n');
|
|
572
|
-
NODE
|
|
573
|
-
|
|
574
|
-
ok "$label"
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
install_agent_instructions "$HOME/.claude/CLAUDE.md" "Claude global instructions"
|
|
578
|
-
install_agent_instructions "$HOME/.codex/CODEX.md" "Codex global instructions"
|
|
579
|
-
|
|
580
|
-
# ── Skills & commands ─────────────────────────────────────────────
|
|
581
|
-
|
|
582
|
-
step "Installing skills and commands"
|
|
583
|
-
|
|
584
|
-
# Resolve the plugin source: a local checkout when present (e.g. --local from this
|
|
585
|
-
# repo), otherwise the installed npm package's bundled plugin/ directory.
|
|
586
|
-
# require.resolve() misses packages under the custom global prefix
|
|
587
|
-
# ($HOME/.drafted/npm-global) when run from an arbitrary cwd (curl | bash), so
|
|
588
|
-
# prefer the already-computed global node_modules root from the install step.
|
|
589
|
-
if [ -n "${NPM_ROOT:-}" ] && [ -d "$NPM_ROOT/drafted" ]; then
|
|
590
|
-
DRAFTED_PKG_DIR="$NPM_ROOT/drafted"
|
|
591
|
-
else
|
|
592
|
-
DRAFTED_PKG_DIR="$(node -e "try { console.log(require.resolve('drafted/package.json').replace('/package.json','')) } catch { process.exit(1) }" 2>/dev/null)" || true
|
|
593
|
-
fi
|
|
594
|
-
if [ -d "$SCRIPT_DIR/plugin/skills" ] || [ -d "$SCRIPT_DIR/plugin/commands" ]; then
|
|
595
|
-
PLUGIN_SRC="$SCRIPT_DIR/plugin"
|
|
596
|
-
elif [ -n "$DRAFTED_PKG_DIR" ] && [ -d "$DRAFTED_PKG_DIR/plugin" ]; then
|
|
597
|
-
PLUGIN_SRC="$DRAFTED_PKG_DIR/plugin"
|
|
598
|
-
else
|
|
599
|
-
PLUGIN_SRC=""
|
|
600
|
-
fi
|
|
601
|
-
|
|
602
|
-
# Skills and commands are LINKED, never copied: a copy is a point-in-time snapshot that only
|
|
603
|
-
# refreshes when someone re-runs this installer, so `npm i -g drafted@latest` would leave the
|
|
604
|
-
# installed skill silently behind the package it came from. A link tracks the upgrade.
|
|
605
|
-
#
|
|
606
|
-
# link_plugin_entry is idempotent and non-destructive: a correct link is left alone, a wrong or
|
|
607
|
-
# dangling one is repointed, and a real file/directory (which the user may have written by hand)
|
|
608
|
-
# is moved aside rather than deleted. Backups go OUTSIDE the skills/commands dirs — a
|
|
609
|
-
# `drafted.backup/` sitting next to `drafted/` would register as a second, stale skill.
|
|
610
|
-
DRAFTED_BACKUP_DIR="$HOME/.claude/.drafted-backups"
|
|
611
|
-
link_plugin_entry() {
|
|
612
|
-
local source="$1" target="$2" source_real backup
|
|
613
|
-
source_real="$(cd "$(dirname "$source")" && pwd)/$(basename "$source")"
|
|
614
|
-
if [ -L "$target" ]; then
|
|
615
|
-
if [ "$(readlink "$target")" = "$source_real" ]; then return 0; fi
|
|
616
|
-
rm -f "$target"
|
|
617
|
-
elif [ -e "$target" ]; then
|
|
618
|
-
# Byte-identical to the source: this is a prior copy-install of ours, not user content, and
|
|
619
|
-
# the link reproduces it losslessly. Anything else is backed up.
|
|
620
|
-
if diff -rq "$source_real" "$target" >/dev/null 2>&1; then
|
|
621
|
-
rm -rf "$target"
|
|
622
|
-
else
|
|
623
|
-
mkdir -p "$DRAFTED_BACKUP_DIR"
|
|
624
|
-
backup="$DRAFTED_BACKUP_DIR/$(basename "$target").$(date +%Y%m%d%H%M%S)"
|
|
625
|
-
mv "$target" "$backup"
|
|
626
|
-
echo -e " ${YELLOW}Existing $(basename "$target") moved aside → $backup${RESET}"
|
|
627
|
-
fi
|
|
628
|
-
fi
|
|
629
|
-
ln -s "$source_real" "$target"
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
# Remove links WE made whose source is gone — an entry dropped from the package, or the package
|
|
633
|
-
# uninstalled entirely. A dangling skill dir is worse than a stale one: it looks installed and
|
|
634
|
-
# loads nothing. Matched on the link's own target path so other installers' links are untouched.
|
|
635
|
-
sweep_plugin_links() {
|
|
636
|
-
local dir="$1" marker="$2" link
|
|
637
|
-
[ -d "$dir" ] || return 0
|
|
638
|
-
while IFS= read -r link; do
|
|
639
|
-
case "$(readlink "$link")" in
|
|
640
|
-
*"$marker"*)
|
|
641
|
-
if [ ! -e "$link" ]; then
|
|
642
|
-
rm -f "$link"
|
|
643
|
-
echo -e " ${YELLOW}Removed stale link: $(basename "$link")${RESET}"
|
|
644
|
-
fi
|
|
645
|
-
;;
|
|
646
|
-
esac
|
|
647
|
-
done < <(find "$dir" -maxdepth 1 -type l 2>/dev/null)
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
# Skills — Claude Code auto-loads these from ~/.claude/skills/<name>/SKILL.md
|
|
651
|
-
SKILLS_DIR="$HOME/.claude/skills"
|
|
652
|
-
sweep_plugin_links "$SKILLS_DIR" "/plugin/skills/"
|
|
653
|
-
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/skills" ]; then
|
|
654
|
-
mkdir -p "$SKILLS_DIR"
|
|
655
|
-
SKILL_COUNT=0
|
|
656
|
-
for d in "$PLUGIN_SRC/skills/"*/; do
|
|
657
|
-
[ -d "$d" ] || continue
|
|
658
|
-
DIRNAME="$(basename "$d")"
|
|
659
|
-
link_plugin_entry "${d%/}" "$SKILLS_DIR/$DIRNAME"
|
|
660
|
-
SKILL_COUNT=$((SKILL_COUNT + 1))
|
|
661
|
-
done
|
|
662
|
-
ok "Linked $SKILL_COUNT skill(s) into $SKILLS_DIR (source: $PLUGIN_SRC/skills)"
|
|
663
|
-
else
|
|
664
|
-
echo -e " ${YELLOW}Skill source not found in package — skipping skills.${RESET}"
|
|
665
|
-
fi
|
|
666
|
-
|
|
667
|
-
# Commands — namespaced under drafted/ so they register as /drafted:<name>,
|
|
668
|
-
# matching the marketplace install and the commands' own cross-references. Linked per FILE into a
|
|
669
|
-
# real directory (not a linked directory) so discovery is unchanged; the containing dir is no
|
|
670
|
-
# longer rm -rf'd, so anything else a user put there survives.
|
|
671
|
-
CLAUDE_CMD_DIR="$HOME/.claude/commands/drafted"
|
|
672
|
-
CODEX_CMD_DIR="$HOME/.codex/prompts/drafted"
|
|
673
|
-
sweep_plugin_links "$CLAUDE_CMD_DIR" "/plugin/commands/"
|
|
674
|
-
sweep_plugin_links "$CODEX_CMD_DIR" "/plugin/commands/"
|
|
675
|
-
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/commands" ]; then
|
|
676
|
-
link_commands_into() {
|
|
677
|
-
local dest="$1" count=0 f
|
|
678
|
-
mkdir -p "$dest"
|
|
679
|
-
for f in "$PLUGIN_SRC/commands/"*.md; do
|
|
680
|
-
[ -f "$f" ] || continue
|
|
681
|
-
link_plugin_entry "$f" "$dest/$(basename "$f")"
|
|
682
|
-
count=$((count + 1))
|
|
683
|
-
done
|
|
684
|
-
ok "Linked $count command(s) into $dest"
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if [ "$CLIENT_CLAUDE_CODE" = true ]; then link_commands_into "$CLAUDE_CMD_DIR"; fi
|
|
688
|
-
if [ "$CLIENT_CODEX" = true ]; then link_commands_into "$CODEX_CMD_DIR"; fi
|
|
689
|
-
else
|
|
690
|
-
echo -e " ${YELLOW}Command source not found in package — skipping commands.${RESET}"
|
|
691
|
-
fi
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
# ── Update menu bar helper ───────────────────────────────────────
|
|
695
|
-
|
|
696
|
-
# Idempotently remove the legacy Swift menu-bar updater so it can neither run alongside the
|
|
697
|
-
# new desktop app nor be resurrected at next login. Safe to call when nothing legacy exists.
|
|
698
|
-
retire_legacy_updater() {
|
|
699
|
-
local uid legacy_plist
|
|
700
|
-
uid="$(id -u)"
|
|
701
|
-
legacy_plist="$HOME/Library/LaunchAgents/live.drafted.updater.plist"
|
|
702
|
-
launchctl bootout "gui/$uid/live.drafted.updater" >/dev/null 2>&1 || true
|
|
703
|
-
if [ -f "$legacy_plist" ]; then
|
|
704
|
-
launchctl bootout "gui/$uid" "$legacy_plist" >/dev/null 2>&1 || true
|
|
705
|
-
fi
|
|
706
|
-
rm -f "$legacy_plist" >/dev/null 2>&1 || true
|
|
707
|
-
pkill -x DraftedUpdater >/dev/null 2>&1 || true
|
|
708
|
-
rm -rf "/Applications/Drafted Updater.app" "$HOME/Applications/Drafted Updater.app" >/dev/null 2>&1 || true
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
# Install (or detect) the Tauri desktop app that replaces the legacy menu-bar updater.
|
|
712
|
-
# Returns 0 ONLY when a valid Drafted.app is present afterward, so the caller can safely
|
|
713
|
-
# retire the legacy updater. Returns 1 when no new bundle is available yet — the caller then
|
|
714
|
-
# falls back to building the legacy Swift updater, so users are never left without a tray.
|
|
715
|
-
# Developer ID Application: Beoflow B.V. (2D9Q24N9UQ) — the identity desktop/RELEASE.md signs with.
|
|
716
|
-
DESKTOP_SIGNING_REQUIREMENT='anchor apple generic and certificate leaf[subject.OU] = "2D9Q24N9UQ"'
|
|
717
|
-
|
|
718
|
-
install_desktop_app() {
|
|
719
|
-
local install_dir app_bundle app_exe uid plist tmp tarball url
|
|
720
|
-
install_dir="/Applications"
|
|
721
|
-
[ -w "$install_dir" ] || install_dir="$HOME/Applications"
|
|
722
|
-
mkdir -p "$install_dir"
|
|
723
|
-
app_bundle="$install_dir/Drafted.app"
|
|
724
|
-
|
|
725
|
-
if [ -n "${DRAFTED_DESKTOP_APP:-}" ] && [ -d "${DRAFTED_DESKTOP_APP}" ]; then
|
|
726
|
-
# Local prebuilt bundle — lets us test the cutover before the CDN endpoint exists.
|
|
727
|
-
rm -rf "$app_bundle" >/dev/null 2>&1 || true
|
|
728
|
-
ditto "${DRAFTED_DESKTOP_APP}" "$app_bundle" >/dev/null 2>&1 || true
|
|
729
|
-
else
|
|
730
|
-
# Signed bundle tarball (expects Drafted.app at the archive root). Until this endpoint is
|
|
731
|
-
# published the request 404s and we fall through to legacy — that is the cutover gate.
|
|
732
|
-
url="${DRAFTED_DESKTOP_URL:-$INSTALL_SERVER/desktop/Drafted-macos.tar.gz}"
|
|
733
|
-
tmp="$(mktemp -d)"
|
|
734
|
-
tarball="$tmp/Drafted-macos.tar.gz"
|
|
735
|
-
if curl -fsSL --max-time 120 "$url" -o "$tarball" >/dev/null 2>&1 \
|
|
736
|
-
&& mkdir -p "$tmp/x" && tar -xzf "$tarball" -C "$tmp/x" >/dev/null 2>&1; then
|
|
737
|
-
# Quarantine is stripped below, so Gatekeeper never checks this bundle: check it
|
|
738
|
-
# here. It must carry an intact Developer ID signature from OUR team; a tampered
|
|
739
|
-
# or re-signed (e.g. ad-hoc) bundle is refused and the installed app is kept
|
|
740
|
-
# (audit 2026-10-13).
|
|
741
|
-
if codesign --verify --deep --strict -R="$DESKTOP_SIGNING_REQUIREMENT" "$tmp/x/Drafted.app" >/dev/null 2>&1; then
|
|
742
|
-
rm -rf "$app_bundle" >/dev/null 2>&1 || true
|
|
743
|
-
mv "$tmp/x/Drafted.app" "$app_bundle" >/dev/null 2>&1 || true
|
|
744
|
-
else
|
|
745
|
-
echo -e " ${YELLOW}Desktop app download failed its signature check; not installing it.${RESET}"
|
|
746
|
-
fi
|
|
747
|
-
fi
|
|
748
|
-
rm -rf "$tmp" >/dev/null 2>&1 || true
|
|
749
|
-
fi
|
|
750
|
-
|
|
751
|
-
# Validate: a real bundle whose declared executable exists. The bundle dir is Drafted.app
|
|
752
|
-
# (productName) but the binary is the Cargo bin name (drafted-desktop), so read the actual
|
|
753
|
-
# name from Info.plist rather than assuming. If absent/invalid, signal "not available yet".
|
|
754
|
-
local exe_name
|
|
755
|
-
exe_name="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app_bundle/Contents/Info.plist" 2>/dev/null || true)"
|
|
756
|
-
app_exe="$app_bundle/Contents/MacOS/$exe_name"
|
|
757
|
-
if [ -z "$exe_name" ] || [ ! -x "$app_exe" ]; then
|
|
758
|
-
return 1
|
|
759
|
-
fi
|
|
760
|
-
|
|
761
|
-
# Clear quarantine so Gatekeeper doesn't block a freshly downloaded bundle.
|
|
762
|
-
xattr -dr com.apple.quarantine "$app_bundle" >/dev/null 2>&1 || true
|
|
763
|
-
|
|
764
|
-
# Register run-at-login for the new app (mirrors the legacy launch agent). Write the
|
|
765
|
-
# plist BEFORE touching launchd so the bootstrap below loads the new binary path.
|
|
766
|
-
uid="$(id -u)"
|
|
767
|
-
plist="$HOME/Library/LaunchAgents/live.drafted.desktop.plist"
|
|
768
|
-
mkdir -p "$HOME/Library/LaunchAgents"
|
|
769
|
-
cat > "$plist" <<PLIST
|
|
770
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
771
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
772
|
-
<plist version="1.0">
|
|
773
|
-
<dict>
|
|
774
|
-
<key>Label</key>
|
|
775
|
-
<string>live.drafted.desktop</string>
|
|
776
|
-
<key>ProgramArguments</key>
|
|
777
|
-
<array>
|
|
778
|
-
<string>$app_exe</string>
|
|
779
|
-
</array>
|
|
780
|
-
<key>RunAtLoad</key>
|
|
781
|
-
<true/>
|
|
782
|
-
<key>KeepAlive</key>
|
|
783
|
-
<true/>
|
|
784
|
-
</dict>
|
|
785
|
-
</plist>
|
|
786
|
-
PLIST
|
|
787
|
-
# Restart under launchd DETERMINISTICALLY. bootout is async: bootstrapping before the old
|
|
788
|
-
# service is fully torn down races, and when it loses the label is left UNLOADED — the app
|
|
789
|
-
# dies with no KeepAlive to save it (that bricked the in-app updater, which is itself a child
|
|
790
|
-
# of the very app being replaced). And a bare pkill while the service is still loaded just
|
|
791
|
-
# trips KeepAlive into respawning the old binary mid-swap. So: bootout, POLL until the label
|
|
792
|
-
# is actually gone, kill any stray non-launchd instance, bootstrap the new plist, and
|
|
793
|
-
# kickstart -k to guarantee it comes up (RunAtLoad alone is not proof it started).
|
|
794
|
-
local svc="gui/$uid/live.drafted.desktop"
|
|
795
|
-
launchctl bootout "$svc" >/dev/null 2>&1 || true
|
|
796
|
-
local waited=0
|
|
797
|
-
while [ "$waited" -lt 40 ] && launchctl print "$svc" >/dev/null 2>&1; do
|
|
798
|
-
sleep 0.25
|
|
799
|
-
waited=$((waited + 1))
|
|
800
|
-
done
|
|
801
|
-
pkill -x "$exe_name" >/dev/null 2>&1 || true
|
|
802
|
-
launchctl bootstrap "gui/$uid" "$plist" >/dev/null 2>&1 || true
|
|
803
|
-
launchctl kickstart -k "$svc" >/dev/null 2>&1 || true
|
|
804
|
-
return 0
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
install_update_menu_icon() {
|
|
808
|
-
if [ "${DRAFTED_HEADLESS:-}" = "1" ]; then
|
|
809
|
-
echo -e " ${YELLOW}Headless mode: skipping update menu icon.${RESET}"
|
|
810
|
-
return 0
|
|
811
|
-
fi
|
|
812
|
-
if [ "$(uname -s)" != "Darwin" ]; then
|
|
813
|
-
echo -e " ${YELLOW}Update menu icon is only installed on macOS by this script.${RESET}"
|
|
814
|
-
return 0
|
|
815
|
-
fi
|
|
816
|
-
|
|
817
|
-
# Prefer the Tauri desktop app. If it installs (or is already present and valid), retire the
|
|
818
|
-
# legacy Swift updater so the two can never run side by side or be resurrected at next login.
|
|
819
|
-
if install_desktop_app; then
|
|
820
|
-
retire_legacy_updater
|
|
821
|
-
ok "Drafted desktop app"
|
|
822
|
-
return 0
|
|
823
|
-
fi
|
|
824
|
-
echo -e " ${YELLOW}Desktop app bundle not available yet; using the menu bar updater.${RESET}"
|
|
825
|
-
|
|
826
|
-
if ! command -v swiftc >/dev/null 2>&1; then
|
|
827
|
-
echo -e " ${YELLOW}Swift compiler not found; skipping macOS menu bar updater.${RESET}"
|
|
828
|
-
return 0
|
|
829
|
-
fi
|
|
830
|
-
|
|
831
|
-
local work_dir install_dir app_bundle app_macos app_resources swift_file exe_path plist_dir plist uid logo_path legacy_bundle
|
|
832
|
-
work_dir="$HOME/.drafted/updater"
|
|
833
|
-
install_dir="/Applications"
|
|
834
|
-
if [ ! -w "$install_dir" ]; then
|
|
835
|
-
install_dir="$HOME/Applications"
|
|
836
|
-
fi
|
|
837
|
-
app_bundle="$install_dir/Drafted Updater.app"
|
|
838
|
-
app_macos="$app_bundle/Contents/MacOS"
|
|
839
|
-
app_resources="$app_bundle/Contents/Resources"
|
|
840
|
-
swift_file="$work_dir/DraftedUpdater.swift"
|
|
841
|
-
exe_path="$app_macos/DraftedUpdater"
|
|
842
|
-
plist_dir="$HOME/Library/LaunchAgents"
|
|
843
|
-
plist="$plist_dir/live.drafted.updater.plist"
|
|
844
|
-
uid="$(id -u)"
|
|
845
|
-
legacy_bundle="$work_dir/DraftedUpdater.app"
|
|
846
|
-
mkdir -p "$work_dir" "$install_dir" "$app_macos" "$app_resources" "$plist_dir"
|
|
847
|
-
rm -rf "$legacy_bundle"
|
|
848
|
-
|
|
849
|
-
logo_path="$app_resources/logo.svg"
|
|
850
|
-
if [ -f "$SCRIPT_DIR/server/vendor/logo.svg" ]; then
|
|
851
|
-
cp "$SCRIPT_DIR/server/vendor/logo.svg" "$logo_path"
|
|
852
|
-
else
|
|
853
|
-
curl -fsSL "$INSTALL_SERVER/vendor/logo.svg" -o "$logo_path" >/dev/null 2>&1 || true
|
|
854
|
-
fi
|
|
855
|
-
if [ -f "$logo_path" ]; then
|
|
856
|
-
perl -0pi -e 's/fill="currentColor"/fill="#FFFFFF"/g; s/fill="var\(--logo-letter, #[^)]+\)"/fill="#0A2540"/g' "$logo_path" >/dev/null 2>&1 || true
|
|
857
|
-
fi
|
|
858
|
-
|
|
859
|
-
cat > "$swift_file" <<'SWIFT'
|
|
860
|
-
import SwiftUI
|
|
861
|
-
import AppKit
|
|
862
|
-
|
|
863
|
-
@main
|
|
864
|
-
struct DraftedUpdaterApp: App {
|
|
865
|
-
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
866
|
-
|
|
867
|
-
init() {
|
|
868
|
-
Telemetry.report(event: "drafted_update_helper_started", updateHelperStatus: "running")
|
|
869
|
-
Telemetry.report(event: "drafted_heartbeat", updateHelperStatus: "running")
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
var body: some Scene {
|
|
873
|
-
MenuBarExtra {
|
|
874
|
-
Button("Check for Updates…") { Actions.checkForUpdate(userInitiated: true) }
|
|
875
|
-
Button("Update Drafted") { Actions.updateDrafted() }
|
|
876
|
-
Button("View Logs") { Actions.viewLogs() }
|
|
877
|
-
Button("Open Drafted") { Actions.openDrafted() }
|
|
878
|
-
Divider()
|
|
879
|
-
Button("Quit") { NSApp.terminate(nil) }
|
|
880
|
-
} label: {
|
|
881
|
-
if let image = Actions.logoImage() {
|
|
882
|
-
Image(nsImage: image)
|
|
883
|
-
.resizable()
|
|
884
|
-
.frame(width: 16, height: 18)
|
|
885
|
-
.accessibilityLabel("Drafted")
|
|
886
|
-
} else {
|
|
887
|
-
Text("Drafted")
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
.menuBarExtraStyle(.menu)
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Drives the genuine auto-update: a background check shortly after login and
|
|
895
|
-
// every 6 hours while the menu-bar helper is resident. When a newer published
|
|
896
|
-
// version exists (and the user hasn't skipped it), it prompts Update / Skip.
|
|
897
|
-
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
898
|
-
private var timer: Timer?
|
|
899
|
-
|
|
900
|
-
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
901
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 25) {
|
|
902
|
-
Actions.checkForUpdate(userInitiated: false)
|
|
903
|
-
}
|
|
904
|
-
let t = Timer(timeInterval: 6 * 3600, repeats: true) { _ in
|
|
905
|
-
Actions.checkForUpdate(userInitiated: false)
|
|
906
|
-
}
|
|
907
|
-
RunLoop.main.add(t, forMode: .common)
|
|
908
|
-
timer = t
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
final class UpdateStatusWindow: NSWindowController {
|
|
914
|
-
private let stack = NSStackView()
|
|
915
|
-
private let spinner = NSProgressIndicator()
|
|
916
|
-
private let titleField = NSTextField(labelWithString: "Updating Drafted…")
|
|
917
|
-
private let bodyField = NSTextField(labelWithString: "Downloading and running the installer. This can take a minute.")
|
|
918
|
-
private let closeButton = NSButton(title: "OK", target: nil, action: nil)
|
|
919
|
-
|
|
920
|
-
init() {
|
|
921
|
-
let panel = NSPanel(
|
|
922
|
-
contentRect: NSRect(x: 0, y: 0, width: 380, height: 168),
|
|
923
|
-
styleMask: [.titled, .closable],
|
|
924
|
-
backing: .buffered,
|
|
925
|
-
defer: false
|
|
926
|
-
)
|
|
927
|
-
panel.title = "Drafted Update"
|
|
928
|
-
panel.isReleasedWhenClosed = false
|
|
929
|
-
panel.level = .floating
|
|
930
|
-
super.init(window: panel)
|
|
931
|
-
|
|
932
|
-
stack.orientation = .vertical
|
|
933
|
-
stack.alignment = .centerX
|
|
934
|
-
stack.spacing = 12
|
|
935
|
-
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
936
|
-
|
|
937
|
-
spinner.style = .spinning
|
|
938
|
-
spinner.controlSize = .regular
|
|
939
|
-
|
|
940
|
-
titleField.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
941
|
-
titleField.alignment = .center
|
|
942
|
-
|
|
943
|
-
bodyField.font = NSFont.systemFont(ofSize: 13)
|
|
944
|
-
bodyField.textColor = .secondaryLabelColor
|
|
945
|
-
bodyField.alignment = .center
|
|
946
|
-
bodyField.maximumNumberOfLines = 3
|
|
947
|
-
bodyField.lineBreakMode = .byWordWrapping
|
|
948
|
-
|
|
949
|
-
closeButton.target = self
|
|
950
|
-
closeButton.action = #selector(closeClicked)
|
|
951
|
-
closeButton.isHidden = true
|
|
952
|
-
|
|
953
|
-
stack.addArrangedSubview(spinner)
|
|
954
|
-
stack.addArrangedSubview(titleField)
|
|
955
|
-
stack.addArrangedSubview(bodyField)
|
|
956
|
-
stack.addArrangedSubview(closeButton)
|
|
957
|
-
|
|
958
|
-
panel.contentView = NSView()
|
|
959
|
-
panel.contentView?.addSubview(stack)
|
|
960
|
-
NSLayoutConstraint.activate([
|
|
961
|
-
stack.leadingAnchor.constraint(equalTo: panel.contentView!.leadingAnchor, constant: 28),
|
|
962
|
-
stack.trailingAnchor.constraint(equalTo: panel.contentView!.trailingAnchor, constant: -28),
|
|
963
|
-
stack.centerYAnchor.constraint(equalTo: panel.contentView!.centerYAnchor)
|
|
964
|
-
])
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
968
|
-
|
|
969
|
-
func showUpdating() {
|
|
970
|
-
titleField.stringValue = "Updating Drafted…"
|
|
971
|
-
bodyField.stringValue = "Downloading and running the installer. This can take a minute."
|
|
972
|
-
closeButton.isHidden = true
|
|
973
|
-
spinner.isHidden = false
|
|
974
|
-
spinner.startAnimation(nil)
|
|
975
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
976
|
-
showWindow(nil)
|
|
977
|
-
window?.center()
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
func showCompleted(success: Bool, message: String) {
|
|
981
|
-
spinner.stopAnimation(nil)
|
|
982
|
-
spinner.isHidden = true
|
|
983
|
-
titleField.stringValue = success ? "Update completed" : "Update failed"
|
|
984
|
-
bodyField.stringValue = message
|
|
985
|
-
closeButton.isHidden = false
|
|
986
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
987
|
-
showWindow(nil)
|
|
988
|
-
window?.center()
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
@objc private func closeClicked() { window?.close() }
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
struct Actions {
|
|
995
|
-
static let home = FileManager.default.homeDirectoryForCurrentUser
|
|
996
|
-
|
|
997
|
-
static func logoImage() -> NSImage? {
|
|
998
|
-
guard
|
|
999
|
-
let url = Bundle.main.url(forResource: "logo", withExtension: "svg"),
|
|
1000
|
-
let image = NSImage(contentsOf: url)
|
|
1001
|
-
else { return nil }
|
|
1002
|
-
image.size = NSSize(width: 16, height: 18)
|
|
1003
|
-
image.isTemplate = false
|
|
1004
|
-
return image
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
private static var isUpdating = false
|
|
1008
|
-
private static var updateWindow: UpdateStatusWindow?
|
|
1009
|
-
|
|
1010
|
-
// MARK: - Auto-update check
|
|
1011
|
-
|
|
1012
|
-
// Compare the installed CLI version to npm's published `latest`. Prompt only
|
|
1013
|
-
// when newer and not skipped (background), or always report (user-initiated).
|
|
1014
|
-
static func checkForUpdate(userInitiated: Bool) {
|
|
1015
|
-
let installed = installedVersion()
|
|
1016
|
-
latestVersion { latest in
|
|
1017
|
-
DispatchQueue.main.async {
|
|
1018
|
-
guard let latest = latest else {
|
|
1019
|
-
if userInitiated {
|
|
1020
|
-
showInfo(title: "Couldn’t check for updates", body: "Please check your connection and try again.")
|
|
1021
|
-
}
|
|
1022
|
-
return
|
|
1023
|
-
}
|
|
1024
|
-
guard let installed = installed, isNewer(latest, than: installed) else {
|
|
1025
|
-
if userInitiated {
|
|
1026
|
-
showInfo(title: "Drafted is up to date", body: "You’re on \(installed ?? "the latest") version.")
|
|
1027
|
-
}
|
|
1028
|
-
return
|
|
1029
|
-
}
|
|
1030
|
-
if !userInitiated && skippedVersion() == latest { return }
|
|
1031
|
-
presentUpdateAlert(latest: latest, installed: installed)
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
static func presentUpdateAlert(latest: String, installed: String) {
|
|
1037
|
-
Telemetry.report(event: "drafted_update_available", updateHelperStatus: "running")
|
|
1038
|
-
let alert = NSAlert()
|
|
1039
|
-
alert.messageText = "Drafted \(latest) is available"
|
|
1040
|
-
alert.informativeText = "You have \(installed). Update now? Your editor needs to be restarted afterward to load the new MCP tools."
|
|
1041
|
-
alert.addButton(withTitle: "Update")
|
|
1042
|
-
alert.addButton(withTitle: "Skip This Version")
|
|
1043
|
-
alert.addButton(withTitle: "Later")
|
|
1044
|
-
if let logo = logoImage() { alert.icon = logo }
|
|
1045
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
1046
|
-
switch alert.runModal() {
|
|
1047
|
-
case .alertFirstButtonReturn:
|
|
1048
|
-
updateDrafted()
|
|
1049
|
-
case .alertSecondButtonReturn:
|
|
1050
|
-
setSkippedVersion(latest)
|
|
1051
|
-
Telemetry.report(event: "drafted_update_skipped", updateHelperStatus: "running")
|
|
1052
|
-
default:
|
|
1053
|
-
break
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
static func showInfo(title: String, body: String) {
|
|
1058
|
-
let alert = NSAlert()
|
|
1059
|
-
alert.messageText = title
|
|
1060
|
-
alert.informativeText = body
|
|
1061
|
-
alert.addButton(withTitle: "OK")
|
|
1062
|
-
if let logo = logoImage() { alert.icon = logo }
|
|
1063
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
1064
|
-
alert.runModal()
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// Installed version, resolved WITHOUT depending on the login-shell PATH —
|
|
1068
|
-
// the installer's npm prefix (~/.drafted/npm-global) is never written to any
|
|
1069
|
-
// login profile, so `bash -lc "drafted"` finds nothing. Read the installed
|
|
1070
|
-
// package.json at the fixed prefix first (the source npm itself writes), then
|
|
1071
|
-
// fall back to the absolute binary, then a login shell (custom prefixes on PATH).
|
|
1072
|
-
static func installedVersion() -> String? {
|
|
1073
|
-
let pkg = home.appendingPathComponent(".drafted/npm-global/lib/node_modules/drafted/package.json")
|
|
1074
|
-
if
|
|
1075
|
-
let data = try? Data(contentsOf: pkg),
|
|
1076
|
-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1077
|
-
let version = json["version"] as? String,
|
|
1078
|
-
isSemver(version)
|
|
1079
|
-
{ return version }
|
|
1080
|
-
|
|
1081
|
-
let bin = home.appendingPathComponent(".drafted/npm-global/bin/drafted").path
|
|
1082
|
-
for command in ["\"\(bin)\" --version 2>/dev/null", "drafted --version 2>/dev/null"] {
|
|
1083
|
-
let process = Process()
|
|
1084
|
-
process.executableURL = URL(fileURLWithPath: "/bin/bash")
|
|
1085
|
-
process.arguments = ["-lc", command]
|
|
1086
|
-
let pipe = Pipe()
|
|
1087
|
-
process.standardOutput = pipe
|
|
1088
|
-
process.standardError = FileHandle.nullDevice
|
|
1089
|
-
do { try process.run(); process.waitUntilExit() } catch { continue }
|
|
1090
|
-
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
1091
|
-
if let out = String(data: data, encoding: .utf8), let version = firstSemver(in: out) {
|
|
1092
|
-
return version
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
return nil
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
static func latestVersion(_ completion: @escaping (String?) -> Void) {
|
|
1099
|
-
guard let url = URL(string: "https://registry.npmjs.org/drafted/latest") else { completion(nil); return }
|
|
1100
|
-
var request = URLRequest(url: url)
|
|
1101
|
-
request.timeoutInterval = 15
|
|
1102
|
-
URLSession.shared.dataTask(with: request) { data, _, _ in
|
|
1103
|
-
guard
|
|
1104
|
-
let data = data,
|
|
1105
|
-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1106
|
-
let version = json["version"] as? String
|
|
1107
|
-
else { completion(nil); return }
|
|
1108
|
-
completion(version)
|
|
1109
|
-
}.resume()
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
static func skippedVersion() -> String? {
|
|
1113
|
-
let path = home.appendingPathComponent(".drafted/install.json")
|
|
1114
|
-
guard
|
|
1115
|
-
let data = try? Data(contentsOf: path),
|
|
1116
|
-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1117
|
-
else { return nil }
|
|
1118
|
-
return json["skippedVersion"] as? String
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
static func setSkippedVersion(_ version: String) {
|
|
1122
|
-
let path = home.appendingPathComponent(".drafted/install.json")
|
|
1123
|
-
var json = ((try? Data(contentsOf: path)).flatMap { try? JSONSerialization.jsonObject(with: $0) } as? [String: Any]) ?? [:]
|
|
1124
|
-
json["skippedVersion"] = version
|
|
1125
|
-
if let out = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) {
|
|
1126
|
-
try? out.write(to: path)
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// First dotted-numeric token in the text, e.g. "1.11.7" from "drafted 1.11.7".
|
|
1131
|
-
static func firstSemver(in text: String) -> String? {
|
|
1132
|
-
var current = ""
|
|
1133
|
-
for ch in text {
|
|
1134
|
-
if ch.isNumber || ch == "." {
|
|
1135
|
-
current.append(ch)
|
|
1136
|
-
} else {
|
|
1137
|
-
if isSemver(current) { return current }
|
|
1138
|
-
current = ""
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
return isSemver(current) ? current : nil
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
static func isSemver(_ s: String) -> Bool {
|
|
1145
|
-
let parts = s.split(separator: ".")
|
|
1146
|
-
return parts.count >= 2 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy { $0.isNumber } }
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
// Numeric, dot-separated comparison (our versions have no prerelease tags).
|
|
1150
|
-
static func isNewer(_ a: String, than b: String) -> Bool {
|
|
1151
|
-
let pa = a.split(separator: ".").map { Int($0) ?? 0 }
|
|
1152
|
-
let pb = b.split(separator: ".").map { Int($0) ?? 0 }
|
|
1153
|
-
for i in 0..<max(pa.count, pb.count) {
|
|
1154
|
-
let x = i < pa.count ? pa[i] : 0
|
|
1155
|
-
let y = i < pb.count ? pb[i] : 0
|
|
1156
|
-
if x != y { return x > y }
|
|
1157
|
-
}
|
|
1158
|
-
return false
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
static func updateDrafted() {
|
|
1162
|
-
if isUpdating {
|
|
1163
|
-
updateWindow?.showUpdating()
|
|
1164
|
-
return
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
isUpdating = true
|
|
1168
|
-
let statusWindow = UpdateStatusWindow()
|
|
1169
|
-
updateWindow = statusWindow
|
|
1170
|
-
statusWindow.showUpdating()
|
|
1171
|
-
|
|
1172
|
-
let command = "tmp=$(mktemp); curl -fsSL https://drafted.live/install.sh -o \"$tmp\" && bash \"$tmp\""
|
|
1173
|
-
let process = Process()
|
|
1174
|
-
process.executableURL = URL(fileURLWithPath: "/bin/bash")
|
|
1175
|
-
process.arguments = ["-lc", command]
|
|
1176
|
-
process.terminationHandler = { proc in
|
|
1177
|
-
if proc.terminationStatus != 0 {
|
|
1178
|
-
Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "installer_exit_\(proc.terminationStatus)")
|
|
1179
|
-
}
|
|
1180
|
-
DispatchQueue.main.async {
|
|
1181
|
-
isUpdating = false
|
|
1182
|
-
let success = proc.terminationStatus == 0
|
|
1183
|
-
statusWindow.showCompleted(
|
|
1184
|
-
success: success,
|
|
1185
|
-
message: success ? "Restart your editor to use the latest MCP tools." : "Run the Drafted installer again from drafted.live/install."
|
|
1186
|
-
)
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
do { try process.run() } catch {
|
|
1190
|
-
isUpdating = false
|
|
1191
|
-
Telemetry.report(event: "drafted_update_helper_failed", updateHelperStatus: "failed", errorCode: "process_run_failed")
|
|
1192
|
-
statusWindow.showCompleted(success: false, message: error.localizedDescription)
|
|
1193
|
-
return
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
static func viewLogs() {
|
|
1198
|
-
let fm = FileManager.default
|
|
1199
|
-
let home = fm.homeDirectoryForCurrentUser
|
|
1200
|
-
let fallback = home.appendingPathComponent(".drafted")
|
|
1201
|
-
try? fm.createDirectory(at: fallback, withIntermediateDirectories: true)
|
|
1202
|
-
let readme = fallback.appendingPathComponent("logs-readme.txt")
|
|
1203
|
-
if !fm.fileExists(atPath: readme.path) {
|
|
1204
|
-
let text = """
|
|
1205
|
-
Drafted MCP logs are written by the host app that launched drafted-mcp.
|
|
1206
|
-
|
|
1207
|
-
Most useful locations:
|
|
1208
|
-
- Drafted MCP client errors: ~/.drafted/mcp-client.log
|
|
1209
|
-
- Claude Desktop: ~/Library/Logs/Claude/mcp-server-drafted.log
|
|
1210
|
-
- Claude Code: ~/.claude/projects (session transcripts) or ~/.claude/logs when present
|
|
1211
|
-
- Drafted installer/updater: ~/.drafted
|
|
1212
|
-
|
|
1213
|
-
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`.
|
|
1214
|
-
"""
|
|
1215
|
-
try? text.write(to: readme, atomically: true, encoding: .utf8)
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
let candidates = [
|
|
1219
|
-
home.appendingPathComponent(".drafted/mcp-client.log"),
|
|
1220
|
-
home.appendingPathComponent("Library/Logs/Claude/mcp-server-drafted.log"),
|
|
1221
|
-
home.appendingPathComponent("Library/Logs/Claude"),
|
|
1222
|
-
home.appendingPathComponent(".claude/logs"),
|
|
1223
|
-
home.appendingPathComponent(".claude/projects"),
|
|
1224
|
-
fallback,
|
|
1225
|
-
]
|
|
1226
|
-
for url in candidates where fm.fileExists(atPath: url.path) {
|
|
1227
|
-
var isDirectory: ObjCBool = false
|
|
1228
|
-
fm.fileExists(atPath: url.path, isDirectory: &isDirectory)
|
|
1229
|
-
if isDirectory.boolValue {
|
|
1230
|
-
NSWorkspace.shared.open(url)
|
|
1231
|
-
} else {
|
|
1232
|
-
NSWorkspace.shared.activateFileViewerSelecting([url])
|
|
1233
|
-
}
|
|
1234
|
-
return
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
static func openDrafted() {
|
|
1239
|
-
NSWorkspace.shared.open(URL(string: "https://drafted.live")!)
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
struct Telemetry {
|
|
1244
|
-
static func report(event: String, updateHelperStatus: String, errorCode: String? = nil) {
|
|
1245
|
-
guard ProcessInfo.processInfo.environment["DRAFTED_TELEMETRY"] != "0" else { return }
|
|
1246
|
-
let path = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".drafted/install.json")
|
|
1247
|
-
guard
|
|
1248
|
-
let data = try? Data(contentsOf: path),
|
|
1249
|
-
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
1250
|
-
json["telemetry"] as? Bool != false,
|
|
1251
|
-
let installId = json["installId"] as? String
|
|
1252
|
-
else { return }
|
|
1253
|
-
var body: [String: Any] = [
|
|
1254
|
-
"installId": installId,
|
|
1255
|
-
"event": event,
|
|
1256
|
-
"schemaVersion": 1,
|
|
1257
|
-
"osFamily": "macos",
|
|
1258
|
-
"osVersion": ProcessInfo.processInfo.operatingSystemVersionString,
|
|
1259
|
-
"arch": SystemVersion.machine,
|
|
1260
|
-
"updateHelperStatus": updateHelperStatus,
|
|
1261
|
-
"source": "macos-helper"
|
|
1262
|
-
]
|
|
1263
|
-
if let errorCode = errorCode { body["errorCode"] = errorCode }
|
|
1264
|
-
guard
|
|
1265
|
-
let url = URL(string: "https://drafted.live/api/installations/report"),
|
|
1266
|
-
let payload = try? JSONSerialization.data(withJSONObject: body)
|
|
1267
|
-
else { return }
|
|
1268
|
-
var request = URLRequest(url: url)
|
|
1269
|
-
request.httpMethod = "POST"
|
|
1270
|
-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
1271
|
-
request.httpBody = payload
|
|
1272
|
-
URLSession.shared.dataTask(with: request).resume()
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
enum SystemVersion {
|
|
1277
|
-
static var machine: String {
|
|
1278
|
-
#if arch(arm64)
|
|
1279
|
-
return "arm64"
|
|
1280
|
-
#elseif arch(x86_64)
|
|
1281
|
-
return "x64"
|
|
1282
|
-
#else
|
|
1283
|
-
return "unknown"
|
|
1284
|
-
#endif
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
SWIFT
|
|
1288
|
-
|
|
1289
|
-
cat > "$app_bundle/Contents/Info.plist" <<APPPLIST
|
|
1290
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1291
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1292
|
-
<plist version="1.0">
|
|
1293
|
-
<dict>
|
|
1294
|
-
<key>CFBundleExecutable</key>
|
|
1295
|
-
<string>DraftedUpdater</string>
|
|
1296
|
-
<key>CFBundleIdentifier</key>
|
|
1297
|
-
<string>live.drafted.updater</string>
|
|
1298
|
-
<key>CFBundleName</key>
|
|
1299
|
-
<string>Drafted Updater</string>
|
|
1300
|
-
<key>CFBundleDisplayName</key>
|
|
1301
|
-
<string>Drafted Updater</string>
|
|
1302
|
-
<key>CFBundlePackageType</key>
|
|
1303
|
-
<string>APPL</string>
|
|
1304
|
-
<key>CFBundleShortVersionString</key>
|
|
1305
|
-
<string>1.0</string>
|
|
1306
|
-
<key>LSMinimumSystemVersion</key>
|
|
1307
|
-
<string>13.0</string>
|
|
1308
|
-
<key>LSUIElement</key>
|
|
1309
|
-
<true/>
|
|
1310
|
-
<key>NSHighResolutionCapable</key>
|
|
1311
|
-
<true/>
|
|
1312
|
-
</dict>
|
|
1313
|
-
</plist>
|
|
1314
|
-
APPPLIST
|
|
1315
|
-
|
|
1316
|
-
if ! swiftc -parse-as-library "$swift_file" -o "$exe_path" -framework SwiftUI -framework AppKit >/dev/null 2>&1; then
|
|
1317
|
-
echo -e " ${YELLOW}Could not build macOS menu bar updater; skipping.${RESET}"
|
|
1318
|
-
return 0
|
|
1319
|
-
fi
|
|
1320
|
-
codesign -s - -f "$exe_path" >/dev/null 2>&1 || true
|
|
1321
|
-
|
|
1322
|
-
cat > "$plist" <<PLIST
|
|
1323
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1324
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1325
|
-
<plist version="1.0">
|
|
1326
|
-
<dict>
|
|
1327
|
-
<key>Label</key>
|
|
1328
|
-
<string>live.drafted.updater</string>
|
|
1329
|
-
<key>ProgramArguments</key>
|
|
1330
|
-
<array>
|
|
1331
|
-
<string>open</string>
|
|
1332
|
-
<string>-a</string>
|
|
1333
|
-
<string>$app_bundle</string>
|
|
1334
|
-
</array>
|
|
1335
|
-
<key>RunAtLoad</key>
|
|
1336
|
-
<true/>
|
|
1337
|
-
<key>KeepAlive</key>
|
|
1338
|
-
<false/>
|
|
1339
|
-
</dict>
|
|
1340
|
-
</plist>
|
|
1341
|
-
PLIST
|
|
1342
|
-
|
|
1343
|
-
launchctl bootout "gui/$uid" "$plist" >/dev/null 2>&1 || true
|
|
1344
|
-
pkill -x DraftedUpdater >/dev/null 2>&1 || true
|
|
1345
|
-
mdimport "$app_bundle" >/dev/null 2>&1 || true
|
|
1346
|
-
open "$app_bundle" >/dev/null 2>&1 || true
|
|
1347
|
-
ok "macOS menu bar updater"
|
|
1348
|
-
}
|
|
1349
|
-
if [ "$INSTALL_MODE" = "production" ]; then
|
|
1350
|
-
step "Installing update helper"
|
|
1351
|
-
install_update_menu_icon
|
|
1352
|
-
fi
|
|
1353
|
-
verify_no_legacy_http_config
|
|
1354
|
-
report_telemetry "drafted_mcp_configured" "installed"
|
|
1355
|
-
report_telemetry "drafted_install" "installed"
|
|
1356
|
-
|
|
1357
|
-
# ── Optional: Google for the local agent ─────────────────────────
|
|
1358
|
-
# Interactive installs only. The auto-updater re-runs this script with no
|
|
1359
|
-
# terminal, so it must never reach a prompt; a "no" is remembered in
|
|
1360
|
-
# ~/.drafted/google-setup.declined so interactive re-runs don't re-ask either.
|
|
1361
|
-
GOOGLE_SETUP_SCRIPT="${DRAFTED_PKG_DIR:+$DRAFTED_PKG_DIR/cli/google-setup.sh}"
|
|
1362
|
-
[ -f "$SCRIPT_DIR/cli/google-setup.sh" ] && GOOGLE_SETUP_SCRIPT="$SCRIPT_DIR/cli/google-setup.sh"
|
|
1363
|
-
if [ "$INSTALL_MODE" = "production" ] && [ "${DRAFTED_HEADLESS:-}" != "1" ] \
|
|
1364
|
-
&& [ -t 1 ] && { : </dev/tty; } 2>/dev/null \
|
|
1365
|
-
&& [ ! -f "$HOME/.drafted/google-setup.declined" ] \
|
|
1366
|
-
&& [ -n "$GOOGLE_SETUP_SCRIPT" ] && [ -f "$GOOGLE_SETUP_SCRIPT" ]; then
|
|
1367
|
-
step "Connecting Google (optional)"
|
|
1368
|
-
NPM_GLOBAL_PREFIX="$NPM_GLOBAL_PREFIX" bash "$GOOGLE_SETUP_SCRIPT" --from-installer || true
|
|
1369
|
-
fi
|
|
1370
|
-
|
|
1371
|
-
# ── Done ─────────────────────────────────────────────────────────
|
|
1372
|
-
|
|
1373
|
-
echo ""
|
|
1374
|
-
echo ""
|
|
1375
|
-
echo -e "${GREEN}${BOLD}You're all set!${RESET}"
|
|
1376
|
-
echo ""
|
|
1377
|
-
echo -e " ${DIM}MCP name:${RESET} ${BOLD}$INSTALL_NAME${RESET}"
|
|
1378
|
-
echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
|
|
1379
|
-
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)"
|
|
1380
|
-
echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
|
|
1381
|
-
echo ""
|
|
1382
|
-
# ── Hand-off: start the first session ───────────────────────────
|
|
1383
|
-
# Interactive production installs only. A fresh agent process loads the new MCP,
|
|
1384
|
-
# so no restart is needed, and onboard-drafted is what turns the install into a
|
|
1385
|
-
# first project. Without this the user is left at a banner, not knowing what to
|
|
1386
|
-
# type next. Anything unusual (no agent CLI, no terminal, a "no") falls back to it.
|
|
1387
|
-
handoff_to_agent() {
|
|
1388
|
-
# DRAFTED_TTY lets tests/install-handoff.test.ts play the terminal.
|
|
1389
|
-
local tty="${DRAFTED_TTY:-/dev/tty}"
|
|
1390
|
-
[ "$INSTALL_MODE" = "production" ] && [ "${DRAFTED_HEADLESS:-}" != "1" ] || return 1
|
|
1391
|
-
[ -n "${DRAFTED_TTY:-}" ] || [ -t 1 ] || return 1
|
|
1392
|
-
# One descriptor for every answer, so a scripted terminal is read line by line.
|
|
1393
|
-
exec 4<"$tty" 2>/dev/null || return 1
|
|
1394
|
-
local agents=() agent="" reply
|
|
1395
|
-
command -v claude >/dev/null 2>&1 && agents+=(claude)
|
|
1396
|
-
command -v codex >/dev/null 2>&1 && agents+=(codex)
|
|
1397
|
-
[ ${#agents[@]} -gt 0 ] || return 1
|
|
1398
|
-
step "Start your first project"
|
|
1399
|
-
read -r -p " Start it now in your agent? [Y/n] " reply <&4 || reply="n"
|
|
1400
|
-
case "$reply" in [nN]*) return 1 ;; esac
|
|
1401
|
-
if [ ${#agents[@]} -eq 1 ]; then
|
|
1402
|
-
agent="${agents[0]}"
|
|
1403
|
-
else
|
|
1404
|
-
read -r -p " Which agent do you use? 1) Claude Code 2) Codex [1] " reply <&4 || reply=""
|
|
1405
|
-
case "$reply" in 2*) agent="codex" ;; *) agent="claude" ;; esac
|
|
1406
|
-
fi
|
|
1407
|
-
if [ ! -s "$HOME/.drafted/auth.json" ]; then
|
|
1408
|
-
echo -e " Signing in to Drafted (a browser tab opens; approve it there)"
|
|
1409
|
-
DRAFTED_SERVER="$INSTALL_SERVER" drafted login <"$tty" || return 1
|
|
1410
|
-
fi
|
|
1411
|
-
local url="$INSTALL_SERVER/?setup=1"
|
|
1412
|
-
echo -e " Opening your surface: ${BOLD}$url${RESET}"
|
|
1413
|
-
echo -e " ${DIM}Signed up in a different browser or a private window? Open that link there.${RESET}"
|
|
1414
|
-
if command -v open >/dev/null 2>&1; then open "$url" >/dev/null 2>&1 || true
|
|
1415
|
-
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url" >/dev/null 2>&1 || true
|
|
1416
|
-
fi
|
|
1417
|
-
if [ "$agent" = "claude" ]; then
|
|
1418
|
-
echo -e " Starting Claude Code with ${BOLD}/drafted:onboard-drafted${RESET}..."
|
|
1419
|
-
exec claude "/drafted:onboard-drafted" <"$tty"
|
|
1420
|
-
fi
|
|
1421
|
-
# Codex has no namespaced slash command for it: pass the command's own text.
|
|
1422
|
-
local cmd_file="${PLUGIN_SRC:-}/commands/onboard-drafted.md" prompt="Run the Drafted onboarding (the onboard-drafted command)."
|
|
1423
|
-
if [ -f "$cmd_file" ]; then
|
|
1424
|
-
prompt="$(awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2' "$cmd_file" | sed 's/\$ARGUMENTS//g')"
|
|
1425
|
-
fi
|
|
1426
|
-
echo -e " Starting Codex with the Drafted onboarding..."
|
|
1427
|
-
exec codex "$prompt" <"$tty"
|
|
1428
|
-
}
|
|
1429
|
-
handoff_to_agent || {
|
|
1430
|
-
echo -e "${YELLOW}${BOLD}"
|
|
1431
|
-
echo " ┌─────────────────────────────────────────────────────────┐"
|
|
1432
|
-
echo " │ │"
|
|
1433
|
-
echo " │ >>> RESTART YOUR EDITOR TO ACTIVATE DRAFTED <<< │"
|
|
1434
|
-
echo " │ │"
|
|
1435
|
-
echo " │ Close and reopen Claude Desktop, Claude Code, │"
|
|
1436
|
-
echo " │ Codex, or Cursor so it picks up the new MCP server. │"
|
|
1437
|
-
echo " │ │"
|
|
1438
|
-
echo " └─────────────────────────────────────────────────────────┘"
|
|
1439
|
-
echo -e "${RESET}"
|
|
1440
|
-
}
|