@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 +101 -1
- package/bin/ai-run +396 -98
- package/bin/cli.js +452 -0
- package/dockerfiles/base/Dockerfile +34 -2
- package/lib/install-base.sh +12 -5
- package/package.json +1 -1
- package/setup.sh +0 -60
- package/bin/ai-network +0 -144
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
|
-
#
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
GIT_CACHE_DIR="$HOME/.ai-cache/git"
|
|
155
|
-
touch "$GIT_ALLOWED_FILE"
|
|
162
|
+
# ============================================================================
|
|
163
|
+
# NETWORK CONFIGURATION
|
|
164
|
+
# ============================================================================
|
|
156
165
|
|
|
157
|
-
|
|
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
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
echo "
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
375
|
+
printf " %s %-30s" "$check" "$net"
|
|
217
376
|
fi
|
|
218
377
|
|
|
219
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
""
|
|
239
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
# No
|
|
273
|
-
|
|
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
|