flashbox 0.1.0__tar.gz

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.
flashbox-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: flashbox
3
+ Version: 0.1.0
4
+ Summary: A dynamic, repository-aware persistent Docker sandbox CLI for AI Agents.
5
+ Author-email: Mark Eyser <markeyser@github.com>
6
+ Project-URL: Homepage, https://github.com/markeyser/flashbox
7
+ Keywords: ai,agents,docker,sandbox,mcp,cli,telemetry
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Classifier: Topic :: Terminals
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: rich>=13.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
25
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # Flashbox ⚡️
29
+
30
+ A lightning-fast, repository-aware persistent Docker sandbox CLI designed specifically for AI Coding Agents.
31
+
32
+ Flashbox allows your AI coding agents (like Antigravity or Cursor) to safely execute terminal commands, run scripts, and compile code in an isolated Linux environment without polluting your local macOS host. It completely replaces the heavy, latency-prone Boxlite MCP server with a streamlined local Python CLI mapping directly to your Docker daemon.
33
+
34
+ ## Why Flashbox?
35
+ - **Zero Token Overhead:** Unlike MCP servers, Flashbox doesn't inject massive JSON schemas into your prompt context.
36
+ - **Dynamic Repository Mounting:** If you run `sandbox start` in `/Projects/MyCoolApp`, Flashbox dynamically mounts that specific directory to `/vault` inside a dedicated `flashbox-mycoolapp` container.
37
+ - **Native Execution Speed:** Bypasses JSON-RPC handshakes. `sandbox exec` streams natively through `subprocess` directly to Docker.
38
+ - **Built-in Telemetry:** Ships with a real-time TUI to monitor the active exact CPU and RAM your AI is drawing.
39
+
40
+ ## System Requirements
41
+ - **Operating System:** Fully cross-platform. Tested heavily on **macOS (Apple Silicon)**, but works flawlessly on **Linux** and **Windows (via WSL2)**.
42
+ - **Docker Daemon:** You **MUST** have the Docker engine running in the background. On macOS/Windows, this means having **Docker Desktop** or **OrbStack** open and active. On Linux, the native `docker` service must be running.
43
+ - **Python:** Python 3.10+ installed on the host machine.
44
+
45
+ ## Installation
46
+
47
+ Because Flashbox is packaged cleanly, you can install it globally via `pipx` or your preferred Python package manager:
48
+
49
+ ```zsh
50
+ # Run this inside the cloned repo
51
+ pipx install .
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ Once installed, the globally accessible `sandbox` command is available from any directory.
57
+
58
+ ### Quick Start
59
+ Navigate to any project directory and initialize the sandbox:
60
+ ```zsh
61
+ cd /Users/markeyser/Projects/MyCoolApp
62
+ sandbox start
63
+ ```
64
+ *This instantly spins up a `python:3.11-slim` container uniquely named `flashbox-mycoolapp` and installs base tools (`git`, `rg`, `jq`, `curl`).*
65
+
66
+ ### AI Execution
67
+ Instruct your AI agent to execute commands using the `exec` flag. The agent's native system terminal will push the command securely into the container:
68
+ ```bash
69
+ sandbox exec "python3 main.py"
70
+ sandbox exec "grep -r 'TODO' ."
71
+ ```
72
+
73
+ ### Telemetry Dashboard
74
+ Launch the TUI to monitor the AI's impact on your system resources and disk space in real-time:
75
+ ```zsh
76
+ sandbox monitor
77
+ ```
78
+
79
+ ### Clean Slate
80
+ If the AI corrupts the environment or installs too many dependencies, instantly wipe the infrastructure (your code on the native host remains untouched):
81
+ ```zsh
82
+ sandbox remove
83
+ sandbox start
84
+ ```
@@ -0,0 +1,57 @@
1
+ # Flashbox ⚡️
2
+
3
+ A lightning-fast, repository-aware persistent Docker sandbox CLI designed specifically for AI Coding Agents.
4
+
5
+ Flashbox allows your AI coding agents (like Antigravity or Cursor) to safely execute terminal commands, run scripts, and compile code in an isolated Linux environment without polluting your local macOS host. It completely replaces the heavy, latency-prone Boxlite MCP server with a streamlined local Python CLI mapping directly to your Docker daemon.
6
+
7
+ ## Why Flashbox?
8
+ - **Zero Token Overhead:** Unlike MCP servers, Flashbox doesn't inject massive JSON schemas into your prompt context.
9
+ - **Dynamic Repository Mounting:** If you run `sandbox start` in `/Projects/MyCoolApp`, Flashbox dynamically mounts that specific directory to `/vault` inside a dedicated `flashbox-mycoolapp` container.
10
+ - **Native Execution Speed:** Bypasses JSON-RPC handshakes. `sandbox exec` streams natively through `subprocess` directly to Docker.
11
+ - **Built-in Telemetry:** Ships with a real-time TUI to monitor the active exact CPU and RAM your AI is drawing.
12
+
13
+ ## System Requirements
14
+ - **Operating System:** Fully cross-platform. Tested heavily on **macOS (Apple Silicon)**, but works flawlessly on **Linux** and **Windows (via WSL2)**.
15
+ - **Docker Daemon:** You **MUST** have the Docker engine running in the background. On macOS/Windows, this means having **Docker Desktop** or **OrbStack** open and active. On Linux, the native `docker` service must be running.
16
+ - **Python:** Python 3.10+ installed on the host machine.
17
+
18
+ ## Installation
19
+
20
+ Because Flashbox is packaged cleanly, you can install it globally via `pipx` or your preferred Python package manager:
21
+
22
+ ```zsh
23
+ # Run this inside the cloned repo
24
+ pipx install .
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Once installed, the globally accessible `sandbox` command is available from any directory.
30
+
31
+ ### Quick Start
32
+ Navigate to any project directory and initialize the sandbox:
33
+ ```zsh
34
+ cd /Users/markeyser/Projects/MyCoolApp
35
+ sandbox start
36
+ ```
37
+ *This instantly spins up a `python:3.11-slim` container uniquely named `flashbox-mycoolapp` and installs base tools (`git`, `rg`, `jq`, `curl`).*
38
+
39
+ ### AI Execution
40
+ Instruct your AI agent to execute commands using the `exec` flag. The agent's native system terminal will push the command securely into the container:
41
+ ```bash
42
+ sandbox exec "python3 main.py"
43
+ sandbox exec "grep -r 'TODO' ."
44
+ ```
45
+
46
+ ### Telemetry Dashboard
47
+ Launch the TUI to monitor the AI's impact on your system resources and disk space in real-time:
48
+ ```zsh
49
+ sandbox monitor
50
+ ```
51
+
52
+ ### Clean Slate
53
+ If the AI corrupts the environment or installs too many dependencies, instantly wipe the infrastructure (your code on the native host remains untouched):
54
+ ```zsh
55
+ sandbox remove
56
+ sandbox start
57
+ ```
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flashbox"
7
+ version = "0.1.0"
8
+ description = "A dynamic, repository-aware persistent Docker sandbox CLI for AI Agents."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Mark Eyser", email = "markeyser@github.com" }
12
+ ]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "rich>=13.0.0",
16
+ ]
17
+ keywords = ["ai", "agents", "docker", "sandbox", "mcp", "cli", "telemetry"]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Build Tools",
28
+ "Topic :: Terminals",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "pytest-mock>=3.10",
35
+ "ruff>=0.3.0"
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/markeyser/flashbox"
40
+
41
+ [project.scripts]
42
+ sandbox = "flashbox.cli:main"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
49
+ target-version = "py310"
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "W", "I"]
53
+ ignore = []
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Flashbox package initialization."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,42 @@
1
+ import argparse
2
+ from flashbox.docker_manager import DockerManager
3
+ from flashbox.monitor import FlashboxMonitor
4
+
5
+ def main():
6
+ parser = argparse.ArgumentParser(
7
+ description="Flashbox: A dynamic, repository-aware persistent Docker sandbox CLI for AI Agents."
8
+ )
9
+ subparsers = parser.add_subparsers(dest="action", required=True)
10
+
11
+ subparsers.add_parser("start", help="Initialize and start the Flashbox for the current directory")
12
+ subparsers.add_parser("stop", help="Stop the active Flashbox")
13
+ subparsers.add_parser("remove", help="Destroy the Flashbox. Good for a clean slate rebuild.")
14
+
15
+ # Monitor command parser
16
+ monitor_parser = subparsers.add_parser("monitor", help="Launch the real-time telemetry dashboard")
17
+ monitor_parser.add_argument("-r", "--refresh", type=float, default=1.0, help="Refresh rate in seconds")
18
+
19
+ # Exec command parser
20
+ exec_parser = subparsers.add_parser("exec", help="Execute a raw bash command securely inside the sandbox")
21
+ exec_parser.add_argument("command", nargs="+", help="The command string to run")
22
+
23
+ args = parser.parse_args()
24
+
25
+ # Initialize the docker manager which dynamically detects the CWD
26
+ manager = DockerManager()
27
+
28
+ if args.action == "start":
29
+ manager.start()
30
+ elif args.action == "stop":
31
+ manager.stop()
32
+ elif args.action == "remove":
33
+ manager.remove()
34
+ elif args.action == "monitor":
35
+ monitor = FlashboxMonitor(manager)
36
+ monitor.run(args.refresh)
37
+ elif args.action == "exec":
38
+ # Join the command pieces back together preserving spaces
39
+ manager.exec_command(" ".join(args.command))
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -0,0 +1,83 @@
1
+ import subprocess
2
+ import sys
3
+ import os
4
+ import re
5
+
6
+ IMAGE_NAME = "python:3.11-slim"
7
+
8
+ class DockerManager:
9
+ def __init__(self, cwd=None):
10
+ self.cwd = cwd or os.getcwd()
11
+ self.container_name = self._generate_container_name(self.cwd)
12
+ self.vault_path = self.cwd
13
+
14
+ def _generate_container_name(self, path):
15
+ """Generates a safe, unique Docker container name based on the current directory."""
16
+ basename = os.path.basename(path).lower()
17
+ # Remove any invalid docker characters
18
+ safe_name = re.sub(r'[^a-zA-Z0-9_.-]', '', basename)
19
+ return f"flashbox-{safe_name}"
20
+
21
+ def _run_cmd(self, cmd, check=True):
22
+ """Executes a shell command and returns the stdout."""
23
+ result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
24
+ if check and result.returncode != 0:
25
+ print(f"Error executing: {cmd}\n{result.stderr}", file=sys.stderr)
26
+ sys.exit(result.returncode)
27
+ return result.stdout.strip()
28
+
29
+ def is_running(self):
30
+ """Checks if this specific project container is currently running."""
31
+ out = self._run_cmd(f"docker ps -q -f name={self.container_name}", check=False)
32
+ return bool(out)
33
+
34
+ def start(self):
35
+ """Initializes the persistent container and mounts the local repo to /vault."""
36
+ from rich.console import Console
37
+ console = Console()
38
+
39
+ if self.is_running():
40
+ console.print(f"[yellow]Flashbox '{self.container_name}' is already running for this directory.[/yellow]")
41
+ return
42
+
43
+ # Check if container exists but is stopped
44
+ out = self._run_cmd(f"docker ps -aq -f name={self.container_name}", check=False)
45
+ if out:
46
+ console.print(f"[cyan]Starting existing Flashbox '{self.container_name}'...[/cyan]")
47
+ self._run_cmd(f"docker start {self.container_name}")
48
+ else:
49
+ console.print(f"[green]Creating and starting new Flashbox '{self.container_name}'...[/green]")
50
+ # We mount the local directory to /vault inside the container
51
+ self._run_cmd(f"docker run -d --name {self.container_name} -v {self.vault_path}:/vault -w /vault {IMAGE_NAME} tail -f /dev/null")
52
+
53
+ console.print("[cyan]Installing base AI tools (git, ripgrep, jq, curl)...[/cyan]")
54
+ self._run_cmd(f"docker exec {self.container_name} apt-get update")
55
+ self._run_cmd(f"docker exec {self.container_name} apt-get install -y git ripgrep jq curl")
56
+ console.print("[bold green]Flashbox initialized successfully and mounted to /vault.[/bold green]")
57
+
58
+ def stop(self):
59
+ """Stops the active container."""
60
+ from rich.console import Console
61
+ console = Console()
62
+ if not self.is_running():
63
+ console.print(f"[yellow]Flashbox '{self.container_name}' is not running.[/yellow]")
64
+ return
65
+ console.print(f"[cyan]Stopping Flashbox '{self.container_name}'...[/cyan]")
66
+ self._run_cmd(f"docker stop {self.container_name}")
67
+
68
+ def remove(self):
69
+ """Destroys the container. Useful for a clean slate."""
70
+ from rich.console import Console
71
+ console = Console()
72
+ console.print(f"[red]Destroying Flashbox '{self.container_name}'...[/red]")
73
+ self._run_cmd(f"docker rm -f {self.container_name}", check=False)
74
+ console.print("[bold green]Environment wiped successfully.[/bold green]")
75
+
76
+ def exec_command(self, command_string):
77
+ """Executes a raw bash command securely inside the sandbox."""
78
+ if not self.is_running():
79
+ self.start()
80
+
81
+ # We pass the execution directly to Docker bypassing capture_output so interactive streams work
82
+ result = subprocess.run(["docker", "exec", "-w", "/vault", self.container_name, "bash", "-c", command_string])
83
+ sys.exit(result.returncode)
@@ -0,0 +1,110 @@
1
+ import os
2
+ import json
3
+ import time
4
+ from rich.console import Console
5
+ from rich.live import Live
6
+ from rich.table import Table
7
+ from rich.panel import Panel
8
+ from rich.layout import Layout
9
+ from rich.align import Align
10
+ import subprocess
11
+
12
+ console = Console()
13
+
14
+ class FlashboxMonitor:
15
+ def __init__(self, manager):
16
+ self.manager = manager
17
+ self.cooldown = 30 # seconds to determine "Idle" vs "Active"
18
+ self.last_active_time = 0
19
+ self.total_size = 0
20
+ self.total_files = 0
21
+
22
+ def _get_docker_stats(self):
23
+ """Fetches real-time CPU and RAM limits from docker stats."""
24
+ if not self.manager.is_running():
25
+ return "N/A", "N/A"
26
+ try:
27
+ # We use no-stream to get a single snapshot of the active container
28
+ out = self.manager._run_cmd(
29
+ f"docker stats --no-stream --format '{{{{.CPUPerc}}}}|{{{{.MemUsage}}}}' {self.manager.container_name}",
30
+ check=False
31
+ )
32
+ if out and "|" in out:
33
+ cpu, mem = out.split("|")
34
+ return cpu.strip(), mem.strip()
35
+ return "0.00%", "0B / 0B"
36
+ except Exception:
37
+ return "Error", "Error"
38
+
39
+ def _get_sandbox_size(self):
40
+ """Scans the volume mount directory specifically for its size."""
41
+ total_size = 0
42
+ total_files = 0
43
+
44
+ for dirpath, _, filenames in os.walk(self.manager.cwd):
45
+ if '.git' in dirpath or '.venv' in dirpath:
46
+ continue
47
+ for f in filenames:
48
+ fp = os.path.join(dirpath, f)
49
+ if not os.path.islink(fp):
50
+ try:
51
+ total_size += os.path.getsize(fp)
52
+ total_files += 1
53
+ # Check modification time
54
+ mtime = os.path.getmtime(fp)
55
+ if time.time() - mtime < self.cooldown:
56
+ self.last_active_time = time.time()
57
+ except (OSError, FileNotFoundError):
58
+ pass
59
+
60
+ self.total_size = total_size
61
+ self.total_files = total_files
62
+
63
+ def _format_size(self, size_bytes):
64
+ for unit in ['B', 'KB', 'MB', 'GB']:
65
+ if size_bytes < 1024.0:
66
+ return f"{size_bytes:.1f} {unit}"
67
+ size_bytes /= 1024.0
68
+ return f"{size_bytes:.1f} TB"
69
+
70
+ def generate_dashboard(self) -> Layout:
71
+ self._get_sandbox_size()
72
+ cpu, mem = self._get_docker_stats()
73
+
74
+ is_active = (time.time() - self.last_active_time) < self.cooldown
75
+ status_color = "green" if is_active else "yellow"
76
+ status_text = "🟢 ACTIVE" if is_active else "🟡 IDLE"
77
+
78
+ # Build Metrics Table
79
+ metrics_table = Table(show_header=False, expand=True, box=None)
80
+ metrics_table.add_column("Metric", style="cyan")
81
+ metrics_table.add_column("Value", justify="right")
82
+
83
+ metrics_table.add_row("Dynamic Container", self.manager.container_name)
84
+ metrics_table.add_row("Agent State", f"[{status_color}]{status_text}[/{status_color}]")
85
+ metrics_table.add_row("Mounted Vault Files", str(self.total_files))
86
+ metrics_table.add_row("Vault Source Vol.", self._format_size(self.total_size))
87
+ metrics_table.add_row("Container CPU", cpu)
88
+ metrics_table.add_row("Container RAM", mem)
89
+
90
+ panel = Panel(
91
+ Align.center(metrics_table, vertical="middle"),
92
+ title=f"[b blue]Flashbox Telemetry[/b blue]",
93
+ border_style="blue",
94
+ )
95
+ return panel
96
+
97
+ def run(self, refresh_rate=1.0):
98
+ if not self.manager.is_running():
99
+ console.print(f"[red]Flashbox '{self.manager.container_name}' is not currently running.[/red]")
100
+ console.print("Start it first with: [cyan]sandbox start[/cyan]")
101
+ return
102
+
103
+ console.clear()
104
+ try:
105
+ with Live(self.generate_dashboard(), console=console, screen=True, refresh_per_second=1/refresh_rate) as live:
106
+ while True:
107
+ time.sleep(refresh_rate)
108
+ live.update(self.generate_dashboard())
109
+ except KeyboardInterrupt:
110
+ console.print("\n[dim]Monitor stopped.[/dim]")
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: flashbox
3
+ Version: 0.1.0
4
+ Summary: A dynamic, repository-aware persistent Docker sandbox CLI for AI Agents.
5
+ Author-email: Mark Eyser <markeyser@github.com>
6
+ Project-URL: Homepage, https://github.com/markeyser/flashbox
7
+ Keywords: ai,agents,docker,sandbox,mcp,cli,telemetry
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Classifier: Topic :: Terminals
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: rich>=13.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=7.0; extra == "dev"
24
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
25
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # Flashbox ⚡️
29
+
30
+ A lightning-fast, repository-aware persistent Docker sandbox CLI designed specifically for AI Coding Agents.
31
+
32
+ Flashbox allows your AI coding agents (like Antigravity or Cursor) to safely execute terminal commands, run scripts, and compile code in an isolated Linux environment without polluting your local macOS host. It completely replaces the heavy, latency-prone Boxlite MCP server with a streamlined local Python CLI mapping directly to your Docker daemon.
33
+
34
+ ## Why Flashbox?
35
+ - **Zero Token Overhead:** Unlike MCP servers, Flashbox doesn't inject massive JSON schemas into your prompt context.
36
+ - **Dynamic Repository Mounting:** If you run `sandbox start` in `/Projects/MyCoolApp`, Flashbox dynamically mounts that specific directory to `/vault` inside a dedicated `flashbox-mycoolapp` container.
37
+ - **Native Execution Speed:** Bypasses JSON-RPC handshakes. `sandbox exec` streams natively through `subprocess` directly to Docker.
38
+ - **Built-in Telemetry:** Ships with a real-time TUI to monitor the active exact CPU and RAM your AI is drawing.
39
+
40
+ ## System Requirements
41
+ - **Operating System:** Fully cross-platform. Tested heavily on **macOS (Apple Silicon)**, but works flawlessly on **Linux** and **Windows (via WSL2)**.
42
+ - **Docker Daemon:** You **MUST** have the Docker engine running in the background. On macOS/Windows, this means having **Docker Desktop** or **OrbStack** open and active. On Linux, the native `docker` service must be running.
43
+ - **Python:** Python 3.10+ installed on the host machine.
44
+
45
+ ## Installation
46
+
47
+ Because Flashbox is packaged cleanly, you can install it globally via `pipx` or your preferred Python package manager:
48
+
49
+ ```zsh
50
+ # Run this inside the cloned repo
51
+ pipx install .
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ Once installed, the globally accessible `sandbox` command is available from any directory.
57
+
58
+ ### Quick Start
59
+ Navigate to any project directory and initialize the sandbox:
60
+ ```zsh
61
+ cd /Users/markeyser/Projects/MyCoolApp
62
+ sandbox start
63
+ ```
64
+ *This instantly spins up a `python:3.11-slim` container uniquely named `flashbox-mycoolapp` and installs base tools (`git`, `rg`, `jq`, `curl`).*
65
+
66
+ ### AI Execution
67
+ Instruct your AI agent to execute commands using the `exec` flag. The agent's native system terminal will push the command securely into the container:
68
+ ```bash
69
+ sandbox exec "python3 main.py"
70
+ sandbox exec "grep -r 'TODO' ."
71
+ ```
72
+
73
+ ### Telemetry Dashboard
74
+ Launch the TUI to monitor the AI's impact on your system resources and disk space in real-time:
75
+ ```zsh
76
+ sandbox monitor
77
+ ```
78
+
79
+ ### Clean Slate
80
+ If the AI corrupts the environment or installs too many dependencies, instantly wipe the infrastructure (your code on the native host remains untouched):
81
+ ```zsh
82
+ sandbox remove
83
+ sandbox start
84
+ ```
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/flashbox/__init__.py
5
+ src/flashbox/cli.py
6
+ src/flashbox/docker_manager.py
7
+ src/flashbox/monitor.py
8
+ src/flashbox.egg-info/PKG-INFO
9
+ src/flashbox.egg-info/SOURCES.txt
10
+ src/flashbox.egg-info/dependency_links.txt
11
+ src/flashbox.egg-info/entry_points.txt
12
+ src/flashbox.egg-info/requires.txt
13
+ src/flashbox.egg-info/top_level.txt
14
+ tests/test_cli.py
15
+ tests/test_docker_manager.py
16
+ tests/test_monitor.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sandbox = flashbox.cli:main
@@ -0,0 +1,6 @@
1
+ rich>=13.0.0
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ pytest-mock>=3.10
6
+ ruff>=0.3.0
@@ -0,0 +1 @@
1
+ flashbox
@@ -0,0 +1,73 @@
1
+ import pytest
2
+ from unittest.mock import MagicMock
3
+ from flashbox.cli import main
4
+
5
+ @pytest.fixture
6
+ def mock_dependencies(mocker):
7
+ """Mocks out the DockerManager and FlashboxMonitor completely to isolate the CLI router."""
8
+ mock_manager_class = mocker.patch("flashbox.cli.DockerManager")
9
+ mock_manager_instance = mock_manager_class.return_value
10
+
11
+ # We patch the monitor class too to avoid it initializing a Real rich UI
12
+ mock_monitor_class = mocker.patch("flashbox.cli.FlashboxMonitor")
13
+ mock_monitor_instance = mock_monitor_class.return_value
14
+
15
+ return mock_manager_instance, mock_monitor_class, mock_monitor_instance
16
+
17
+ def test_cli_start(mocker, mock_dependencies):
18
+ manager, _, _ = mock_dependencies
19
+
20
+ # Simulate typing 'sandbox start' into the terminal
21
+ mocker.patch("sys.argv", ["sandbox", "start"])
22
+ main()
23
+
24
+ manager.start.assert_called_once()
25
+
26
+ def test_cli_stop(mocker, mock_dependencies):
27
+ manager, _, _ = mock_dependencies
28
+
29
+ mocker.patch("sys.argv", ["sandbox", "stop"])
30
+ main()
31
+
32
+ manager.stop.assert_called_once()
33
+
34
+ def test_cli_remove(mocker, mock_dependencies):
35
+ manager, _, _ = mock_dependencies
36
+
37
+ mocker.patch("sys.argv", ["sandbox", "remove"])
38
+ main()
39
+
40
+ manager.remove.assert_called_once()
41
+
42
+ def test_cli_exec(mocker, mock_dependencies):
43
+ """Verifies that the bash command string is rebuilt correctly from argparse nargs."""
44
+ manager, _, _ = mock_dependencies
45
+
46
+ mocker.patch("sys.argv", ["sandbox", "exec", "pwd && ls -la"])
47
+ main()
48
+
49
+ manager.exec_command.assert_called_once_with("pwd && ls -la")
50
+
51
+ def test_cli_monitor_default(mocker, mock_dependencies):
52
+ manager, monitor_class, monitor_instance = mock_dependencies
53
+
54
+ mocker.patch("sys.argv", ["sandbox", "monitor"])
55
+ main()
56
+
57
+ # Verify the monitor takes ownership of the manager and launches the UI with a 1.0s refresh
58
+ monitor_class.assert_called_once_with(manager)
59
+ monitor_instance.run.assert_called_once_with(1.0)
60
+
61
+ def test_cli_monitor_custom_refresh(mocker, mock_dependencies):
62
+ manager, monitor_class, monitor_instance = mock_dependencies
63
+
64
+ mocker.patch("sys.argv", ["sandbox", "monitor", "-r", "0.5"])
65
+ main()
66
+
67
+ monitor_instance.run.assert_called_once_with(0.5)
68
+
69
+ def test_cli_missing_command(mocker):
70
+ """Verifies that the CLI correctly crashes and raises SystemExit if no command is given."""
71
+ mocker.patch("sys.argv", ["sandbox"])
72
+ with pytest.raises(SystemExit):
73
+ main()
@@ -0,0 +1,92 @@
1
+ import os
2
+ import sys
3
+ import pytest
4
+ from unittest.mock import MagicMock
5
+ from flashbox.docker_manager import DockerManager
6
+
7
+ @pytest.fixture
8
+ def mock_run_cmd(mocker):
9
+ return mocker.patch("flashbox.docker_manager.DockerManager._run_cmd")
10
+
11
+ @pytest.fixture
12
+ def manager():
13
+ # Use a fixed path to ensure consistent naming across OS
14
+ return DockerManager(cwd="/Users/test/projects/My-Cool_App.1")
15
+
16
+ def test_generate_container_name_strips_invalid_chars(manager):
17
+ """Verifies the container name strictly enforces docker character requirements."""
18
+ name = manager._generate_container_name("/path/with/wierd/!@#$/Repo Name!")
19
+ assert name == "flashbox-reponame", "Should strip spaces and special characters."
20
+
21
+ name = manager._generate_container_name("/path/normal-repo.app_1")
22
+ assert name == "flashbox-normal-repo.app_1", "Should retain dashes, dots, and underscores."
23
+
24
+ def test_is_running_true(manager, mock_run_cmd):
25
+ """Tests the running check logic when container is active."""
26
+ mock_run_cmd.return_value = "container_id_123"
27
+ assert manager.is_running() is True
28
+ mock_run_cmd.assert_called_once_with(f"docker ps -q -f name={manager.container_name}", check=False)
29
+
30
+ def test_is_running_false(manager, mock_run_cmd):
31
+ """Tests the running check logic when container is absent."""
32
+ mock_run_cmd.return_value = ""
33
+ assert manager.is_running() is False
34
+
35
+ def test_start_already_running(manager, mock_run_cmd, mocker):
36
+ """If the container is already running, start should exit early."""
37
+ mocker.patch.object(manager, "is_running", return_value=True)
38
+ manager.start()
39
+ # It should not call any more docker commands
40
+ mock_run_cmd.assert_not_called()
41
+
42
+ def test_start_existing_but_stopped(manager, mock_run_cmd, mocker):
43
+ """If the container exists but is stopped, it should use docker start instead of run."""
44
+ mocker.patch.object(manager, "is_running", return_value=False)
45
+ # Mock 'docker ps -aq' returning an ID (container exists)
46
+ mock_run_cmd.return_value = "container_id_123"
47
+
48
+ manager.start()
49
+
50
+ mock_run_cmd.assert_any_call(f"docker ps -aq -f name={manager.container_name}", check=False)
51
+ mock_run_cmd.assert_called_with(f"docker start {manager.container_name}")
52
+ # Ensure it doesn't trigger apt-get on existing containers
53
+ assert mock_run_cmd.call_count == 2
54
+
55
+ def test_start_new_initialization(manager, mock_run_cmd, mocker):
56
+ """If the container is missing entirely, it must perform the full run and tool initialization sequence."""
57
+ mocker.patch.object(manager, "is_running", return_value=False)
58
+ # Mock 'docker ps -aq' returning empty string (no container)
59
+ mock_run_cmd.return_value = ""
60
+
61
+ manager.start()
62
+
63
+ # 1. ps -aq check
64
+ # 2. docker run
65
+ # 3. apt-get update
66
+ # 4. apt-get install
67
+ assert mock_run_cmd.call_count == 4
68
+
69
+ # Check that volume mount points correctly to the init CWD
70
+ call_args_str = str(mock_run_cmd.mock_calls[1])
71
+ assert "docker run -d --name flashbox-my-cool_app.1" in call_args_str
72
+ assert f"-v /Users/test/projects/My-Cool_App.1:/vault" in call_args_str
73
+
74
+ def test_stop(manager, mock_run_cmd, mocker):
75
+ """Verify that stop properly checks state and executes the docker command."""
76
+ mocker.patch.object(manager, "is_running", return_value=True)
77
+ manager.stop()
78
+ mock_run_cmd.assert_called_once_with(f"docker stop {manager.container_name}")
79
+
80
+ def test_exec_command_triggers_start(manager, mocker):
81
+ """Verify that running exec explicitly triggers a start if the sandbox is stopped."""
82
+ mock_start = mocker.patch.object(manager, "start")
83
+ mocker.patch.object(manager, "is_running", return_value=False)
84
+
85
+ # Mock the actual system call to prevent running real 'docker exec'
86
+ mock_subprocess = mocker.patch("subprocess.run")
87
+ mock_exit = mocker.patch("sys.exit")
88
+
89
+ manager.exec_command("echo hello")
90
+
91
+ mock_start.assert_called_once()
92
+ mock_subprocess.assert_called_once()
@@ -0,0 +1,114 @@
1
+ import os
2
+ import time
3
+ import pytest
4
+ from unittest.mock import MagicMock
5
+ from flashbox.monitor import FlashboxMonitor
6
+ from flashbox.docker_manager import DockerManager
7
+
8
+ @pytest.fixture
9
+ def manager(mocker):
10
+ # Mock manager to isolate monitor logic
11
+ mgr = MagicMock(spec=DockerManager)
12
+ mgr.cwd = "/fake/repo/path"
13
+ mgr.container_name = "flashbox-fake-repo"
14
+ mgr.is_running.return_value = True
15
+ return mgr
16
+
17
+ @pytest.fixture
18
+ def monitor(manager):
19
+ return FlashboxMonitor(manager)
20
+
21
+ def test_get_docker_stats_success(monitor, manager):
22
+ """Verifies that the raw output from docker stats is parsed correctly."""
23
+ manager._run_cmd.return_value = "5.50%|150MiB / 2GiB"
24
+ cpu, mem = monitor._get_docker_stats()
25
+
26
+ # Needs to match the docker stats command signature in the monitor
27
+ manager._run_cmd.assert_called_once_with(
28
+ f"docker stats --no-stream --format '{{{{.CPUPerc}}}}|{{{{.MemUsage}}}}' {manager.container_name}",
29
+ check=False
30
+ )
31
+ assert cpu == "5.50%"
32
+ assert mem == "150MiB / 2GiB"
33
+
34
+ def test_get_docker_stats_not_running(monitor, manager):
35
+ manager.is_running.return_value = False
36
+ cpu, mem = monitor._get_docker_stats()
37
+ assert cpu == "N/A"
38
+ assert mem == "N/A"
39
+
40
+ def test_get_docker_stats_error_handling(monitor, manager):
41
+ """Verifies the monitor degrades gracefully if the docker daemon fails to return stats."""
42
+ manager._run_cmd.side_effect = Exception("Docker daemon dead")
43
+ cpu, mem = monitor._get_docker_stats()
44
+ assert cpu == "Error"
45
+ assert mem == "Error"
46
+
47
+ def test_format_size(monitor):
48
+ """Verifies the iterative bytes to human readable formatting logic."""
49
+ assert monitor._format_size(500) == "500.0 B"
50
+ assert monitor._format_size(1024) == "1.0 KB"
51
+ assert monitor._format_size(1024 * 1024 * 5) == "5.0 MB"
52
+ assert monitor._format_size(1024 * 1024 * 1024 * 2.5) == "2.5 GB"
53
+
54
+ def test_get_sandbox_size_filtering_and_calculation(monitor, mocker):
55
+ """
56
+ Very important test: The sandbox scans the host volume to detect changes.
57
+ It MUST skip .git and .venv folders, and properly evaluate file modification times.
58
+ """
59
+ # Mocking os.walk to return a fake file structure
60
+ # Structure:
61
+ # /fake/repo/path/
62
+ # - file1.py (size 100)
63
+ # - .git/
64
+ # - obj1.pack (size 5000) -> SHOULD BE SKIPPED
65
+ # - .venv/
66
+ # - bin/python (size 2000) -> SHOULD BE SKIPPED
67
+ # - src/
68
+ # - file2.py (size 300)
69
+
70
+ mock_walk = mocker.patch("os.walk")
71
+ mock_walk.return_value = [
72
+ ("/fake/repo/path", ("src", ".git", ".venv"), ("file1.py",)),
73
+ ("/fake/repo/path/.git", (), ("obj1.pack",)),
74
+ ("/fake/repo/path/.venv", ("bin",), ()),
75
+ ("/fake/repo/path/.venv/bin", (), ("python",)),
76
+ ("/fake/repo/path/src", (), ("file2.py",)),
77
+ ]
78
+
79
+ # Mock sizes
80
+ def mock_getsize(path):
81
+ sizes = {
82
+ "/fake/repo/path/file1.py": 100,
83
+ "/fake/repo/path/src/file2.py": 300,
84
+ # We add these just to ensure they don't break if mistakenly called
85
+ "/fake/repo/path/.git/obj1.pack": 5000,
86
+ "/fake/repo/path/.venv/bin/python": 2000,
87
+ }
88
+ return sizes.get(path, 0)
89
+
90
+ mocker.patch("os.path.getsize", side_effect=mock_getsize)
91
+ mocker.patch("os.path.islink", return_value=False)
92
+
93
+ # Mock the time so we can check activity tracking
94
+ current_time = 1000.0
95
+ mocker.patch("time.time", return_value=current_time)
96
+
97
+ def mock_getmtime(path):
98
+ # file1.py is old (idle)
99
+ # file2.py is new (active, modified 5 seconds ago)
100
+ times = {
101
+ "/fake/repo/path/file1.py": current_time - 100,
102
+ "/fake/repo/path/src/file2.py": current_time - 5,
103
+ }
104
+ return times.get(path, current_time - 100)
105
+
106
+ mocker.patch("os.path.getmtime", side_effect=mock_getmtime)
107
+
108
+ # Execute
109
+ monitor._get_sandbox_size()
110
+
111
+ # Asserts
112
+ assert monitor.total_files == 2, "Should only count file1.py and file2.py, skipping .git and .venv"
113
+ assert monitor.total_size == 400, "Should sum up size of file1.py and file2.py only"
114
+ assert monitor.last_active_time == current_time, "Since file2 was recently edited, active time should be updated to check time"