@alanbem/dclaude 0.0.12 → 0.0.13
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/dclaude +496 -8
- package/package.json +1 -1
package/dclaude
CHANGED
|
@@ -987,6 +987,423 @@ handle_git_auth() {
|
|
|
987
987
|
fi
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
+
# ============================================================================
|
|
991
|
+
# Agent Teams Tmux Passthrough — Relay Functions
|
|
992
|
+
# ============================================================================
|
|
993
|
+
# When the user runs dclaude from inside a host tmux session, these functions
|
|
994
|
+
# set up a TCP relay that forwards Claude Code's tmux commands to the host.
|
|
995
|
+
# This makes Agent Teams sub-agent panes visible in the host's tmux session.
|
|
996
|
+
|
|
997
|
+
# Check if user is in a host tmux session
|
|
998
|
+
detect_host_tmux() {
|
|
999
|
+
[[ -n "${TMUX:-}" ]]
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
# Check if host has required tools for relay (jq + socat or ncat)
|
|
1003
|
+
check_relay_deps() {
|
|
1004
|
+
if ! command -v jq &>/dev/null; then
|
|
1005
|
+
debug "Relay dep missing: jq"
|
|
1006
|
+
return 1
|
|
1007
|
+
fi
|
|
1008
|
+
if command -v socat &>/dev/null; then
|
|
1009
|
+
debug "Relay listener: socat"
|
|
1010
|
+
return 0
|
|
1011
|
+
fi
|
|
1012
|
+
if command -v ncat &>/dev/null; then
|
|
1013
|
+
debug "Relay listener: ncat"
|
|
1014
|
+
return 0
|
|
1015
|
+
fi
|
|
1016
|
+
debug "Relay dep missing: socat or ncat"
|
|
1017
|
+
return 1
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
# Get the address the container uses to reach the host relay
|
|
1021
|
+
get_relay_host() {
|
|
1022
|
+
local platform="$1"
|
|
1023
|
+
local network_mode="$2"
|
|
1024
|
+
# Linux host mode: container shares host network, use loopback
|
|
1025
|
+
if [[ "$platform" == "linux" && "$network_mode" == "host" ]]; then
|
|
1026
|
+
echo "127.0.0.1"
|
|
1027
|
+
else
|
|
1028
|
+
# macOS (any mode) or Linux bridge: use Docker's host gateway
|
|
1029
|
+
echo "host.docker.internal"
|
|
1030
|
+
fi
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
# Get the address the relay binds to on the host
|
|
1034
|
+
get_relay_bind_addr() {
|
|
1035
|
+
local platform="$1"
|
|
1036
|
+
local network_mode="$2"
|
|
1037
|
+
# Linux bridge: container reaches host via bridge gateway, not loopback
|
|
1038
|
+
if [[ "$platform" == "linux" && "$network_mode" == "bridge" ]]; then
|
|
1039
|
+
echo "0.0.0.0"
|
|
1040
|
+
else
|
|
1041
|
+
echo "127.0.0.1"
|
|
1042
|
+
fi
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
# Get or create relay nonce for a container (reused across reattachments)
|
|
1046
|
+
get_or_create_relay_nonce() {
|
|
1047
|
+
local container_name="$1"
|
|
1048
|
+
local nonce_file="$HOME/.dclaude/tmux-relay-nonce-${container_name}"
|
|
1049
|
+
|
|
1050
|
+
mkdir -p "$HOME/.dclaude"
|
|
1051
|
+
|
|
1052
|
+
if [[ -f "$nonce_file" ]]; then
|
|
1053
|
+
cat "$nonce_file"
|
|
1054
|
+
return 0
|
|
1055
|
+
fi
|
|
1056
|
+
|
|
1057
|
+
local nonce
|
|
1058
|
+
nonce=$(head -c 32 /dev/urandom | base64 | tr -d '=/+' | head -c 32)
|
|
1059
|
+
echo "$nonce" > "$nonce_file"
|
|
1060
|
+
echo "$nonce"
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
# Generate the relay handler script (executed by socat/ncat for each connection)
|
|
1064
|
+
# Uses a non-quoted heredoc for baked-in values, then a single-quoted heredoc for logic
|
|
1065
|
+
generate_relay_handler() {
|
|
1066
|
+
local container_name="$1"
|
|
1067
|
+
local relay_port="$2"
|
|
1068
|
+
local nonce="$3"
|
|
1069
|
+
local handler_path="$4"
|
|
1070
|
+
local tracking_file="$5"
|
|
1071
|
+
local relay_host="$6"
|
|
1072
|
+
|
|
1073
|
+
# Header with baked-in values (variable expansion)
|
|
1074
|
+
cat > "$handler_path" <<HANDLER_HEADER
|
|
1075
|
+
#!/bin/bash
|
|
1076
|
+
CONTAINER="$container_name"
|
|
1077
|
+
NONCE="$nonce"
|
|
1078
|
+
RELAY_PORT="$relay_port"
|
|
1079
|
+
RELAY_HOST="$relay_host"
|
|
1080
|
+
TRACKING_FILE="$tracking_file"
|
|
1081
|
+
HANDLER_HEADER
|
|
1082
|
+
|
|
1083
|
+
# Logic body (single-quoted heredoc — no escaping needed)
|
|
1084
|
+
cat >> "$handler_path" <<'HANDLER_BODY'
|
|
1085
|
+
|
|
1086
|
+
# Read one JSON line (max 65536 bytes to prevent memory exhaustion)
|
|
1087
|
+
read -r -n 65536 request
|
|
1088
|
+
if [[ -z "$request" ]]; then
|
|
1089
|
+
printf '{"code":1,"stdout":"","stderr":"empty request"}\n'
|
|
1090
|
+
exit 1
|
|
1091
|
+
fi
|
|
1092
|
+
|
|
1093
|
+
# Validate nonce
|
|
1094
|
+
req_nonce=$(printf '%s' "$request" | jq -r '.nonce // empty')
|
|
1095
|
+
if [[ "$req_nonce" != "$NONCE" ]]; then
|
|
1096
|
+
printf '{"code":1,"stdout":"","stderr":"authentication failed"}\n'
|
|
1097
|
+
exit 1
|
|
1098
|
+
fi
|
|
1099
|
+
|
|
1100
|
+
# Extract command array (bash 3.2 compatible — no mapfile)
|
|
1101
|
+
cmd=()
|
|
1102
|
+
while IFS= read -r line; do
|
|
1103
|
+
cmd+=("$line")
|
|
1104
|
+
done < <(printf '%s' "$request" | jq -r '.cmd[]')
|
|
1105
|
+
subcmd="${cmd[0]}"
|
|
1106
|
+
cwd=$(printf '%s' "$request" | jq -r '.cwd // empty')
|
|
1107
|
+
req_pane=$(printf '%s' "$request" | jq -r '.pane // empty')
|
|
1108
|
+
|
|
1109
|
+
# Allowlist tmux subcommands
|
|
1110
|
+
case "$subcmd" in
|
|
1111
|
+
split-window|send-keys|list-panes|select-layout|resize-pane|\
|
|
1112
|
+
select-pane|kill-pane|has-session|list-windows|display-message|list-sessions)
|
|
1113
|
+
;;
|
|
1114
|
+
*)
|
|
1115
|
+
printf '{"code":1,"stdout":"","stderr":"disallowed: %s"}\n' "$subcmd"
|
|
1116
|
+
exit 1
|
|
1117
|
+
;;
|
|
1118
|
+
esac
|
|
1119
|
+
|
|
1120
|
+
# Build tmux args based on subcommand
|
|
1121
|
+
args=()
|
|
1122
|
+
|
|
1123
|
+
if [[ "$subcmd" == "split-window" ]]; then
|
|
1124
|
+
args+=("split-window")
|
|
1125
|
+
|
|
1126
|
+
# Parse split-window flags, strip -c (handled by docker exec -w)
|
|
1127
|
+
# and strip trailing shell command (replaced by docker exec)
|
|
1128
|
+
i=1
|
|
1129
|
+
while (( i < ${#cmd[@]} )); do
|
|
1130
|
+
case "${cmd[$i]}" in
|
|
1131
|
+
-b|-d|-f|-h|-I|-v|-P|-Z)
|
|
1132
|
+
args+=("${cmd[$i]}")
|
|
1133
|
+
;;
|
|
1134
|
+
-c)
|
|
1135
|
+
# Capture directory, use for docker exec -w
|
|
1136
|
+
(( i++ ))
|
|
1137
|
+
cwd="${cmd[$i]}"
|
|
1138
|
+
;;
|
|
1139
|
+
-e|-l|-t|-F)
|
|
1140
|
+
# Flags with a value argument
|
|
1141
|
+
args+=("${cmd[$i]}")
|
|
1142
|
+
(( i++ ))
|
|
1143
|
+
args+=("${cmd[$i]}")
|
|
1144
|
+
;;
|
|
1145
|
+
-*)
|
|
1146
|
+
# Unknown flag, pass through
|
|
1147
|
+
args+=("${cmd[$i]}")
|
|
1148
|
+
;;
|
|
1149
|
+
*)
|
|
1150
|
+
# Shell command — skip (replaced by docker exec)
|
|
1151
|
+
break
|
|
1152
|
+
;;
|
|
1153
|
+
esac
|
|
1154
|
+
(( i++ ))
|
|
1155
|
+
done
|
|
1156
|
+
|
|
1157
|
+
# Validate cwd: absolute path, safe characters only
|
|
1158
|
+
if [[ -n "$cwd" && ! "$cwd" =~ ^[a-zA-Z0-9/._-]+$ ]]; then
|
|
1159
|
+
printf '{"code":1,"stdout":"","stderr":"invalid cwd"}\n'
|
|
1160
|
+
exit 1
|
|
1161
|
+
fi
|
|
1162
|
+
|
|
1163
|
+
# Append docker exec as the pane command (no sh -c, args as array)
|
|
1164
|
+
args+=(
|
|
1165
|
+
"--"
|
|
1166
|
+
"docker" "exec" "-it"
|
|
1167
|
+
"-e" "DCLAUDE_TMUX_RELAY_PORT=$RELAY_PORT"
|
|
1168
|
+
"-e" "DCLAUDE_TMUX_RELAY_HOST=$RELAY_HOST"
|
|
1169
|
+
"-e" "DCLAUDE_TMUX_RELAY_NONCE=$NONCE"
|
|
1170
|
+
"-e" "DCLAUDE_CONTAINER=$CONTAINER"
|
|
1171
|
+
"-u" "claude"
|
|
1172
|
+
"-w" "${cwd:-/home/claude}"
|
|
1173
|
+
"$CONTAINER"
|
|
1174
|
+
"bash"
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
elif [[ "$subcmd" == "send-keys" ]]; then
|
|
1178
|
+
args+=("send-keys")
|
|
1179
|
+
|
|
1180
|
+
# Validate target pane is one we created
|
|
1181
|
+
for (( i=1; i < ${#cmd[@]}; i++ )); do
|
|
1182
|
+
if [[ "${cmd[$i]}" == "-t" && $(( i + 1 )) -lt ${#cmd[@]} ]]; then
|
|
1183
|
+
target="${cmd[$(( i + 1 ))]}"
|
|
1184
|
+
if [[ -f "$TRACKING_FILE" ]] && ! grep -qxF "$target" "$TRACKING_FILE"; then
|
|
1185
|
+
printf '{"code":1,"stdout":"","stderr":"pane not tracked: %s"}\n' "$target"
|
|
1186
|
+
exit 1
|
|
1187
|
+
fi
|
|
1188
|
+
break
|
|
1189
|
+
fi
|
|
1190
|
+
done
|
|
1191
|
+
|
|
1192
|
+
# Pass through all args
|
|
1193
|
+
for (( i=1; i < ${#cmd[@]}; i++ )); do
|
|
1194
|
+
args+=("${cmd[$i]}")
|
|
1195
|
+
done
|
|
1196
|
+
|
|
1197
|
+
elif [[ "$subcmd" == "kill-pane" ]]; then
|
|
1198
|
+
args+=("kill-pane")
|
|
1199
|
+
for (( i=1; i < ${#cmd[@]}; i++ )); do
|
|
1200
|
+
args+=("${cmd[$i]}")
|
|
1201
|
+
done
|
|
1202
|
+
else
|
|
1203
|
+
# All other allowed commands: pass through args
|
|
1204
|
+
# Inject -t $req_pane for commands that need pane context
|
|
1205
|
+
# (relay runs outside tmux pane context, so tmux needs explicit targeting)
|
|
1206
|
+
args+=("$subcmd")
|
|
1207
|
+
has_target=false
|
|
1208
|
+
for (( i=1; i < ${#cmd[@]}; i++ )); do
|
|
1209
|
+
[[ "${cmd[$i]}" == "-t" ]] && has_target=true
|
|
1210
|
+
[[ "${cmd[$i]}" =~ ^-t.+ ]] && has_target=true
|
|
1211
|
+
done
|
|
1212
|
+
# Only inject -t for commands that accept it (not list-sessions, has-session)
|
|
1213
|
+
case "$subcmd" in
|
|
1214
|
+
display-message|list-panes|list-windows|select-layout|resize-pane|select-pane)
|
|
1215
|
+
if [[ "$has_target" == "false" && -n "$req_pane" ]]; then
|
|
1216
|
+
args+=("-t" "$req_pane")
|
|
1217
|
+
fi
|
|
1218
|
+
;;
|
|
1219
|
+
esac
|
|
1220
|
+
for (( i=1; i < ${#cmd[@]}; i++ )); do
|
|
1221
|
+
args+=("${cmd[$i]}")
|
|
1222
|
+
done
|
|
1223
|
+
fi
|
|
1224
|
+
|
|
1225
|
+
# Execute tmux command, capture output
|
|
1226
|
+
stdout_file=$(mktemp)
|
|
1227
|
+
stderr_file=$(mktemp)
|
|
1228
|
+
tmux "${args[@]}" >"$stdout_file" 2>"$stderr_file"
|
|
1229
|
+
code=$?
|
|
1230
|
+
stdout=$(cat "$stdout_file")
|
|
1231
|
+
stderr=$(cat "$stderr_file")
|
|
1232
|
+
rm -f "$stdout_file" "$stderr_file"
|
|
1233
|
+
|
|
1234
|
+
# Post-processing for split-window: track new pane ID
|
|
1235
|
+
if [[ "$subcmd" == "split-window" && $code -eq 0 ]]; then
|
|
1236
|
+
pane_id=$(echo "$stdout" | head -1 | tr -d '[:space:]')
|
|
1237
|
+
if [[ "$pane_id" =~ ^%[0-9]+$ ]]; then
|
|
1238
|
+
echo "$pane_id" >> "$TRACKING_FILE"
|
|
1239
|
+
fi
|
|
1240
|
+
fi
|
|
1241
|
+
|
|
1242
|
+
# Post-processing for kill-pane: clean up orphaned processes after delay
|
|
1243
|
+
if [[ "$subcmd" == "kill-pane" && $code -eq 0 ]]; then
|
|
1244
|
+
(
|
|
1245
|
+
sleep 2
|
|
1246
|
+
docker exec "$CONTAINER" bash -c '
|
|
1247
|
+
ps -eo pid,ppid,tty,comm --no-headers 2>/dev/null | while read pid ppid tty comm; do
|
|
1248
|
+
[[ "$ppid" == "0" && "$comm" == "bash" ]] || continue
|
|
1249
|
+
[[ "$tty" =~ ^pts/([0-9]+)$ ]] || continue
|
|
1250
|
+
(( ${BASH_REMATCH[1]} >= 2 )) || continue
|
|
1251
|
+
children=$(ps --ppid "$pid" --no-headers 2>/dev/null | wc -l)
|
|
1252
|
+
if [[ "$children" -eq 0 ]]; then
|
|
1253
|
+
kill -9 "$pid" 2>/dev/null
|
|
1254
|
+
fi
|
|
1255
|
+
done
|
|
1256
|
+
'
|
|
1257
|
+
) &>/dev/null &
|
|
1258
|
+
fi
|
|
1259
|
+
|
|
1260
|
+
# Return JSON response
|
|
1261
|
+
jq -nc --arg code "$code" --arg stdout "$stdout" --arg stderr "$stderr" \
|
|
1262
|
+
'{code: ($code | tonumber), stdout: $stdout, stderr: $stderr}'
|
|
1263
|
+
HANDLER_BODY
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
# Start the relay listener on the host
|
|
1267
|
+
start_relay() {
|
|
1268
|
+
local container_name="$1"
|
|
1269
|
+
local relay_port="$2"
|
|
1270
|
+
local nonce="$3"
|
|
1271
|
+
local bind_addr="$4"
|
|
1272
|
+
local relay_host="$5"
|
|
1273
|
+
|
|
1274
|
+
local handler_path="$HOME/.dclaude/tmux-relay-handler-${container_name}.sh"
|
|
1275
|
+
local pid_file="$HOME/.dclaude/tmux-relay-pid-${container_name}"
|
|
1276
|
+
local tracking_file="$HOME/.dclaude/tmux-relay-panes-${container_name}"
|
|
1277
|
+
|
|
1278
|
+
mkdir -p "$HOME/.dclaude"
|
|
1279
|
+
|
|
1280
|
+
# Generate handler script
|
|
1281
|
+
generate_relay_handler "$container_name" "$relay_port" "$nonce" \
|
|
1282
|
+
"$handler_path" "$tracking_file" "$relay_host"
|
|
1283
|
+
chmod +x "$handler_path"
|
|
1284
|
+
|
|
1285
|
+
# Clear pane tracking
|
|
1286
|
+
> "$tracking_file"
|
|
1287
|
+
|
|
1288
|
+
# Start listener
|
|
1289
|
+
if command -v socat &>/dev/null; then
|
|
1290
|
+
debug "Starting socat relay on $bind_addr:$relay_port"
|
|
1291
|
+
socat "TCP-LISTEN:$relay_port,bind=$bind_addr,reuseaddr,fork,max-children=10" \
|
|
1292
|
+
"EXEC:bash $handler_path" 2>/dev/null &
|
|
1293
|
+
elif command -v ncat &>/dev/null; then
|
|
1294
|
+
debug "Starting ncat relay on $bind_addr:$relay_port"
|
|
1295
|
+
ncat -l -k "$bind_addr" "$relay_port" --sh-exec "$handler_path" &
|
|
1296
|
+
else
|
|
1297
|
+
error "No relay listener available (need socat or ncat)"
|
|
1298
|
+
return 1
|
|
1299
|
+
fi
|
|
1300
|
+
|
|
1301
|
+
echo $! > "$pid_file"
|
|
1302
|
+
debug "Relay PID: $(cat "$pid_file")"
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
# Stop the relay and clean up
|
|
1306
|
+
stop_relay() {
|
|
1307
|
+
local container_name="$1"
|
|
1308
|
+
|
|
1309
|
+
local pid_file="$HOME/.dclaude/tmux-relay-pid-${container_name}"
|
|
1310
|
+
local handler_path="$HOME/.dclaude/tmux-relay-handler-${container_name}.sh"
|
|
1311
|
+
local tracking_file="$HOME/.dclaude/tmux-relay-panes-${container_name}"
|
|
1312
|
+
|
|
1313
|
+
if [[ -f "$pid_file" ]]; then
|
|
1314
|
+
local pid
|
|
1315
|
+
pid=$(cat "$pid_file")
|
|
1316
|
+
if [[ -n "$pid" ]]; then
|
|
1317
|
+
# Kill forked handler processes first
|
|
1318
|
+
pkill -P "$pid" 2>/dev/null || true
|
|
1319
|
+
# Verify process identity before killing (guard against PID recycling)
|
|
1320
|
+
local comm
|
|
1321
|
+
comm=$(ps -p "$pid" -o comm= 2>/dev/null || true)
|
|
1322
|
+
if [[ "$comm" =~ (socat|ncat) ]]; then
|
|
1323
|
+
kill "$pid" 2>/dev/null || true
|
|
1324
|
+
fi
|
|
1325
|
+
fi
|
|
1326
|
+
fi
|
|
1327
|
+
|
|
1328
|
+
# Clean up orphaned bash processes in container
|
|
1329
|
+
cleanup_container_orphans "$container_name"
|
|
1330
|
+
|
|
1331
|
+
# Remove relay files (but keep nonce for reattachment)
|
|
1332
|
+
rm -f "$pid_file" "$handler_path" "$tracking_file"
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
# Kill orphaned bash sessions in container (from dead docker exec panes)
|
|
1336
|
+
cleanup_container_orphans() {
|
|
1337
|
+
local container_name="$1"
|
|
1338
|
+
|
|
1339
|
+
# Check container is running before trying
|
|
1340
|
+
if ! docker ps -q -f name="^${container_name}$" 2>/dev/null | grep -q .; then
|
|
1341
|
+
return 0
|
|
1342
|
+
fi
|
|
1343
|
+
|
|
1344
|
+
docker exec "$container_name" bash -c '
|
|
1345
|
+
ps -eo pid,ppid,tty,comm --no-headers 2>/dev/null | while read pid ppid tty comm; do
|
|
1346
|
+
[[ "$ppid" == "0" && "$comm" == "bash" ]] || continue
|
|
1347
|
+
[[ "$tty" =~ ^pts/([0-9]+)$ ]] || continue
|
|
1348
|
+
(( ${BASH_REMATCH[1]} >= 2 )) || continue
|
|
1349
|
+
children=$(ps --ppid "$pid" --no-headers 2>/dev/null | wc -l)
|
|
1350
|
+
if [[ "$children" -eq 0 ]]; then
|
|
1351
|
+
kill -9 "$pid" 2>/dev/null
|
|
1352
|
+
fi
|
|
1353
|
+
done
|
|
1354
|
+
' 2>/dev/null || true
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
# Set up Agent Teams relay if conditions are met
|
|
1358
|
+
# Sets globals: RELAY_STARTED, TMUX_SESSION_ENV_ARGS
|
|
1359
|
+
setup_agent_teams_relay() {
|
|
1360
|
+
local container_name="$1"
|
|
1361
|
+
local relay_port="$2"
|
|
1362
|
+
local platform="$3"
|
|
1363
|
+
local network_mode="$4"
|
|
1364
|
+
|
|
1365
|
+
RELAY_STARTED=false
|
|
1366
|
+
TMUX_SESSION_ENV_ARGS=()
|
|
1367
|
+
|
|
1368
|
+
if [[ -z "$relay_port" ]]; then
|
|
1369
|
+
debug "No relay port configured for this container"
|
|
1370
|
+
TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
|
|
1371
|
+
return
|
|
1372
|
+
fi
|
|
1373
|
+
|
|
1374
|
+
if ! detect_host_tmux; then
|
|
1375
|
+
debug "Not in host tmux - Agent Teams will use in-process mode"
|
|
1376
|
+
TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
|
|
1377
|
+
return
|
|
1378
|
+
fi
|
|
1379
|
+
|
|
1380
|
+
if ! check_relay_deps; then
|
|
1381
|
+
info "Tip: Install jq + socat (or ncat) for Agent Teams pane mode in tmux"
|
|
1382
|
+
TMUX_SESSION_ENV_ARGS=(-e "DCLAUDE_HIDE_TMUX=1")
|
|
1383
|
+
return
|
|
1384
|
+
fi
|
|
1385
|
+
|
|
1386
|
+
local bind_addr relay_host relay_nonce host_pane
|
|
1387
|
+
bind_addr=$(get_relay_bind_addr "$platform" "$network_mode")
|
|
1388
|
+
relay_host=$(get_relay_host "$platform" "$network_mode")
|
|
1389
|
+
relay_nonce=$(get_or_create_relay_nonce "$container_name")
|
|
1390
|
+
host_pane=$(tmux display-message -p '#{pane_id}')
|
|
1391
|
+
|
|
1392
|
+
start_relay "$container_name" "$relay_port" "$relay_nonce" "$bind_addr" "$relay_host"
|
|
1393
|
+
RELAY_STARTED=true
|
|
1394
|
+
|
|
1395
|
+
TMUX_SESSION_ENV_ARGS=(
|
|
1396
|
+
-e "DCLAUDE_TMUX_RELAY_PORT=$relay_port"
|
|
1397
|
+
-e "DCLAUDE_TMUX_RELAY_HOST=$relay_host"
|
|
1398
|
+
-e "DCLAUDE_TMUX_RELAY_NONCE=$relay_nonce"
|
|
1399
|
+
-e "DCLAUDE_HOST_PANE=$host_pane"
|
|
1400
|
+
-e "DCLAUDE_CONTAINER=$container_name"
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
debug "Agent Teams relay: $bind_addr:$relay_port -> $container_name (pane $host_pane)"
|
|
1404
|
+
}
|
|
1405
|
+
# ============================================================================
|
|
1406
|
+
|
|
990
1407
|
# Detect TTY availability and return appropriate Docker flags
|
|
991
1408
|
# Detect TTY status early (before any subshells)
|
|
992
1409
|
# Must be done at script top-level since $() subshells don't inherit TTY
|
|
@@ -1031,6 +1448,17 @@ should_skip_tmux() {
|
|
|
1031
1448
|
return 1
|
|
1032
1449
|
}
|
|
1033
1450
|
|
|
1451
|
+
# Debug countdown before launching Claude (gives time to read debug output)
|
|
1452
|
+
# Press Enter to skip the countdown and launch immediately
|
|
1453
|
+
debug_countdown() {
|
|
1454
|
+
if [[ "$DEBUG" == "true" ]]; then
|
|
1455
|
+
for i in 5 4 3 2 1; do
|
|
1456
|
+
printf '\r%b' "${CYAN}Debug: Launching in ${i}... (press Enter to skip)${NC}" >&2
|
|
1457
|
+
read -t 1 -s 2>/dev/null && break
|
|
1458
|
+
done
|
|
1459
|
+
printf '\r%b\n' "${CYAN}Debug: Launching... ${NC}" >&2
|
|
1460
|
+
fi
|
|
1461
|
+
}
|
|
1034
1462
|
|
|
1035
1463
|
# Main execution
|
|
1036
1464
|
main() {
|
|
@@ -1244,10 +1672,23 @@ main() {
|
|
|
1244
1672
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1245
1673
|
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
1246
1674
|
|
|
1675
|
+
# Set up Agent Teams relay (or hide $TMUX for in-process mode)
|
|
1676
|
+
local relay_port
|
|
1677
|
+
relay_port=$(docker inspect --format='{{index .Config.Labels "dclaude.relay.port"}}' "$container_name" 2>/dev/null || true)
|
|
1678
|
+
setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
|
|
1679
|
+
if [[ "$RELAY_STARTED" == "true" ]]; then
|
|
1680
|
+
trap "stop_relay '$container_name'" EXIT
|
|
1681
|
+
fi
|
|
1682
|
+
|
|
1247
1683
|
debug "Creating new tmux session running Claude"
|
|
1248
1684
|
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1249
1685
|
info "Starting new Claude session..."
|
|
1250
|
-
|
|
1686
|
+
debug_countdown
|
|
1687
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
|
|
1688
|
+
/usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf \
|
|
1689
|
+
new-session -s "$tmux_session" \
|
|
1690
|
+
"${TMUX_SESSION_ENV_ARGS[@]}" \
|
|
1691
|
+
claude-launcher.sh "${claude_args[@]}" "$@"
|
|
1251
1692
|
exit $?
|
|
1252
1693
|
else
|
|
1253
1694
|
# Non-interactive or print mode - run claude directly without tmux
|
|
@@ -1298,6 +1739,17 @@ main() {
|
|
|
1298
1739
|
debug "SSH port reserved: $ssh_port"
|
|
1299
1740
|
DOCKER_ARGS+=(--label "dclaude.ssh.port=${ssh_port}")
|
|
1300
1741
|
|
|
1742
|
+
# Reserve relay port for Agent Teams tmux passthrough
|
|
1743
|
+
local relay_port
|
|
1744
|
+
relay_port=$(find_available_port 30000 60000)
|
|
1745
|
+
debug "Relay port reserved: $relay_port"
|
|
1746
|
+
DOCKER_ARGS+=(--label "dclaude.relay.port=${relay_port}")
|
|
1747
|
+
|
|
1748
|
+
# Add host.docker.internal for Linux bridge mode (needed for relay connectivity)
|
|
1749
|
+
if [[ "$platform" == "linux" && "$network_mode" == "bridge" ]]; then
|
|
1750
|
+
DOCKER_ARGS+=(--add-host "host.docker.internal:host-gateway")
|
|
1751
|
+
fi
|
|
1752
|
+
|
|
1301
1753
|
# Port mapping only needed for bridge mode (host mode shares network stack)
|
|
1302
1754
|
if [[ "$network_mode" != "host" ]]; then
|
|
1303
1755
|
DOCKER_ARGS+=(-p "${ssh_port}:${ssh_port}")
|
|
@@ -1397,10 +1849,21 @@ main() {
|
|
|
1397
1849
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1398
1850
|
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
1399
1851
|
|
|
1852
|
+
# Set up Agent Teams relay (or hide $TMUX for in-process mode)
|
|
1853
|
+
setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
|
|
1854
|
+
if [[ "$RELAY_STARTED" == "true" ]]; then
|
|
1855
|
+
trap "stop_relay '$container_name'" EXIT
|
|
1856
|
+
fi
|
|
1857
|
+
|
|
1400
1858
|
debug "Creating new tmux session running Claude"
|
|
1401
1859
|
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1402
1860
|
info "Starting new Claude session..."
|
|
1403
|
-
|
|
1861
|
+
debug_countdown
|
|
1862
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
|
|
1863
|
+
/usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf \
|
|
1864
|
+
new-session -s "$tmux_session" \
|
|
1865
|
+
"${TMUX_SESSION_ENV_ARGS[@]}" \
|
|
1866
|
+
claude-launcher.sh "${claude_args[@]}" "$@"
|
|
1404
1867
|
exit $?
|
|
1405
1868
|
else
|
|
1406
1869
|
# Non-interactive or print mode - run claude directly without tmux
|
|
@@ -1530,11 +1993,11 @@ cmd_attach() {
|
|
|
1530
1993
|
fi
|
|
1531
1994
|
fi
|
|
1532
1995
|
|
|
1533
|
-
# Check if session exists
|
|
1534
|
-
if ! docker exec -u claude "$container_name" tmux has-session -t "$session_name" 2>/dev/null; then
|
|
1996
|
+
# Check if session exists (use -L dclaude-inner for namespaced socket)
|
|
1997
|
+
if ! docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner has-session -t "$session_name" 2>/dev/null; then
|
|
1535
1998
|
error "Session '$session_name' not found in container"
|
|
1536
1999
|
info "Available sessions:"
|
|
1537
|
-
docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null || echo " (no sessions running)"
|
|
2000
|
+
docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null || echo " (no sessions running)"
|
|
1538
2001
|
exit 1
|
|
1539
2002
|
fi
|
|
1540
2003
|
|
|
@@ -1546,9 +2009,22 @@ cmd_attach() {
|
|
|
1546
2009
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1547
2010
|
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
1548
2011
|
|
|
2012
|
+
# Restart relay if the session uses it (relay was stopped when previous dclaude exited)
|
|
2013
|
+
local relay_port
|
|
2014
|
+
relay_port=$(docker inspect --format='{{index .Config.Labels "dclaude.relay.port"}}' "$container_name" 2>/dev/null || true)
|
|
2015
|
+
local platform
|
|
2016
|
+
platform=$(detect_platform)
|
|
2017
|
+
local network_mode
|
|
2018
|
+
network_mode=$(docker inspect --format='{{.HostConfig.NetworkMode}}' "$container_name" 2>/dev/null || echo "bridge")
|
|
2019
|
+
setup_agent_teams_relay "$container_name" "$relay_port" "$platform" "$network_mode"
|
|
2020
|
+
if [[ "$RELAY_STARTED" == "true" ]]; then
|
|
2021
|
+
trap "stop_relay '$container_name'" EXIT
|
|
2022
|
+
fi
|
|
2023
|
+
|
|
1549
2024
|
# Attach to existing session
|
|
1550
2025
|
info "Attaching to session: $session_name"
|
|
1551
|
-
docker exec -it -u claude "${exec_env_args[@]}" "$container_name"
|
|
2026
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" \
|
|
2027
|
+
/usr/bin/tmux -L dclaude-inner -f /home/claude/.tmux.conf attach-session -t "$session_name"
|
|
1552
2028
|
exit $?
|
|
1553
2029
|
}
|
|
1554
2030
|
|
|
@@ -1770,7 +2246,7 @@ cmd_stop() {
|
|
|
1770
2246
|
if [[ "$container_status" == "running" ]]; then
|
|
1771
2247
|
# Count active tmux sessions
|
|
1772
2248
|
local session_count
|
|
1773
|
-
session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
2249
|
+
session_count=$(docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
1774
2250
|
|
|
1775
2251
|
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1776
2252
|
info "Stopping container $container_name ($session_count active session(s))..."
|
|
@@ -1778,6 +2254,9 @@ cmd_stop() {
|
|
|
1778
2254
|
info "Stopping container $container_name..."
|
|
1779
2255
|
fi
|
|
1780
2256
|
|
|
2257
|
+
# Stop relay if running
|
|
2258
|
+
stop_relay "$container_name"
|
|
2259
|
+
|
|
1781
2260
|
if docker stop "$container_name" >/dev/null; then
|
|
1782
2261
|
success "Container stopped"
|
|
1783
2262
|
else
|
|
@@ -1956,7 +2435,7 @@ cmd_rm() {
|
|
|
1956
2435
|
if [[ "$force" == "true" ]]; then
|
|
1957
2436
|
# Count active tmux sessions
|
|
1958
2437
|
local session_count
|
|
1959
|
-
session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
2438
|
+
session_count=$(docker exec -u claude "$container_name" /usr/bin/tmux -L dclaude-inner list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
1960
2439
|
|
|
1961
2440
|
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1962
2441
|
info "Removing running container $container_name ($session_count active session(s))..."
|
|
@@ -1964,6 +2443,10 @@ cmd_rm() {
|
|
|
1964
2443
|
info "Removing running container $container_name..."
|
|
1965
2444
|
fi
|
|
1966
2445
|
|
|
2446
|
+
# Clean up relay (including nonce file since container is being removed)
|
|
2447
|
+
stop_relay "$container_name"
|
|
2448
|
+
rm -f "$HOME/.dclaude/tmux-relay-nonce-${container_name}"
|
|
2449
|
+
|
|
1967
2450
|
if docker rm -f "$container_name" >/dev/null; then
|
|
1968
2451
|
success "Container removed"
|
|
1969
2452
|
else
|
|
@@ -1978,6 +2461,11 @@ cmd_rm() {
|
|
|
1978
2461
|
fi
|
|
1979
2462
|
else
|
|
1980
2463
|
info "Removing container $container_name..."
|
|
2464
|
+
|
|
2465
|
+
# Clean up relay files (including nonce since container is being removed)
|
|
2466
|
+
stop_relay "$container_name"
|
|
2467
|
+
rm -f "$HOME/.dclaude/tmux-relay-nonce-${container_name}"
|
|
2468
|
+
|
|
1981
2469
|
if docker rm "$container_name" >/dev/null; then
|
|
1982
2470
|
success "Container removed"
|
|
1983
2471
|
else
|