claude-cac 1.5.4 → 1.5.6

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/cac +144 -25
  2. package/package.json +1 -1
package/cac CHANGED
@@ -11,7 +11,7 @@ VERSIONS_DIR="$CAC_DIR/versions"
11
11
  # ── utils: colors, read/write, UUID, proxy parsing ───────────────────────
12
12
 
13
13
  # shellcheck disable=SC2034 # used in build-concatenated cac script
14
- CAC_VERSION="1.5.4"
14
+ CAC_VERSION="1.5.6"
15
15
 
16
16
  _read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
17
17
  _die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
@@ -58,6 +58,19 @@ _gen_uuid() {
58
58
  _new_uuid() { _gen_uuid | tr '[:lower:]' '[:upper:]'; }
59
59
  _new_user_id() { python3 -c "import os; print(os.urandom(32).hex())" || _die "python3 required"; }
60
60
  _new_machine_id() { _gen_uuid | tr -d '-' | tr '[:upper:]' '[:lower:]'; }
61
+ _new_hostname_suffix() { _gen_uuid | tr -d '-' | tr '[:lower:]' '[:upper:]' | cut -c1-5; }
62
+ _detect_hostname_platform() {
63
+ local os; os=$(_detect_os)
64
+ if [[ "$os" == "linux" ]]; then
65
+ if [[ -n "${WSL_DISTRO_NAME:-}" ]] || [[ -n "${WSL_INTEROP:-}" ]] || \
66
+ grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null || \
67
+ uname -r 2>/dev/null | grep -qi microsoft; then
68
+ echo "windows"
69
+ return
70
+ fi
71
+ fi
72
+ echo "$os"
73
+ }
61
74
  _new_hostname() {
62
75
  local -a _first_names=(
63
76
  "James" "John" "Robert" "Michael" "William" "David" "Richard" "Joseph"
@@ -68,10 +81,25 @@ _new_hostname() {
68
81
  "Liam" "Noah" "Oliver" "Elijah" "Lucas" "Mason" "Ethan" "Aiden"
69
82
  "Alex" "Ryan" "Tyler" "Jordan" "Taylor" "Morgan" "Casey" "Riley"
70
83
  )
71
- local -a _models=("MacBook-Pro" "MacBook-Air" "MacBook-Pro" "MacBook-Pro")
72
84
  local _name="${_first_names[$((RANDOM % ${#_first_names[@]}))]}"
73
- local _model="${_models[$((RANDOM % ${#_models[@]}))]}"
74
- echo "${_name}s-${_model}.local"
85
+ local _platform; _platform=$(_detect_hostname_platform)
86
+ case "$_platform" in
87
+ macos)
88
+ local -a _models=("MacBook-Pro" "MacBook-Air" "MacBook-Pro" "MacBook-Pro")
89
+ local _model="${_models[$((RANDOM % ${#_models[@]}))]}"
90
+ echo "${_name}s-${_model}.local"
91
+ ;;
92
+ windows)
93
+ local -a _prefixes=("DESKTOP" "LAPTOP")
94
+ local _prefix="${_prefixes[$((RANDOM % ${#_prefixes[@]}))]}"
95
+ echo "${_prefix}-$(_new_hostname_suffix)"
96
+ ;;
97
+ *)
98
+ local -a _devices=("desktop" "laptop" "workstation" "thinkpad")
99
+ local _device="${_devices[$((RANDOM % ${#_devices[@]}))]}"
100
+ echo "$(printf '%s' "$_name" | tr '[:upper:]' '[:lower:]')-${_device}"
101
+ ;;
102
+ esac
75
103
  }
76
104
  _new_mac() { printf '02:%02x:%02x:%02x:%02x:%02x' $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)); }
77
105
  _new_git_remote() { echo "https://github.com/user-$(_gen_uuid | cut -d- -f1)/project-$(_gen_uuid | cut -d- -f2).git"; }
@@ -86,11 +114,30 @@ _get_real_cmd() {
86
114
  }
87
115
 
88
116
  # host:port:user:pass → http://user:pass@host:port
