ai-agentbox 0.1.1__py3-none-any.whl

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.
agentbox/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ agentbox — Token-efficient remote execution, AST outlining, and safe patching tool for AI agents.
3
+ """
4
+
5
+ __version__ = "0.1.1"
agentbox/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Executable module entry point for agentbox: python -m agentbox
5
+ """
6
+
7
+ import sys
8
+ from .cli import main
9
+
10
+ if __name__ == "__main__":
11
+ sys.exit(main())
agentbox/cli.py ADDED
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ agentbox.cli — Command-line interface for AgentBox.
5
+ Entry point for 'agentbox' executable installed via pip.
6
+ """
7
+
8
+ import sys
9
+ import argparse
10
+ from typing import List, Optional
11
+
12
+ from . import __version__
13
+ from .config import get_config, save_global_config, save_local_config
14
+ from .client import execute_action
15
+
16
+
17
+ def create_parser(default_cfg: dict) -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(
19
+ prog="agentbox",
20
+ description=f"AgentBox v{__version__} — Token-efficient execution and AST code tools for AI agents."
21
+ )
22
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
23
+ parser.add_argument("--host", default=default_cfg.get("host", ""), help="Target SSH host (e.g. user@server, or empty for local)")
24
+ parser.add_argument("--cwd", default=default_cfg.get("cwd", ""), help="Target working directory")
25
+
26
+ sub = parser.add_subparsers(dest="cmd", required=True)
27
+
28
+ # config command
29
+ p_cfg = sub.add_parser("config", help="View or update host and directory settings")
30
+ p_cfg.add_argument("--set-host", help="Set default SSH host")
31
+ p_cfg.add_argument("--set-cwd", help="Set default directory")
32
+ p_cfg.add_argument("--global", dest="is_global", action="store_true", help="Save globally to ~/.agentbox/config.json")
33
+
34
+ # view
35
+ p_view = sub.add_parser("view", help="View slice of file with line numbers")
36
+ p_view.add_argument("path", help="Path to file")
37
+ p_view.add_argument("--start", type=int, default=1, help="Start line (default: 1)")
38
+ p_view.add_argument("--end", type=int, default=100, help="End line (default: 100)")
39
+
40
+ # outline
41
+ p_out = sub.add_parser("outline", help="Extract AST outline (classes, functions, methods)")
42
+ p_out.add_argument("path", help="Path to file (Python, Rust, JS, TS, C, Go)")
43
+
44
+ # get-fn
45
+ p_fn = sub.add_parser("get-fn", help="Extract specific function or class definition")
46
+ p_fn.add_argument("path", help="Path to file")
47
+ p_fn.add_argument("name", help="Name of function or class")
48
+
49
+ # patch
50
+ p_patch = sub.add_parser("patch", help="Atomic find-and-replace with auto-backup")
51
+ p_patch.add_argument("path", help="Path to file")
52
+ p_patch.add_argument("--old", required=True, help="Exact target text to replace")
53
+ p_patch.add_argument("--new", required=True, help="Replacement text")
54
+ p_patch.add_argument("--all", action="store_true", help="Replace all occurrences (default: exactly 1)")
55
+
56
+ # rollback
57
+ p_rb = sub.add_parser("rollback", help="Restore file from automatic .bak backup")
58
+ p_rb.add_argument("path", help="Path to file")
59
+
60
+ # tree
61
+ p_tree = sub.add_parser("tree", help="Compact directory tree ignoring build artifacts and VCS")
62
+ p_tree.add_argument("path", nargs="?", default="", help="Directory path")
63
+ p_tree.add_argument("--depth", type=int, default=2, help="Max depth (default: 2)")
64
+
65
+ # grep
66
+ p_grep = sub.add_parser("grep", help="Fast text search with limits")
67
+ p_grep.add_argument("query", help="Search query or regex")
68
+ p_grep.add_argument("path", nargs="?", default="", help="Directory path")
69
+ p_grep.add_argument("--max", type=int, default=20, help="Max results (default: 20)")
70
+
71
+ # run
72
+ p_run = sub.add_parser("run", help="Silent execution: suppresses noise, buffers to log, notifies on completion")
73
+ p_run.add_argument("command", help="Shell command to execute")
74
+ p_run.add_argument("--tail", type=int, default=default_cfg.get("tail", 15), help="Lines of log tail to display")
75
+ p_run.add_argument("--timeout", type=int, default=default_cfg.get("timeout", None), help="Timeout in seconds")
76
+
77
+ # spawn
78
+ p_spawn = sub.add_parser("spawn", help="Spawn detached background process (survives disconnects)")
79
+ p_spawn.add_argument("command", help="Shell command to spawn")
80
+
81
+ # status
82
+ p_stat = sub.add_parser("status", help="Check status of background task")
83
+ p_stat.add_argument("task_id", help="Task ID from spawn command")
84
+
85
+ # logs / tail
86
+ p_logs = sub.add_parser("logs", help="View or stream log output of background task")
87
+ p_logs.add_argument("task_id", help="Task ID from spawn command")
88
+ p_logs.add_argument("-n", "--lines", type=int, default=50, help="Number of trailing lines (default: 50)")
89
+ p_logs.add_argument("-f", "--follow", action="store_true", help="Continuously stream new log output (tail -f)")
90
+
91
+ # kill
92
+ p_kill = sub.add_parser("kill", help="Terminate background task")
93
+ p_kill.add_argument("task_id", help="Task ID from spawn command")
94
+
95
+ return parser
96
+
97
+
98
+ def main(argv: Optional[List[str]] = None):
99
+ cfg = get_config()
100
+ parser = create_parser(cfg)
101
+ args = parser.parse_args(argv)
102
+
103
+ if args.cmd == "config":
104
+ updates = {}
105
+ if args.set_host is not None:
106
+ updates["host"] = args.set_host
107
+ if args.set_cwd is not None:
108
+ updates["cwd"] = args.set_cwd
109
+
110
+ if updates:
111
+ if args.is_global:
112
+ save_global_config(updates)
113
+ print(f"Global config updated (~/.agentbox/config.json): {updates}")
114
+ else:
115
+ save_local_config(updates)
116
+ print(f"Local config updated (.agentbox.json): {updates}")
117
+ else:
118
+ print("Current configuration:")
119
+ for k, v in cfg.items():
120
+ print(f" {k}: {v}")
121
+ return 0
122
+
123
+ host = args.host
124
+ cwd = args.cwd
125
+
126
+ if args.cmd == "view":
127
+ code = execute_action(host, cwd, "view", {"path": args.path, "start": args.start, "end": args.end})
128
+ elif args.cmd == "outline":
129
+ code = execute_action(host, cwd, "outline", {"path": args.path})
130
+ elif args.cmd == "get-fn":
131
+ code = execute_action(host, cwd, "get_fn", {"path": args.path, "name": args.name})
132
+ elif args.cmd == "patch":
133
+ code = execute_action(host, cwd, "patch", {"path": args.path, "old": args.old, "new": args.new, "all": args.all})
134
+ elif args.cmd == "rollback":
135
+ code = execute_action(host, cwd, "rollback", {"path": args.path})
136
+ elif args.cmd == "tree":
137
+ code = execute_action(host, cwd, "tree", {"path": args.path or cwd, "depth": args.depth})
138
+ elif args.cmd == "grep":
139
+ code = execute_action(host, cwd, "grep", {"query": args.query, "path": args.path or cwd, "max": args.max})
140
+ elif args.cmd == "run":
141
+ code = execute_action(host, cwd, "run", {"cmd": args.command, "cwd": cwd, "tail": args.tail, "timeout": args.timeout})
142
+ elif args.cmd == "spawn":
143
+ code = execute_action(host, cwd, "spawn", {"cmd": args.command, "cwd": cwd})
144
+ elif args.cmd == "status":
145
+ code = execute_action(host, cwd, "status", {"task_id": args.task_id})
146
+ elif args.cmd == "logs":
147
+ code = execute_action(host, cwd, "logs", {"task_id": args.task_id, "lines": args.lines, "follow": args.follow})
148
+ elif args.cmd == "kill":
149
+ code = execute_action(host, cwd, "kill", {"task_id": args.task_id})
150
+ else:
151
+ code = 1
152
+
153
+ sys.exit(code)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
agentbox/client.py ADDED
@@ -0,0 +1,121 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ agentbox.client — Local Driver & SSH/Local Transport.
4
+ Handles local execution and remote SSH execution with zero bash quoting issues.
5
+ Auto-bootstraps worker.py to remote servers.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ import hashlib
11
+ import subprocess
12
+ from pathlib import Path
13
+ from typing import Dict, Any
14
+
15
+ from . import worker
16
+
17
+ WORKER_LOCAL_FILE = Path(__file__).parent / "worker.py"
18
+ REMOTE_WORKER_PATH = "~/.agentbox_worker.py"
19
+
20
+
21
+ def get_local_worker_bytes() -> bytes:
22
+ if WORKER_LOCAL_FILE.is_file():
23
+ return WORKER_LOCAL_FILE.read_bytes()
24
+ # Fallback to reading worker module source
25
+ import inspect
26
+ return inspect.getsource(worker).encode("utf-8")
27
+
28
+
29
+ def ensure_remote_worker(host: str) -> None:
30
+ """Verifies that the remote worker matches local worker SHA-256; uploads if needed."""
31
+ local_bytes = get_local_worker_bytes()
32
+ local_hash = hashlib.sha256(local_bytes).hexdigest()
33
+
34
+ # Check remote hash
35
+ check_cmd = ["ssh", host, f"sha256sum {REMOTE_WORKER_PATH} 2>/dev/null || echo NONE"]
36
+ proc = subprocess.run(check_cmd, capture_output=True, text=True)
37
+ remote_out = proc.stdout.strip()
38
+ remote_hash = remote_out.split()[0] if remote_out else "NONE"
39
+
40
+ if remote_hash != local_hash:
41
+ upload_cmd = ["ssh", host, f"cat > {REMOTE_WORKER_PATH} && chmod +x {REMOTE_WORKER_PATH}"]
42
+ up_proc = subprocess.run(upload_cmd, input=local_bytes)
43
+ if up_proc.returncode != 0:
44
+ print(f"ERROR: Failed to bootstrap worker to {host}", file=sys.stderr)
45
+ sys.exit(up_proc.returncode)
46
+
47
+
48
+ def execute_action(host: str, cwd: str, action: str, args: Dict[str, Any]) -> int:
49
+ """Executes an action either locally or remotely."""
50
+ is_remote = bool(host and host not in ("local", "localhost", "127.0.0.1", "."))
51
+
52
+ if not is_remote:
53
+ # Local execution via worker functions
54
+ return execute_local_action(cwd, action, args)
55
+
56
+ # Remote execution over SSH with JSON-IPC over stdin
57
+ ensure_remote_worker(host)
58
+
59
+ if "path" in args and args["path"]:
60
+ p = str(args["path"])
61
+ if not p.startswith("/") and not p.startswith("~") and cwd:
62
+ args["path"] = f"{cwd.rstrip('/')}/{p}"
63
+
64
+ if "cwd" in args and not args["cwd"]:
65
+ args["cwd"] = cwd
66
+
67
+ payload = json.dumps({"action": action, "args": args})
68
+ cmd = ["ssh", host, f"python3 {REMOTE_WORKER_PATH} --ipc"]
69
+ try:
70
+ proc = subprocess.run(cmd, input=payload, text=True)
71
+ return proc.returncode
72
+ except KeyboardInterrupt:
73
+ return 0
74
+
75
+
76
+ def execute_local_action(cwd: str, action: str, args: Dict[str, Any]) -> int:
77
+ """Invokes worker directly in local process."""
78
+ if cwd:
79
+ orig_cwd = Path.cwd()
80
+ try:
81
+ import os
82
+ os.chdir(os.path.expanduser(cwd))
83
+ except Exception:
84
+ pass
85
+
86
+ try:
87
+ if action == "view":
88
+ worker.cmd_view(args.get("path"), args.get("start", 1), args.get("end", 100))
89
+ elif action == "outline":
90
+ worker.cmd_outline(args.get("path"))
91
+ elif action == "get_fn":
92
+ worker.cmd_get_fn(args.get("path"), args.get("name"))
93
+ elif action == "patch":
94
+ worker.cmd_patch(args.get("path"), args.get("old"), args.get("new"), args.get("all", False))
95
+ elif action == "rollback":
96
+ worker.cmd_rollback(args.get("path"))
97
+ elif action == "tree":
98
+ worker.cmd_tree(args.get("path", "."), args.get("depth", 2))
99
+ elif action == "grep":
100
+ worker.cmd_grep(args.get("query"), args.get("path", "."), args.get("max", 20))
101
+ elif action == "run":
102
+ worker.cmd_run(args.get("cmd"), args.get("cwd"), args.get("tail", 15), args.get("timeout"))
103
+ elif action == "spawn":
104
+ worker.cmd_spawn(args.get("cmd"), args.get("cwd"))
105
+ elif action == "status":
106
+ worker.cmd_status(args.get("task_id"))
107
+ elif action == "logs":
108
+ worker.cmd_logs(args.get("task_id"), args.get("lines", 50), args.get("follow", False))
109
+ elif action == "kill":
110
+ worker.cmd_kill(args.get("task_id"))
111
+ else:
112
+ print(f"ERROR: Unknown action '{action}'", file=sys.stderr)
113
+ return 1
114
+ return 0
115
+ except KeyboardInterrupt:
116
+ return 0
117
+ except SystemExit as se:
118
+ return se.code if isinstance(se.code, int) else 1
119
+ except Exception as e:
120
+ print(f"ERROR: Local execution failed: {e}", file=sys.stderr)
121
+ return 1
agentbox/config.py ADDED
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ agentbox.config — Configuration manager for AgentBox.
4
+ Supports global (~/.agentbox/config.json) and project-level (.agentbox.json) settings.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Dict, Any
11
+
12
+ GLOBAL_CONFIG_DIR = Path.home() / ".agentbox"
13
+ GLOBAL_CONFIG_FILE = GLOBAL_CONFIG_DIR / "config.json"
14
+ LOCAL_CONFIG_FILE = Path(".agentbox.json")
15
+
16
+ DEFAULT_CONFIG: Dict[str, Any] = {
17
+ "host": "",
18
+ "cwd": "",
19
+ "timeout": 300,
20
+ "tail": 15,
21
+ }
22
+
23
+
24
+ def get_config() -> Dict[str, Any]:
25
+ """Loads merged configuration: default -> global -> local."""
26
+ cfg = dict(DEFAULT_CONFIG)
27
+
28
+ if GLOBAL_CONFIG_FILE.is_file():
29
+ try:
30
+ cfg.update(json.loads(GLOBAL_CONFIG_FILE.read_text(encoding="utf-8")))
31
+ except Exception:
32
+ pass
33
+
34
+ if LOCAL_CONFIG_FILE.is_file():
35
+ try:
36
+ cfg.update(json.loads(LOCAL_CONFIG_FILE.read_text(encoding="utf-8")))
37
+ except Exception:
38
+ pass
39
+
40
+ return cfg
41
+
42
+
43
+ def save_global_config(updates: Dict[str, Any]) -> None:
44
+ """Updates and saves global configuration."""
45
+ GLOBAL_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
46
+ cfg = get_config()
47
+ cfg.update(updates)
48
+ GLOBAL_CONFIG_FILE.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
49
+
50
+
51
+ def save_local_config(updates: Dict[str, Any]) -> None:
52
+ """Updates and saves local project-level configuration."""
53
+ cfg = get_config()
54
+ cfg.update(updates)
55
+ LOCAL_CONFIG_FILE.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
agentbox/worker.py ADDED
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ agentbox.worker — Universal High-Efficiency Worker.
5
+ Runs on the target server (or locally). Requires ONLY Python 3 standard library.
6
+ Handles AST outlining, token-efficient slicing, atomic patching, and silent task execution.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import re
12
+ import ast
13
+ import json
14
+ import time
15
+ import signal
16
+ import shutil
17
+ import subprocess
18
+ import argparse
19
+ from pathlib import Path
20
+
21
+ DEFAULT_IGNORED_DIRS = {
22
+ ".git", ".venv", "venv", "env", "__pycache__", "node_modules",
23
+ "target", "dist", "build", ".vscode", ".idea", ".agents",
24
+ "graphify-out", ".mypy_cache", ".pytest_cache"
25
+ }
26
+
27
+
28
+ def to_path(p: str = None) -> Path:
29
+ if not p:
30
+ return Path.cwd()
31
+ return Path(os.path.expanduser(str(p))).resolve()
32
+
33
+
34
+ def cmd_view(path: str, start: int = 1, end: int = 100) -> None:
35
+ p = to_path(path)
36
+ if not p.is_file():
37
+ print(f"ERROR: File not found: {path}", file=sys.stderr)
38
+ sys.exit(1)
39
+
40
+ try:
41
+ with open(p, "r", encoding="utf-8", errors="replace") as f:
42
+ lines = f.readlines()
43
+ except Exception as e:
44
+ print(f"ERROR: Failed to read {path}: {e}", file=sys.stderr)
45
+ sys.exit(1)
46
+
47
+ total = len(lines)
48
+ s = max(1, start)
49
+ e = min(total, end)
50
+
51
+ print(f"[{p.name}: lines {s}-{e} of {total}]")
52
+ for i in range(s, e + 1):
53
+ print(f"{i:4d} | {lines[i - 1].rstrip()}")
54
+
55
+
56
+ def cmd_outline(path: str) -> None:
57
+ p = to_path(path)
58
+ if not p.is_file():
59
+ print(f"ERROR: File not found: {path}", file=sys.stderr)
60
+ sys.exit(1)
61
+
62
+ ext = p.suffix.lower()
63
+ try:
64
+ content = p.read_text(encoding="utf-8", errors="replace")
65
+ lines = content.splitlines()
66
+ except Exception as e:
67
+ print(f"ERROR: Failed to read {path}: {e}", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+ print(f"=== OUTLINE: {p.name} ({len(lines)} lines) ===")
71
+
72
+ if ext == ".py":
73
+ try:
74
+ tree = ast.parse(content, filename=str(p))
75
+ for node in tree.body:
76
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
77
+ args = [a.arg for a in node.args.args]
78
+ ret = f" -> {ast.unparse(node.returns)}" if getattr(node, 'returns', None) else ""
79
+ prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
80
+ end_ln = getattr(node, 'end_lineno', node.lineno)
81
+ print(f"{node.lineno:4d}-{end_ln:4d} | {prefix} {node.name}({', '.join(args)}){ret}")
82
+ elif isinstance(node, ast.ClassDef):
83
+ end_ln = getattr(node, 'end_lineno', node.lineno)
84
+ bases = [ast.unparse(b) for b in node.bases]
85
+ base_str = f"({', '.join(bases)})" if bases else ""
86
+ print(f"{node.lineno:4d}-{end_ln:4d} | class {node.name}{base_str}:")
87
+ for sub in node.body:
88
+ if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
89
+ args = [a.arg for a in sub.args.args]
90
+ ret = f" -> {ast.unparse(sub.returns)}" if getattr(sub, 'returns', None) else ""
91
+ sub_prefix = "async def" if isinstance(sub, ast.AsyncFunctionDef) else "def"
92
+ sub_end = getattr(sub, 'end_lineno', sub.lineno)
93
+ print(f"{sub.lineno:4d}-{sub_end:4d} | {sub_prefix} {sub.name}({', '.join(args)}){ret}")
94
+ return
95
+ except Exception:
96
+ pass # Fallback to regex
97
+
98
+ # Regex-based outline for other languages (Rust, JS, TS, C, Go, etc.)
99
+ regexes = [
100
+ re.compile(r"^\s*(pub\s+)?(async\s+)?fn\s+([a-zA-Z0-9_]+)\s*(<[^>]+>)?\s*\((.*?)\)"),
101
+ re.compile(r"^\s*(pub\s+)?(struct|enum|trait|impl)\s+([a-zA-Z0-9_]+)"),
102
+ re.compile(r"^\s*(export\s+)?(async\s+)?function\s+([a-zA-Z0-9_]+)\s*\((.*?)\)"),
103
+ re.compile(r"^\s*(export\s+)?class\s+([a-zA-Z0-9_]+)"),
104
+ re.compile(r"^\s*(const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*(async\s*)?\((.*?)\)\s*=>"),
105
+ re.compile(r"^\s*func\s+([a-zA-Z0-9_]+)\s*\((.*?)\)"),
106
+ ]
107
+
108
+ for idx, line in enumerate(lines, start=1):
109
+ for rx in regexes:
110
+ if rx.search(line):
111
+ print(f"{idx:4d} | {line.strip()[:100]}")
112
+ break
113
+
114
+
115
+ def cmd_get_fn(path: str, name: str) -> None:
116
+ p = to_path(path)
117
+ if not p.is_file():
118
+ print(f"ERROR: File not found: {path}", file=sys.stderr)
119
+ sys.exit(1)
120
+
121
+ ext = p.suffix.lower()
122
+ try:
123
+ content = p.read_text(encoding="utf-8", errors="replace")
124
+ lines = content.splitlines()
125
+ except Exception as e:
126
+ print(f"ERROR: Failed to read {path}: {e}", file=sys.stderr)
127
+ sys.exit(1)
128
+
129
+ if ext == ".py":
130
+ try:
131
+ tree = ast.parse(content, filename=str(p))
132
+ for node in ast.walk(tree):
133
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
134
+ if node.name == name:
135
+ s = node.lineno
136
+ e = getattr(node, 'end_lineno', s)
137
+ print(f"[{p.name}: {node.name} lines {s}-{e}]")
138
+ for i in range(s, e + 1):
139
+ print(f"{i:4d} | {lines[i - 1]}")
140
+ return
141
+ except Exception:
142
+ pass
143
+
144
+ # Generic search
145
+ target_pattern = re.compile(rf"(def|fn|function|class|func)\s+{re.escape(name)}\b")
146
+ for idx, line in enumerate(lines, start=1):
147
+ if target_pattern.search(line):
148
+ print(f"[{p.name}: match at line {idx}]")
149
+ for j in range(idx, min(len(lines) + 1, idx + 40)):
150
+ print(f"{j:4d} | {lines[j - 1]}")
151
+ return
152
+
153
+ print(f"ERROR: Function or class '{name}' not found in {p.name}", file=sys.stderr)
154
+ sys.exit(1)
155
+
156
+
157
+ def cmd_patch(path: str, old_text: str, new_text: str, replace_all: bool = False) -> None:
158
+ p = to_path(path)
159
+ if not p.is_file():
160
+ print(f"ERROR: File not found: {path}", file=sys.stderr)
161
+ sys.exit(1)
162
+
163
+ try:
164
+ content = p.read_text(encoding="utf-8")
165
+ except Exception as e:
166
+ print(f"ERROR: Failed to read {path}: {e}", file=sys.stderr)
167
+ sys.exit(1)
168
+
169
+ count = content.count(old_text)
170
+ if count == 0:
171
+ print(f"ERROR: Target content not found in {p.name}", file=sys.stderr)
172
+ sys.exit(1)
173
+ if count > 1 and not replace_all:
174
+ print(f"ERROR: Target content found {count} times in {p.name}. Provide unique context or use --all", file=sys.stderr)
175
+ sys.exit(1)
176
+
177
+ # Backup
178
+ bak_path = p.with_suffix(p.suffix + ".bak")
179
+ try:
180
+ bak_path.write_text(content, encoding="utf-8")
181
+ except Exception as e:
182
+ print(f"ERROR: Failed to create backup {bak_path.name}: {e}", file=sys.stderr)
183
+ sys.exit(1)
184
+
185
+ # Replace
186
+ new_content = content.replace(old_text, new_text) if replace_all else content.replace(old_text, new_text, 1)
187
+ p.write_text(new_content, encoding="utf-8")
188
+
189
+ print(f"OK: Replaced {count if replace_all else 1} occurrence(s) in {p.name}. Backup: {bak_path.name}")
190
+
191
+
192
+ def cmd_rollback(path: str) -> None:
193
+ p = to_path(path)
194
+ bak_path = p.with_suffix(p.suffix + ".bak")
195
+ if not bak_path.is_file():
196
+ print(f"ERROR: Backup file {bak_path.name} not found", file=sys.stderr)
197
+ sys.exit(1)
198
+
199
+ bak_content = bak_path.read_text(encoding="utf-8")
200
+ p.write_text(bak_content, encoding="utf-8")
201
+ bak_path.unlink()
202
+ print(f"OK: Restored {p.name} from backup and removed backup.")
203
+
204
+
205
+ def cmd_tree(path: str = ".", max_depth: int = 2) -> None:
206
+ root = to_path(path)
207
+ if not root.exists():
208
+ print(f"ERROR: Directory not found: {path}", file=sys.stderr)
209
+ sys.exit(1)
210
+
211
+ print(f"=== TREE: {root} (depth <= {max_depth}) ===")
212
+ root_level = len(root.parts)
213
+
214
+ for current_root, dirs, files in os.walk(root):
215
+ cur_path = Path(current_root)
216
+ level = len(cur_path.parts) - root_level
217
+ if level > max_depth:
218
+ dirs.clear()
219
+ continue
220
+
221
+ dirs[:] = [d for d in dirs if d not in DEFAULT_IGNORED_DIRS and not d.startswith(".")]
222
+
223
+ indent = " " * level
224
+ if level > 0:
225
+ print(f"{indent}📂 {cur_path.name}/")
226
+
227
+ sub_indent = " " * (level + 1)
228
+ visible_files = [f for f in sorted(files) if not f.startswith(".") and not f.endswith((".pyc", ".bak", ".swp"))]
229
+ for f in visible_files[:15]:
230
+ print(f"{sub_indent}📄 {f}")
231
+ if len(visible_files) > 15:
232
+ print(f"{sub_indent}... and {len(visible_files) - 15} more files")
233
+
234
+
235
+ def _grep_python(pattern: re.Pattern, root: Path, max_results: int) -> None:
236
+ count = 0
237
+ for cur_root, dirs, files in os.walk(root):
238
+ dirs[:] = [d for d in dirs if d not in DEFAULT_IGNORED_DIRS]
239
+ for f in files:
240
+ if f.endswith((".pyc", ".png", ".jpg", ".bin", ".tar", ".gz", ".zip", ".iso", ".frb")):
241
+ continue
242
+ fpath = Path(cur_root) / f
243
+ try:
244
+ with open(fpath, "r", encoding="utf-8", errors="ignore") as fh:
245
+ for idx, line in enumerate(fh, start=1):
246
+ if pattern.search(line):
247
+ try:
248
+ rel = fpath.relative_to(root)
249
+ except ValueError:
250
+ rel = fpath
251
+ print(f"{rel}:{idx} | {line.strip()[:120]}")
252
+ count += 1
253
+ if count >= max_results:
254
+ print(f"... [stopped at max {max_results} results]")
255
+ return
256
+ except Exception:
257
+ pass
258
+
259
+
260
+ def cmd_grep(query: str, path: str = ".", max_results: int = 20) -> None:
261
+ root = to_path(path)
262
+ if not root.exists():
263
+ print(f"ERROR: Path not found: {path}", file=sys.stderr)
264
+ sys.exit(1)
265
+
266
+ # Check for ripgrep (rg) adaptive acceleration
267
+ rg_bin = shutil.which("rg")
268
+ if rg_bin:
269
+ rg_cmd = [
270
+ rg_bin,
271
+ "-i",
272
+ "--line-number",
273
+ "--no-heading",
274
+ "--color", "never",
275
+ f"--max-count={max_results}",
276
+ ]
277
+ for ign in DEFAULT_IGNORED_DIRS:
278
+ rg_cmd.extend(["-g", f"!{ign}"])
279
+ rg_cmd.extend([query, str(root)])
280
+
281
+ try:
282
+ proc = subprocess.Popen(
283
+ rg_cmd,
284
+ stdout=subprocess.PIPE,
285
+ stderr=subprocess.PIPE,
286
+ text=True,
287
+ encoding="utf-8",
288
+ errors="replace"
289
+ )
290
+ count = 0
291
+ if proc.stdout:
292
+ for line in proc.stdout:
293
+ line = line.rstrip("\r\n")
294
+ if not line:
295
+ continue
296
+ # format: /abs/path:line:content or rel/path:line:content
297
+ parts = line.split(":", 2)
298
+ if len(parts) >= 3:
299
+ match_path, line_no, content = parts[0], parts[1], parts[2]
300
+ try:
301
+ rel = Path(match_path).relative_to(root)
302
+ except ValueError:
303
+ rel = Path(match_path)
304
+ print(f"{rel}:{line_no} | {content.strip()[:120]}")
305
+ else:
306
+ print(line[:120])
307
+
308
+ count += 1
309
+ if count >= max_results:
310
+ print(f"... [stopped at max {max_results} results]")
311
+ proc.terminate()
312
+ try:
313
+ proc.wait(timeout=0.5)
314
+ except Exception:
315
+ pass
316
+ return
317
+ proc.wait()
318
+ return
319
+ except Exception:
320
+ # Fallback to pure Python search on any rg execution issue
321
+ pass
322
+
323
+ pattern = re.compile(query, re.IGNORECASE)
324
+ _grep_python(pattern, root, max_results)
325
+
326
+
327
+ def cmd_run(cmd: str, cwd: str = None, tail: int = 15, timeout: int = None) -> None:
328
+ log_dir = Path("/tmp/agent_logs") if os.name != "nt" else Path(os.environ.get("TEMP", ".")) / "agent_logs"
329
+ log_dir.mkdir(parents=True, exist_ok=True)
330
+ task_id = f"task_{int(time.time() * 1000) % 1000000}"
331
+ log_file = log_dir / f"{task_id}.log"
332
+
333
+ work_dir = to_path(cwd)
334
+ start_time = time.time()
335
+
336
+ with open(log_file, "w", encoding="utf-8") as out:
337
+ out.write(f"=== Command: {cmd}\n=== CWD: {work_dir}\n=== Started: {time.ctime(start_time)}\n\n")
338
+ out.flush()
339
+ try:
340
+ proc = subprocess.Popen(
341
+ cmd,
342
+ shell=True,
343
+ cwd=str(work_dir),
344
+ stdout=out,
345
+ stderr=subprocess.STDOUT,
346
+ text=True,
347
+ )
348
+ proc.wait(timeout=timeout)
349
+ ret_code = proc.returncode
350
+ except subprocess.TimeoutExpired:
351
+ proc.kill()
352
+ ret_code = -1
353
+ out.write("\n=== ERROR: Timeout expired\n")
354
+
355
+ duration = time.time() - start_time
356
+ dur_str = f"{duration:.2f}s" if duration < 60 else f"{int(duration // 60)}m {int(duration % 60)}s"
357
+
358
+ with open(log_file, "r", encoding="utf-8", errors="replace") as f:
359
+ lines = f.readlines()
360
+
361
+ tail_lines = lines[-tail:] if len(lines) > tail else lines
362
+ tail_str = "".join(tail_lines).strip()
363
+
364
+ if ret_code == 0:
365
+ print(f"OK [exit 0, {dur_str}] Log: {log_file}")
366
+ if tail > 0 and len(lines) > 3:
367
+ print(f"--- Tail ({len(tail_lines)} lines) ---")
368
+ print(tail_str)
369
+ else:
370
+ print(f"FAILED [exit {ret_code}, {dur_str}] Log: {log_file}")
371
+ print(f"--- Failure Tail ({len(tail_lines)} lines) ---")
372
+ print(tail_str)
373
+ sys.exit(ret_code)
374
+
375
+
376
+ def cmd_spawn(cmd: str, cwd: str = None) -> None:
377
+ log_dir = Path("/tmp/agent_logs") if os.name != "nt" else Path(os.environ.get("TEMP", ".")) / "agent_logs"
378
+ log_dir.mkdir(parents=True, exist_ok=True)
379
+ task_id = f"bg_{int(time.time() * 1000) % 1000000}"
380
+ log_file = log_dir / f"{task_id}.log"
381
+ meta_file = log_dir / f"{task_id}.json"
382
+
383
+ work_dir = to_path(cwd)
384
+ start_time = time.time()
385
+
386
+ out = open(log_file, "w", encoding="utf-8")
387
+ out.write(f"=== Spawned: {cmd}\n=== CWD: {work_dir}\n=== Started: {time.ctime(start_time)}\n\n")
388
+ out.flush()
389
+
390
+ kwargs = {"start_new_session": True} if os.name != "nt" else {}
391
+
392
+ proc = subprocess.Popen(
393
+ cmd,
394
+ shell=True,
395
+ cwd=str(work_dir),
396
+ stdout=out,
397
+ stderr=subprocess.STDOUT,
398
+ **kwargs
399
+ )
400
+
401
+ meta = {
402
+ "task_id": task_id,
403
+ "pid": proc.pid,
404
+ "cmd": cmd,
405
+ "cwd": str(work_dir),
406
+ "start_time": start_time,
407
+ "log_file": str(log_file),
408
+ }
409
+ meta_file.write_text(json.dumps(meta, indent=2), encoding="utf-8")
410
+
411
+ print(f"SPAWNED: task_id={task_id} pid={proc.pid} log={log_file}")
412
+
413
+
414
+ def cmd_status(task_id: str) -> None:
415
+ log_dir = Path("/tmp/agent_logs") if os.name != "nt" else Path(os.environ.get("TEMP", ".")) / "agent_logs"
416
+ meta_file = log_dir / f"{task_id}.json"
417
+ if not meta_file.is_file():
418
+ print(f"ERROR: Task {task_id} not found", file=sys.stderr)
419
+ sys.exit(1)
420
+
421
+ meta = json.loads(meta_file.read_text(encoding="utf-8"))
422
+ pid = meta["pid"]
423
+ alive = False
424
+ try:
425
+ if os.name != "nt":
426
+ os.kill(pid, 0)
427
+ alive = True
428
+ else:
429
+ proc = subprocess.run(["tasklist", "/FI", f"PID eq {pid}"], capture_output=True, text=True)
430
+ alive = str(pid) in proc.stdout
431
+ except OSError:
432
+ alive = False
433
+
434
+ runtime = time.time() - meta["start_time"]
435
+ status_str = "RUNNING" if alive else "FINISHED"
436
+
437
+ log_file = Path(meta["log_file"])
438
+ tail_str = ""
439
+ if log_file.is_file():
440
+ lines = log_file.read_text(encoding="utf-8", errors="replace").splitlines()
441
+ tail_str = "\n".join(lines[-5:]) if lines else ""
442
+
443
+ print(f"TASK: {task_id} | Status: {status_str} | PID: {pid} | Runtime: {runtime:.1f}s")
444
+ if tail_str:
445
+ print(f"--- Last 5 lines ---\n{tail_str}")
446
+
447
+
448
+ def cmd_kill(task_id: str) -> None:
449
+ log_dir = Path("/tmp/agent_logs") if os.name != "nt" else Path(os.environ.get("TEMP", ".")) / "agent_logs"
450
+ meta_file = log_dir / f"{task_id}.json"
451
+ if not meta_file.is_file():
452
+ print(f"ERROR: Task {task_id} not found", file=sys.stderr)
453
+ sys.exit(1)
454
+
455
+ meta = json.loads(meta_file.read_text(encoding="utf-8"))
456
+ pid = meta["pid"]
457
+ try:
458
+ if os.name != "nt":
459
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
460
+ time.sleep(0.3)
461
+ try:
462
+ os.killpg(os.getpgid(pid), signal.SIGKILL)
463
+ except OSError:
464
+ pass
465
+ else:
466
+ subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)])
467
+ print(f"OK: Task {task_id} (PID {pid}) terminated.")
468
+ except Exception as e:
469
+ print(f"ERROR: Failed to kill PID {pid}: {e}", file=sys.stderr)
470
+
471
+
472
+ def cmd_logs(task_id: str, lines: int = 50, follow: bool = False) -> None:
473
+ log_dir = Path("/tmp/agent_logs") if os.name != "nt" else Path(os.environ.get("TEMP", ".")) / "agent_logs"
474
+ meta_file = log_dir / f"{task_id}.json"
475
+ log_file = log_dir / f"{task_id}.log"
476
+
477
+ if meta_file.is_file():
478
+ try:
479
+ meta = json.loads(meta_file.read_text(encoding="utf-8"))
480
+ if "log_file" in meta:
481
+ log_file = Path(meta["log_file"])
482
+ except Exception:
483
+ pass
484
+
485
+ if not log_file.is_file():
486
+ print(f"ERROR: Log file for task {task_id} not found at {log_file}", file=sys.stderr)
487
+ sys.exit(1)
488
+
489
+ try:
490
+ with open(log_file, "r", encoding="utf-8", errors="replace") as f:
491
+ if lines > 0:
492
+ all_lines = f.readlines()
493
+ for line in all_lines[-lines:]:
494
+ sys.stdout.write(line)
495
+ sys.stdout.flush()
496
+ else:
497
+ f.seek(0, os.SEEK_END)
498
+
499
+ if not follow:
500
+ return
501
+
502
+ # Follow / streaming mode (tail -f)
503
+ while True:
504
+ line = f.readline()
505
+ if line:
506
+ sys.stdout.write(line)
507
+ sys.stdout.flush()
508
+ else:
509
+ time.sleep(0.2)
510
+ except KeyboardInterrupt:
511
+ pass
512
+ except Exception as e:
513
+ print(f"ERROR: Failed reading log: {e}", file=sys.stderr)
514
+ sys.exit(1)
515
+
516
+
517
+ def handle_ipc_mode() -> None:
518
+ """Reads a JSON request from stdin and executes it without bash shell escaping."""
519
+ try:
520
+ payload = json.loads(sys.stdin.read())
521
+ except Exception as e:
522
+ print(f"ERROR: Invalid JSON input: {e}", file=sys.stderr)
523
+ sys.exit(1)
524
+
525
+ action = payload.get("action")
526
+ args = payload.get("args", {})
527
+
528
+ if action == "view":
529
+ cmd_view(args.get("path"), args.get("start", 1), args.get("end", 100))
530
+ elif action == "outline":
531
+ cmd_outline(args.get("path"))
532
+ elif action == "get_fn":
533
+ cmd_get_fn(args.get("path"), args.get("name"))
534
+ elif action == "patch":
535
+ cmd_patch(args.get("path"), args.get("old"), args.get("new"), args.get("all", False))
536
+ elif action == "rollback":
537
+ cmd_rollback(args.get("path"))
538
+ elif action == "tree":
539
+ cmd_tree(args.get("path", "."), args.get("depth", 2))
540
+ elif action == "grep":
541
+ cmd_grep(args.get("query"), args.get("path", "."), args.get("max", 20))
542
+ elif action == "run":
543
+ cmd_run(args.get("cmd"), args.get("cwd"), args.get("tail", 15), args.get("timeout"))
544
+ elif action == "spawn":
545
+ cmd_spawn(args.get("cmd"), args.get("cwd"))
546
+ elif action == "status":
547
+ cmd_status(args.get("task_id"))
548
+ elif action == "logs":
549
+ cmd_logs(args.get("task_id"), args.get("lines", 50), args.get("follow", False))
550
+ elif action == "kill":
551
+ cmd_kill(args.get("task_id"))
552
+ else:
553
+ print(f"ERROR: Unknown action '{action}'", file=sys.stderr)
554
+ sys.exit(1)
555
+
556
+
557
+ def main():
558
+ if "--ipc" in sys.argv:
559
+ handle_ipc_mode()
560
+ return
561
+
562
+ parser = argparse.ArgumentParser(description="Universal High-Efficiency AgentBox Worker")
563
+ sub = parser.add_subparsers(dest="cmd", required=True)
564
+
565
+ p_view = sub.add_parser("view")
566
+ p_view.add_argument("path")
567
+ p_view.add_argument("--start", type=int, default=1)
568
+ p_view.add_argument("--end", type=int, default=100)
569
+
570
+ p_out = sub.add_parser("outline")
571
+ p_out.add_argument("path")
572
+
573
+ p_fn = sub.add_parser("get-fn")
574
+ p_fn.add_argument("path")
575
+ p_fn.add_argument("name")
576
+
577
+ p_patch = sub.add_parser("patch")
578
+ p_patch.add_argument("path")
579
+ p_patch.add_argument("--old", required=True)
580
+ p_patch.add_argument("--new", required=True)
581
+ p_patch.add_argument("--all", action="store_true")
582
+
583
+ p_rb = sub.add_parser("rollback")
584
+ p_rb.add_argument("path")
585
+
586
+ p_tree = sub.add_parser("tree")
587
+ p_tree.add_argument("path", nargs="?", default=".")
588
+ p_tree.add_argument("--depth", type=int, default=2)
589
+
590
+ p_grep = sub.add_parser("grep")
591
+ p_grep.add_argument("query")
592
+ p_grep.add_argument("path", nargs="?", default=".")
593
+ p_grep.add_argument("--max", type=int, default=20)
594
+
595
+ p_run = sub.add_parser("run")
596
+ p_run.add_argument("command")
597
+ p_run.add_argument("--cwd", default=None)
598
+ p_run.add_argument("--tail", type=int, default=15)
599
+ p_run.add_argument("--timeout", type=int, default=None)
600
+
601
+ p_spawn = sub.add_parser("spawn")
602
+ p_spawn.add_argument("command")
603
+ p_spawn.add_argument("--cwd", default=None)
604
+
605
+ p_stat = sub.add_parser("status")
606
+ p_stat.add_argument("task_id")
607
+
608
+ p_logs = sub.add_parser("logs")
609
+ p_logs.add_argument("task_id")
610
+ p_logs.add_argument("-n", "--lines", type=int, default=50)
611
+ p_logs.add_argument("-f", "--follow", action="store_true")
612
+
613
+ p_kill = sub.add_parser("kill")
614
+ p_kill.add_argument("task_id")
615
+
616
+ args = parser.parse_args()
617
+
618
+ if args.cmd == "view":
619
+ cmd_view(args.path, args.start, args.end)
620
+ elif args.cmd == "outline":
621
+ cmd_outline(args.path)
622
+ elif args.cmd == "get-fn":
623
+ cmd_get_fn(args.path, args.name)
624
+ elif args.cmd == "patch":
625
+ cmd_patch(args.path, args.old, args.new, args.all)
626
+ elif args.cmd == "rollback":
627
+ cmd_rollback(args.path)
628
+ elif args.cmd == "tree":
629
+ cmd_tree(args.path, args.depth)
630
+ elif args.cmd == "grep":
631
+ cmd_grep(args.query, args.path, args.max)
632
+ elif args.cmd == "run":
633
+ cmd_run(args.command, args.cwd, args.tail, args.timeout)
634
+ elif args.cmd == "spawn":
635
+ cmd_spawn(args.command, args.cwd)
636
+ elif args.cmd == "status":
637
+ cmd_status(args.task_id)
638
+ elif args.cmd == "logs":
639
+ cmd_logs(args.task_id, args.lines, args.follow)
640
+ elif args.cmd == "kill":
641
+ cmd_kill(args.task_id)
642
+
643
+
644
+ if __name__ == "__main__":
645
+ main()
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.5
2
+ Name: ai-agentbox
3
+ Version: 0.1.1
4
+ Summary: Token-efficient remote execution, AST outlining, and safe patching tool for AI agents.
5
+ Project-URL: Homepage, https://github.com/agentbox-cli/agentbox
6
+ Project-URL: Repository, https://github.com/agentbox-cli/agentbox
7
+ Project-URL: Issues, https://github.com/agentbox-cli/agentbox/issues
8
+ Author: AgentBox Team
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,ast,cli,llm,remote-development,ssh,tokens
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.8
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: twine>=4.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # 📦 AgentBox (`ai-agentbox`)
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/ai-agentbox.svg)](https://pypi.org/project/ai-agentbox/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
36
+ [![Python: 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
37
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#zero-dependencies)
38
+
39
+ **Ultra-lean, token-efficient remote execution, AST outlining, and safe atomic patching proxy for AI coding agents.**
40
+
41
+ Designed for LLM agents (**Claude Code, Cursor, Antigravity, Aider, Roo-Code, Windsurf**) working on remote servers or local machines.
42
+
43
+ ---
44
+
45
+ ## 🎯 The Problem
46
+
47
+ When AI coding assistants work over SSH or terminal sessions, they burn massive token budgets on:
48
+ 1. **Dumping entire files** (hundreds of lines of code just to locate one function).
49
+ 2. **Intermediate noise from long commands** (`cargo build`, `pytest`, `docker build` streaming lines every second and triggering unnecessary agent turns).
50
+ 3. **Broken shell quoting** (escaping nested quotes `\"` and `'` in bash over SSH leads to repeated syntax errors and retry loops).
51
+
52
+ ---
53
+
54
+ ## ⚡ The Solution: AgentBox
55
+
56
+ AgentBox provides a **Client $\leftrightarrow$ Worker** architecture that reduces token consumption by **80% to 95%**:
57
+
58
+ | Task | Standard AI Agent over SSH | With AgentBox | Token Savings |
59
+ |---|:---:|:---:|:---:|
60
+ | **Analyze 800-line file** | Reads full file (`~3,000` tokens) | `agentbox outline` (`~120` tokens) | **-96%** ⚡ |
61
+ | **Inspect target function** | Guesses line ranges (`~600` tokens) | `agentbox get-fn` (`~150` tokens) | **-75%** 📉 |
62
+ | **Run long build (`cargo`)** | Streams 200 lines (`~1,500` tokens) | `agentbox run` silent (`~40` tokens) | **-97%** ⚡ |
63
+ | **Atomic in-place patch** | Raw diff / cat (`~400` tokens) | `agentbox patch` (`~60` tokens) | **-85%** 📉 |
64
+ | **Project navigation** | `find .` / `tree` (`~1,200` tokens) | `agentbox tree` (`~150` tokens) | **-88%** 📉 |
65
+
66
+ ---
67
+
68
+ ## 🚀 Key Features
69
+
70
+ * **Zero External Dependencies**: Built entirely on the Python 3 standard library.
71
+ * **Self-Bootstrapping Worker**: When targeting a remote SSH server, AgentBox automatically verifies, hashes, and deploys its standalone worker in milliseconds.
72
+ * **Silent Task Runner**: Suppresses intermediate stdout noise, streams to a local log file, and returns only execution time, exit code, and failure tail.
73
+ * **Background Process Manager**: Detached process spawning (`spawn`, `status`, `logs`, `kill`) that survives SSH disconnections.
74
+ * **AST-Powered Skeletons**: Extracts class and function signatures for Python, Rust, JavaScript, TypeScript, C/C++, and Go.
75
+ * **Escape-Proof Atomic Patching**: Sends replacement payloads over stdin IPC streams, eliminating bash quote escaping bugs forever.
76
+
77
+ ---
78
+
79
+ ## 📦 Installation
80
+
81
+ ```bash
82
+ pip install ai-agentbox
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 🛠️ Quickstart
88
+
89
+ ### 1. Configure Target (Optional)
90
+
91
+ Configure your remote SSH server once (stored in `.agentbox.json` or `~/.agentbox/config.json`):
92
+
93
+ ```bash
94
+ agentbox config --set-host user@192.168.1.171 --set-cwd ~/my_project
95
+ ```
96
+
97
+ Or pass `--host` and `--cwd` directly to any command:
98
+
99
+ ```bash
100
+ agentbox --host user@192.168.1.171 --cwd ~/my_project tree
101
+ ```
102
+
103
+ To run **locally**, simply omit the host:
104
+ ```bash
105
+ agentbox tree
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 📖 Command Reference
111
+
112
+ ### 1. Code Navigation & Skeletons
113
+
114
+ #### `agentbox outline <file>`
115
+ Extracts only class, function, and method signatures with line numbers:
116
+ ```bash
117
+ agentbox outline src/gateway.py
118
+ ```
119
+ *Output:*
120
+ ```text
121
+ === OUTLINE: gateway.py (350 lines) ===
122
+ 12- 45 | class GatewayConfig:
123
+ 48- 110 | async def handle_connection(reader, writer) -> bool
124
+ 115- 180 | def commit_flash_block(offset: int) -> None
125
+ ```
126
+
127
+ #### `agentbox get-fn <file> <function_name>`
128
+ Extracts the exact definition and body of a target function:
129
+ ```bash
130
+ agentbox get-fn src/gateway.py handle_connection
131
+ ```
132
+
133
+ #### `agentbox view <file> [--start N] [--end M]`
134
+ Displays an exact line slice with 1-indexed line numbers:
135
+ ```bash
136
+ agentbox view src/gateway.py --start 48 --end 65
137
+ ```
138
+
139
+ ---
140
+
141
+ ### 2. Silent & Background Process Runner
142
+
143
+ #### `agentbox run "<command>" [--tail N] [--timeout T]`
144
+ Silently executes a command, redirects all stdout/stderr to a log file, and returns only the final verdict:
145
+ ```bash
146
+ agentbox run "cargo build --release"
147
+ ```
148
+ *Output on success:*
149
+ ```text
150
+ OK [exit 0, 42.15s] Log: /tmp/agent_logs/task_123456.log
151
+ ```
152
+ *Output on failure (automatically prints tail):*
153
+ ```text
154
+ FAILED [exit 1, 12.30s] Log: /tmp/agent_logs/task_123456.log
155
+ --- Failure Tail (5 lines) ---
156
+ error[E0308]: mismatched types
157
+ --> src/main.rs:42:15
158
+ |
159
+ 42 | let x: u8 = val;
160
+ | ^^^ expected `u8`, found `u16`
161
+ ```
162
+
163
+ #### `agentbox spawn "<command>"`
164
+ Spawns a detached background task that survives SSH disconnects:
165
+ ```bash
166
+ agentbox spawn "python3 train_model.py"
167
+ # Output: SPAWNED: task_id=bg_849201 pid=4215 log=/tmp/agent_logs/bg_849201.log
168
+
169
+ agentbox status bg_849201
170
+ # Output: TASK: bg_849201 | Status: RUNNING | PID: 4215 | Runtime: 1m 24s
171
+
172
+ agentbox logs bg_849201 -n 20
173
+ # Output: displays last 20 lines of log
174
+
175
+ agentbox logs bg_849201 -f
176
+ # Output: live real-time log streaming (tail -f)
177
+
178
+ agentbox kill bg_849201
179
+ # Output: OK: Task bg_849201 (PID 4215) terminated.
180
+ ```
181
+
182
+ ---
183
+
184
+ ### 3. Safe Atomic Patching
185
+
186
+ #### `agentbox patch <file> --old "<exact_code>" --new "<replacement>"`
187
+ Safely replaces code with automatic `.bak` backup creation:
188
+ ```bash
189
+ agentbox patch src/config.py --old 'PORT = 8000' --new 'PORT = 9000'
190
+ # Output: OK: Replaced 1 occurrence(s) in config.py. Backup: config.py.bak
191
+ ```
192
+
193
+ #### `agentbox rollback <file>`
194
+ Restores file from backup in case of errors:
195
+ ```bash
196
+ agentbox rollback src/config.py
197
+ # Output: OK: Restored config.py from backup and removed backup.
198
+ ```
199
+
200
+ ---
201
+
202
+ ### 4. Search & Directory Tree
203
+
204
+ #### `agentbox tree [path] [--depth N]`
205
+ Prints clean directory structure while automatically ignoring `.git`, `node_modules`, `target`, `__pycache__`, `.venv`, and build artifacts:
206
+ ```bash
207
+ agentbox tree --depth 2
208
+ ```
209
+
210
+ #### `agentbox grep "<query>" [path] [--max N]`
211
+ Fast token-limited search across text files:
212
+ ```bash
213
+ agentbox grep "handle_packet" src/ --max 10
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 🤖 System Prompt for AI Agents
219
+
220
+ Add this rule to your project's `AGENTS.md` or `.cursorrules` to instruct your AI agent to use `agentbox`:
221
+
222
+ ```markdown
223
+ ## Remote & Local Execution (AgentBox)
224
+ Always use `agentbox` for remote server operations to minimize token consumption:
225
+ 1. Use `agentbox outline <file>` before reading entire files.
226
+ 2. Use `agentbox get-fn <file> <name>` to read specific functions.
227
+ 3. Use `agentbox run "<command>"` for all builds, tests, and long commands.
228
+ 4. Use `agentbox patch <file> --old "..." --new "..."` for code modifications.
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 📄 License
234
+
235
+ MIT License. Free for open-source and commercial use.
@@ -0,0 +1,11 @@
1
+ agentbox/__init__.py,sha256=YXhK6vAU_wusl27OANfv_7Hyig3sr5VNeChu8AX-h6o,131
2
+ agentbox/__main__.py,sha256=k2TqtPw9lGhA7pvv1rDrkLs4W9QHk7TSLP4_oUltqEQ,201
3
+ agentbox/cli.py,sha256=4vbWH4uwP16diXbV_bakgyLb26HZsYlYHUwCvH5PDzg,7074
4
+ agentbox/client.py,sha256=-FF3ixYezj5eQT-PGO48CfPSOn06H_Z-guGt-KMJlM4,4496
5
+ agentbox/config.py,sha256=rX09QpI0Rb907mIJhYB-qSK1g-TBYwCJPwJ6pkolyD8,1543
6
+ agentbox/worker.py,sha256=kOAbKWTAysSp11kmIYZofWV77QEMp2RFnEnkH1XZtOk,23052
7
+ ai_agentbox-0.1.1.dist-info/METADATA,sha256=dJZJ3PTq7XodoOU8SPehxjldCWOWSt_b-xZ0mTbuXco,8223
8
+ ai_agentbox-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ ai_agentbox-0.1.1.dist-info/entry_points.txt,sha256=u_320LVs7Vndr2ti0gFQXhR8o9kkjW0LmNjLzEXLr50,47
10
+ ai_agentbox-0.1.1.dist-info/licenses/LICENSE,sha256=od0e72-BzeJ4bNya5Q-4D-BI9kut0MC171LTCVEoXuM,1078
11
+ ai_agentbox-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentbox = agentbox.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentBox Contributors
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.