@kokorolx/ai-sandbox-wrapper 1.1.2 โ†’ 1.3.0

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 CHANGED
@@ -252,6 +252,50 @@ echo '/path/to/project' >> ~/.ai-workspaces
252
252
  cat ~/.ai-workspaces
253
253
  ```
254
254
 
255
+ ### Network Configuration
256
+
257
+ AI containers can join Docker networks to communicate with other services (databases, APIs, MetaMCP).
258
+
259
+ #### Runtime Selection (Recommended)
260
+
261
+ ```bash
262
+ # Interactive network selection
263
+ ai-run opencode -n
264
+
265
+ # Direct network specification
266
+ ai-run opencode -n metamcp_metamcp-network
267
+ ai-run opencode -n network1,network2,network3
268
+ ```
269
+
270
+ #### Saved Configuration
271
+
272
+ Network selections are saved to `~/.ai-sandbox/config.json`:
273
+ - **Per-workspace**: Saved for specific project directories
274
+ - **Global**: Default for all workspaces
275
+
276
+ ```bash
277
+ # View current config
278
+ cat ~/.ai-sandbox/config.json
279
+
280
+ # Example config structure
281
+ {
282
+ "version": 1,
283
+ "networks": {
284
+ "global": ["shared-services"],
285
+ "workspaces": {
286
+ "/Users/you/projects/my-app": ["my-app_default", "redis_network"]
287
+ }
288
+ }
289
+ }
290
+ ```
291
+
292
+ #### Without `-n` Flag
293
+
294
+ When running without the flag, saved networks are used silently:
295
+ - Workspace-specific config takes priority
296
+ - Falls back to global config
297
+ - Non-existent networks are skipped silently
298
+
255
299
  ### Environment Variables
256
300
 
257
301
  All environment variables are configured in `~/.ai-env` or passed at runtime:
@@ -588,6 +632,62 @@ source ~/.zshrc
588
632
 
589
633
  # Update to latest version
590
634
  npx @kokorolx/ai-sandbox-wrapper@latest setup
635
+
636
+ # Clean up caches and configs
637
+ npx @kokorolx/ai-sandbox-wrapper clean
638
+ ```
639
+
640
+ ### Cleanup Command
641
+
642
+ The `clean` command provides an interactive way to remove AI Sandbox directories:
643
+
644
+ ```bash
645
+ npx @kokorolx/ai-sandbox-wrapper clean
646
+ ```
647
+
648
+ **Features:**
649
+ - Two-level menu: First select category, then specific tools/items
650
+ - Shows directory sizes before deletion
651
+ - Groups items by risk level (๐ŸŸข Safe, ๐ŸŸก Medium, ๐Ÿ”ด Critical)
652
+ - Requires typing "yes" to confirm deletion
653
+
654
+ **Categories:**
655
+ | Category | Contents | Risk |
656
+ |----------|----------|------|
657
+ | Tool caches | `~/.ai-cache/{tool}/` | ๐ŸŸข Safe to delete |
658
+ | Tool configs | `~/.ai-home/{tool}/` | ๐ŸŸก Loses settings |
659
+ | Global config | `~/.ai-sandbox/`, `~/.ai-workspaces`, `~/.ai-env`, etc. | ๐ŸŸก๐Ÿ”ด Mixed |
660
+
661
+ **Example:**
662
+ ```
663
+ ๐Ÿงน AI Sandbox Cleanup
664
+
665
+ What would you like to clean?
666
+ 1. Tool caches (~/.ai-cache/) - Safe to delete
667
+ 2. Tool configs (~/.ai-home/) - Loses settings
668
+ 3. Global config files - Loses preferences
669
+ 4. All of the above
670
+
671
+ Enter selection (or 'q' to quit): 1
672
+
673
+ ๐Ÿ“ Tool Caches (~/.ai-cache/)
674
+
675
+ Select tools to clear:
676
+ 1. claude/ (45.2 MB)
677
+ 2. opencode/ (120.5 MB)
678
+
679
+ Enter selection (comma-separated, 'all', or 'b' to go back): 1
680
+
681
+ You are about to delete:
682
+ - ~/.ai-cache/claude/ (45.2 MB)
683
+
684
+ Total: 45.2 MB
685
+
686
+ Type 'yes' to confirm: yes
687
+
688
+ โœ“ Deleted ~/.ai-cache/claude/
689
+
690
+ Deleted 1 items, freed 45.2 MB
591
691
  ```
