@kokorolx/ai-sandbox-wrapper 2.7.0 → 3.0.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/bin/ai-run CHANGED
@@ -4,9 +4,10 @@ set -e
4
4
  # Show help if requested
5
5
  show_help() {
6
6
  cat << 'EOF'
7
- Usage: ai-run <tool> [options] [-- tool-args...]
7
+ Usage: ai-run [tool] [options] [-- tool-args...]
8
8
 
9
9
  Run AI tools in a secure Docker sandbox.
10
+ When no tool is specified, opens an interactive shell with all installed tools.
10
11
 
11
12
  Options:
12
13
  -s, --shell Start interactive shell instead of running tool directly
@@ -35,12 +36,13 @@ OpenCode Server Authentication (web/serve mode only):
35
36
  Precedence: --password > --password-env > OPENCODE_SERVER_PASSWORD env > interactive prompt
36
37
 
37
38
  Examples:
39
+ ai-run # Open interactive shell with all tools
38
40
  ai-run claude # Run Claude in sandbox
39
41
  ai-run opencode web -e 4096 # Run OpenCode web with port exposed
40
42
  ai-run opencode web -p secret # Run with password
41
43
  ai-run opencode --shell # Start shell, run tool manually
42
44
  ai-run aider -n mynetwork # Connect to Docker network
43
- ai-run opencode --git-fetch # Git fetch only (no push)
45
+ ai-run opencode --git-fetch # Git fetch only (no push)
44
46
 
45
47
  Documentation: https://github.com/kokorolx/ai-sandbox-wrapper
46
48
  EOF
@@ -126,11 +128,26 @@ if [[ "${1:-}" == "--help-env" ]]; then
126
128
  show_help_env
127
129
  fi
128
130
 
129
- TOOL="$1"
130
- shift 2>/dev/null || { echo " ERROR: No tool specified. Use 'ai-run --help' for usage."; exit 1; }
131
+ TOOL="${1:-}"
132
+ if [[ -n "$TOOL" && ! "$TOOL" =~ ^- ]]; then
133
+ shift
134
+ else
135
+ TOOL=""
136
+ fi
137
+
138
+ # Handle no tool specified
139
+ if [[ -z "$TOOL" ]]; then
140
+ if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
141
+ echo "❌ ERROR: No tool specified and no TTY available."
142
+ echo " Use: ai-run <tool> [args...]"
143
+ exit 1
144
+ fi
145
+ SHELL_MODE=true
146
+ fi
131
147
 
132
148
  # Parse flags
133
- SHELL_MODE=false
149
+ # Note: SHELL_MODE may already be true if no tool was specified
150
+ SHELL_MODE="${SHELL_MODE:-false}"
134
151
  NETWORK_FLAG=false
135
152
  NETWORK_ARG=""
136
153
  EXPOSE_ARG=""
@@ -462,7 +479,7 @@ while IFS= read -r ws; do
462
479
  done < <(read_workspaces)
463
480
 
464
481
  if [[ "$ALLOWED" != "true" ]]; then
465
- echo "⚠️ SECURITY WARNING: You are running $TOOL outside a whitelisted workspace."
482
+ echo "⚠️ SECURITY WARNING: You are running ${TOOL:-shell} outside a whitelisted workspace."
466
483
  echo " Current path: $CURRENT_DIR"
467
484
  echo ""
468
485
  echo "Allowing this path gives the AI container access to this folder."
@@ -540,17 +557,86 @@ if [[ "$(uname)" == "Darwin" ]] && [[ -t 0 ]]; then
540
557
  fi
541
558
 
542
559
  if [[ "$AI_IMAGE_SOURCE" == "registry" ]]; then
543
- IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-${TOOL}:latest"
560
+ IMAGE="registry.gitlab.com/kokorolee/ai-sandbox-wrapper/ai-sandbox:latest"
544
561
  else
545
- IMAGE="ai-${TOOL}:latest"
562
+ IMAGE="ai-sandbox:latest"
563
+ fi
564
+
565
+ # Tool validation (warn if tool not in config.json)
566
+ if [[ -n "$TOOL" ]]; then
567
+ if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
568
+ INSTALLED_TOOLS=$(jq -r '.tools.installed // [] | .[]' "$AI_SANDBOX_CONFIG" 2>/dev/null)
569
+ if [[ -n "$INSTALLED_TOOLS" ]] && ! echo "$INSTALLED_TOOLS" | grep -qx "$TOOL"; then
570
+ echo "⚠️ WARNING: Tool '$TOOL' may not be installed in the sandbox image."
571
+ echo " Installed tools: $(echo "$INSTALLED_TOOLS" | tr '\n' ', ' | sed 's/,$//')"
572
+ echo " Run setup.sh to add tools."
573
+ echo ""
546
574
  fi
575
+ fi
576
+ fi
547
577
 
548
- # V2 tool-centric paths
549
- HOME_DIR="$SANDBOX_DIR/tools/$TOOL/home"
578
+ # Shared home directory for all tools (unified image)
579
+ HOME_DIR="$SANDBOX_DIR/home"
550
580
  GIT_SHARED_DIR="$SANDBOX_DIR/shared/git"
581
+ CACHE_DIR="$SANDBOX_DIR/cache"
551
582
 
552
583
  mkdir -p "$HOME_DIR"
553
584
  mkdir -p "$GIT_SHARED_DIR"
585
+ mkdir -p "$CACHE_DIR/npm" "$CACHE_DIR/bun" "$CACHE_DIR/pip" "$CACHE_DIR/playwright-browsers"
586
+
587
+ # ============================================================================
588
+ # MIGRATION V3: Merge per-tool home directories into unified home
589
+ # ============================================================================
590
+ migrate_to_unified_home() {
591
+ local marker_file="$SANDBOX_DIR/.migrated-unified-home"
592
+
593
+ # Skip if already migrated
594
+ [[ -f "$marker_file" ]] && return 0
595
+
596
+ # Check if any per-tool home directories exist
597
+ local needs_migration=false
598
+ for tool_home in "$SANDBOX_DIR"/tools/*/home; do
599
+ if [[ -d "$tool_home" ]]; then
600
+ needs_migration=true
601
+ break
602
+ fi
603
+ done
604
+
605
+ [[ "$needs_migration" == "false" ]] && { touch "$marker_file"; return 0; }
606
+
607
+ echo "🔄 Migrating per-tool home directories to unified home..."
608
+
609
+ local migrated_count=0
610
+ for tool_home in "$SANDBOX_DIR"/tools/*/home; do
611
+ [[ -d "$tool_home" ]] || continue
612
+ local tool_name
613
+ tool_name=$(basename "$(dirname "$tool_home")")
614
+ echo " 🔄 Migrating tools/$tool_name/home/ → home/"
615
+
616
+ # Use cp -rn (no-clobber) - works on macOS and Linux
617
+ # Falls back to rsync if cp -rn fails (some older systems)
618
+ # KNOWN ISSUE: glob * doesn't match dotfiles (.nano-brain, .config, etc.)
619
+ # TODO: fix with dotglob or rsync-first approach
620
+ if cp -rn "$tool_home/"* "$HOME_DIR/" 2>/dev/null; then
621
+ ((migrated_count++))
622
+ elif command -v rsync &>/dev/null; then
623
+ rsync -a --ignore-existing "$tool_home/" "$HOME_DIR/" 2>/dev/null && ((migrated_count++))
624
+ else
625
+ echo " ⚠️ Could not migrate $tool_name (cp -rn and rsync unavailable)"
626
+ fi
627
+ done
628
+
629
+ # Create marker file
630
+ date -Iseconds > "$marker_file" 2>/dev/null || date > "$marker_file"
631
+
632
+ if [[ $migrated_count -gt 0 ]]; then
633
+ echo "✅ Migration complete! Merged $migrated_count tool home directories."
634
+ echo ""
635
+ echo "ℹ️ Old per-tool home directories preserved. Run 'npx @kokorolx/ai-sandbox-wrapper clean' to remove them."
636
+ fi
637
+ }
638
+
639
+ migrate_to_unified_home
554
640
 
555
641
  # Build volume mounts for all whitelisted workspaces
556
642
  VOLUME_MOUNTS=""
@@ -558,13 +644,47 @@ while IFS= read -r ws; do
558
644
  VOLUME_MOUNTS="$VOLUME_MOUNTS -v $ws:$ws:delegated"
559
645
  done < "$WORKSPACES_FILE"
560
646
 
561
- # Tool-specific config migration and setup
562
- # All tool configs now live in HOME_DIR (~/.ai-sandbox/home/$TOOL/)
563
- # which is mounted as /home/agent in the container
647
+ # Config mount registry for all tools
648
+ # Maps tool name to space-separated config paths (relative to $HOME)
649
+ get_tool_configs() {
650
+ case "$1" in
651
+ amp) echo ".config/amp .local/share/amp" ;;
652
+ opencode) echo ".config/opencode .local/share/opencode" ;;
653
+ claude) echo ".claude .ccs" ;;
654
+ openclaw) echo ".openclaw" ;;
655
+ droid) echo ".config/droid" ;;
656
+ qoder) echo ".config/qoder" ;;
657
+ auggie) echo ".config/auggie" ;;
658
+ codebuddy) echo ".config/codebuddy" ;;
659
+ jules) echo ".config/jules" ;;
660
+ shai) echo ".config/shai" ;;
661
+ gemini) echo ".config/gemini" ;;
662
+ aider) echo ".config/aider .aider" ;;
663
+ kilo) echo ".config/kilo" ;;
664
+ codex) echo ".config/codex" ;;
665
+ qwen) echo ".config/qwen" ;;
666
+ esac
667
+ }
668
+
669
+ # All known tools (for fallback)
670
+ ALL_KNOWN_TOOLS="amp opencode claude openclaw droid qoder auggie codebuddy jules shai gemini aider kilo codex qwen"
564
671
 
