agent-director 0.4.0 → 0.4.2
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 +62 -0
- package/dist/client.d.ts +99 -0
- package/dist/errors.d.ts +173 -0
- package/dist/ffi.d.ts +42 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +603 -0
- package/dist/internal/bindingSpec.d.ts +37 -0
- package/dist/internal/bootstrapFfi.d.ts +37 -0
- package/dist/internal/freeGuard.d.ts +39 -0
- package/dist/internal/tilde.d.ts +16 -0
- package/dist/internal/tsOnlyErrors.d.ts +25 -0
- package/dist/internal/verbs.d.ts +57 -0
- package/dist/internal/worker.d.ts +36 -0
- package/dist/internal/workerProxy.d.ts +45 -0
- package/dist/platform.d.ts +80 -0
- package/dist/types.d.ts +301 -0
- package/package.json +18 -5
- package/scripts/postinstall.ts +646 -0
- package/skills/install-agent-director/SKILL.md +481 -0
- package/skills/install-agent-director/install.sh +620 -0
- package/skills/install-agent-director/uninstall.sh +210 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install.sh — install or upgrade agent-director on this machine.
|
|
3
|
+
#
|
|
4
|
+
# Per SRD §16.2. The script is deliberately bash + jq + standard
|
|
5
|
+
# coreutils — no Go, no exotic deps. The Apiary skill harness invokes
|
|
6
|
+
# it; an operator can also run it directly from a checked-out tree.
|
|
7
|
+
#
|
|
8
|
+
# Flags:
|
|
9
|
+
# --binary <path> Source binary to install. Defaults to looking
|
|
10
|
+
# next to the script first, then to whatever
|
|
11
|
+
# `command -v agent-director` resolves to.
|
|
12
|
+
# --from-release [tag] Download a pre-built binary for this host's
|
|
13
|
+
# OS/arch from GitHub Releases and install it.
|
|
14
|
+
# With no tag, resolves the latest release via
|
|
15
|
+
# `gh release view` (if available) or
|
|
16
|
+
# `curl + jq` against api.github.com. Mutually
|
|
17
|
+
# exclusive with --binary.
|
|
18
|
+
# --sha256 <hex> Verify the downloaded asset against this
|
|
19
|
+
# sha256 (lowercase hex, 64 chars). Only
|
|
20
|
+
# meaningful with --from-release. Optional —
|
|
21
|
+
# omit to skip verification.
|
|
22
|
+
# --symlink-dir <dir> Drop a PATH symlink at <dir>/agent-director.
|
|
23
|
+
# Default: ~/.local/bin if on PATH; otherwise
|
|
24
|
+
# no symlink.
|
|
25
|
+
# --no-symlink Suppress symlink creation regardless of dir.
|
|
26
|
+
# --register-mcp Run `claude mcp add` for the stdio server.
|
|
27
|
+
# --no-hooks Skip the ~/.claude/settings.json hook
|
|
28
|
+
# injection step entirely. settings.json is
|
|
29
|
+
# left byte-identical (no .bak backup, no
|
|
30
|
+
# edit). Default OFF — defaulting to skip
|
|
31
|
+
# would defeat install.sh's main value over a
|
|
32
|
+
# bare binary copy.
|
|
33
|
+
# --keep-prior Before overwriting an existing binary,
|
|
34
|
+
# snapshot it to <target>.prior (overwriting
|
|
35
|
+
# any previous .prior). Roll back with
|
|
36
|
+
# `mv <target>.prior <target>`. Default OFF.
|
|
37
|
+
#
|
|
38
|
+
# Exit codes:
|
|
39
|
+
# 0 success
|
|
40
|
+
# 2 pre-flight failure (claude/tmux missing, whitespace in path)
|
|
41
|
+
# 3 binary source not found / not executable
|
|
42
|
+
# 4 hook merge failure (~/.claude/settings.json malformed)
|
|
43
|
+
# 5 store warmup failure
|
|
44
|
+
#
|
|
45
|
+
# Idempotent: re-running the script with no flags after a clean
|
|
46
|
+
# install is a no-op (returns 0, prints "already installed at vX").
|
|
47
|
+
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------
|
|
51
|
+
# Defaults + flag parsing
|
|
52
|
+
# --------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
readonly DEFAULT_INSTALL_ROOT="${HOME}/.agent-director"
|
|
55
|
+
readonly DEFAULT_BIN_DIR="${DEFAULT_INSTALL_ROOT}/bin"
|
|
56
|
+
readonly DEFAULT_SETTINGS_PATH="${HOME}/.claude/settings.json"
|
|
57
|
+
|
|
58
|
+
BINARY_SRC=""
|
|
59
|
+
FROM_RELEASE=0
|
|
60
|
+
FROM_RELEASE_TAG=""
|
|
61
|
+
SHA256_EXPECTED=""
|
|
62
|
+
SYMLINK_DIR=""
|
|
63
|
+
SYMLINK_DEFAULT=""
|
|
64
|
+
NO_SYMLINK=0
|
|
65
|
+
REGISTER_MCP=0
|
|
66
|
+
NO_HOOKS=0
|
|
67
|
+
KEEP_PRIOR=0
|
|
68
|
+
|
|
69
|
+
# GitHub repo slug used by --from-release. Matches go.mod's module path
|
|
70
|
+
# and the release.sh asset naming.
|
|
71
|
+
readonly RELEASE_REPO_SLUG="gabemahoney/agent-director"
|
|
72
|
+
|
|
73
|
+
# Pick a sensible default symlink dir: ~/.local/bin if on PATH.
|
|
74
|
+
if printf '%s' ":${PATH}:" | grep -q ":${HOME}/.local/bin:"; then
|
|
75
|
+
SYMLINK_DEFAULT="${HOME}/.local/bin"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
while [[ $# -gt 0 ]]; do
|
|
79
|
+
case "$1" in
|
|
80
|
+
--binary)
|
|
81
|
+
BINARY_SRC="$2"; shift 2 ;;
|
|
82
|
+
--from-release)
|
|
83
|
+
FROM_RELEASE=1
|
|
84
|
+
# Optional tag argument: accept only if the next arg
|
|
85
|
+
# doesn't look like another flag.
|
|
86
|
+
if [[ $# -ge 2 && -n "${2:-}" && "${2:-}" != -* ]]; then
|
|
87
|
+
FROM_RELEASE_TAG="$2"; shift 2
|
|
88
|
+
else
|
|
89
|
+
shift
|
|
90
|
+
fi
|
|
91
|
+
;;
|
|
92
|
+
--sha256)
|
|
93
|
+
SHA256_EXPECTED="$2"; shift 2 ;;
|
|
94
|
+
--symlink-dir)
|
|
95
|
+
SYMLINK_DIR="$2"; shift 2 ;;
|
|
96
|
+
--no-symlink)
|
|
97
|
+
NO_SYMLINK=1; shift ;;
|
|
98
|
+
--register-mcp)
|
|
99
|
+
REGISTER_MCP=1; shift ;;
|
|
100
|
+
--no-hooks)
|
|
101
|
+
NO_HOOKS=1; shift ;;
|
|
102
|
+
--keep-prior)
|
|
103
|
+
KEEP_PRIOR=1; shift ;;
|
|
104
|
+
-h|--help)
|
|
105
|
+
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
|
106
|
+
exit 0 ;;
|
|
107
|
+
*)
|
|
108
|
+
echo "install.sh: unknown flag: $1" >&2
|
|
109
|
+
exit 2 ;;
|
|
110
|
+
esac
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
[[ -z "$SYMLINK_DIR" && "$NO_SYMLINK" -eq 0 ]] && SYMLINK_DIR="$SYMLINK_DEFAULT"
|
|
114
|
+
|
|
115
|
+
if [[ "$FROM_RELEASE" -eq 1 && -n "$BINARY_SRC" ]]; then
|
|
116
|
+
echo "install.sh: --from-release and --binary are mutually exclusive" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
if [[ -n "$SHA256_EXPECTED" && "$FROM_RELEASE" -eq 0 ]]; then
|
|
120
|
+
echo "install.sh: --sha256 only applies with --from-release" >&2
|
|
121
|
+
exit 2
|
|
122
|
+
fi
|
|
123
|
+
if [[ -n "$SHA256_EXPECTED" && ! "$SHA256_EXPECTED" =~ ^[0-9a-f]{64}$ ]]; then
|
|
124
|
+
echo "install.sh: --sha256 must be 64 lowercase hex characters" >&2
|
|
125
|
+
exit 2
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------------------------
|
|
129
|
+
# Pre-flight
|
|
130
|
+
# --------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
# SRD §4.3: tmux's direct-argv invocation requires shell-safe paths.
|
|
133
|
+
# Reject any whitespace in the install root up front so an operator
|
|
134
|
+
# whose $HOME has a space sees the error immediately, not at the
|
|
135
|
+
# first spawn.
|
|
136
|
+
if [[ "$DEFAULT_INSTALL_ROOT" =~ [[:space:]] ]]; then
|
|
137
|
+
echo "install.sh: install path contains whitespace: $DEFAULT_INSTALL_ROOT" >&2
|
|
138
|
+
echo " SRD §4.3 requires a whitespace-free install path." >&2
|
|
139
|
+
exit 2
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# SRD §SR-2.1 / Idea Bee b.fg3: hard-refuse any host outside the v0.4.1
|
|
143
|
+
# supported set {Linux/x86_64, Darwin/arm64} at preflight time, mirroring
|
|
144
|
+
# the umbrella's npm/bun-side os/cpu gate + postinstall host-pair refusal
|
|
145
|
+
# (Pattern A). install.sh is the direct-invocation surface (Pattern B
|
|
146
|
+
# fallback + Pattern A second step); without this gate an operator on an
|
|
147
|
+
# unsupported host could still copy a wrong-arch CLI into place.
|
|
148
|
+
uname_s="$(uname -s)"
|
|
149
|
+
uname_m="$(uname -m)"
|
|
150
|
+
case "${uname_s}/${uname_m}" in
|
|
151
|
+
Linux/x86_64|Darwin/arm64)
|
|
152
|
+
;;
|
|
153
|
+
*)
|
|
154
|
+
echo "install.sh: unsupported host: ${uname_s}/${uname_m}. Supported: Linux/x86_64, Darwin/arm64. See b.fg3 for cross-platform expansion status." >&2
|
|
155
|
+
exit 2
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
|
|
159
|
+
# claude + tmux must be on PATH. `file` is required for the --binary
|
|
160
|
+
# architecture probe (SR-2.2) — hard requirement; never silent-skip.
|
|
161
|
+
required_tools=(claude tmux jq file)
|
|
162
|
+
[[ "$FROM_RELEASE" -eq 1 ]] && required_tools+=(curl)
|
|
163
|
+
for tool in "${required_tools[@]}"; do
|
|
164
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
165
|
+
echo "install.sh: required tool not found on PATH: $tool" >&2
|
|
166
|
+
case "$tool" in
|
|
167
|
+
claude) echo " Install Claude Code first: https://claude.com/claude-code" >&2 ;;
|
|
168
|
+
tmux) echo " Install tmux via your package manager (apt/brew/dnf/etc.)." >&2 ;;
|
|
169
|
+
jq) echo " Install jq via your package manager (we use it to safely edit settings.json)." >&2 ;;
|
|
170
|
+
file) echo " Install file via your package manager (apt install file / brew install file-formula / dnf install file). Required for the --binary architecture probe." >&2 ;;
|
|
171
|
+
curl) echo " --from-release downloads via curl; install it via your package manager." >&2 ;;
|
|
172
|
+
esac
|
|
173
|
+
exit 2
|
|
174
|
+
fi
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
echo "install.sh: pre-flight OK"
|
|
178
|
+
echo " claude : $(claude --version 2>/dev/null || echo '<unknown>')"
|
|
179
|
+
echo " tmux : $(tmux -V 2>/dev/null || echo '<unknown>')"
|
|
180
|
+
|
|
181
|
+
# --------------------------------------------------------------------
|
|
182
|
+
# --from-release: resolve tag, download asset for this OS/arch, hand
|
|
183
|
+
# the temp path to the rest of the install flow as if --binary had
|
|
184
|
+
# been passed.
|
|
185
|
+
# --------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
if [[ "$FROM_RELEASE" -eq 1 ]]; then
|
|
188
|
+
case "$(uname -s)" in
|
|
189
|
+
Linux) rel_os="linux" ;;
|
|
190
|
+
Darwin) rel_os="darwin" ;;
|
|
191
|
+
*)
|
|
192
|
+
echo "install.sh: --from-release: unsupported OS $(uname -s)" >&2
|
|
193
|
+
echo " release.sh only publishes linux and darwin builds." >&2
|
|
194
|
+
exit 3 ;;
|
|
195
|
+
esac
|
|
196
|
+
case "$(uname -m)" in
|
|
197
|
+
x86_64|amd64) rel_arch="amd64" ;;
|
|
198
|
+
arm64|aarch64) rel_arch="arm64" ;;
|
|
199
|
+
*)
|
|
200
|
+
echo "install.sh: --from-release: unsupported arch $(uname -m)" >&2
|
|
201
|
+
exit 3 ;;
|
|
202
|
+
esac
|
|
203
|
+
asset="agent-director-${rel_os}-${rel_arch}"
|
|
204
|
+
|
|
205
|
+
# Resolve the tag if the operator didn't supply one. Prefer `gh`
|
|
206
|
+
# (carries the operator's auth, avoids the unauthenticated API
|
|
207
|
+
# rate limit); fall back to curl + jq against the public API.
|
|
208
|
+
if [[ -z "$FROM_RELEASE_TAG" ]]; then
|
|
209
|
+
if command -v gh >/dev/null 2>&1; then
|
|
210
|
+
FROM_RELEASE_TAG=$(gh release view --repo "$RELEASE_REPO_SLUG" \
|
|
211
|
+
--json tagName -q .tagName 2>/dev/null || true)
|
|
212
|
+
fi
|
|
213
|
+
if [[ -z "$FROM_RELEASE_TAG" ]]; then
|
|
214
|
+
api_url="https://api.github.com/repos/${RELEASE_REPO_SLUG}/releases/latest"
|
|
215
|
+
FROM_RELEASE_TAG=$(curl -fsSL "$api_url" 2>/dev/null \
|
|
216
|
+
| jq -r '.tag_name // empty' 2>/dev/null || true)
|
|
217
|
+
fi
|
|
218
|
+
if [[ -z "$FROM_RELEASE_TAG" || "$FROM_RELEASE_TAG" == "null" ]]; then
|
|
219
|
+
echo "install.sh: --from-release: no releases published for $RELEASE_REPO_SLUG yet" >&2
|
|
220
|
+
echo " options:" >&2
|
|
221
|
+
echo " - build from source: make build && bash $0" >&2
|
|
222
|
+
echo " - point at a local binary: bash $0 --binary <path>" >&2
|
|
223
|
+
exit 3
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
echo " release : $RELEASE_REPO_SLUG @ $FROM_RELEASE_TAG ($asset)"
|
|
227
|
+
|
|
228
|
+
asset_url="https://github.com/${RELEASE_REPO_SLUG}/releases/download/${FROM_RELEASE_TAG}/${asset}"
|
|
229
|
+
tmp_bin="$(mktemp -t agent-director.XXXXXX)"
|
|
230
|
+
# Defer-cleanup the tempfile on any exit path that doesn't move
|
|
231
|
+
# past the BINARY_SRC assignment. install -m 0755 later in the
|
|
232
|
+
# script copies the contents into place, so the tempfile being
|
|
233
|
+
# cleaned up at script exit is fine.
|
|
234
|
+
trap 'rm -f "$tmp_bin"' EXIT
|
|
235
|
+
if ! curl -fsSL --retry 2 -o "$tmp_bin" "$asset_url"; then
|
|
236
|
+
echo "install.sh: --from-release: failed to download $asset_url" >&2
|
|
237
|
+
echo " check that the asset exists for $FROM_RELEASE_TAG on $RELEASE_REPO_SLUG." >&2
|
|
238
|
+
exit 3
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
if [[ -n "$SHA256_EXPECTED" ]]; then
|
|
242
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
243
|
+
actual=$(sha256sum "$tmp_bin" | awk '{print $1}')
|
|
244
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
245
|
+
actual=$(shasum -a 256 "$tmp_bin" | awk '{print $1}')
|
|
246
|
+
else
|
|
247
|
+
echo "install.sh: --sha256: neither sha256sum nor shasum available" >&2
|
|
248
|
+
exit 3
|
|
249
|
+
fi
|
|
250
|
+
if [[ "$actual" != "$SHA256_EXPECTED" ]]; then
|
|
251
|
+
echo "install.sh: --from-release: sha256 mismatch" >&2
|
|
252
|
+
echo " expected: $SHA256_EXPECTED" >&2
|
|
253
|
+
echo " actual : $actual" >&2
|
|
254
|
+
exit 3
|
|
255
|
+
fi
|
|
256
|
+
echo " sha256 : verified"
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
chmod +x "$tmp_bin"
|
|
260
|
+
BINARY_SRC="$tmp_bin"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# --------------------------------------------------------------------
|
|
264
|
+
# Locate source binary
|
|
265
|
+
# --------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
268
|
+
|
|
269
|
+
# Defaults to 1; cleared to 0 only when BINARY_SRC came from PATH
|
|
270
|
+
# (option (c)), since "whatever's on PATH" makes no claim about the
|
|
271
|
+
# operator's source tree.
|
|
272
|
+
VERSION_CHECK_REQUIRED=1
|
|
273
|
+
|
|
274
|
+
if [[ -z "$BINARY_SRC" ]]; then
|
|
275
|
+
# Prefer the in-repo build (skills/install-agent-director sits two
|
|
276
|
+
# levels under the repo root; bin/ is at the root).
|
|
277
|
+
candidate="${SCRIPT_DIR}/../../bin/agent-director"
|
|
278
|
+
if [[ -x "$candidate" ]]; then
|
|
279
|
+
BINARY_SRC="$candidate"
|
|
280
|
+
elif command -v agent-director >/dev/null 2>&1; then
|
|
281
|
+
BINARY_SRC="$(command -v agent-director)"
|
|
282
|
+
VERSION_CHECK_REQUIRED=0
|
|
283
|
+
else
|
|
284
|
+
echo "install.sh: no source binary found." >&2
|
|
285
|
+
echo " Tried: $candidate" >&2
|
|
286
|
+
echo " Tried: command -v agent-director" >&2
|
|
287
|
+
echo " Pass --binary <path> to override." >&2
|
|
288
|
+
exit 3
|
|
289
|
+
fi
|
|
290
|
+
fi
|
|
291
|
+
if [[ ! -x "$BINARY_SRC" ]]; then
|
|
292
|
+
echo "install.sh: source binary not executable: $BINARY_SRC" >&2
|
|
293
|
+
exit 3
|
|
294
|
+
fi
|
|
295
|
+
echo " source : $BINARY_SRC"
|
|
296
|
+
|
|
297
|
+
# --------------------------------------------------------------------
|
|
298
|
+
# --binary architecture probe (SR-2.2, preflight step 6)
|
|
299
|
+
#
|
|
300
|
+
# Catches the case where a supported host receives a wrong-arch binary
|
|
301
|
+
# (e.g. operator passes a darwin-arm64 artifact on a Linux/x86_64 host).
|
|
302
|
+
# Runs file(1) against $BINARY_SRC and pattern-matches against the
|
|
303
|
+
# host pair captured by the OS/CPU gate (T1). On mismatch: exit 2 with
|
|
304
|
+
# the SR-2.2 message. file(1) is a hard preflight requirement (T2 +
|
|
305
|
+
# required_tools); never silent-skip.
|
|
306
|
+
#
|
|
307
|
+
# Multiple substring matches joined by && rather than a single regex —
|
|
308
|
+
# file's output format varies subtly across distros (`x86-64` vs
|
|
309
|
+
# `x86_64`), and a brittle regex would silently misclassify a valid
|
|
310
|
+
# binary on a future toolchain.
|
|
311
|
+
# --------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
file_out="$(file -L -b "$BINARY_SRC")"
|
|
314
|
+
arch_ok=0
|
|
315
|
+
case "${uname_s}/${uname_m}" in
|
|
316
|
+
Linux/x86_64)
|
|
317
|
+
if grep -q "ELF 64-bit LSB" <<<"$file_out" \
|
|
318
|
+
&& { grep -q "x86-64" <<<"$file_out" || grep -q "x86_64" <<<"$file_out"; }; then
|
|
319
|
+
arch_ok=1
|
|
320
|
+
fi
|
|
321
|
+
;;
|
|
322
|
+
Darwin/arm64)
|
|
323
|
+
if grep -q "Mach-O" <<<"$file_out" \
|
|
324
|
+
&& { grep -q "arm64e" <<<"$file_out" || grep -q "arm64" <<<"$file_out"; }; then
|
|
325
|
+
arch_ok=1
|
|
326
|
+
fi
|
|
327
|
+
;;
|
|
328
|
+
esac
|
|
329
|
+
|
|
330
|
+
if [[ "$arch_ok" -ne 1 ]]; then
|
|
331
|
+
# Distil the diagnostic excerpt from file's output — first ~60 chars
|
|
332
|
+
# is plenty to surface "Mach-O arm64" or "ELF 64-bit LSB x86-64".
|
|
333
|
+
detected="$(printf '%s' "$file_out" | head -c 80 | tr '\n' ' ')"
|
|
334
|
+
echo "install.sh: --binary $BINARY_SRC: architecture mismatch (binary appears to be ${detected}; host is ${uname_s}/${uname_m}). Did you pass the wrong --binary?" >&2
|
|
335
|
+
exit 2
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# --------------------------------------------------------------------
|
|
339
|
+
# Source-tree version check
|
|
340
|
+
#
|
|
341
|
+
# When the operator points install.sh at a local binary (either via
|
|
342
|
+
# --binary or via the in-repo ./bin/agent-director fallback) AND
|
|
343
|
+
# install.sh itself lives inside a git checkout, refuse to install a
|
|
344
|
+
# binary whose embedded commit doesn't match the checkout's HEAD.
|
|
345
|
+
# Catches the "operator forgot to `make build` after pulling new
|
|
346
|
+
# code" footgun — installing a stale artifact silently is exactly
|
|
347
|
+
# what b.qag flagged.
|
|
348
|
+
#
|
|
349
|
+
# Skipped when:
|
|
350
|
+
# - --from-release was used (the asset is by construction not the
|
|
351
|
+
# operator's source tree)
|
|
352
|
+
# - install.sh is not inside a git checkout (curled tarball case)
|
|
353
|
+
# - BINARY_SRC came from `command -v` (option (c)): there's no
|
|
354
|
+
# promise it was built from this tree, and the user explicitly
|
|
355
|
+
# asked for "whatever's on PATH"
|
|
356
|
+
# --------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
if [[ "$FROM_RELEASE" -eq 0 && "${VERSION_CHECK_REQUIRED:-1}" -eq 1 ]]; then
|
|
359
|
+
# Find the script's repo root (walking up from SCRIPT_DIR). The
|
|
360
|
+
# script-path is what tells us "this checkout"; CWD might be
|
|
361
|
+
# somewhere unrelated.
|
|
362
|
+
repo_root=""
|
|
363
|
+
probe="$SCRIPT_DIR"
|
|
364
|
+
while [[ "$probe" != "/" && -n "$probe" ]]; do
|
|
365
|
+
if [[ -d "$probe/.git" ]]; then
|
|
366
|
+
repo_root="$probe"; break
|
|
367
|
+
fi
|
|
368
|
+
probe="$(dirname "$probe")"
|
|
369
|
+
done
|
|
370
|
+
|
|
371
|
+
if [[ -n "$repo_root" ]] && head_sha=$(git -C "$repo_root" rev-parse HEAD 2>/dev/null); then
|
|
372
|
+
# Run the binary's `version` verb. An older binary without the
|
|
373
|
+
# verb will exit non-zero / emit an err_name envelope; jq -e
|
|
374
|
+
# returns non-zero if .commit is absent or null. Either way we
|
|
375
|
+
# land in the mismatch path with bin_commit empty.
|
|
376
|
+
bin_commit=$("$BINARY_SRC" version 2>/dev/null \
|
|
377
|
+
| jq -er '.commit // empty' 2>/dev/null \
|
|
378
|
+
|| true)
|
|
379
|
+
|
|
380
|
+
if [[ -z "$bin_commit" || "$bin_commit" == "unknown" || "$bin_commit" != "$head_sha" ]]; then
|
|
381
|
+
echo "install.sh: source-tree version check failed." >&2
|
|
382
|
+
echo " binary : $BINARY_SRC" >&2
|
|
383
|
+
if [[ -z "$bin_commit" ]]; then
|
|
384
|
+
echo " built from: <no version stamp — binary is older than this verb, or built without ldflags>" >&2
|
|
385
|
+
elif [[ "$bin_commit" == "unknown" ]]; then
|
|
386
|
+
echo " built from: <unstamped — likely a plain 'go build' without -ldflags>" >&2
|
|
387
|
+
else
|
|
388
|
+
echo " built from: $bin_commit" >&2
|
|
389
|
+
fi
|
|
390
|
+
echo " HEAD : $head_sha ($repo_root)" >&2
|
|
391
|
+
echo "" >&2
|
|
392
|
+
echo " The binary at $BINARY_SRC was not built from this checkout's" >&2
|
|
393
|
+
echo " current HEAD. Installing it would silently substitute stale code" >&2
|
|
394
|
+
echo " for the source you're sitting on. Either:" >&2
|
|
395
|
+
echo " - rebuild it first: make build" >&2
|
|
396
|
+
echo " - or download release: rerun with --from-release (omit --binary)" >&2
|
|
397
|
+
exit 3
|
|
398
|
+
fi
|
|
399
|
+
echo " version-check: binary commit matches HEAD ($head_sha)"
|
|
400
|
+
fi
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
# --------------------------------------------------------------------
|
|
404
|
+
# Create install root + bin dir
|
|
405
|
+
# --------------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
mkdir -p "$DEFAULT_INSTALL_ROOT"
|
|
408
|
+
chmod 0700 "$DEFAULT_INSTALL_ROOT"
|
|
409
|
+
mkdir -p "$DEFAULT_BIN_DIR"
|
|
410
|
+
chmod 0755 "$DEFAULT_BIN_DIR"
|
|
411
|
+
|
|
412
|
+
# --------------------------------------------------------------------
|
|
413
|
+
# Atomic install: write to a sibling temp path, then mv over the target.
|
|
414
|
+
#
|
|
415
|
+
# `mv` within the same filesystem is atomic at the inode level —
|
|
416
|
+
# concurrent readers see either the old binary or the new, never half.
|
|
417
|
+
# A running process holds the old inode reference, so an in-flight
|
|
418
|
+
# exec is unaffected by the swap.
|
|
419
|
+
#
|
|
420
|
+
# This is the standard pattern for single-binary CLI installers
|
|
421
|
+
# (gh, kubectl, terraform). The version-manager pattern (canonical
|
|
422
|
+
# symlink → versioned files) is only worth the complexity when you
|
|
423
|
+
# actually manage multiple concurrent versions; we don't.
|
|
424
|
+
# --------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
CANONICAL="${DEFAULT_BIN_DIR}/agent-director"
|
|
427
|
+
PRIOR="${CANONICAL}.prior"
|
|
428
|
+
TMP="${CANONICAL}.tmp.$$"
|
|
429
|
+
|
|
430
|
+
if [[ "$KEEP_PRIOR" -eq 1 && -f "$CANONICAL" ]]; then
|
|
431
|
+
cp -f "$CANONICAL" "$PRIOR"
|
|
432
|
+
chmod 0755 "$PRIOR"
|
|
433
|
+
echo " prior : snapshotted to $PRIOR"
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
cp "$BINARY_SRC" "$TMP"
|
|
437
|
+
chmod 0755 "$TMP"
|
|
438
|
+
mv "$TMP" "$CANONICAL"
|
|
439
|
+
|
|
440
|
+
echo " binary : $CANONICAL"
|
|
441
|
+
|
|
442
|
+
# --------------------------------------------------------------------
|
|
443
|
+
# Optional PATH symlink
|
|
444
|
+
# --------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
if [[ "$NO_SYMLINK" -eq 0 && -n "$SYMLINK_DIR" ]]; then
|
|
447
|
+
if [[ ! -d "$SYMLINK_DIR" ]]; then
|
|
448
|
+
echo " symlink : skipped — $SYMLINK_DIR does not exist"
|
|
449
|
+
elif [[ "$SYMLINK_DIR" =~ [[:space:]] ]]; then
|
|
450
|
+
echo " symlink : skipped — $SYMLINK_DIR contains whitespace"
|
|
451
|
+
else
|
|
452
|
+
target="${SYMLINK_DIR}/agent-director"
|
|
453
|
+
ln -sfn "$CANONICAL" "${target}.new"
|
|
454
|
+
mv -f "${target}.new" "$target"
|
|
455
|
+
echo " symlink : $target → $CANONICAL"
|
|
456
|
+
fi
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
# --------------------------------------------------------------------
|
|
460
|
+
# Warm up state.db via `agent-director help`
|
|
461
|
+
# --------------------------------------------------------------------
|
|
462
|
+
|
|
463
|
+
if "$CANONICAL" help >/dev/null 2>&1; then
|
|
464
|
+
state_db="${DEFAULT_INSTALL_ROOT}/state.db"
|
|
465
|
+
if [[ -f "$state_db" ]]; then
|
|
466
|
+
chmod 0600 "$state_db" 2>/dev/null || true
|
|
467
|
+
echo " state.db: $(stat -c '%a' "$state_db" 2>/dev/null || stat -f '%Lp' "$state_db") at $state_db"
|
|
468
|
+
fi
|
|
469
|
+
else
|
|
470
|
+
echo "install.sh: store warmup (agent-director help) failed" >&2
|
|
471
|
+
exit 5
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# --------------------------------------------------------------------
|
|
475
|
+
# Hook injection — additive merge into ~/.claude/settings.json
|
|
476
|
+
#
|
|
477
|
+
# Skipped entirely under --no-hooks: settings.json is not read, not
|
|
478
|
+
# backed up, not written. There's no edit, so there's nothing to back
|
|
479
|
+
# up — leaving settings.json byte-identical to its pre-install state.
|
|
480
|
+
# --------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
if [[ "$NO_HOOKS" -eq 1 ]]; then
|
|
483
|
+
echo " hooks : skipped (--no-hooks)"
|
|
484
|
+
else
|
|
485
|
+
mkdir -p "$(dirname "$DEFAULT_SETTINGS_PATH")"
|
|
486
|
+
|
|
487
|
+
# Read existing settings or start from {}.
|
|
488
|
+
if [[ -f "$DEFAULT_SETTINGS_PATH" ]]; then
|
|
489
|
+
existing=$(<"$DEFAULT_SETTINGS_PATH")
|
|
490
|
+
if ! printf '%s' "$existing" | jq empty >/dev/null 2>&1; then
|
|
491
|
+
echo "install.sh: ~/.claude/settings.json is not valid JSON" >&2
|
|
492
|
+
exit 4
|
|
493
|
+
fi
|
|
494
|
+
else
|
|
495
|
+
existing='{}'
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# Our hook entries are uniquely identified by the command string
|
|
499
|
+
# (the canonical binary path + " help"). Idempotency check: only add
|
|
500
|
+
# if the command isn't already present in that event's hook list.
|
|
501
|
+
help_cmd="${CANONICAL} help"
|
|
502
|
+
|
|
503
|
+
# Merge logic (jq):
|
|
504
|
+
# - Ensure hooks.SessionStart is an array; append our entry if not
|
|
505
|
+
# already there (matched by command).
|
|
506
|
+
# - Ensure hooks.SessionEnd is an array; append our compact-matcher
|
|
507
|
+
# entry if not already there.
|
|
508
|
+
new_settings=$(printf '%s' "$existing" | jq \
|
|
509
|
+
--arg cmd "$help_cmd" '
|
|
510
|
+
.hooks //= {}
|
|
511
|
+
| .hooks.SessionStart //= []
|
|
512
|
+
| .hooks.SessionEnd //= []
|
|
513
|
+
| (
|
|
514
|
+
if any(.hooks.SessionStart[]?; .hooks[]?.command == $cmd)
|
|
515
|
+
then .
|
|
516
|
+
else .hooks.SessionStart += [{"hooks":[{"type":"command","command":$cmd}]}]
|
|
517
|
+
end
|
|
518
|
+
)
|
|
519
|
+
| (
|
|
520
|
+
if any(.hooks.SessionEnd[]?; .matcher == "compact" and (.hooks[]?.command == $cmd))
|
|
521
|
+
then .
|
|
522
|
+
else .hooks.SessionEnd += [{"matcher":"compact","hooks":[{"type":"command","command":$cmd}]}]
|
|
523
|
+
end
|
|
524
|
+
)
|
|
525
|
+
')
|
|
526
|
+
|
|
527
|
+
# Backup-before-edit: snapshot the prior settings.json (if any) into a
|
|
528
|
+
# timestamped .bak alongside the original so a regressed jq filter is
|
|
529
|
+
# recoverable. Only the *prior* contents are backed up; in-place
|
|
530
|
+
# re-runs of the install will keep the most recent pre-edit copy.
|
|
531
|
+
if [[ -f "$DEFAULT_SETTINGS_PATH" ]]; then
|
|
532
|
+
backup_settings="${DEFAULT_SETTINGS_PATH}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
533
|
+
cp -f "$DEFAULT_SETTINGS_PATH" "$backup_settings"
|
|
534
|
+
echo " backup : $backup_settings"
|
|
535
|
+
fi
|
|
536
|
+
|
|
537
|
+
# Atomic write: tempfile + mv.
|
|
538
|
+
tmp_settings="${DEFAULT_SETTINGS_PATH}.new"
|
|
539
|
+
printf '%s\n' "$new_settings" > "$tmp_settings"
|
|
540
|
+
mv -f "$tmp_settings" "$DEFAULT_SETTINGS_PATH"
|
|
541
|
+
|
|
542
|
+
echo " hooks : injected into $DEFAULT_SETTINGS_PATH"
|
|
543
|
+
fi
|
|
544
|
+
|
|
545
|
+
# --------------------------------------------------------------------
|
|
546
|
+
# inject_help_hook config flag — opt-in dynamic per-Spawn help hook.
|
|
547
|
+
#
|
|
548
|
+
# Driven by the same Q4 (inject persistent help hooks?) signal: when
|
|
549
|
+
# the operator picked "yes" (i.e. did NOT pass --no-hooks),
|
|
550
|
+
# agent-director should also tag its own Spawns with a help hook
|
|
551
|
+
# regardless of the Spawn's CLAUDE_CONFIG_DIR. install.sh sets the
|
|
552
|
+
# flag here; the binary reads it at spawn-synth time.
|
|
553
|
+
#
|
|
554
|
+
# Q4=no (--no-hooks) leaves config.toml untouched — the flag stays at
|
|
555
|
+
# its zero-value default of false.
|
|
556
|
+
# --------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
if [[ "$NO_HOOKS" -eq 0 ]]; then
|
|
559
|
+
CONFIG_TOML="${DEFAULT_INSTALL_ROOT}/config.toml"
|
|
560
|
+
if [[ -f "$CONFIG_TOML" ]]; then
|
|
561
|
+
backup_cfg="${CONFIG_TOML}.bak.$(date +%Y%m%d-%H%M%S)"
|
|
562
|
+
cp -f "$CONFIG_TOML" "$backup_cfg"
|
|
563
|
+
# awk merge: rewrite an existing inject_help_hook line under
|
|
564
|
+
# [defaults] to =true; if [defaults] exists but lacks the key,
|
|
565
|
+
# append it inside the section; if no [defaults] section exists
|
|
566
|
+
# at all, add one at end of file. Preserves every other key
|
|
567
|
+
# and section verbatim.
|
|
568
|
+
merged=$(awk '
|
|
569
|
+
BEGIN { written = 0; in_defaults = 0 }
|
|
570
|
+
/^\[/ {
|
|
571
|
+
if (in_defaults && !written) {
|
|
572
|
+
print "inject_help_hook = true"
|
|
573
|
+
written = 1
|
|
574
|
+
}
|
|
575
|
+
in_defaults = ($0 ~ /^\[defaults\][[:space:]]*$/) ? 1 : 0
|
|
576
|
+
print
|
|
577
|
+
next
|
|
578
|
+
}
|
|
579
|
+
in_defaults && /^[[:space:]]*inject_help_hook[[:space:]]*=/ {
|
|
580
|
+
print "inject_help_hook = true"
|
|
581
|
+
written = 1
|
|
582
|
+
next
|
|
583
|
+
}
|
|
584
|
+
{ print }
|
|
585
|
+
END {
|
|
586
|
+
if (in_defaults && !written) {
|
|
587
|
+
print "inject_help_hook = true"
|
|
588
|
+
written = 1
|
|
589
|
+
}
|
|
590
|
+
if (!written) {
|
|
591
|
+
print ""
|
|
592
|
+
print "[defaults]"
|
|
593
|
+
print "inject_help_hook = true"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
' "$CONFIG_TOML")
|
|
597
|
+
tmp_cfg="${CONFIG_TOML}.new"
|
|
598
|
+
printf '%s\n' "$merged" > "$tmp_cfg"
|
|
599
|
+
mv -f "$tmp_cfg" "$CONFIG_TOML"
|
|
600
|
+
echo " config : merged inject_help_hook=true into $CONFIG_TOML (backup $backup_cfg)"
|
|
601
|
+
else
|
|
602
|
+
printf '[defaults]\ninject_help_hook = true\n' > "$CONFIG_TOML"
|
|
603
|
+
chmod 0600 "$CONFIG_TOML"
|
|
604
|
+
echo " config : created $CONFIG_TOML with inject_help_hook=true"
|
|
605
|
+
fi
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
# --------------------------------------------------------------------
|
|
609
|
+
# Optional MCP registration
|
|
610
|
+
# --------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
if [[ "$REGISTER_MCP" -eq 1 ]]; then
|
|
613
|
+
if claude mcp add agent-director "$CANONICAL" serve --stdio 2>/dev/null; then
|
|
614
|
+
echo " mcp : registered with claude mcp"
|
|
615
|
+
else
|
|
616
|
+
echo " mcp : registration failed (continuing anyway)" >&2
|
|
617
|
+
fi
|
|
618
|
+
fi
|
|
619
|
+
|
|
620
|
+
echo "install.sh: done. Try: $CANONICAL help"
|