592
692
 
593
693
  ## ๐Ÿค Contributing
@@ -596,4 +696,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
596
696
 
597
697
  ## ๐Ÿ“ License
598
698
 
599
- MIT
699
+ MIT
package/bin/ai-run CHANGED
@@ -6,6 +6,8 @@ shift
6
6
 
7
7
  # Parse flags
8
8
  SHELL_MODE=false
9
+ NETWORK_FLAG=false
10
+ NETWORK_ARG=""
9
11
  TOOL_ARGS=()
10
12
 
11
13
  while [[ $# -gt 0 ]]; do
@@ -14,6 +16,15 @@ while [[ $# -gt 0 ]]; do
14
16
  SHELL_MODE=true
15
17
  shift
16
18
  ;;
19
+ --network|-n)
20
+ NETWORK_FLAG=true
21
+ shift
22
+ # Check if next arg is a network name (not another flag)
23
+ if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
24
+ NETWORK_ARG="$1"
25
+ shift
26
+ fi
27
+ ;;
17
28
  *)
18
29
  TOOL_ARGS+=("$1")
19
30
  shift
@@ -148,131 +159,418 @@ else
148
159
  esac
149
160
  fi
150
161
 
151
- # Git access control (opt-in per workspace)
152
- GIT_MOUNTS=""
153
- GIT_ALLOWED_FILE="$HOME/.ai-git-allowed"
154
- GIT_CACHE_DIR="$HOME/.ai-cache/git"
155
- touch "$GIT_ALLOWED_FILE"
162
+ # ============================================================================
163
+ # NETWORK CONFIGURATION
164
+ # ============================================================================
156
165
 
157
- # Network configuration for Docker network access
158
- NETWORK_FILE="$HOME/.ai-networks"
159
- NETWORK_MOUNTS=""
160
- NETWORK_OPTIONS=""
161
- HOST_ACCESS_ARGS=""
162
- METAMCP_JOINED=false
166
+ AI_SANDBOX_CONFIG="$HOME/.ai-sandbox/config.json"
163
167
 
164
- # Read configured networks (with deduplication)
165
- if [[ -f "$NETWORK_FILE" ]]; then
166
- while IFS= read -r network; do
167
- [ -z "$network" ] && continue
168
-
169
- # Skip if already added to NETWORK_OPTIONS (deduplication)
170
- if [[ "$NETWORK_OPTIONS" == *"--network $network"* ]]; then
171
- continue
168
+ # Ensure jq is available (fallback to basic parsing if not)
169
+ has_jq() {
170
+ command -v jq &>/dev/null
171
+ }
172
+
173
+ # Initialize config file if it doesn't exist
174
+ init_config() {
175
+ mkdir -p "$(dirname "$AI_SANDBOX_CONFIG")"
176
+ if [[ ! -f "$AI_SANDBOX_CONFIG" ]]; then
177
+ echo '{"version":1,"networks":{"global":[],"workspaces":{}}}' > "$AI_SANDBOX_CONFIG"
178
+ chmod 600 "$AI_SANDBOX_CONFIG"
179
+ fi
180
+ }
181
+
182
+ # Read networks for current workspace (workspace > global > empty)
183
+ read_network_config() {
184
+ init_config
185
+ local workspace="$CURRENT_DIR"
186
+
187
+ if has_jq; then
188
+ # Try workspace-specific first
189
+ local ws_networks=$(jq -r --arg ws "$workspace" '.networks.workspaces[$ws] // empty | .[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null)
190
+ if [[ -n "$ws_networks" ]]; then
191
+ echo "$ws_networks"
192
+ return
172
193
  fi
194
+ # Fall back to global
195
+ jq -r '.networks.global[]?' "$AI_SANDBOX_CONFIG" 2>/dev/null
196
+ else
197
+ # Fallback: grep-based parsing (basic)
198
+ grep -o '"global":\s*\[[^]]*\]' "$AI_SANDBOX_CONFIG" 2>/dev/null | grep -o '"[^"]*"' | tr -d '"' | grep -v global
199
+ fi
200
+ }
201
+
202
+ # Write networks to config (scope: workspace or global)
203
+ write_network_config() {
204
+ local scope="$1" # "workspace" or "global"
205
+ shift
206
+ local networks=("$@")
207
+
208
+ init_config
209
+
210
+ if has_jq; then
211
+ local json_array=$(printf '%s\n' "${networks[@]}" | jq -R . | jq -s .)
173
212
 
174
- # Check if network exists
175
- if docker network inspect "$network" >/dev/null 2>&1; then
176
- NETWORK_OPTIONS="$NETWORK_OPTIONS --network $network"
177
-
178
- # Check if this network requires host access
179
- if [[ "$network" == *"metamcp"* ]]; then
180
- # Add host.docker.internal for host service access (Linux requires --add-host)
181
- # Docker Desktop on Mac/Windows has this by default
182
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
183
- METAMCP_JOINED=true
184
- fi
213
+ if [[ "$scope" == "workspace" ]]; then
214
+ jq --arg ws "$CURRENT_DIR" --argjson nets "$json_array" \
215
+ '.networks.workspaces[$ws] = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
216
+ && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
185
217
  else
186
- echo "โš ๏ธ Network '$network' not found (may have been removed)"
218
+ jq --argjson nets "$json_array" \
219
+ '.networks.global = $nets' "$AI_SANDBOX_CONFIG" > "$AI_SANDBOX_CONFIG.tmp" \
220
+ && mv "$AI_SANDBOX_CONFIG.tmp" "$AI_SANDBOX_CONFIG"
187
221
  fi
188
- done < "$NETWORK_FILE"
189
- fi
222
+ chmod 600 "$AI_SANDBOX_CONFIG"
223
+ else
224
+ echo "โš ๏ธ jq not found. Please install jq to save network configuration."
225
+ return 1
226
+ fi
227
+ }
190
228
 
191
- # Check if we should prompt about detected MetaMCP network
192
- # Only prompt if: network exists AND not already joined AND interactive mode
193
- if [[ "$METAMCP_JOINED" != "true" ]] && docker network inspect "metamcp_metamcp-network" >/dev/null 2>&1; then
194
- if [[ -t 0 ]]; then
195
- # Interactive arrow-key menu
196
- cursor=0
197
- options=("Join network (container-to-container)" "Use host.docker.internal (host access)")
229
+ # Validate networks exist, return only valid ones
230
+ validate_networks() {
231
+ local networks=("$@")
232
+ local valid=()
233
+
234
+ for net in "${networks[@]}"; do
235
+ [[ -z "$net" ]] && continue
236
+ if docker network inspect "$net" &>/dev/null; then
237
+ valid+=("$net")
238
+ fi
239
+ done
240
+
241
+ printf '%s\n' "${valid[@]}"
242
+ }
243
+
244
+ # Discover Docker Compose networks (have com.docker.compose.project label)
245
+ discover_compose_networks() {
246
+ docker network ls --filter "label=com.docker.compose.project" --format "{{.Name}}" 2>/dev/null | sort
247
+ }
248
+
249
+ # Discover custom networks (not system, not compose)
250
+ discover_custom_networks() {
251
+ local compose_nets=$(discover_compose_networks)
252
+ docker network ls --format "{{.Name}}" 2>/dev/null | while read -r net; do
253
+ # Skip system networks
254
+ [[ "$net" == "bridge" || "$net" == "host" || "$net" == "none" ]] && continue
255
+ # Skip compose networks
256
+ echo "$compose_nets" | grep -q "^${net}$" && continue
257
+ echo "$net"
258
+ done | sort
259
+ }
260
+
261
+ # Get containers in a network
262
+ get_network_containers() {
263
+ local network="$1"
264
+ docker network inspect "$network" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | xargs | tr ' ' ', '
265
+ }
266
+
267
+ # Interactive network selection menu (multi-select)
268
+ show_network_menu() {
269
+ local compose_nets=()
270
+ local custom_nets=()
271
+ local all_nets=()
272
+ local selected=()
273
+
274
+ echo "๐Ÿ” Discovering Docker networks..."
275
+ echo ""
276
+
277
+ # Gather networks
278
+ while IFS= read -r net; do
279
+ [[ -n "$net" ]] && compose_nets+=("$net")
280
+ done < <(discover_compose_networks)
281
+
282
+ while IFS= read -r net; do
283
+ [[ -n "$net" ]] && custom_nets+=("$net")
284
+ done < <(discover_custom_networks)
285
+
286
+ # Build combined list: select-all first, then compose, then custom, then "none"
287
+ # Index layout: [0]=select-all, [1..compose_count]=compose, [compose_count+1..custom_end]=custom, [last]=none
288
+ local network_count=$((${#compose_nets[@]} + ${#custom_nets[@]}))
289
+ all_nets=("select-all" "${compose_nets[@]}" "${custom_nets[@]}" "none")
290
+
291
+ if [[ $network_count -eq 0 ]]; then
292
+ echo "โ„น๏ธ No Docker networks found (besides system networks)."
293
+ SELECTED_NETWORKS=()
294
+ return 0
295
+ fi
296
+
297
+ # Initialize selection array (none is pre-selected)
298
+ local sel=()
299
+ local select_all_idx=0
300
+ local compose_start=1
301
+ local compose_end=$((1 + ${#compose_nets[@]}))
302
+ local custom_start=$compose_end
303
+ local none_idx=$((${#all_nets[@]} - 1))
304
+
305
+ for ((i=0; i<${#all_nets[@]}; i++)); do
306
+ if [[ "${all_nets[$i]}" == "none" ]]; then
307
+ sel[$i]=1
308
+ else
309
+ sel[$i]=0
310
+ fi
311
+ done
312
+
313
+ local cursor=0
314
+
315
+ tput civis
316
+ trap 'tput cnorm' INT TERM EXIT
317
+
318
+ while true; do
319
+ clear
320
+ echo "๐Ÿ”— Network Selection"
321
+ echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
322
+ echo ""
198
323
 
199
- tput civis
200
- trap 'tput cnorm; exit' INT TERM
324
+ # Select All option
325
+ local check="[ ]"
326
+ [[ ${sel[$select_all_idx]} -eq 1 ]] && check="[x]"
327
+ if [[ $cursor -eq $select_all_idx ]]; then
328
+ tput setaf 6
329
+ printf " โž” %s Select All\n" "$check"
330
+ else
331
+ printf " %s Select All\n" "$check"
332
+ fi
333
+ tput sgr0
334
+ echo ""
201
335
 
202
- while true; do
203
- clear
204
- echo "๐Ÿ”— MetaMCP Network Configuration"
205
- echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
206
- echo "A Docker network 'metamcp_metamcp-network' was detected."
207
- echo ""
208
- echo "Choose how AI tools should access MetaMCP:"
336
+ # Compose Networks section
337
+ if [[ ${#compose_nets[@]} -gt 0 ]]; then
338
+ echo "Compose Networks:"
339
+ for ((i=compose_start; i<compose_end; i++)); do
340
+ local net="${all_nets[$i]}"
341
+ local containers=$(get_network_containers "$net")
342
+ local check="[ ]"
343
+ [[ ${sel[$i]} -eq 1 ]] && check="[x]"
344
+
345
+ if [[ $i -eq $cursor ]]; then
346
+ tput setaf 6
347
+ printf " โž” %s %-30s" "$check" "$net"
348
+ else
349
+ printf " %s %-30s" "$check" "$net"
350
+ fi
351
+
352
+ if [[ -n "$containers" ]]; then
353
+ tput setaf 8
354
+ printf " (%s)" "$containers"
355
+ fi
356
+ tput sgr0
357
+ echo ""
358
+ done
209
359
  echo ""
210
-
211
- for i in "${!options[@]}"; do
212
- if [ "$i" -eq "$cursor" ]; then
213
- prefix="โž” "
360
+ fi
361
+
362
+ # Custom Networks section
363
+ if [[ ${#custom_nets[@]} -gt 0 ]]; then
364
+ echo "Other Networks:"
365
+ for ((i=custom_start; i<none_idx; i++)); do
366
+ local net="${all_nets[$i]}"
367
+ local containers=$(get_network_containers "$net")
368
+ local check="[ ]"
369
+ [[ ${sel[$i]} -eq 1 ]] && check="[x]"
370
+
371
+ if [[ $i -eq $cursor ]]; then
214
372
  tput setaf 6
373
+ printf " โž” %s %-30s" "$check" "$net"
215
374
  else
216
- prefix=" "
375
+ printf " %s %-30s" "$check" "$net"
217
376
  fi
218
377
 
219
- printf "%s %s\n" "$prefix" "${options[$i]}"
378
+ if [[ -n "$containers" ]]; then
379
+ tput setaf 8
380
+ printf " (%s)" "$containers"
381
+ fi
220
382
  tput sgr0
383
+ echo ""
221
384
  done
222
-
223
385
  echo ""
224
- echo "Use ARROWS to move, ENTER to select"
225
-
226
- IFS= read -rsn1 key
227
- if [[ "$key" == $'\x1b' ]]; then
228
- read -rsn1 -t 1 next1
229
- read -rsn1 -t 1 next2
230
- case "$next1$next2" in
231
- '[A') ((cursor--)) ;;
232
- '[B') ((cursor++)) ;;
233
- esac
386
+ fi
387
+
388
+ # None option
389
+ echo ""
390
+ local check="[ ]"
391
+ [[ ${sel[$none_idx]} -eq 1 ]] && check="[x]"
392
+ if [[ $cursor -eq $none_idx ]]; then
393
+ tput setaf 6
394
+ printf " โž” %s None (no network)\n" "$check"
395
+ else
396
+ printf " %s None (no network)\n" "$check"
397
+ fi
398
+ tput sgr0
399
+
400
+ echo ""
401
+ echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
402
+ echo "โ†‘โ†“ Move SPACE Select a Select All ENTER Confirm"
403
+
404
+ # Read input
405
+ IFS= read -rsn1 key
406
+ if [[ "$key" == $'\x1b' ]]; then
407
+ read -rsn2 -t 1 escape_seq
408
+ case "$escape_seq" in
409
+ '[A') ((cursor > 0)) && ((cursor--)) ;;
410
+ '[B') ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) ;;
411
+ esac
412
+ else
413
+ case "$key" in
414
+ ' ')
415
+ # Toggle selection
416
+ if [[ $cursor -eq $select_all_idx ]]; then
417
+ # Toggle select all
418
+ if [[ ${sel[$select_all_idx]} -eq 1 ]]; then
419
+ # Deselect all, select none
420
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
421
+ sel[$none_idx]=1
422
+ else
423
+ # Select all networks, deselect none
424
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
425
+ sel[$none_idx]=0
426
+ fi
427
+ elif [[ $cursor -eq $none_idx ]]; then
428
+ # Selecting "none" clears all others
429
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=0; done
430
+ sel[$none_idx]=1
431
+ else
432
+ # Selecting a network clears "none" and updates select-all state
433
+ sel[$none_idx]=0
434
+ if [[ ${sel[$cursor]} -eq 1 ]]; then
435
+ sel[$cursor]=0
436
+ sel[$select_all_idx]=0
437
+ else
438
+ sel[$cursor]=1
439
+ # Check if all networks are now selected
440
+ local all_selected=1
441
+ for ((i=compose_start; i<none_idx; i++)); do
442
+ [[ ${sel[$i]} -eq 0 ]] && all_selected=0 && break
443
+ done
444
+ sel[$select_all_idx]=$all_selected
445
+ fi
446
+ fi
447
+ ;;
448
+ 'a'|'A')
449
+ # Quick select all
450
+ for ((i=0; i<${#all_nets[@]}; i++)); do sel[$i]=1; done
451
+ sel[$none_idx]=0
452
+ ;;
453
+ ''|$'\n'|$'\r')
454
+ break
455
+ ;;
456
+ k) ((cursor > 0)) && ((cursor--)) ;;
457
+ j) ((cursor < ${#all_nets[@]} - 1)) && ((cursor++)) ;;
458
+ esac
459
+ fi
460
+ done
461
+
462
+ tput cnorm
463
+ trap - INT TERM EXIT
464
+
465
+ # Collect selected networks (exclude select-all and none)
466
+ SELECTED_NETWORKS=()
467
+ for ((i=1; i<${#all_nets[@]}; i++)); do
468
+ if [[ ${sel[$i]} -eq 1 && "${all_nets[$i]}" != "none" && "${all_nets[$i]}" != "select-all" ]]; then
469
+ SELECTED_NETWORKS+=("${all_nets[$i]}")
470
+ fi
471
+ done
472
+ }
473
+
474
+ # Save prompt after selection
475
+ show_save_prompt() {
476
+ local options=("This workspace" "Global (all workspaces)" "Don't save")
477
+ local cursor=0
478
+
479
+ echo ""
480
+
481
+ tput civis
482
+ trap 'tput cnorm' INT TERM EXIT
483
+
484
+ while true; do
485
+ # Move cursor up to redraw (3 options + header)
486
+ tput cuu 5 2>/dev/null || true
487
+ tput ed 2>/dev/null || true
488
+
489
+ echo "Save selection?"
490
+ for ((i=0; i<${#options[@]}; i++)); do
491
+ if [[ $i -eq $cursor ]]; then
492
+ tput setaf 6
493
+ if [[ $i -eq 0 ]]; then
494
+ printf " โž” %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
495
+ else
496
+ printf " โž” %s\n" "${options[$i]}"
497
+ fi
234
498
  else
235
- case "$key" in
236
- k) ((cursor--)) ;;
237
- j) ((cursor++)) ;;
238
- "") break ;;
239
- $'\n'|$'\r') break ;;
240
- esac
499
+ if [[ $i -eq 0 ]]; then
500
+ printf " %s (%s)\n" "${options[$i]}" "$CURRENT_DIR"
501
+ else
502
+ printf " %s\n" "${options[$i]}"
503
+ fi
241
504
  fi
242
-
243
- if [ "$cursor" -lt 0 ]; then cursor=$((${#options[@]} - 1)); fi
244
- if [ "$cursor" -ge "${#options[@]}" ]; then cursor=0; fi
505
+ tput sgr0
245
506
  done
246
507
 
247
- tput cnorm
248
-
249
- if [ "$cursor" -eq 0 ]; then
250
- # Join network - but only if not already in file
251
- if ! grep -q "^metamcp_metamcp-network$" "$NETWORK_FILE" 2>/dev/null; then
252
- echo "metamcp_metamcp-network" >> "$NETWORK_FILE"
253
- fi
254
- NETWORK_OPTIONS="$NETWORK_OPTIONS --network metamcp_metamcp-network"
255
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
256
- METAMCP_JOINED=true
257
- echo ""
258
- echo "โœ… Network joined. Both host.docker.internal and MetaMCP network enabled."
508
+ IFS= read -rsn1 key
509
+ if [[ "$key" == $'\x1b' ]]; then
510
+ read -rsn2 -t 1 escape_seq
511
+ case "$escape_seq" in
512
+ '[A') ((cursor > 0)) && ((cursor--)) ;;
513
+ '[B') ((cursor < 2)) && ((cursor++)) ;;
514
+ esac
259
515
  else
260
- # Use host.docker.internal
261
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
262
- echo ""
263
- echo "โ„น๏ธ Using host.docker.internal only. MetaMCP accessible at localhost:12008 on host."
516
+ case "$key" in
517
+ ''|$'\n'|$'\r') break ;;
518
+ k) ((cursor > 0)) && ((cursor--)) ;;
519
+ j) ((cursor < 2)) && ((cursor++)) ;;
520
+ esac
521
+ fi
522
+ done
523
+
524
+ tput cnorm
525
+ trap - INT TERM EXIT
526
+
527
+ SAVE_CHOICE=$cursor # 0=workspace, 1=global, 2=don't save
528
+ }
529
+
530
+ # Network configuration
531
+ NETWORK_OPTIONS=""
532
+ HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
533
+ SELECTED_NETWORKS=()
534
+
535
+ if [[ "$NETWORK_FLAG" == "true" ]]; then
536
+ if [[ -n "$NETWORK_ARG" ]]; then
537
+ # Direct specification: -n net1,net2
538
+ IFS=',' read -ra SELECTED_NETWORKS <<< "$NETWORK_ARG"
539
+ elif [[ -t 0 ]]; then
540
+ # Interactive mode: show menu
541
+ show_network_menu
542
+
543
+ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
544
+ show_save_prompt
545
+ case $SAVE_CHOICE in
546
+ 0) write_network_config "workspace" "${SELECTED_NETWORKS[@]}" && echo "โœ… Saved for this workspace" ;;
547
+ 1) write_network_config "global" "${SELECTED_NETWORKS[@]}" && echo "โœ… Saved globally" ;;
548
+ *) echo "โ„น๏ธ Using for this session only" ;;
549
+ esac
264
550
  fi
265
- echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”"
266
- echo ""
267
551
  else
268
- # Non-interactive: just use host.docker.internal
269
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
552
+ echo "โš ๏ธ Non-interactive mode. Use -n network1,network2 to specify networks."
270
553
  fi
271
- elif [[ "$METAMCP_JOINED" != "true" ]]; then
272
- # No network, but ensure host.docker.internal is available
273
- HOST_ACCESS_ARGS="--add-host=host.docker.internal:host-gateway"
554
+ else
555
+ # No flag: use saved config silently
556
+ while IFS= read -r net; do
557
+ [[ -n "$net" ]] && SELECTED_NETWORKS+=("$net")
558
+ done < <(read_network_config)
274
559
  fi
275
560
 
561
+ # Validate and build network options
562
+ if [[ ${#SELECTED_NETWORKS[@]} -gt 0 ]]; then
563
+ while IFS= read -r net; do
564
+ [[ -n "$net" ]] && NETWORK_OPTIONS="$NETWORK_OPTIONS --network $net"
565
+ done < <(validate_networks "${SELECTED_NETWORKS[@]}")
566
+ fi
567
+
568
+ # Git access control (opt-in per workspace)
569
+ GIT_MOUNTS=""
570
+ GIT_ALLOWED_FILE="$HOME/.ai-git-allowed"
571
+ GIT_CACHE_DIR="$HOME/.ai-cache/git"
572
+ touch "$GIT_ALLOWED_FILE"
573
+
276
574
  # Check if Git access is allowed for this workspace
277
575
  if grep -q "^$CURRENT_DIR$" "$GIT_ALLOWED_FILE" 2>/dev/null; then
278
576
  # Previously allowed for this workspace