89
- # or pass a full URL directly (http://, https://, socks5://)
117
+ # socks5://host:port:user:pass socks5://user:pass@host:port
118
+ # or pass a standard URL directly (http://, https://, socks5://)
90
119
  _parse_proxy() {
91
120
  local raw="$1"
92
- # Already a full URL, return as-is
121
+ local proto rest
122
+
123
+ [[ -n "$raw" ]] || return 0
124
+
125
+ # Normalize protocol-prefixed legacy form:
126
+ # socks5://host:port:user:pass -> socks5://user:pass@host:port
93
127
  if [[ "$raw" =~ ^(http|https|socks5):// ]]; then
128
+ proto="${raw%%://*}"
129
+ rest="${raw#*://}"
130
+ if [[ "$rest" != *"@"* ]] && [[ "$rest" == *:*:* ]]; then
131
+ local host port user pass
132
+ host=$(echo "$rest" | cut -d: -f1)
133
+ port=$(echo "$rest" | cut -d: -f2)
134
+ user=$(echo "$rest" | cut -d: -f3)
135
+ pass=$(echo "$rest" | cut -d: -f4-)
136
+ if [[ -n "$host" ]] && [[ -n "$port" ]] && [[ -n "$user" ]]; then
137
+ echo "${proto}://${user}:${pass}@${host}:${port}"
138
+ return
139
+ fi
140
+ fi
94
141
  echo "$raw"
95
142
  return
96
143
  fi
@@ -107,9 +154,24 @@ _parse_proxy() {
107
154
  fi
108
155
  }
109
156
 
157
+ # curl-based health probes should use remote DNS for SOCKS5.
158
+ # Otherwise local DNS pollution can resolve probe domains to sinkhole/test
159
+ # addresses, causing false negatives even when the proxy itself is healthy.
160
+ _curl_proxy_url() {
161
+ local normalized
162
+ normalized=$(_parse_proxy "$1")
163
+ if [[ "$normalized" =~ ^socks5:// ]]; then
164
+ echo "socks5h://${normalized#socks5://}"
165
+ else
166
+ echo "$normalized"
167
+ fi
168
+ }
169
+
110
170
  # socks5://user:pass@host:port → host:port
111
171
  _proxy_host_port() {
112
- echo "$1" | sed 's|.*@||' | sed 's|.*://||'
172
+ local normalized
173
+ normalized=$(_parse_proxy "$1")
174
+ echo "$normalized" | sed 's|.*@||' | sed 's|.*://||'
113
175
  }
114
176
 
115
177
  _proxy_reachable() {
@@ -263,10 +325,20 @@ _envs_using_version() {
263
325
  }
264
326
 
265
327
  # Elapsed time helper: call _timer_start, then _timer_elapsed
266
- _timer_start() { _TIMER_START=$(date +%s%N 2>/dev/null || date +%s); }
328
+ _time_now() {
329
+ local now
330
+ now=$(date +%s%N 2>/dev/null || true)
331
+ if [[ "$now" =~ ^[0-9]{11,}$ ]]; then
332
+ echo "$now"
333
+ else
334
+ date +%s
335
+ fi
336
+ }
337
+
338
+ _timer_start() { _TIMER_START=$(_time_now); }
267
339
  _timer_elapsed() {
268
- local now; now=$(date +%s%N 2>/dev/null || date +%s)
269
- if [[ ${#now} -gt 10 ]]; then
340
+ local now; now=$(_time_now)
341
+ if [[ "$now" =~ ^[0-9]{11,}$ && "${_TIMER_START:-}" =~ ^[0-9]{11,}$ ]]; then
270
342
  # nanoseconds available
271
343
  local ms=$(( (now - _TIMER_START) / 1000000 ))
272
344
  if [[ $ms -ge 1000 ]]; then
@@ -298,19 +370,31 @@ _detect_rc_file() {
298
370
  local shell_name
299
371
  shell_name=$(basename "${SHELL:-/bin/bash}")
300
372
  case "$shell_name" in
373
+ fish)
374
+ echo "$HOME/.config/fish/config.fish"
375
+ return
376
+ ;;
301
377
  zsh)
302
- [[ -f "$HOME/.zshrc" ]] && { echo "$HOME/.zshrc"; return; }
378
+ echo "$HOME/.zshrc"
379
+ return
303
380
  ;;
304
381
  bash)
305
382
  [[ -f "$HOME/.bashrc" ]] && { echo "$HOME/.bashrc"; return; }
306
383
  [[ -f "$HOME/.bash_profile" ]] && { echo "$HOME/.bash_profile"; return; }
384
+ echo "$HOME/.bashrc"
385
+ return
307
386
  ;;
308
387
  esac
309
388
  # Fallback: try common rc files
310
389
  [[ -f "$HOME/.bashrc" ]] && { echo "$HOME/.bashrc"; return; }
311
390
  [[ -f "$HOME/.zshrc" ]] && { echo "$HOME/.zshrc"; return; }
312
391
  [[ -f "$HOME/.bash_profile" ]] && { echo "$HOME/.bash_profile"; return; }
313
- echo ""
392
+ [[ -f "$HOME/.config/fish/config.fish" ]] && { echo "$HOME/.config/fish/config.fish"; return; }
393
+ echo "$HOME/.bashrc"
394
+ }
395
+
396
+ _is_fish_rc() {
397
+ [[ "$1" == */.config/fish/config.fish ]]
314
398
  }
315
399
 
316
400
  _install_method() {
@@ -339,16 +423,36 @@ _write_path_to_rc() {
339
423
  return 0
340
424
  fi
341
425
 
426
+ mkdir -p "$(dirname "$rc_file")"
427
+ [[ -f "$rc_file" ]] || touch "$rc_file"
428
+
342
429
  if grep -q '# >>> cac >>>' "$rc_file" 2>/dev/null; then
343
430
  echo " ✓ PATH already exists in $rc_file, skipping"
344
431
  return 0
345
432
  fi
346
433
 
347
434
  # Compat: remove old format if present
348
- if grep -q '\.cac/bin' "$rc_file" 2>/dev/null; then
435
+ if ! _is_fish_rc "$rc_file" && grep -q '\.cac/bin' "$rc_file" 2>/dev/null; then
349
436
  _remove_path_from_rc "$rc_file"
350
437
  fi
351
438
 
439
+ if _is_fish_rc "$rc_file"; then
440
+ cat >> "$rc_file" << 'CACEOF'
441
+
442
+ # >>> cac — Claude Code Cloak >>>
443
+ fish_add_path --move --path "$HOME/.cac/bin" >/dev/null 2>&1
444
+ function cac
445
+ command cac $argv
446
+ set -l _rc $status
447
+ fish_add_path --move --path "$HOME/.cac/bin" >/dev/null 2>&1
448
+ return $_rc
449
+ end
450
+ # <<< cac — Claude Code Cloak <<<
451
+ CACEOF
452
+ echo " ✓ PATH written to $rc_file"
453
+ return 0
454
+ fi
455
+
352
456
  cat >> "$rc_file" << 'CACEOF'
353
457
 
354
458
  # >>> cac — Claude Code Cloak >>>
@@ -1144,13 +1248,26 @@ fi
1144
1248
  PROXY=""
1145
1249
  if [[ -f "$_env_dir/proxy" ]]; then
1146
1250
  PROXY=$(tr -d '[:space:]' < "$_env_dir/proxy")
1251
+ if [[ "$PROXY" =~ ^(http|https|socks5):// ]]; then
1252
+ _proto="${PROXY%%://*}"
1253
+ _rest="${PROXY#*://}"
1254
+ if [[ "$_rest" != *"@"* ]] && [[ "$_rest" == *:*:* ]]; then
1255
+ _phost=$(echo "$_rest" | cut -d: -f1)
1256
+ _pport=$(echo "$_rest" | cut -d: -f2)
1257
+ _puser=$(echo "$_rest" | cut -d: -f3)
1258
+ _ppass=$(echo "$_rest" | cut -d: -f4-)
1259
+ if [[ -n "$_phost" ]] && [[ -n "$_pport" ]] && [[ -n "$_puser" ]]; then
1260
+ PROXY="${_proto}://${_puser}:${_ppass}@${_phost}:${_pport}"
1261
+ fi
1262
+ fi
1263
+ fi
1147
1264
  fi
1148
1265
 
1149
1266
  if [[ -n "$PROXY" ]]; then
1150
1267
  # pre-flight: proxy connectivity (pure bash, no fork)
1151
1268
  _hp="${PROXY##*@}"; _hp="${_hp##*://}"
1152
1269
  _host="${_hp%%:*}"
1153
- _port="${_hp##*:}"
1270
+ _port=$(echo "$_hp" | cut -d: -f2)
1154
1271
  if ! (echo >/dev/tcp/"$_host"/"$_port") 2>/dev/null; then
1155
1272
  echo "[cac] error: [$_name] proxy $_hp unreachable, refusing to start." >&2
1156
1273
  echo "[cac] hint: run 'cac check' to diagnose, or 'cac stop' to disable temporarily" >&2
@@ -1700,7 +1817,7 @@ _env_cmd_create() {
1700
1817
  esac
1701
1818
  done
1702
1819
 
1703
- [[ -n "$name" ]] || _die "usage: cac env create <name> [-p <proxy>] [-c <version>] [--telemetry <mode>] [--persona <preset>]"
1820
+ [[ -n "$name" ]] || _die "usage: cac env create <name> [-p <proxy>] [-c <version>] [--clone [source]] [--no-link] [--telemetry <mode>] [--persona <preset>]"
1704
1821
  [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]] || _die "invalid name '$name' (use alphanumeric, dash, underscore)"
1705
1822
 
1706
1823
  local env_dir="$ENVS_DIR/$name"
@@ -1733,7 +1850,7 @@ _env_cmd_create() {
1733
1850
  if [[ -n "$proxy_url" ]]; then
1734
1851
  printf " $(_dim "Detecting timezone ...") "
1735
1852
  local ip_info
1736
- ip_info=$(curl -s --proxy "$proxy_url" --connect-timeout 8 "http://ip-api.com/json/?fields=timezone,countryCode" 2>/dev/null || true)
1853
+ ip_info=$(curl -s --proxy "$(_curl_proxy_url "$proxy_url")" --connect-timeout 8 "http://ip-api.com/json/?fields=timezone,countryCode" 2>/dev/null || true)
1737
1854
  if [[ -n "$ip_info" ]]; then
1738
1855
  local detected_tz country_code
1739
1856
  read -r detected_tz country_code < <(echo "$ip_info" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('timezone',''), d.get('countryCode',''))" 2>/dev/null || echo "")
@@ -1891,7 +2008,7 @@ _env_cmd_ls() {
1891
2008
  [[ -d "$env_dir" ]] || continue
1892
2009
  names+=("$(basename "$env_dir")")
1893
2010
  versions+=("$(_read "$env_dir/version" "system")")
1894
- local p; p=$(_read "$env_dir/proxy" "")
2011
+ local p; p=$(_parse_proxy "$(_read "$env_dir/proxy" "")")
1895
2012
  if [[ -n "$p" ]] && [[ "$p" == *"://"*"@"* ]]; then
1896
2013
  p=$(echo "$p" | sed 's|://[^@]*@|://***@|')
1897
2014
  fi
@@ -2098,8 +2215,9 @@ cmd_env() {
2098
2215
  echo
2099
2216
  echo " $(_bold "cac env") — environment management"
2100
2217
  echo
2101
- echo " $(_green "create") <name> [-p proxy] [-c ver] [--telemetry mode] [--persona preset]"
2218
+ echo " $(_green "create") <name> [-p proxy] [-c ver] [--clone [source]] [--no-link] [--telemetry mode] [--persona preset]"
2102
2219
  echo " Create isolated environment (auto-activates)"
2220
+ echo " --clone inherits commands/hooks/skills/plugins; --no-link copies instead of symlink"
2103
2221
  echo " $(_green "set") [name] <key> <value> Modify environment"
2104
2222
  echo " proxy, version, telemetry, or persona"
2105
2223
  echo " $(_green "ls") List all environments"
@@ -2126,7 +2244,7 @@ cmd_env() {
2126
2244
  _relay_start() {
2127
2245
  local name="${1:-$(_current_env)}"
2128
2246
  local env_dir="$ENVS_DIR/$name"
2129
- local proxy; proxy=$(_read "$env_dir/proxy")
2247
+ local proxy; proxy=$(_parse_proxy "$(_read "$env_dir/proxy")")
2130
2248
  [[ -z "$proxy" ]] && return 1
2131
2249
 
2132
2250
  local relay_js="$CAC_DIR/relay.js"
@@ -2289,7 +2407,7 @@ cmd_relay() {
2289
2407
 
2290
2408
  # --route flag: add direct route
2291
2409
  if [[ "$flag" == "--route" ]]; then
2292
- local proxy; proxy=$(_read "$env_dir/proxy")
2410
+ local proxy; proxy=$(_parse_proxy "$(_read "$env_dir/proxy")")
2293
2411
  _relay_add_route "$proxy"
2294
2412
  fi
2295
2413
 
@@ -2359,7 +2477,7 @@ cmd_check() {
2359
2477
  fi
2360
2478
 
2361
2479
  local env_dir="$ENVS_DIR/$current"
2362
- local proxy; proxy=$(_read "$env_dir/proxy" "")
2480
+ local proxy; proxy=$(_parse_proxy "$(_read "$env_dir/proxy" "")")
2363
2481
 
2364
2482
  # Resolve version
2365
2483
  local ver; ver=$(_read "$env_dir/version" "")
@@ -2545,7 +2663,7 @@ cmd_check() {
2545
2663
  for _ip_url in $_urls; do
2546
2664
  _dots="${_dots}."
2547
2665
  printf "\r · exit IP $(_dim "detecting${_dots}")"
2548
- proxy_ip=$(curl --proxy "$proxy" --connect-timeout 3 --max-time 6 "$_ip_url" 2>/dev/null || true)
2666
+ proxy_ip=$(curl --proxy "$(_curl_proxy_url "$proxy")" --connect-timeout 3 --max-time 6 "$_ip_url" 2>/dev/null || true)
2549
2667
  [[ "$proxy_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && break
2550
2668
  proxy_ip=""
2551
2669
  done
@@ -2556,7 +2674,7 @@ cmd_check() {
2556
2674
  local env_tz; env_tz=$(_read "$env_dir/tz" "")
2557
2675
  if [[ -n "$env_tz" ]] && [[ -n "$proxy_ip" ]]; then
2558
2676
  local ip_tz
2559
- ip_tz=$(curl -s --proxy "$proxy" --connect-timeout 5 "http://ip-api.com/json/$proxy_ip?fields=timezone" 2>/dev/null | \
2677
+ ip_tz=$(curl -s --proxy "$(_curl_proxy_url "$proxy")" --connect-timeout 5 "http://ip-api.com/json/$proxy_ip?fields=timezone" 2>/dev/null | \
2560
2678
  python3 -c "import sys,json; print(json.load(sys.stdin).get('timezone',''))" 2>/dev/null || true)
2561
2679
  if [[ -n "$ip_tz" ]] && [[ "$ip_tz" != "$env_tz" ]]; then
2562
2680
  echo " $(_yellow "⚠") TZ mismatch: env=$env_tz, IP=$ip_tz"
@@ -2564,7 +2682,8 @@ cmd_check() {
2564
2682
  fi
2565
2683
  fi
2566
2684
  else
2567
- printf "\r $(_green "") exit IP $(_dim "run again to detect exit IP")\033[K\n"
2685
+ printf "\r $(_red "") exit IP $(_dim "unable to verify via proxy")\033[K\n"
2686
+ problems+=("exit IP undetected via proxy")
2568
2687
  fi
2569
2688
 
2570
2689
  # TUN conflict detection
@@ -3498,7 +3617,7 @@ cmd_help() {
3498
3617
  echo
3499
3618
 
3500
3619
  echo " $(_bold "Environment")"
3501
- echo " $(_green "cac env create") <name> [-p proxy] [-c ver]"
3620
+ echo " $(_green "cac env create") <name> [-p proxy] [-c ver] [--clone [source]] [--no-link]"
3502
3621
  echo " $(_green "cac env set") [name] <key> <value> Modify environment"
3503
3622
  echo " $(_green "cac env ls") List all environments"
3504
3623
  echo " $(_green "cac env rm") <name> Remove an environment"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cac",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Isolate, protect, and manage your Claude Code — versions, environments, identity, and proxy.",
5
5
  "bin": {
6
6
  "cac": "cac"