@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.
Files changed (2) hide show
  1. package/dclaude +496 -8
  2. 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
- docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
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
- docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
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" tmux -f /home/claude/.tmux.conf attach-session -t "$session_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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alanbem/dclaude",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Dockerized Claude Code CLI launcher with MCP support",
5
5
  "main": "dclaude",
6
6
  "bin": {