565
- # Tool-specific config persistence
566
- # Instead of copying (one-way), we now bind-mount host paths directly
567
- # to ensure changes inside the container persist to the host.
672
+ # Get list of installed tools from config.json, fallback to all known tools
673
+ get_installed_tools() {
674
+ local installed
675
+ if command -v jq &>/dev/null && [[ -f "$AI_SANDBOX_CONFIG" ]]; then
676
+ installed=$(jq -r '.tools.installed[]? // empty' "$AI_SANDBOX_CONFIG" 2>/dev/null)
677
+ fi
678
+ if [[ -z "$installed" ]]; then
679
+ # Fallback: return all known tools in registry
680
+ echo "$ALL_KNOWN_TOOLS"
681
+ else
682
+ echo "$installed"
683
+ fi
684
+ }
685
+
686
+ # Tool config persistence via bind mounts
687
+ # Bind-mount host paths directly to ensure changes persist to the host.
568
688
  TOOL_CONFIG_MOUNTS=""
569
689
 
570
690
  mount_tool_config() {
@@ -582,81 +702,45 @@ mount_tool_config() {
582
702
  fi
583
703
  }
584
704
 
585
- case "$TOOL" in
586
- amp)
587
- mount_tool_config "$HOME/.config/amp" ".config/amp"
588
- mount_tool_config "$HOME/.local/share/amp" ".local/share/amp"
589
- ;;
590
- opencode)
591
- mount_tool_config "$HOME/.config/opencode" ".config/opencode"
592
- mount_tool_config "$HOME/.local/share/opencode" ".local/share/opencode"
593
- # Bundle default skills (copy if not already present)
594
- AIRUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
595
- BUNDLED_SKILLS_DIR="$AIRUN_DIR/../skills"
596
- if [[ -d "$BUNDLED_SKILLS_DIR" ]]; then
597
- for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
598
- [[ ! -d "$skill_dir" ]] && continue
599
- skill_name=$(basename "$skill_dir")
600
- target_dir="$HOME/.config/opencode/skills/$skill_name"
601
- if [[ ! -d "$target_dir" ]]; then
602
- mkdir -p "$target_dir"
603
- cp -r "$skill_dir"* "$target_dir/" 2>/dev/null || true
604
- fi
605
- done
606
- fi
607
- ;;
608
- openclaw)
609
- mount_tool_config "$HOME/.openclaw" ".openclaw"
610
- ;;
611
- claude)
612
- mount_tool_config "$HOME/.claude" ".claude"
613
- mount_tool_config "$HOME/.ccs" ".ccs"
614
- ;;
615
- droid)
616
- mount_tool_config "$HOME/.config/droid" ".config/droid"
617
- ;;
618
- qoder)
619
- mount_tool_config "$HOME/.config/qoder" ".config/qoder"
620
- ;;
621
- auggie)
622
- mount_tool_config "$HOME/.config/auggie" ".config/auggie"
623
- ;;
624
- codebuddy)
625
- mount_tool_config "$HOME/.config/codebuddy" ".config/codebuddy"
626
- ;;
627
- jules)
628
- mount_tool_config "$HOME/.config/jules" ".config/jules"
629
- ;;
630
- shai)
631
- mount_tool_config "$HOME/.config/shai" ".config/shai"
632
- ;;
633
- gemini)
634
- mount_tool_config "$HOME/.config/gemini" ".config/gemini"
635
- ;;
636
- aider)
637
- mount_tool_config "$HOME/.config/aider" ".config/aider"
638
- mount_tool_config "$HOME/.aider" ".aider"
639
- ;;
640
- kilo)
641
- mount_tool_config "$HOME/.config/kilo" ".config/kilo"
642
- ;;
643
- codex)
644
- mount_tool_config "$HOME/.config/codex" ".config/codex"
645
- ;;
646
- qwen)
647
- mount_tool_config "$HOME/.config/qwen" ".config/qwen"
648
- ;;
649
- esac
705
+ # Mount configs for all installed tools (unified image)
706
+ for tool in $(get_installed_tools); do
707
+ for cfg_path in $(get_tool_configs "$tool"); do
708
+ mount_tool_config "$HOME/$cfg_path" "$cfg_path"
709
+ done
710
+ done
711
+
712
+ # Bundle OpenCode default skills (if opencode is installed)
713
+ if get_installed_tools | grep -qw "opencode"; then
714
+ AIRUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
715
+ BUNDLED_SKILLS_DIR="$AIRUN_DIR/../skills"
716
+ if [[ -d "$BUNDLED_SKILLS_DIR" ]]; then
717
+ for skill_dir in "$BUNDLED_SKILLS_DIR"/*/; do
718
+ [[ ! -d "$skill_dir" ]] && continue
719
+ skill_name=$(basename "$skill_dir")
720
+ target_dir="$HOME/.config/opencode/skills/$skill_name"
721
+ if [[ ! -d "$target_dir" ]]; then
722
+ mkdir -p "$target_dir"
723
+ cp -r "$skill_dir"* "$target_dir/" 2>/dev/null || true
724
+ fi
725
+ done
726
+ fi
727
+ fi
650
728
 
651
729
  # Ensure required directories exist in HOME_DIR
652
730
  mkdir -p "$HOME_DIR/.config" "$HOME_DIR/.local/share" "$HOME_DIR/.cache" "$HOME_DIR/.bun"
653
731
 
654
- # Project-level config mount (if exists)
655
- CONFIG_MOUNT=""
656
- PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
732
+ SHARED_CACHE_MOUNTS="-v $CACHE_DIR/npm:/home/agent/.npm:delegated"
733
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/bun:/home/agent/.bun/install/cache:delegated"
734
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/pip:/home/agent/.cache/pip:delegated"
735
+ SHARED_CACHE_MOUNTS="$SHARED_CACHE_MOUNTS -v $CACHE_DIR/playwright-browsers:/opt/playwright-browsers:delegated"
657
736
 
658
- if [[ -f "$PROJECT_CONFIG" ]]; then
659
- CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
737
+ # Project-level config mount (if exists and tool specified)
738
+ CONFIG_MOUNT=""
739
+ if [[ -n "$TOOL" ]]; then
740
+ PROJECT_CONFIG="$CURRENT_DIR/.$TOOL.json"
741
+ if [[ -f "$PROJECT_CONFIG" ]]; then
742
+ CONFIG_MOUNT="-v $PROJECT_CONFIG:$CURRENT_DIR/.$TOOL.json:delegated"
743
+ fi
660
744
  fi
661
745
 
662
746
  # ============================================================================
@@ -1436,7 +1520,7 @@ generate_container_name() {
1436
1520
  # Generate random 6-character suffix (hex)
1437
1521
  local random_suffix=$(openssl rand -hex 3)
1438
1522
 
1439
- echo "${TOOL}-${folder_name}-${random_suffix}"
1523
+ echo "${TOOL:-shell}-${folder_name}-${random_suffix}"
1440
1524
  }
1441
1525
 
1442
1526
  # Container name and TTY allocation
@@ -2019,15 +2103,33 @@ fi
2019
2103
 
2020
2104
  # Prepare command based on mode
2021
2105
  ENTRYPOINT_OVERRIDE=""
2022
- if [[ "$SHELL_MODE" == "true" ]]; then
2023
- # Shell mode: override entrypoint to bash and show welcome message
2106
+ if [[ -n "$TOOL" && "$SHELL_MODE" != "true" ]]; then
2107
+ # Direct tool execution: override entrypoint to the tool
2108
+ ENTRYPOINT_OVERRIDE="--entrypoint $TOOL"
2109
+ DOCKER_COMMAND=("${TOOL_ARGS[@]}")
2110
+ elif [[ "$SHELL_MODE" == "true" || -z "$TOOL" ]]; then
2111
+ # Shell mode (explicit --shell or no tool specified)
2024
2112
  ENTRYPOINT_OVERRIDE="--entrypoint bash"
2025
- DOCKER_COMMAND=(
2026
- "-c"
2027
- "echo ''; echo '🚀 AI Tool Container - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo \"Tool available: ${TOOL}\"; echo 'Run the tool: ${TOOL}'; echo 'Exit container: exit or Ctrl+D'; echo ''; echo \"Additional tools:\"; echo ' - specify (spec-kit): Spec-driven development'; echo ' - uipro (ux-ui-promax): UI/UX design intelligence'; echo ' - openspec: OpenSpec workflow'; echo ''; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2028
- )
2113
+ # Build welcome message with installed tools
2114
+ INSTALLED_TOOLS_MSG=""
2115
+ if command -v jq &>/dev/null && [[ -f "$SANDBOX_DIR/config.json" ]]; then
2116
+ INSTALLED_TOOLS_MSG=$(jq -r '.tools.installed // [] | join(", ")' "$SANDBOX_DIR/config.json" 2>/dev/null || echo "")
2117
+ fi
2118
+ if [[ -n "$TOOL" ]]; then
2119
+ # Shell mode with specific tool (ai-run claude --shell)
2120
+ DOCKER_COMMAND=(
2121
+ "-c"
2122
+ "echo ''; echo '🚀 AI Tool Container - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo \"Tool available: ${TOOL}\"; echo 'Run the tool: ${TOOL}'; echo 'Exit container: exit or Ctrl+D'; echo ''; echo \"Additional tools:\"; echo ' - specify (spec-kit): Spec-driven development'; echo ' - uipro (ux-ui-promax): UI/UX design intelligence'; echo ' - openspec: OpenSpec workflow'; echo ''; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2123
+ )
2124
+ else
2125
+ # Shell mode without tool (ai-run with no args)
2126
+ DOCKER_COMMAND=(
2127
+ "-c"
2128
+ "echo ''; echo '🚀 AI Sandbox - Interactive Shell'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; echo 'Installed tools: ${INSTALLED_TOOLS_MSG:-unknown}'; echo ''; echo 'Run any tool by name: claude, opencode, gemini, etc.'; echo 'Exit: exit or Ctrl+D'; echo ''; echo 'Enhancement tools: specify, uipro, openspec, rtk'; echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'; echo ''; exec bash"
2129
+ )
2130
+ fi
2029
2131
  else
2030
- # Direct mode: use image's default entrypoint with arguments
2132
+ # Fallback: direct mode with image's default entrypoint
2031
2133
  DOCKER_COMMAND=("${TOOL_ARGS[@]}")
2032
2134
  fi
2033
2135
 
@@ -2061,13 +2163,17 @@ fi
2061
2163
  mkdir -p "$HOME_DIR" "$GIT_SHARED_DIR"
2062
2164
  chmod -R u+w "$HOME_DIR" "$GIT_SHARED_DIR" 2>/dev/null || true
2063
2165
 
2064
- # Selective Cache Isolation (Anonymous Volumes)
2065
- # Use anonymous volumes to "hide" host-poisoned node_modules and caches
2066
- # while keeping configurations and auth data.
2067
- CACHE_MOUNTS=""
2068
- if [[ "$TOOL" == "opencode" ]]; then
2069
- # Isolate heavy caches and node_modules to prevent Bun/Node conflicts
2070
- CACHE_MOUNTS="-v /home/agent/.npm -v /home/agent/.cache -v /home/agent/.opencode/node_modules"
2166
+ if [[ ! -f "$CACHE_DIR/playwright-browsers/.seeded" ]]; then
2167
+ if docker run --rm "$IMAGE" test -d /opt/playwright-browsers 2>/dev/null; then
2168
+ echo "🔄 Seeding shared Playwright browser cache..."
2169
+ docker run --rm -v "$CACHE_DIR/playwright-browsers":/export "$IMAGE" \
2170
+ cp -a /opt/playwright-browsers/. /export/ 2>/dev/null && \
2171
+ touch "$CACHE_DIR/playwright-browsers/.seeded" && \
2172
+ echo " Playwright browsers cached" || \
2173
+ echo "⚠️ Playwright browser seeding failed (will retry next run)"
2174
+ else
2175
+ touch "$CACHE_DIR/playwright-browsers/.seeded"
2176
+ fi
2071
2177
  fi
2072
2178
 
2073
2179
  # Detect display configuration (clipboard integration)
@@ -2145,12 +2251,12 @@ docker run $CONTAINER_NAME --rm $TTY_FLAGS \
2145
2251
  $TOOL_CONFIG_MOUNTS \
2146
2252
  $GIT_MOUNTS \
2147
2253
  $NETWORK_OPTIONS \
2148
- $CACHE_MOUNTS \
2149
2254
  $DISPLAY_FLAGS \
2150
2255
  $HOST_ACCESS_ARGS \
2151
2256
  $PORT_MAPPINGS \
2152
2257
  $OPENCODE_PASSWORD_ENV \
2153
2258
  -v "$HOME_DIR":/home/agent \
2259
+ $SHARED_CACHE_MOUNTS \
2154
2260
  -w "$CURRENT_DIR" \
2155
2261
  --env-file "$ENV_FILE" \
2156
2262
  -e TERM="$TERM" \
package/bin/cli.js CHANGED
@@ -23,6 +23,7 @@ Commands:
23
23
  setup Run interactive setup (configure workspaces, select tools)
24
24
  update Interactive menu to manage config (workspaces, git, networks)
25
25
  clean Interactive cleanup for caches/configs
26
+ clean cache [type] Clear shared package caches (npm, bun, pip, playwright-browsers)
26
27
  config show [--json] Display current global configuration
27
28
  config tool <tool> [--show] Display host paths and config for a specific tool
28
29
 
@@ -326,9 +327,9 @@ async function runConfigTool(toolName, showContent) {
326
327
  process.exit(1);
327
328
  }
328
329
 
329
- const toolHome = path.join(SANDBOX_DIR, 'tools', toolName, 'home');
330
+ const toolHome = path.join(SANDBOX_DIR, 'home');
330
331
  console.log(`\n🔍 Sandbox Configuration for: ${toolName}`);
331
- console.log(`Host Home: ${toolHome}`);
332
+ console.log(`Sandbox Home: ${toolHome}`);
332
333
 
333
334
  if (!fs.existsSync(toolHome)) {
334
335
  console.log('Status: ⚠️ Not yet initialized (folder missing on host)');
@@ -1259,6 +1260,45 @@ async function manageNetworksMenu(rl) {
1259
1260
  }
1260
1261
  }
1261
1262
 
1263
+ // ============================================================================
1264
+ // CLEAN CACHE COMMAND (non-interactive)
1265
+ // ============================================================================
1266
+ const CACHE_TYPES = ['npm', 'bun', 'pip', 'playwright-browsers']
1267
+
1268
+ function runCleanCache(cacheType) {
1269
+ const cacheDir = path.join(SANDBOX_DIR, 'cache')
1270
+
1271
+ if (cacheType && !CACHE_TYPES.includes(cacheType)) {
1272
+ console.error(`❌ Unknown cache type: ${cacheType}`)
1273
+ console.error(`Valid types: ${CACHE_TYPES.join(', ')}`)
1274
+ process.exit(1)
1275
+ }
1276
+
1277
+ const targets = cacheType ? [cacheType] : CACHE_TYPES
1278
+
1279
+ let totalFreed = 0
1280
+ for (const t of targets) {
1281
+ const targetPath = path.join(cacheDir, t)
1282
+ if (!pathExists(targetPath)) {
1283
+ console.log(` ⏭ ${t}/ (not found)`)
1284
+ continue
1285
+ }
1286
+ const size = getPathSize(targetPath)
1287
+ const sizeNum = typeof size === 'number' ? size : 0
1288
+ try {
1289
+ fs.rmSync(targetPath, { recursive: true, force: true })
1290
+ fs.mkdirSync(targetPath, { recursive: true })
1291
+ totalFreed += sizeNum
1292
+ console.log(` ✓ ${t}/ cleared (${formatBytes(sizeNum)})`)
1293
+ } catch (err) {
1294
+ const msg = err && err.message ? err.message : String(err)
1295
+ console.error(` ❌ ${t}/: ${msg}`)
1296
+ }
1297
+ }
1298
+
1299
+ console.log(`\n🧹 Freed ${formatBytes(totalFreed)}`)
1300
+ }
1301
+
1262
1302
  // Parse subcommand and options
1263
1303
  const subCommand = positionalArgs[1];
1264
1304
  const subArg = positionalArgs[2];
@@ -1286,11 +1326,15 @@ switch (command) {
1286
1326
  });
1287
1327
  break;
1288
1328
  case 'clean':
1289
- runClean().catch((err) => {
1290
- const message = err && err.message ? err.message : String(err);
1291
- console.error('❌ Cleanup failed:', message);
1292
- process.exit(1);
1293
- });
1329
+ if (subCommand === 'cache') {
1330
+ runCleanCache(subArg)
1331
+ } else {
1332
+ runClean().catch((err) => {
1333
+ const message = err && err.message ? err.message : String(err)
1334
+ console.error('❌ Cleanup failed:', message)
1335
+ process.exit(1)
1336
+ })
1337
+ }
1294
1338
  break;
1295
1339
  case 'config':
1296
1340
  if (subCommand === 'show') {
@@ -10,6 +10,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends git cur
10
10
 
11
11
  RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && apt-get update && apt-get install -y gh && rm -rf /var/lib/apt/lists/*
12
12
 
13
+ # Install bun (used by most AI tool install scripts)
14
+ RUN npm install -g bun
15
+
13
16
  # Install pnpm globally using npm (not bun, for stability)
14
17
  RUN npm install -g pnpm
15
18
 
@@ -40,6 +43,10 @@ RUN mkdir -p /usr/local/lib/openspec && \
40
43
  chmod +x /usr/local/bin/openspec
41
44
  # Install RTK - token optimizer for AI coding agents (built from source)
42
45
  COPY --from=rtk-builder /usr/local/cargo/bin/rtk /usr/local/bin/rtk
46
+ # Install RTK OpenCode skills (auto-discovered by OpenCode agents)
47
+ RUN mkdir -p /home/agent/.config/opencode/skills/rtk /home/agent/.config/opencode/skills/rtk-setup
48
+ COPY skills/rtk/SKILL.md /home/agent/.config/opencode/skills/rtk/SKILL.md
49
+ COPY skills/rtk-setup/SKILL.md /home/agent/.config/opencode/skills/rtk-setup/SKILL.md
43
50
  RUN apt-get update && apt-get install -y --no-install-recommends \
44
51
  libglib2.0-0 \
45
52
  libnspr4 \
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: rtk
3
+ description: "Token optimizer for AI coding agents. Wraps common CLI commands with rtk to reduce LLM token consumption by 60-90%."
4
+ compatibility: "OpenCode with RTK binary installed"
5
+ metadata:
6
+ author: Sisyphus
7
+ version: "1.0.0"
8
+ ---
9
+
10
+ # RTK — Rust Token Killer
11
+
12
+ **Purpose**: Reduce token consumption by 60-90% on shell commands. RTK filters and compresses command output before it reaches your context.
13
+
14
+ ## When This Skill Activates
15
+
16
+ This skill is loaded automatically. Once active, **always** prefix supported commands with `rtk`.
17
+
18
+ ## Command Mappings (MANDATORY)
19
+
20
+ When running shell commands via Bash tool, use `rtk` prefix for these commands:
21
+
22
+ | Instead of | Use | Savings |
23
+ |-----------|-----|---------|
24
+ | `git status` | `rtk git status` | ~80% |
25
+ | `git diff` | `rtk git diff` | ~75% |
26
+ | `git log` | `rtk git log` | ~80% |
27
+ | `git add/commit/push/pull` | `rtk git add/commit/push/pull` | ~90% |
28
+ | `ls` / `ls -la` | `rtk ls` | ~80% |
29
+ | `cat file` | `rtk read file` | ~70% |
30
+ | `grep pattern .` | `rtk grep pattern .` | ~80% |
31
+ | `rg pattern` | `rtk grep pattern .` | ~80% |
32
+ | `npm test` / `cargo test` / `pytest` | `rtk test <cmd>` | ~90% |
33
+ | `npm run build` / `cargo build` | `rtk err <cmd>` | ~80% |
34
+ | `gh pr list/view` | `rtk gh pr list/view` | ~70% |
35
+ | `docker ps` | `rtk docker ps` | ~80% |
36
+ | `eslint` / `tsc` | `rtk lint` / `rtk tsc` | ~80% |
37
+
38
+ ## Searching Inside `node_modules` / Ignored Directories
39
+
40
+ By default, `rtk grep` respects `.gitignore` rules — meaning `node_modules`, `.nuxt`, `dist`, etc. are **excluded**. This is the right behavior 99% of the time.
41
+
42
+ When you **need** to search inside ignored directories (debugging a library, checking an API signature, tracing a dependency bug):
43
+
44
+ ```bash
45
+ # Search all files including node_modules (--no-ignore bypasses .gitignore)
46
+ rtk grep "defineStore" . --no-ignore
47
+
48
+ # Search a specific package only (combine --no-ignore with --glob)
49
+ rtk grep "defineStore" . --no-ignore --glob 'node_modules/pinia/**'
50
+ ```
51
+
52
+ **What does NOT work:**
53
+ - `rtk grep "pattern" node_modules/pinia/` — still excluded even with direct path
54
+ - `rtk grep "pattern" . --glob 'node_modules/**'` — glob alone doesn't override .gitignore
55
+
56
+ **Key flag: `--no-ignore`** — this is the ONLY way to search ignored directories with rtk grep.
57
+
58
+ ### Other useful `rtk grep` flags
59
+
60
+ ```bash
61
+ rtk grep "pattern" . -t ts # Filter by file type (ts, py, rust, etc.)
62
+ rtk grep "pattern" . -m 100 # Increase max results (default: 50)
63
+ rtk grep "pattern" . -u # Ultra-compact mode (even fewer tokens)
64
+ rtk grep "pattern" . -l 120 # Max line length before truncation (default: 80)
65
+ ```
66
+
67
+ ## Commands to NOT Wrap
68
+
69
+ Do NOT prefix these with `rtk` (unsupported or counterproductive):
70
+
71
+ - `npx`, `npm install`, `pip install` (package managers)
72
+ - `node`, `python3`, `ruby` (interpreters)
73
+ - `nano-brain`, `openspec`, `opencode` (custom tools)
74
+ - Heredocs (`<<EOF`)
75
+ - Piped commands (`cmd1 | cmd2`) — wrap only the first command if applicable
76
+ - Commands already prefixed with `rtk`
77
+
78
+ ## How RTK Works
79
+
80
+ ```
81
+ Without RTK: git status → 50 lines raw output → 2,000 tokens
82
+ With RTK: rtk git status → "3 modified, 1 untracked ✓" → 200 tokens
83
+ ```
84
+
85
+ RTK runs the real command, then filters/compresses the output. The agent sees a compact summary instead of verbose raw output.
86
+
87
+ ## Detection
88
+
89
+ Before using RTK commands, verify it's installed:
90
+ ```bash
91
+ rtk --version
92
+ ```
93
+
94
+ If `rtk` is not found, skip this skill — run commands normally without the `rtk` prefix.
95
+
96
+ ## Token Savings Reference
97
+
98
+ Typical 30-min coding session:
99
+ - Without RTK: ~150,000 tokens
100
+ - With RTK: ~45,000 tokens
101
+ - **Savings: ~70%**
102
+
103
+ Biggest wins: test output (`rtk test` — 90%), git operations (`rtk git` — 80%), file reading (`rtk read` — 70%).