@alanbem/dclaude 0.0.11 → 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/README.md +53 -0
- package/dclaude +597 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,6 +151,21 @@ dclaude chrome # Launch Chrome with DevTools
|
|
|
151
151
|
dclaude # Claude can now interact with the browser
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
### iTerm2 Shell Integration
|
|
155
|
+
|
|
156
|
+
If you use iTerm2 on macOS, dclaude automatically enables [iTerm2 Shell Integration](https://iterm2.com/documentation-shell-integration.html):
|
|
157
|
+
|
|
158
|
+
- **Click URLs in output** - Opens in your Mac's browser
|
|
159
|
+
- **imgcat** - Display images inline in terminal
|
|
160
|
+
- **it2copy** - Copy to Mac clipboard from inside container
|
|
161
|
+
- **Marks** - Navigate between command prompts
|
|
162
|
+
|
|
163
|
+
This only activates when running in iTerm2. To disable:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
DCLAUDE_ITERM2=false dclaude
|
|
167
|
+
```
|
|
168
|
+
|
|
154
169
|
### System Context
|
|
155
170
|
|
|
156
171
|
dclaude automatically tells Claude about its container environment so it can give better suggestions:
|
|
@@ -175,10 +190,12 @@ DCLAUDE_SYSTEM_CONTEXT=false dclaude
|
|
|
175
190
|
| `DCLAUDE_NAMESPACE` | (none) | Namespace for isolated credentials/config |
|
|
176
191
|
| `DCLAUDE_NETWORK` | `auto` | Network mode: `auto`, `host`, `bridge` |
|
|
177
192
|
| `DCLAUDE_GIT_AUTH` | `auto` | SSH auth: `auto`, `agent-forwarding`, `key-mount`, `none` |
|
|
193
|
+
| `DCLAUDE_MOUNT_ROOT` | (working dir) | Mount parent directory for sibling access |
|
|
178
194
|
| `DCLAUDE_DEBUG` | `false` | Enable debug output |
|
|
179
195
|
| `DCLAUDE_QUIET` | `false` | Suppress info messages |
|
|
180
196
|
| `DCLAUDE_NO_UPDATE` | `false` | Skip image update check |
|
|
181
197
|
| `DCLAUDE_SYSTEM_CONTEXT` | `true` | Inform Claude about container environment |
|
|
198
|
+
| `DCLAUDE_ITERM2` | `true` | Enable iTerm2 shell integration (only affects iTerm2) |
|
|
182
199
|
|
|
183
200
|
## Configuration File
|
|
184
201
|
|
|
@@ -200,6 +217,7 @@ dclaude walks up the directory tree to find `.dclaude` files. Any dclaude sessio
|
|
|
200
217
|
| `NAMESPACE` | Isolate credentials/config (see Namespace Isolation) |
|
|
201
218
|
| `NETWORK` | Network mode (`host`, `bridge`) |
|
|
202
219
|
| `GIT_AUTH` | Git auth mode |
|
|
220
|
+
| `MOUNT_ROOT` | Mount directory (relative to config file, or absolute path) |
|
|
203
221
|
| `DEBUG` | Enable debug output (`true`, `false`) |
|
|
204
222
|
| `CHROME_PORT` | Chrome DevTools port |
|
|
205
223
|
|
|
@@ -238,6 +256,41 @@ Each namespace gets its own:
|
|
|
238
256
|
- Git configuration
|
|
239
257
|
- Container instance
|
|
240
258
|
|
|
259
|
+
## Mount Root (Parent Directory Access)
|
|
260
|
+
|
|
261
|
+
By default, dclaude only mounts the current working directory. Use `MOUNT_ROOT` to mount a parent directory, enabling access to sibling directories.
|
|
262
|
+
|
|
263
|
+
**Use case:** You're working in a subdirectory but need access to related projects:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
/Users/alan/projects/mycompany/
|
|
267
|
+
├── shared-libs/ # Common libraries
|
|
268
|
+
├── api-service/ # API backend
|
|
269
|
+
├── web-app/ # Frontend
|
|
270
|
+
└── infrastructure/
|
|
271
|
+
└── terraform/ # ← You're here, but need access to siblings
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Option 1: Using `.dclaude` file (recommended)**
|
|
275
|
+
```bash
|
|
276
|
+
# Create config at the directory you want as mount root
|
|
277
|
+
echo "MOUNT_ROOT=." > ~/projects/mycompany/.dclaude
|
|
278
|
+
|
|
279
|
+
# Relative paths are resolved from config file's directory
|
|
280
|
+
echo "MOUNT_ROOT=.." > ~/projects/mycompany/subdir/.dclaude # mounts mycompany/
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Option 2: Using environment variable**
|
|
284
|
+
```bash
|
|
285
|
+
# Relative path (from working directory)
|
|
286
|
+
DCLAUDE_MOUNT_ROOT=../.. dclaude
|
|
287
|
+
|
|
288
|
+
# Absolute path
|
|
289
|
+
DCLAUDE_MOUNT_ROOT=/Users/alan/projects/mycompany dclaude
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Now Claude can see and work with all sibling directories while your working directory remains `terraform/`.
|
|
293
|
+
|
|
241
294
|
## Networking
|
|
242
295
|
|
|
243
296
|
dclaude auto-detects the best networking mode:
|
package/dclaude
CHANGED
|
@@ -27,6 +27,7 @@ CHROME_FLAGS="${DCLAUDE_CHROME_FLAGS:-}"
|
|
|
27
27
|
# - DCLAUDE_GIT_AUTH (git auth mode)
|
|
28
28
|
# - DCLAUDE_DEBUG (enable debug output)
|
|
29
29
|
# - DCLAUDE_CHROME_PORT (chrome devtools port)
|
|
30
|
+
# - DCLAUDE_MOUNT_ROOT (mount a parent directory instead of working directory)
|
|
30
31
|
|
|
31
32
|
# Colors for output (only if terminal supports it)
|
|
32
33
|
if [[ -t 1 ]]; then
|
|
@@ -122,6 +123,18 @@ load_config_file() {
|
|
|
122
123
|
CHROME_PORT)
|
|
123
124
|
[[ -z "${DCLAUDE_CHROME_PORT:-}" ]] && DCLAUDE_CHROME_PORT="$value"
|
|
124
125
|
;;
|
|
126
|
+
MOUNT_ROOT)
|
|
127
|
+
if [[ -z "${DCLAUDE_MOUNT_ROOT:-}" ]]; then
|
|
128
|
+
# Resolve relative paths relative to config file's directory
|
|
129
|
+
if [[ "$value" != /* ]]; then
|
|
130
|
+
local config_dir
|
|
131
|
+
config_dir=$(dirname "$config_file")
|
|
132
|
+
DCLAUDE_MOUNT_ROOT=$(cd "$config_dir" && cd "$value" 2>/dev/null && pwd)
|
|
133
|
+
else
|
|
134
|
+
DCLAUDE_MOUNT_ROOT="$value"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
;;
|
|
125
138
|
esac
|
|
126
139
|
fi
|
|
127
140
|
done < "$config_file"
|
|
@@ -147,6 +160,7 @@ if [[ -n "$DCLAUDE_CONFIG_FILE" ]]; then
|
|
|
147
160
|
[[ -n "${DCLAUDE_NETWORK:-}" ]] && debug " NETWORK=$DCLAUDE_NETWORK"
|
|
148
161
|
[[ -n "${DCLAUDE_GIT_AUTH:-}" ]] && debug " GIT_AUTH=$DCLAUDE_GIT_AUTH"
|
|
149
162
|
[[ -n "${DCLAUDE_CHROME_PORT:-}" ]] && debug " CHROME_PORT=$DCLAUDE_CHROME_PORT"
|
|
163
|
+
[[ -n "${DCLAUDE_MOUNT_ROOT:-}" ]] && debug " MOUNT_ROOT=$DCLAUDE_MOUNT_ROOT"
|
|
150
164
|
fi
|
|
151
165
|
fi
|
|
152
166
|
|
|
@@ -577,6 +591,55 @@ get_host_path() {
|
|
|
577
591
|
echo "$host_path"
|
|
578
592
|
}
|
|
579
593
|
|
|
594
|
+
# Compute mount root path from DCLAUDE_MOUNT_ROOT
|
|
595
|
+
# Supports: absolute paths, relative paths (../, ../../, etc.)
|
|
596
|
+
# Returns: Absolute path to mount root, or HOST_PATH if not set
|
|
597
|
+
get_mount_root() {
|
|
598
|
+
local host_path="$1"
|
|
599
|
+
local mount_root="${DCLAUDE_MOUNT_ROOT:-}"
|
|
600
|
+
|
|
601
|
+
# If not set, use host path (current behavior)
|
|
602
|
+
if [[ -z "$mount_root" ]]; then
|
|
603
|
+
echo "$host_path"
|
|
604
|
+
return 0
|
|
605
|
+
fi
|
|
606
|
+
|
|
607
|
+
local resolved_root
|
|
608
|
+
|
|
609
|
+
# Handle relative paths (resolve relative to host_path)
|
|
610
|
+
if [[ "$mount_root" != /* ]]; then
|
|
611
|
+
# Relative path - resolve it
|
|
612
|
+
resolved_root=$(cd "$host_path" && cd "$mount_root" 2>/dev/null && pwd) || {
|
|
613
|
+
error "Mount root path not accessible: $mount_root (relative to $host_path)"
|
|
614
|
+
exit 1
|
|
615
|
+
}
|
|
616
|
+
else
|
|
617
|
+
# Absolute path - use directly
|
|
618
|
+
resolved_root="$mount_root"
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
# Validate mount root exists
|
|
622
|
+
if [[ ! -d "$resolved_root" ]]; then
|
|
623
|
+
error "Mount root directory does not exist: $resolved_root"
|
|
624
|
+
exit 1
|
|
625
|
+
fi
|
|
626
|
+
|
|
627
|
+
# Validate that host_path is under mount_root
|
|
628
|
+
# Use realpath to normalize paths for comparison
|
|
629
|
+
local real_host real_root
|
|
630
|
+
real_host=$(cd "$host_path" && pwd -P)
|
|
631
|
+
real_root=$(cd "$resolved_root" && pwd -P)
|
|
632
|
+
|
|
633
|
+
if [[ "$real_host" != "$real_root" && "$real_host" != "$real_root"/* ]]; then
|
|
634
|
+
error "Working directory is not under mount root"
|
|
635
|
+
error " Working directory: $real_host"
|
|
636
|
+
error " Mount root: $real_root"
|
|
637
|
+
exit 1
|
|
638
|
+
fi
|
|
639
|
+
|
|
640
|
+
echo "$resolved_root"
|
|
641
|
+
}
|
|
642
|
+
|
|
580
643
|
# Generate deterministic container name from path (and namespace if set)
|
|
581
644
|
get_container_name() {
|
|
582
645
|
local path="${1:-$HOST_PATH}"
|
|
@@ -924,6 +987,423 @@ handle_git_auth() {
|
|
|
924
987
|
fi
|
|
925
988
|
}
|
|
926
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
|
+
|
|
927
1407
|
# Detect TTY availability and return appropriate Docker flags
|
|
928
1408
|
# Detect TTY status early (before any subshells)
|
|
929
1409
|
# Must be done at script top-level since $() subshells don't inherit TTY
|
|
@@ -968,6 +1448,17 @@ should_skip_tmux() {
|
|
|
968
1448
|
return 1
|
|
969
1449
|
}
|
|
970
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
|
+
}
|
|
971
1462
|
|
|
972
1463
|
# Main execution
|
|
973
1464
|
main() {
|
|
@@ -976,6 +1467,9 @@ main() {
|
|
|
976
1467
|
|
|
977
1468
|
info "Verifying environment..."
|
|
978
1469
|
debug "Host path: $HOST_PATH"
|
|
1470
|
+
if [[ "$MOUNT_ROOT" != "$HOST_PATH" ]]; then
|
|
1471
|
+
debug "Mount root: $MOUNT_ROOT"
|
|
1472
|
+
fi
|
|
979
1473
|
|
|
980
1474
|
# Check prerequisites
|
|
981
1475
|
check_docker
|
|
@@ -1108,6 +1602,9 @@ main() {
|
|
|
1108
1602
|
debug " SYSTEM_CONTEXT=$ENABLE_SYSTEM_CONTEXT"
|
|
1109
1603
|
debug " IMAGE=$IMAGE"
|
|
1110
1604
|
debug " HOST_PATH=$HOST_PATH"
|
|
1605
|
+
if [[ "$MOUNT_ROOT" != "$HOST_PATH" ]]; then
|
|
1606
|
+
debug " MOUNT_ROOT=$MOUNT_ROOT"
|
|
1607
|
+
fi
|
|
1111
1608
|
debug " PLATFORM=$platform"
|
|
1112
1609
|
fi
|
|
1113
1610
|
|
|
@@ -1145,6 +1642,16 @@ main() {
|
|
|
1145
1642
|
done
|
|
1146
1643
|
fi
|
|
1147
1644
|
|
|
1645
|
+
# Ensure SSH proxy is running if container uses agent forwarding (macOS)
|
|
1646
|
+
if [[ "$platform" == "darwin" ]]; then
|
|
1647
|
+
local container_ssh_sock
|
|
1648
|
+
container_ssh_sock=$(docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$container_name" 2>/dev/null | grep "^SSH_AUTH_SOCK=" | cut -d= -f2)
|
|
1649
|
+
if [[ "$container_ssh_sock" == "/tmp/ssh-proxy/agent" ]]; then
|
|
1650
|
+
debug "Container uses SSH agent forwarding, ensuring proxy is running"
|
|
1651
|
+
setup_ssh_proxy_container
|
|
1652
|
+
fi
|
|
1653
|
+
fi
|
|
1654
|
+
|
|
1148
1655
|
# Check if interactive (TTY available) and not in print mode
|
|
1149
1656
|
if [[ -n "$tty_flags" ]] && ! should_skip_tmux "${claude_args[@]}" "$@"; then
|
|
1150
1657
|
# Interactive and not print mode - use tmux for session management
|
|
@@ -1163,11 +1670,25 @@ main() {
|
|
|
1163
1670
|
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1164
1671
|
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1165
1672
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1673
|
+
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
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
|
|
1166
1682
|
|
|
1167
1683
|
debug "Creating new tmux session running Claude"
|
|
1168
1684
|
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1169
1685
|
info "Starting new Claude session..."
|
|
1170
|
-
|
|
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[@]}" "$@"
|
|
1171
1692
|
exit $?
|
|
1172
1693
|
else
|
|
1173
1694
|
# Non-interactive or print mode - run claude directly without tmux
|
|
@@ -1200,11 +1721,11 @@ main() {
|
|
|
1200
1721
|
claude_volume=$(get_volume_name)
|
|
1201
1722
|
|
|
1202
1723
|
DOCKER_ARGS+=(
|
|
1203
|
-
# Mount
|
|
1204
|
-
-v "${
|
|
1724
|
+
# Mount directory tree (mount root allows access to parent directories)
|
|
1725
|
+
-v "${MOUNT_ROOT}:${MOUNT_ROOT}"
|
|
1205
1726
|
# Mount persistent Claude configuration volume
|
|
1206
1727
|
-v "${claude_volume}:/home/claude/.claude"
|
|
1207
|
-
# Set working directory
|
|
1728
|
+
# Set working directory (within the mounted tree)
|
|
1208
1729
|
-w "${HOST_PATH}"
|
|
1209
1730
|
# Network mode
|
|
1210
1731
|
--network="$network_mode"
|
|
@@ -1218,6 +1739,17 @@ main() {
|
|
|
1218
1739
|
debug "SSH port reserved: $ssh_port"
|
|
1219
1740
|
DOCKER_ARGS+=(--label "dclaude.ssh.port=${ssh_port}")
|
|
1220
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
|
+
|
|
1221
1753
|
# Port mapping only needed for bridge mode (host mode shares network stack)
|
|
1222
1754
|
if [[ "$network_mode" != "host" ]]; then
|
|
1223
1755
|
DOCKER_ARGS+=(-p "${ssh_port}:${ssh_port}")
|
|
@@ -1240,6 +1772,11 @@ main() {
|
|
|
1240
1772
|
DOCKER_ARGS+=(-e "COLORTERM=${COLORTERM}")
|
|
1241
1773
|
fi
|
|
1242
1774
|
|
|
1775
|
+
# Pass through iTerm2 integration opt-out if set
|
|
1776
|
+
if [[ -n "${DCLAUDE_ITERM2:-}" ]]; then
|
|
1777
|
+
DOCKER_ARGS+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
1778
|
+
fi
|
|
1779
|
+
|
|
1243
1780
|
# Mount Docker socket if detected (detection done earlier for system context)
|
|
1244
1781
|
if [[ -n "$DOCKER_SOCKET" ]] && [[ -S "$DOCKER_SOCKET" ]]; then
|
|
1245
1782
|
DOCKER_ARGS+=(-v "${DOCKER_SOCKET}:/var/run/docker.sock")
|
|
@@ -1310,11 +1847,23 @@ main() {
|
|
|
1310
1847
|
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1311
1848
|
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1312
1849
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1850
|
+
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
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
|
|
1313
1857
|
|
|
1314
1858
|
debug "Creating new tmux session running Claude"
|
|
1315
1859
|
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1316
1860
|
info "Starting new Claude session..."
|
|
1317
|
-
|
|
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[@]}" "$@"
|
|
1318
1867
|
exit $?
|
|
1319
1868
|
else
|
|
1320
1869
|
# Non-interactive or print mode - run claude directly without tmux
|
|
@@ -1444,11 +1993,11 @@ cmd_attach() {
|
|
|
1444
1993
|
fi
|
|
1445
1994
|
fi
|
|
1446
1995
|
|
|
1447
|
-
# Check if session exists
|
|
1448
|
-
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
|
|
1449
1998
|
error "Session '$session_name' not found in container"
|
|
1450
1999
|
info "Available sessions:"
|
|
1451
|
-
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)"
|
|
1452
2001
|
exit 1
|
|
1453
2002
|
fi
|
|
1454
2003
|
|
|
@@ -1458,10 +2007,24 @@ cmd_attach() {
|
|
|
1458
2007
|
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1459
2008
|
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1460
2009
|
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
2010
|
+
[[ -n "${DCLAUDE_ITERM2:-}" ]] && exec_env_args+=(-e "DCLAUDE_ITERM2=${DCLAUDE_ITERM2}")
|
|
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
|
|
1461
2023
|
|
|
1462
2024
|
# Attach to existing session
|
|
1463
2025
|
info "Attaching to session: $session_name"
|
|
1464
|
-
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"
|
|
1465
2028
|
exit $?
|
|
1466
2029
|
}
|
|
1467
2030
|
|
|
@@ -1514,7 +2077,7 @@ cmd_chrome() {
|
|
|
1514
2077
|
fi
|
|
1515
2078
|
|
|
1516
2079
|
# 2. Setup profile directory
|
|
1517
|
-
local profile_dir="$HOST_PATH/.dclaude/chrome/profiles/$CHROME_PROFILE"
|
|
2080
|
+
local profile_dir="$HOST_PATH/.dclaude.d/chrome/profiles/$CHROME_PROFILE"
|
|
1518
2081
|
mkdir -p "$profile_dir"
|
|
1519
2082
|
debug "Profile directory: $profile_dir"
|
|
1520
2083
|
|
|
@@ -1683,7 +2246,7 @@ cmd_stop() {
|
|
|
1683
2246
|
if [[ "$container_status" == "running" ]]; then
|
|
1684
2247
|
# Count active tmux sessions
|
|
1685
2248
|
local session_count
|
|
1686
|
-
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
|
|
1687
2250
|
|
|
1688
2251
|
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1689
2252
|
info "Stopping container $container_name ($session_count active session(s))..."
|
|
@@ -1691,6 +2254,9 @@ cmd_stop() {
|
|
|
1691
2254
|
info "Stopping container $container_name..."
|
|
1692
2255
|
fi
|
|
1693
2256
|
|
|
2257
|
+
# Stop relay if running
|
|
2258
|
+
stop_relay "$container_name"
|
|
2259
|
+
|
|
1694
2260
|
if docker stop "$container_name" >/dev/null; then
|
|
1695
2261
|
success "Container stopped"
|
|
1696
2262
|
else
|
|
@@ -1869,7 +2435,7 @@ cmd_rm() {
|
|
|
1869
2435
|
if [[ "$force" == "true" ]]; then
|
|
1870
2436
|
# Count active tmux sessions
|
|
1871
2437
|
local session_count
|
|
1872
|
-
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
|
|
1873
2439
|
|
|
1874
2440
|
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1875
2441
|
info "Removing running container $container_name ($session_count active session(s))..."
|
|
@@ -1877,6 +2443,10 @@ cmd_rm() {
|
|
|
1877
2443
|
info "Removing running container $container_name..."
|
|
1878
2444
|
fi
|
|
1879
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
|
+
|
|
1880
2450
|
if docker rm -f "$container_name" >/dev/null; then
|
|
1881
2451
|
success "Container removed"
|
|
1882
2452
|
else
|
|
@@ -1891,6 +2461,11 @@ cmd_rm() {
|
|
|
1891
2461
|
fi
|
|
1892
2462
|
else
|
|
1893
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
|
+
|
|
1894
2469
|
if docker rm "$container_name" >/dev/null; then
|
|
1895
2470
|
success "Container removed"
|
|
1896
2471
|
else
|
|
@@ -1989,8 +2564,9 @@ cmd_git() {
|
|
|
1989
2564
|
exit 0
|
|
1990
2565
|
}
|
|
1991
2566
|
|
|
1992
|
-
# Initialize HOST_PATH once for all commands
|
|
2567
|
+
# Initialize HOST_PATH and MOUNT_ROOT once for all commands
|
|
1993
2568
|
HOST_PATH=$(get_host_path)
|
|
2569
|
+
MOUNT_ROOT=$(get_mount_root "$HOST_PATH")
|
|
1994
2570
|
|
|
1995
2571
|
# Handle subcommands
|
|
1996
2572
|
if [[ $# -gt 0 ]]; then
|
|
@@ -2067,6 +2643,7 @@ Environment Variables:
|
|
|
2067
2643
|
DCLAUDE_NAMESPACE Namespace for isolated credentials/config
|
|
2068
2644
|
DCLAUDE_GIT_AUTH SSH auth for Git: auto, agent-forwarding, key-mount, none
|
|
2069
2645
|
DCLAUDE_NETWORK Network mode: auto, host, bridge
|
|
2646
|
+
DCLAUDE_MOUNT_ROOT Mount parent directory (absolute or relative path)
|
|
2070
2647
|
DCLAUDE_DOCKER_SOCKET Override Docker socket path
|
|
2071
2648
|
DCLAUDE_TMUX_SESSION Custom tmux session name (default: claude-TIMESTAMP)
|
|
2072
2649
|
DCLAUDE_CHROME_BIN Chrome executable path (auto-detected if not set)
|
|
@@ -2079,6 +2656,8 @@ Configuration File (.dclaude):
|
|
|
2079
2656
|
NAMESPACE=mycompany
|
|
2080
2657
|
NETWORK=host
|
|
2081
2658
|
DEBUG=true
|
|
2659
|
+
MOUNT_ROOT=. # Mount config file's directory
|
|
2660
|
+
MOUNT_ROOT=.. # Mount parent of config file's directory
|
|
2082
2661
|
dclaude walks up the directory tree to find .dclaude files.
|
|
2083
2662
|
Environment variables override .dclaude settings.
|
|
2084
2663
|
|
|
@@ -2102,6 +2681,11 @@ Examples:
|
|
|
2102
2681
|
DCLAUDE_NAMESPACE=mycompany dclaude
|
|
2103
2682
|
# Or create .dclaude file: echo "NAMESPACE=mycompany" > ~/projects/mycompany/.dclaude
|
|
2104
2683
|
|
|
2684
|
+
# Mount parent directory to access sibling directories
|
|
2685
|
+
DCLAUDE_MOUNT_ROOT=.. dclaude # Relative path
|
|
2686
|
+
DCLAUDE_MOUNT_ROOT=/Users/alan/projects dclaude # Absolute path
|
|
2687
|
+
# Or in .dclaude file: MOUNT_ROOT=..
|
|
2688
|
+
|
|
2105
2689
|
# Update image and restart container
|
|
2106
2690
|
dclaude pull # Pull latest image
|
|
2107
2691
|
dclaude update # Update Claude CLI in container
|