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 +5 -0
- agentbox/__main__.py +11 -0
- agentbox/cli.py +157 -0
- agentbox/client.py +121 -0
- agentbox/config.py +55 -0
- agentbox/worker.py +645 -0
- ai_agentbox-0.1.1.dist-info/METADATA +235 -0
- ai_agentbox-0.1.1.dist-info/RECORD +11 -0
- ai_agentbox-0.1.1.dist-info/WHEEL +4 -0
- ai_agentbox-0.1.1.dist-info/entry_points.txt +2 -0
- ai_agentbox-0.1.1.dist-info/licenses/LICENSE +21 -0
agentbox/__init__.py
ADDED
agentbox/__main__.py
ADDED
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
|
+
[](https://pypi.org/project/ai-agentbox/)
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
[](#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,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.
|