@monster-orange/den 0.4.21 → 0.4.23
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/CHANGELOG.md +115 -0
- package/docs/distributions.md +186 -42
- package/package.json +1 -1
- package/service/install.sh +213 -63
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/guardrails/rotation-hook.ts +5 -0
- package/src/shared/agents/records.ts +6 -0
- package/src/shared/agents/rotate-settings.ts +309 -0
- package/src/shared/agents/rotation.ts +110 -5
- package/src/shared/agents/runtime.ts +6 -0
- package/src/shared/agents/session-name.ts +21 -1
- package/src/shared/den-config.ts +27 -12
- package/src/shared/launchd-session.ts +92 -0
- package/src/shared/sessions/client.ts +24 -0
- package/src/shared/sessions/host.ts +114 -5
- package/src/shared/sessions/types.ts +5 -0
- package/src/ui/cli/agent-ops.ts +469 -17
- package/src/ui/cli/agents-command.ts +200 -3
- package/src/ui/cli/doctor-command.ts +72 -0
- package/src/ui/cli/doctor-where.ts +203 -0
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/server-agent-ops.ts +2 -2
- package/src/ui/cli/service-command.ts +217 -11
- package/src/ui/cli/sessions-command.ts +46 -12
- package/src/ui/cli/web-command.ts +36 -5
- package/src/ui/cli/web-service.ts +160 -0
- package/src/ui/server/agent-routes.ts +149 -5
- package/src/ui/server/index.ts +17 -5
- package/src/ui/server/public/agents-page.js +316 -27
- package/src/ui/server/public/agents.html +62 -0
- package/src/ui/server/public/i18n/es.js +58 -2
- package/src/ui/server/public/i18n/ja.js +54 -2
- package/src/ui/server/public/settings-registry.js +204 -0
- package/src/ui/server/public/settings.html +43 -4
- package/src/ui/server/public/shell.js +56 -2
- package/src/ui/server/route-classes.ts +21 -0
- package/src/ui/server/session-routes.ts +27 -5
package/service/install.sh
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Run the web UI as
|
|
3
|
-
# started at login, restarted if
|
|
2
|
+
# Run the web UI and the session host as long-lived macOS launchd jobs:
|
|
3
|
+
# started at boot or login, restarted if they exit, not tied to a terminal.
|
|
4
4
|
#
|
|
5
|
-
# service/install.sh install (or refresh) and start
|
|
6
|
-
# service/install.sh uninstall stop
|
|
7
|
-
# service/install.sh status launchd's view of
|
|
8
|
-
# service/install.sh print write the
|
|
5
|
+
# service/install.sh install (or refresh) and start them
|
|
6
|
+
# service/install.sh uninstall stop them and remove their plists
|
|
7
|
+
# service/install.sh status launchd's view of them
|
|
8
|
+
# service/install.sh print write the web plist to stdout only
|
|
9
|
+
# service/install.sh print-host write the session host plist to stdout only
|
|
10
|
+
#
|
|
11
|
+
# Two modes (D18), DEN_SERVICE_MODE:
|
|
12
|
+
# login (default) LaunchAgents in the desktop session (gui/<uid>): they
|
|
13
|
+
# start at login and stop at logout, with every agent.
|
|
14
|
+
# always LaunchDaemons that run as you (UserName): they start at boot and
|
|
15
|
+
# keep running across logout, so agents do too. One sudo writes
|
|
16
|
+
# and loads them. No login keychain, no GUI there: agents use the
|
|
17
|
+
# 1Password service-account token, headless browsers, and Full
|
|
18
|
+
# Disk Access for protected folders.
|
|
19
|
+
#
|
|
20
|
+
# The session host job runs DEN_SERVICE_HOST_EXE, a copy of node Den owns
|
|
21
|
+
# (<stateDir>/bin/<cli>-sessions-host): Full Disk Access is granted to that
|
|
22
|
+
# path, so it never goes to the shared node. A node change replaces the
|
|
23
|
+
# copy; the grant holds when the code signature's requirement is the same
|
|
24
|
+
# (an official node build), and install says to grant it again when not.
|
|
9
25
|
#
|
|
10
26
|
# Works from Den's checkout and from a distribution, where it lives in
|
|
11
27
|
# node_modules/@monster-orange/den/service/. The distribution root is
|
|
12
|
-
# DEN_ROOT, else this checkout when it holds den.yml.
|
|
13
|
-
# engine's server with the node found on PATH now, from the distribution
|
|
14
|
-
# root; re-run install after changing node versions.
|
|
28
|
+
# DEN_ROOT, else this checkout when it holds den.yml.
|
|
15
29
|
#
|
|
16
|
-
# DEN_SERVICE_LABEL
|
|
17
|
-
#
|
|
18
|
-
#
|
|
30
|
+
# DEN_SERVICE_LABEL web job label (default local.<dist dir name>.web)
|
|
31
|
+
# DEN_SERVICE_HOST_LABEL host job label (default: the web label's .web → .sessions)
|
|
32
|
+
# DEN_SERVICE_LOGS log directory (default ~/Library/Logs/<dist dir name>)
|
|
33
|
+
# DEN_SERVICE_NODE the node the jobs run (default: see pick_node)
|
|
34
|
+
# DEN_SERVICE_HOST_EXE the host's node copy (default: none, the host runs that node)
|
|
19
35
|
# DEN_SERVICE_READY_CHECK=external install does not print "running": the
|
|
20
36
|
# caller (`<cli> service install`) checks the server
|
|
21
37
|
# answers first.
|
|
38
|
+
# DEN_SERVICE_DAEMON_DIR, DEN_SERVICE_SUDO (tests) where LaunchDaemons go
|
|
39
|
+
# and what runs as root (default /Library/LaunchDaemons, sudo)
|
|
22
40
|
set -euo pipefail
|
|
23
41
|
|
|
24
42
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
@@ -34,9 +52,31 @@ fi
|
|
|
34
52
|
[ -f "$DIST/den.yml" ] || { echo "✗ $DIST has no den.yml" >&2; exit 1; }
|
|
35
53
|
NAME="$(basename "$DIST")"
|
|
36
54
|
LABEL="${DEN_SERVICE_LABEL:-local.$NAME.web}"
|
|
55
|
+
HOST_LABEL="${DEN_SERVICE_HOST_LABEL:-${LABEL%.web}.sessions}"
|
|
37
56
|
LOGS="${DEN_SERVICE_LOGS:-$HOME/Library/Logs/$NAME}"
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
SVC_MODE="${DEN_SERVICE_MODE:-login}"
|
|
58
|
+
case "$SVC_MODE" in
|
|
59
|
+
login|always) ;;
|
|
60
|
+
*) echo "✗ DEN_SERVICE_MODE must be login or always, not \"$SVC_MODE\"" >&2; exit 2 ;;
|
|
61
|
+
esac
|
|
62
|
+
UID_N="$(id -u)"
|
|
63
|
+
USER_N="$(id -un)"
|
|
64
|
+
AGENT_DIR="$HOME/Library/LaunchAgents"
|
|
65
|
+
DAEMON_DIR="${DEN_SERVICE_DAEMON_DIR:-/Library/LaunchDaemons}"
|
|
66
|
+
SUDO="${DEN_SERVICE_SUDO-sudo}"
|
|
67
|
+
GUI="gui/$UID_N"
|
|
68
|
+
|
|
69
|
+
# The plist and domain for a label in a mode.
|
|
70
|
+
plist_for() { # $1 label $2 mode
|
|
71
|
+
if [ "$2" = always ]; then echo "$DAEMON_DIR/$1.plist"; else echo "$AGENT_DIR/$1.plist"; fi
|
|
72
|
+
}
|
|
73
|
+
domain_for() { if [ "$1" = always ]; then echo system; else echo "$GUI"; fi; }
|
|
74
|
+
other_mode() { if [ "$1" = always ]; then echo login; else echo always; fi; }
|
|
75
|
+
# Root runs it in the system domain (a LaunchDaemon); else as you.
|
|
76
|
+
as_mode() { # $1 mode, then the command
|
|
77
|
+
local m="$1"; shift
|
|
78
|
+
if [ "$m" = always ] && [ -n "$SUDO" ]; then "$SUDO" "$@"; else "$@"; fi
|
|
79
|
+
}
|
|
40
80
|
|
|
41
81
|
# tsx: the nearest node_modules/tsx at or above the engine (hoisted installs).
|
|
42
82
|
find_tsx() {
|
|
@@ -76,20 +116,75 @@ pick_node() {
|
|
|
76
116
|
echo "$node"
|
|
77
117
|
}
|
|
78
118
|
|
|
79
|
-
|
|
119
|
+
# The code signature's designated requirement, or "" (unsigned, no codesign).
|
|
120
|
+
requirement() { codesign -d -r- "$1" 2>&1 | sed -n 's/^designated => //p' || true; }
|
|
121
|
+
|
|
122
|
+
# The host's node copy (D18): refreshed when node changed. Prints the path
|
|
123
|
+
# the host job runs: the copy, or node itself when it cannot be copied.
|
|
124
|
+
# A node linked to libraries beside it (Homebrew's) does not run copied.
|
|
125
|
+
self_contained() {
|
|
126
|
+
! { command -v otool >/dev/null 2>&1 && otool -L "$1" 2>/dev/null | grep -q '@rpath/\|/opt/homebrew/\|/usr/local/opt/'; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
host_exe() { # $1 node
|
|
130
|
+
local node="$1" exe="${DEN_SERVICE_HOST_EXE:-}" c src=""
|
|
131
|
+
if [ -z "$exe" ]; then echo "$node"; return; fi
|
|
132
|
+
# The node the jobs run, else (unless DEN_SERVICE_NODE names one) the
|
|
133
|
+
# PATH's: a version manager's is fine here, since the copy outlives it.
|
|
134
|
+
for c in "$node" $([ -z "${DEN_SERVICE_NODE:-}" ] && command -v node 2>/dev/null); do
|
|
135
|
+
if self_contained "$c"; then src="$c"; break; fi
|
|
136
|
+
done
|
|
137
|
+
if [ -z "$src" ]; then
|
|
138
|
+
echo "⚠ $node is not self-contained (it loads libraries beside it), so the session host" >&2
|
|
139
|
+
echo " runs it directly: grant Full Disk Access to $node, and again after each node" >&2
|
|
140
|
+
echo " upgrade. An official node build (nodejs.org, nvm) avoids that: set DEN_SERVICE_NODE." >&2
|
|
141
|
+
echo "$node"; return
|
|
142
|
+
fi
|
|
143
|
+
node="$src"
|
|
144
|
+
if [ -f "$exe" ] && cmp -s "$node" "$exe"; then echo "$exe"; return; fi
|
|
145
|
+
local before="" after
|
|
146
|
+
[ -f "$exe" ] && before="$(requirement "$exe")"
|
|
147
|
+
mkdir -p "$(dirname "$exe")"
|
|
148
|
+
chmod 700 "$(dirname "$exe")"
|
|
149
|
+
cp "$node" "$exe.new"
|
|
150
|
+
chmod 700 "$exe.new"
|
|
151
|
+
mv -f "$exe.new" "$exe"
|
|
152
|
+
after="$(requirement "$exe")"
|
|
153
|
+
case "$after" in
|
|
154
|
+
"" | cdhash*)
|
|
155
|
+
echo "⚠ $exe is a copy of $node, which has no stable code signature: Full Disk" >&2
|
|
156
|
+
echo " Access must be granted to it again after every node change. An official" >&2
|
|
157
|
+
echo " node build (nodejs.org, nvm) keeps the grant: set DEN_SERVICE_NODE." >&2 ;;
|
|
158
|
+
*)
|
|
159
|
+
if [ -n "$before" ] && [ "$before" != "$after" ]; then
|
|
160
|
+
echo "⚠ $exe changed signer: grant it Full Disk Access again" >&2
|
|
161
|
+
echo " (System Settings → Privacy & Security → Full Disk Access)." >&2
|
|
162
|
+
fi ;;
|
|
163
|
+
esac
|
|
164
|
+
echo "$exe"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
write_plist() { # $1 = output file, $2 = web|host, $3 = mode
|
|
80
168
|
NODE="$(pick_node "$MODE")"
|
|
81
169
|
TSX="$(find_tsx)" || { echo "✗ tsx not installed: run npm ci in $DIST" >&2; exit 1; }
|
|
170
|
+
local exe="$NODE" label="$LABEL" log="web.log"
|
|
171
|
+
if [ "$2" = host ]; then
|
|
172
|
+
if [ "$MODE" = install ]; then exe="$(host_exe "$NODE")"; else exe="${DEN_SERVICE_HOST_EXE:-$NODE}"; fi
|
|
173
|
+
label="$HOST_LABEL"; log="sessions-host.log"
|
|
174
|
+
fi
|
|
82
175
|
DOCKER_DIR="$(dirname "$(command -v docker || echo /usr/local/bin/docker)")"
|
|
83
176
|
SVC_PATH="$(dirname "$NODE"):$DOCKER_DIR:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
|
84
|
-
python3 -I - "$1" "$
|
|
177
|
+
python3 -I - "$1" "$2" "$3" "$label" "$exe" "$TSX" "$ENGINE" "$DIST" "$SVC_PATH" "$HOME" "$LOGS/$log" "$USER_N" <<'PY'
|
|
85
178
|
import plistlib, sys
|
|
86
|
-
out, label,
|
|
179
|
+
out, kind, mode, label, exe, tsx, engine, dist, path, home, log, user = sys.argv[1:]
|
|
180
|
+
entry = (f"{engine}/src/ui/server/index.ts", "--silent") if kind == "web" \
|
|
181
|
+
else (f"{engine}/src/ui/cli/index.ts", "sessions", "host", "--stand-by")
|
|
87
182
|
data = {
|
|
88
183
|
"Label": label,
|
|
89
|
-
"ProgramArguments": [
|
|
90
|
-
f"{engine}/src/ui/server/index.ts", "--silent"],
|
|
184
|
+
"ProgramArguments": [exe, "--import", f"{tsx}/dist/loader.mjs", *entry],
|
|
91
185
|
"WorkingDirectory": dist,
|
|
92
|
-
"EnvironmentVariables": {"PATH": path, "HOME": home,
|
|
186
|
+
"EnvironmentVariables": {"PATH": path, "HOME": home, "USER": user,
|
|
187
|
+
"LOGNAME": user,
|
|
93
188
|
"DEN_ROOT": dist, "DEN_ENGINE_ROOT": engine},
|
|
94
189
|
"RunAtLoad": True,
|
|
95
190
|
"KeepAlive": True,
|
|
@@ -97,9 +192,12 @@ data = {
|
|
|
97
192
|
# Interactive, not Background: macOS gives Background agents efficiency
|
|
98
193
|
# cores and throttled I/O, and the UI stalled for seconds under load.
|
|
99
194
|
"ProcessType": "Interactive",
|
|
100
|
-
"StandardOutPath":
|
|
101
|
-
"StandardErrorPath":
|
|
195
|
+
"StandardOutPath": log,
|
|
196
|
+
"StandardErrorPath": log,
|
|
102
197
|
}
|
|
198
|
+
if mode == "always":
|
|
199
|
+
# A LaunchDaemon runs as root unless told otherwise: run as the person.
|
|
200
|
+
data["UserName"] = user
|
|
103
201
|
if out == "-":
|
|
104
202
|
sys.stdout.buffer.write(plistlib.dumps(data))
|
|
105
203
|
else:
|
|
@@ -108,58 +206,110 @@ else:
|
|
|
108
206
|
PY
|
|
109
207
|
}
|
|
110
208
|
|
|
209
|
+
# Stop and remove a job (any mode), quietly when it is not there.
|
|
210
|
+
remove_job() { # $1 label $2 mode
|
|
211
|
+
local plist; plist="$(plist_for "$1" "$2")"
|
|
212
|
+
if [ "$2" = always ]; then
|
|
213
|
+
[ -f "$plist" ] || return 0
|
|
214
|
+
fi
|
|
215
|
+
as_mode "$2" launchctl bootout "$(domain_for "$2")/$1" 2>/dev/null || true
|
|
216
|
+
[ -f "$plist" ] && as_mode "$2" rm -f "$plist"
|
|
217
|
+
return 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Install and start one job in SVC_MODE.
|
|
221
|
+
load_job() { # $1 label $2 web|host
|
|
222
|
+
local label="$1" plist domain tmp
|
|
223
|
+
plist="$(plist_for "$label" "$SVC_MODE")"
|
|
224
|
+
domain="$(domain_for "$SVC_MODE")"
|
|
225
|
+
tmp="$(mktemp "${TMPDIR:-/tmp}/den-plist.XXXXXX")"
|
|
226
|
+
write_plist "$tmp" "$2" "$SVC_MODE"
|
|
227
|
+
plutil -lint "$tmp" >/dev/null
|
|
228
|
+
# The other mode's job is the same service: never both.
|
|
229
|
+
remove_job "$label" "$(other_mode "$SVC_MODE")"
|
|
230
|
+
as_mode "$SVC_MODE" launchctl bootout "$domain/$label" 2>/dev/null || true
|
|
231
|
+
# bootout returns before launchd has let go of the label; a bootstrap
|
|
232
|
+
# straight after fails ("Bootstrap failed: 5") and leaves NO service.
|
|
233
|
+
# Wait for it to go, then retry bootstrap a few times.
|
|
234
|
+
for _ in $(seq 1 50); do
|
|
235
|
+
as_mode "$SVC_MODE" launchctl print "$domain/$label" >/dev/null 2>&1 || break
|
|
236
|
+
sleep 0.2
|
|
237
|
+
done
|
|
238
|
+
if [ "$SVC_MODE" = always ]; then
|
|
239
|
+
as_mode always install -o root -g wheel -m 644 "$tmp" "$plist"
|
|
240
|
+
rm -f "$tmp"
|
|
241
|
+
else
|
|
242
|
+
mkdir -p "$(dirname "$plist")"
|
|
243
|
+
mv "$tmp" "$plist"
|
|
244
|
+
fi
|
|
245
|
+
local loaded=0
|
|
246
|
+
for _ in 1 2 3 4 5; do
|
|
247
|
+
if as_mode "$SVC_MODE" launchctl bootstrap "$domain" "$plist" 2>/dev/null; then loaded=1; break; fi
|
|
248
|
+
sleep 1
|
|
249
|
+
done
|
|
250
|
+
[ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $label (plist: $plist)" >&2; exit 1; }
|
|
251
|
+
as_mode "$SVC_MODE" launchctl kickstart -k "$domain/$label"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# The job's state, as status shows it.
|
|
255
|
+
job_status() { # $1 label
|
|
256
|
+
local m plist domain svc_node out
|
|
257
|
+
for m in always login; do
|
|
258
|
+
plist="$(plist_for "$1" "$m")"
|
|
259
|
+
[ -f "$plist" ] || continue
|
|
260
|
+
domain="$(domain_for "$m")"
|
|
261
|
+
# The node it was installed with may be gone (a removed nvm version):
|
|
262
|
+
# launchd then restarts it forever with no useful message.
|
|
263
|
+
if command -v plutil >/dev/null 2>&1; then
|
|
264
|
+
svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$plist" 2>/dev/null || true)"
|
|
265
|
+
if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
|
|
266
|
+
echo "✗ $1 runs $svc_node, which no longer exists: re-run service install" >&2
|
|
267
|
+
return 1
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
echo "$1 ($m: $domain)"
|
|
271
|
+
if out="$(launchctl print "$domain/$1" 2>/dev/null)"; then
|
|
272
|
+
printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
|
|
273
|
+
else
|
|
274
|
+
echo " plist present, job not loaded: $([ "$m" = always ] && echo "sudo ")launchctl bootstrap $domain $plist"
|
|
275
|
+
fi
|
|
276
|
+
return 0
|
|
277
|
+
done
|
|
278
|
+
# launchctl's own "Could not find service" is noise when it isn't loaded.
|
|
279
|
+
echo "$1 is not loaded"
|
|
280
|
+
}
|
|
281
|
+
|
|
111
282
|
MODE="${1:-install}"
|
|
112
283
|
case "$MODE" in
|
|
113
284
|
install)
|
|
114
|
-
mkdir -p "$LOGS"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# Wait for it to go, then retry bootstrap a few times.
|
|
121
|
-
for _ in $(seq 1 50); do
|
|
122
|
-
launchctl print "$DOMAIN/$LABEL" >/dev/null 2>&1 || break
|
|
123
|
-
sleep 0.2
|
|
124
|
-
done
|
|
125
|
-
mv "$PLIST.new" "$PLIST"
|
|
126
|
-
loaded=0
|
|
127
|
-
for _ in 1 2 3 4 5; do
|
|
128
|
-
if launchctl bootstrap "$DOMAIN" "$PLIST" 2>/dev/null; then loaded=1; break; fi
|
|
129
|
-
sleep 1
|
|
130
|
-
done
|
|
131
|
-
[ "$loaded" = 1 ] || { echo "✗ launchctl bootstrap failed for $LABEL (plist: $PLIST)" >&2; exit 1; }
|
|
132
|
-
launchctl kickstart -k "$DOMAIN/$LABEL"
|
|
285
|
+
mkdir -p "$LOGS"
|
|
286
|
+
if [ "$SVC_MODE" = always ] && [ -n "$SUDO" ]; then
|
|
287
|
+
echo "… $SVC_MODE: LaunchDaemons running as $USER_N need your admin password once (sudo)"
|
|
288
|
+
fi
|
|
289
|
+
load_job "$HOST_LABEL" host
|
|
290
|
+
load_job "$LABEL" web
|
|
133
291
|
if [ "${DEN_SERVICE_READY_CHECK:-}" = external ]; then
|
|
134
|
-
echo "… $LABEL started; waiting for it to answer"
|
|
292
|
+
echo "… $LABEL started ($SVC_MODE); waiting for it to answer"
|
|
135
293
|
else
|
|
136
|
-
echo "✓ $LABEL running (logs: $LOGS/web.log)"
|
|
294
|
+
echo "✓ $LABEL running ($SVC_MODE; logs: $LOGS/web.log)"
|
|
137
295
|
fi
|
|
138
296
|
;;
|
|
139
297
|
uninstall)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
298
|
+
for m in login always; do
|
|
299
|
+
remove_job "$LABEL" "$m"
|
|
300
|
+
remove_job "$HOST_LABEL" "$m"
|
|
301
|
+
done
|
|
302
|
+
echo "✓ $LABEL and $HOST_LABEL removed"
|
|
143
303
|
;;
|
|
144
304
|
print)
|
|
145
|
-
write_plist -
|
|
305
|
+
write_plist - web "$SVC_MODE"
|
|
306
|
+
;;
|
|
307
|
+
print-host)
|
|
308
|
+
write_plist - host "$SVC_MODE"
|
|
146
309
|
;;
|
|
147
310
|
status)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if [ -f "$PLIST" ] && command -v plutil >/dev/null 2>&1; then
|
|
151
|
-
svc_node="$(plutil -extract ProgramArguments.0 raw -o - "$PLIST" 2>/dev/null || true)"
|
|
152
|
-
if [ -n "$svc_node" ] && [ ! -x "$svc_node" ]; then
|
|
153
|
-
echo "✗ $LABEL runs $svc_node, which no longer exists: re-run service install" >&2
|
|
154
|
-
exit 1
|
|
155
|
-
fi
|
|
156
|
-
fi
|
|
157
|
-
# launchctl's own "Could not find service" is noise when it isn't loaded.
|
|
158
|
-
if out="$(launchctl print "$DOMAIN/$LABEL" 2>/dev/null)"; then
|
|
159
|
-
printf '%s\n' "$out" | grep -E '^\s*(state|pid|last exit code|runs|spawn type)'
|
|
160
|
-
else
|
|
161
|
-
echo "$LABEL is not loaded"
|
|
162
|
-
fi
|
|
311
|
+
job_status "$LABEL"
|
|
312
|
+
job_status "$HOST_LABEL"
|
|
163
313
|
;;
|
|
164
|
-
*) echo "usage: $0 [install|uninstall|status|print]" >&2; exit 2 ;;
|
|
314
|
+
*) echo "usage: $0 [install|uninstall|status|print|print-host]" >&2; exit 2 ;;
|
|
165
315
|
esac
|
|
@@ -455,6 +455,11 @@ export function firstPromptArg(text: string | undefined): string[] {
|
|
|
455
455
|
return [t.startsWith("-") ? `Prompt: ${t}` : t];
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
/** D24: `--name <label>`, so peers tell Den's agents apart; or nothing. */
|
|
459
|
+
function nameArg(label: string | undefined): string[] {
|
|
460
|
+
return label ? ["--name", label] : [];
|
|
461
|
+
}
|
|
462
|
+
|
|
458
463
|
const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
459
464
|
|
|
460
465
|
/**
|
|
@@ -503,6 +508,7 @@ export const denClaude: Runtime = {
|
|
|
503
508
|
bin,
|
|
504
509
|
"--session-id",
|
|
505
510
|
id,
|
|
511
|
+
...nameArg(input.sessionLabel),
|
|
506
512
|
...c.argv,
|
|
507
513
|
...firstPromptArg(input.firstPrompt),
|
|
508
514
|
],
|
|
@@ -517,7 +523,13 @@ export const denClaude: Runtime = {
|
|
|
517
523
|
const bin = binFor(input);
|
|
518
524
|
const c = common(input);
|
|
519
525
|
return {
|
|
520
|
-
argv: [
|
|
526
|
+
argv: [
|
|
527
|
+
bin,
|
|
528
|
+
"--resume",
|
|
529
|
+
input.resumeToken,
|
|
530
|
+
...nameArg(input.sessionLabel),
|
|
531
|
+
...c.argv,
|
|
532
|
+
],
|
|
521
533
|
env: {},
|
|
522
534
|
files: c.files,
|
|
523
535
|
warnings: c.warnings,
|
|
@@ -59,6 +59,11 @@ export function rotationHookOutput(
|
|
|
59
59
|
now: Date = new Date(),
|
|
60
60
|
): string {
|
|
61
61
|
if (!input || typeof input !== "object") return "";
|
|
62
|
+
// D28: the limit in force is the one Den keeps in rotation.json (it
|
|
63
|
+
// follows the settings); the launch's --after is the first one.
|
|
64
|
+
const stored = readRotation(args.files);
|
|
65
|
+
if (stored && stored.after !== args.after)
|
|
66
|
+
args = { ...args, after: stored.after };
|
|
62
67
|
const i = input as Record<string, unknown>;
|
|
63
68
|
const event = i.hook_event_name;
|
|
64
69
|
const conversation =
|
|
@@ -86,6 +86,12 @@ export interface AgentRecord {
|
|
|
86
86
|
displayName?: string;
|
|
87
87
|
/** When displayName was given (ISO 8601): the latest rename wins (AG-4b). */
|
|
88
88
|
displayNameAt?: string;
|
|
89
|
+
/**
|
|
90
|
+
* D24: the name Den last gave its runtime session (Claude Code's
|
|
91
|
+
* `--name`, peerSessionName), so reading it back is never taken for a
|
|
92
|
+
* rename.
|
|
93
|
+
*/
|
|
94
|
+
peerName?: string;
|
|
89
95
|
/**
|
|
90
96
|
* The model version it runs (AG-12.3): "latest" (the model's newest), or
|
|
91
97
|
* a pinned version. Absent: before AG-12, `model` as given.
|