Added many tools

This commit is contained in:
AuroraCrimsonRose
2026-06-03 06:01:06 -05:00
parent 3723d2381d
commit e471f9bc54
28 changed files with 3488 additions and 205 deletions

View File

@@ -13,12 +13,18 @@ WORKSPACE_ROOT = Path(os.getenv("WORKSPACE_ROOT", Path.cwd())).resolve()
LOG_DIR = WORKSPACE_ROOT / "logs"
TMP_DIR = WORKSPACE_ROOT / "tmp"
CONFIG_DIR = WORKSPACE_ROOT / "config"
LOG_DIR.mkdir(parents=True, exist_ok=True)
TMP_DIR.mkdir(parents=True, exist_ok=True)
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
# =========================
# OLLAMA CONFIG
# =========================
OLLAMA_URL = "http://localhost:11434"
# =========================
# SYSTEM LIMITS

View File

@@ -167,7 +167,7 @@ def shutdown_gracefully() -> None:
"""Clean shutdown of executor and other resources."""
logger.info("Initiating graceful shutdown...")
try:
executor.shutdown()
executor.shutdown() # type: ignore[attr-defined]
logger.info("Executor shut down successfully.")
except Exception as e:
logger.error(f"Error during executor shutdown: {e}")

1
memory_store.json Normal file
View File

@@ -0,0 +1 @@
[]

View File

@@ -1,16 +1,36 @@
"""
Tool bootstrap.
Tool bootstrap module.
Importing this module forces
tool registration into registry.
This package previously used explicit imports to force tool registration,
but has since moved to a dynamic discovery system.
Current system behavior:
- tools are loaded via tools.discovery.load_all_tools()
- each tool self-registers into the global registry
- MCP bindings happen after registry population
"""
from tools.ping import PingTool
from tools.filesystem import FilesystemTool
from tools.subprocess import SubprocessTool
# ------------------------------------------------------------
# LEGACY APPROACH (STATIC IMPORT REGISTRATION)
# ------------------------------------------------------------
# These imports were previously used to force tool registration
# at import-time. This approach was replaced to support scalable
# plugin discovery via pkgutil-based loading.
#
# from tools.ping import PingTool
# from tools.filesystem import FilesystemTool
# from tools.subprocess import SubprocessTool
#
# __all__ = [
# "PingTool",
# "FilesystemTool",
# "SubprocessTool",
# ]
__all__ = [
"PingTool",
"FilesystemTool",
"SubprocessTool",
]
# ------------------------------------------------------------
# CURRENT DESIGN
# ------------------------------------------------------------
# Tool registration is now fully dynamic.
# See: tools.discovery.load_all_tools()
#
# No explicit imports are required here.

178
tools/agent_loop.py Normal file
View File

@@ -0,0 +1,178 @@
from __future__ import annotations
from typing import Any
from core.tools.registry import registry
from core.events import bus
class AgentLoop:
"""
Minimal deterministic agent loop.
Orchestrates tools like:
- search
- intelligent_search
- crawler
- research
Can later be upgraded with LLM-based planning.
"""
def __init__(self, ctx):
self.ctx = ctx
self.max_steps = 5
# =========================
# ENTRY POINT
# =========================
def run(self, goal: str) -> dict[str, Any]:
state = {
"goal": goal,
"steps": [],
"memory": [],
"final": None
}
bus.log(
"AGENT",
"agent_loop_start",
"INFO",
{"goal": goal}
)
for step in range(self.max_steps):
action = self._decide(state)
if action["type"] == "stop":
state["final"] = action.get("result")
break
result = self._execute(action, state)
state["steps"].append(action)
state["memory"].append(result)
# simple convergence heuristic
if self._is_satisfied(state):
state["final"] = result
break
if not state["final"]:
state["final"] = state["memory"][-1] if state["memory"] else {}
return state
# =========================
# DECISION LOGIC (RULE-BASED FOR NOW)
# =========================
def _decide(self, state: dict[str, Any]) -> dict[str, Any]:
"""
Very simple heuristic planner.
Later upgrade point: replace with LLM planner.
"""
goal = state["goal"]
memory = state["memory"]
if not memory:
return {
"type": "tool",
"tool": "research",
"input": {
"query": goal,
"depth": 1
}
}
last = memory[-1]
# if research returned sources, refine or stop
if isinstance(last, dict) and "sources" in last:
if len(last["sources"]) >= 2:
return {
"type": "stop",
"result": last
}
# if weak results, deepen search
return {
"type": "tool",
"tool": "research",
"input": {
"query": goal,
"depth": 2,
"max_sources": 5
}
}
return {
"type": "stop",
"result": last
}
# =========================
# TOOL EXECUTION
# =========================
def _execute(self, action: dict[str, Any], state: dict[str, Any]) -> Any:
tool_name = action.get("tool")
tool_input = action.get("input", {})
bus.log(
"AGENT",
"agent_tool_call",
"INFO",
{
"tool": tool_name,
"input": tool_input
}
)
if not tool_name:
return {"error": "No tool specified"}
try:
result = registry.run(
tool_name,
tool_input,
self.ctx
)
return result
except Exception as e:
return {
"error": str(e),
"tool": tool_name
}
# =========================
# STOPPING CONDITION
# =========================
def _is_satisfied(self, state: dict[str, Any]) -> bool:
"""
Simple satisfaction heuristic.
Later upgrade: LLM-based evaluation.
"""
memory = state["memory"]
if not memory:
return False
last = memory[-1]
if isinstance(last, dict):
# if research returned structured sources, assume OK
if "sources" in last and len(last["sources"]) > 0:
return True
if "error" in last:
return True
return False

View File

@@ -0,0 +1,141 @@
from __future__ import annotations
from typing import Any
from pathlib import Path
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.safety import safety
from core.events import bus
from core.subprocess import run_command
class BochsTool(BaseTool):
name = "bochs"
description = "Bochs emulator control (run, validate, debug, config execution)"
# =========================================================
# EXECUTE ROUTER
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"BOCHS",
"bochs_execute",
"INFO",
{"action": action}
)
match action:
case "run":
return self.run_vm(payload)
case "validate":
return self.validate_config(payload)
case "debug":
return self.debug_vm(payload)
case _:
raise ValueError(f"Unknown bochs action: {action}")
# =========================================================
# HELPERS
# =========================================================
def _path(self, value: str) -> Path:
return safety.validate_path(value)
# =========================================================
# RUN VM
# =========================================================
def run_vm(self, payload: dict[str, Any]):
config = payload.get("config")
if not isinstance(config, str):
raise ValueError("config must be string")
config_path = self._path(config)
if not config_path.exists():
raise ValueError(f"Bochs config not found: {config}")
result = run_command(
cmd=["bochs", "-f", str(config_path), "-q"],
)
return {
"action": "run",
"config": str(config_path),
"status": "success" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# VALIDATE CONFIG
# =========================================================
def validate_config(self, payload: dict[str, Any]):
config = payload.get("config")
if not isinstance(config, str):
raise ValueError("config must be string")
config_path = self._path(config)
if not config_path.exists():
return {
"status": "error",
"error": "config file not found"
}
# Bochs has no strict "validate" mode, so we simulate dry run parse
result = run_command(
cmd=["bochs", "-f", str(config_path), "-n"],
)
return {
"action": "validate",
"config": str(config_path),
"status": "ok" if result.get("return_code") == 0 else "warning",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# DEBUG MODE
# =========================================================
def debug_vm(self, payload: dict[str, Any]):
config = payload.get("config")
if not isinstance(config, str):
raise ValueError("config must be string")
config_path = self._path(config)
if not config_path.exists():
raise ValueError("config not found")
result = run_command(
cmd=["bochs", "-f", str(config_path), "-q", "-debug"],
)
return {
"action": "debug",
"config": str(config_path),
"status": "success" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(BochsTool())

View File

@@ -0,0 +1,188 @@
from __future__ import annotations
from typing import Any
from pathlib import Path
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.safety import safety
from core.events import bus
from core.subprocess import run_command
class CMakeTool(BaseTool):
name = "cmake"
description = "CMake build system operations (configure, build, generate, clean)"
# =========================================================
# EXECUTE ROUTER
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"CMAKE",
"cmake_execute",
"INFO",
{"action": action}
)
match action:
case "configure":
return self.configure(payload)
case "build":
return self.build(payload)
case "clean":
return self.clean(payload)
case "generate":
return self.generate(payload)
case _:
raise ValueError(f"Unknown cmake action: {action}")
# =========================================================
# HELPERS
# =========================================================
def _path(self, value: str) -> Path:
return safety.validate_path(value)
# =========================================================
# CONFIGURE
# =========================================================
def configure(self, payload: dict[str, Any]):
source_dir = payload.get("source_dir", ".")
build_dir = payload.get("build_dir", "build")
generator = payload.get("generator") # optional
build_type = payload.get("build_type", "Release")
if not isinstance(source_dir, str):
raise ValueError("source_dir must be string")
if not isinstance(build_dir, str):
raise ValueError("build_dir must be string")
src = self._path(source_dir)
bld = self._path(build_dir)
bld.mkdir(parents=True, exist_ok=True)
cmd = [
"cmake",
"-S", str(src),
"-B", str(bld),
f"-DCMAKE_BUILD_TYPE={build_type}"
]
if generator:
if not isinstance(generator, str):
raise ValueError("generator must be string")
cmd.extend(["-G", generator])
result = run_command(cmd=cmd)
return {
"action": "configure",
"source_dir": str(src),
"build_dir": str(bld),
"status": "success" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# BUILD
# =========================================================
def build(self, payload: dict[str, Any]):
build_dir = payload.get("build_dir", "build")
target = payload.get("target") # optional
jobs = payload.get("jobs", 0)
if not isinstance(build_dir, str):
raise ValueError("build_dir must be string")
bld = self._path(build_dir)
cmd = ["cmake", "--build", str(bld)]
if target:
if not isinstance(target, str):
raise ValueError("target must be string")
cmd.extend(["--target", target])
if isinstance(jobs, int) and jobs > 0:
cmd.extend(["--parallel", str(jobs)])
result = run_command(cmd=cmd)
return {
"action": "build",
"build_dir": str(bld),
"target": target,
"status": "success" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# GENERATE (alias convenience)
# =========================================================
def generate(self, payload: dict[str, Any]):
# CMake modern workflow usually doesn't need this separately,
# but kept for explicit "generate-only" workflows.
return self.configure(payload)
# =========================================================
# CLEAN
# =========================================================
def clean(self, payload: dict[str, Any]):
build_dir = payload.get("build_dir", "build")
if not isinstance(build_dir, str):
raise ValueError("build_dir must be string")
bld = self._path(build_dir)
if not bld.exists():
return {
"action": "clean",
"status": "skipped",
"message": "build directory does not exist"
}
# safe clean: only remove cache artifacts, not full directory unless requested
cache_file = bld / "CMakeCache.txt"
removed = []
if cache_file.exists():
cache_file.unlink()
removed.append("CMakeCache.txt")
# optional: remove CMakeFiles
cmake_files = bld / "CMakeFiles"
if cmake_files.exists():
import shutil
shutil.rmtree(cmake_files)
removed.append("CMakeFiles/")
return {
"action": "clean",
"build_dir": str(bld),
"removed": removed,
"status": "ok"
}
# =========================================================
# REGISTER
# =========================================================
registry.register(CMakeTool())

194
tools/crawler.py Normal file
View File

@@ -0,0 +1,194 @@
from __future__ import annotations
from typing import Any
from urllib.parse import urljoin, urlparse
import urllib.request
import re
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class CrawlerTool(BaseTool):
"""
Lightweight safe web crawler.
Designed for:
- page fetching
- link extraction
- basic text scraping
- bounded crawling
"""
name = "crawler"
description = "Fetch and crawl web pages safely"
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "fetch")).strip()
bus.log(
"CRAWLER",
"crawler_execute",
"INFO",
{"action": action}
)
match action:
case "fetch":
return self.fetch(payload)
case "links":
return self.extract_links(payload)
case "crawl":
return self.crawl(payload)
case _:
raise ValueError(f"Unknown crawler action: {action}")
# =========================
# FETCH PAGE
# =========================
def fetch(self, payload: dict[str, Any]):
url = payload.get("url")
if not isinstance(url, str):
raise ValueError("url must be string")
req = urllib.request.Request(
url,
headers={"User-Agent": "MCP-Crawler/1.0"}
)
try:
with urllib.request.urlopen(req, timeout=6) as resp:
html = resp.read().decode("utf-8", errors="ignore")
text = self._strip_html(html)
return {
"url": url,
"text": text[:5000],
"length": len(text)
}
except Exception as e:
return {
"url": url,
"error": str(e)
}
# =========================
# EXTRACT LINKS
# =========================
def extract_links(self, payload: dict[str, Any]):
url = payload.get("url")
if not isinstance(url, str):
raise ValueError("url must be string")
try:
req = urllib.request.Request(url)
with urllib.request.urlopen(req, timeout=6) as resp:
html = resp.read().decode("utf-8", errors="ignore")
links = re.findall(r'href=["\'](.*?)["\']', html)
normalized = []
for link in links:
normalized.append(urljoin(url, link))
return {
"url": url,
"links": normalized[:200],
"count": len(normalized)
}
except Exception as e:
return {
"url": url,
"error": str(e)
}
# =========================
# SIMPLE CRAWL (DEPTH 12)
# =========================
def crawl(self, payload: dict[str, Any]):
start_url = payload.get("url")
depth = payload.get("depth", 1)
if not isinstance(start_url, str):
raise ValueError("url must be string")
if not isinstance(depth, int):
depth = 1
visited = set()
results = []
def safe_fetch(u: str):
try:
req = urllib.request.Request(u)
with urllib.request.urlopen(req, timeout=5) as resp:
return resp.read().decode("utf-8", errors="ignore")
except Exception:
return ""
def crawl_url(url: str, d: int):
if d < 0 or url in visited:
return
visited.add(url)
html = safe_fetch(url)
text = self._strip_html(html)
results.append({
"url": url,
"text": text[:2000]
})
if d == 0:
return
links = re.findall(r'href=["\'](.*?)["\']', html)
for link in links[:20]:
full = urljoin(url, link)
# safety: stay same domain
if urlparse(full).netloc == urlparse(start_url).netloc:
crawl_url(full, d - 1)
crawl_url(start_url, depth)
return {
"start": start_url,
"depth": depth,
"pages": results,
"count": len(results)
}
# =========================
# HTML STRIPPER
# =========================
def _strip_html(self, html: str) -> str:
html = re.sub(r"<script.*?>.*?</script>", "", html, flags=re.S)
html = re.sub(r"<style.*?>.*?</style>", "", html, flags=re.S)
html = re.sub(r"<.*?>", " ", html)
html = re.sub(r"\s+", " ", html)
return html.strip()
# =========================
# REGISTER
# =========================
registry.register(CrawlerTool())

View File

@@ -2,9 +2,12 @@ from __future__ import annotations
import importlib
import pkgutil
import logging
import tools as tools_pkg
logger = logging.getLogger(__name__)
def load_all_tools():
"""
@@ -12,11 +15,36 @@ def load_all_tools():
Imports all modules inside /tools so they register
themselves into the registry.
Safe version:
- isolates import failures per module
- logs instead of crashing system boot
"""
for module in pkgutil.iter_modules(
tools_pkg.__path__
):
importlib.import_module(
f"tools.{module.name}"
)
loaded = 0
failed = 0
for module in pkgutil.iter_modules(tools_pkg.__path__):
module_name = f"tools.{module.name}"
try:
importlib.import_module(module_name)
loaded += 1
logger.info(f"[TOOLS] Loaded: {module_name}")
except Exception as e:
failed += 1
logger.exception(
f"[TOOLS] Failed to load {module_name}: {e}"
)
logger.info(
f"[TOOLS] Discovery complete: loaded={loaded}, failed={failed}"
)
return {
"loaded": loaded,
"failed": failed
}

View File

@@ -0,0 +1,225 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.subprocess import run_command
class DockerTool(BaseTool):
name = "docker"
description = "Docker container and image management"
# =========================================================
# EXECUTE ROUTER
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"DOCKER",
"docker_execute",
"INFO",
{"action": action}
)
match action:
case "ps":
return self.ps(payload)
case "run":
return self.run_container(payload, ctx)
case "stop":
return self.stop_container(payload)
case "start":
return self.start_container(payload)
case "restart":
return self.restart_container(payload)
case "logs":
return self.logs(payload)
case "images":
return self.images(payload)
case "build":
return self.build_image(payload, ctx)
case _:
raise ValueError(f"Unknown docker action: {action}")
# =========================================================
# CONTAINERS LIST
# =========================================================
def ps(self, payload: dict[str, Any]):
all_containers = payload.get("all", False)
cmd = ["docker", "ps"]
if all_containers:
cmd.append("-a")
result = run_command(cmd=cmd)
return {
"action": "ps",
"status": "ok" if result.get("return_code") == 0 else "error",
"output": result.get("stdout", ""),
"error": result.get("stderr", "")
}
# =========================================================
# RUN CONTAINER
# =========================================================
def run_container(self, payload: dict[str, Any], ctx: ToolContext):
image = payload.get("image")
name = payload.get("name")
args = payload.get("args", [])
if not isinstance(image, str):
raise ValueError("image must be string")
if not isinstance(args, list):
raise ValueError("args must be list")
cmd = ["docker", "run"]
if name:
if not isinstance(name, str):
raise ValueError("name must be string")
cmd += ["--name", name]
cmd += args
cmd.append(image)
if ctx.dry_run:
return {
"dry_run": True,
"command": cmd
}
result = run_command(cmd=cmd)
return {
"action": "run",
"image": image,
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# STOP / START / RESTART
# =========================================================
def stop_container(self, payload: dict[str, Any]):
return self._simple_container_action("stop", payload)
def start_container(self, payload: dict[str, Any]):
return self._simple_container_action("start", payload)
def restart_container(self, payload: dict[str, Any]):
return self._simple_container_action("restart", payload)
def _simple_container_action(self, action: str, payload: dict[str, Any]):
container = payload.get("container")
if not isinstance(container, str):
raise ValueError("container must be string")
result = run_command(
cmd=["docker", action, container]
)
return {
"action": action,
"container": container,
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# LOGS
# =========================================================
def logs(self, payload: dict[str, Any]):
container = payload.get("container")
tail = payload.get("tail", 100)
if not isinstance(container, str):
raise ValueError("container must be string")
cmd = ["docker", "logs", "--tail", str(tail), container]
result = run_command(cmd=cmd)
return {
"action": "logs",
"container": container,
"output": result.get("stdout", ""),
"error": result.get("stderr", "")
}
# =========================================================
# IMAGES
# =========================================================
def images(self, payload: dict[str, Any]):
result = run_command(cmd=["docker", "images"])
return {
"action": "images",
"output": result.get("stdout", ""),
"error": result.get("stderr", "")
}
# =========================================================
# BUILD IMAGE
# =========================================================
def build_image(self, payload: dict[str, Any], ctx: ToolContext):
path = payload.get("path", ".")
tag = payload.get("tag")
if not isinstance(path, str):
raise ValueError("path must be string")
cmd = ["docker", "build", "-t"]
if isinstance(tag, str):
cmd.append(tag)
else:
cmd.append("untagged-image")
cmd.append(path)
if ctx.dry_run:
return {
"dry_run": True,
"command": cmd
}
result = run_command(cmd=cmd)
return {
"action": "build",
"path": path,
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(DockerTool())

View File

@@ -0,0 +1,254 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.subprocess import run_command
class FFmpegTool(BaseTool):
name = "ffmpeg"
description = "Media processing using FFmpeg"
# =========================================================
# ROUTER
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"FFMPEG",
"ffmpeg_execute",
"INFO",
{"action": action}
)
match action:
case "convert":
return self.convert(payload, ctx)
case "extract_audio":
return self.extract_audio(payload, ctx)
case "trim":
return self.trim(payload, ctx)
case "merge":
return self.merge(payload, ctx)
case "probe":
return self.probe(payload)
case "thumbnail":
return self.thumbnail(payload, ctx)
case _:
raise ValueError(f"Unknown ffmpeg action: {action}")
# =========================================================
# CONVERT
# =========================================================
def convert(self, payload: dict[str, Any], ctx: ToolContext):
input_file = payload.get("input")
output_file = payload.get("output")
codec = payload.get("codec") # optional
if not isinstance(input_file, str) or not isinstance(output_file, str):
raise ValueError("input/output must be strings")
cmd = ["ffmpeg", "-y", "-i", input_file]
if isinstance(codec, str):
cmd += ["-c:v", codec]
cmd.append(output_file)
if ctx.dry_run:
return {"dry_run": True, "command": cmd}
result = run_command(cmd=cmd)
return {
"action": "convert",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# EXTRACT AUDIO
# =========================================================
def extract_audio(self, payload: dict[str, Any], ctx: ToolContext):
input_file = payload.get("input")
output_file = payload.get("output")
if not isinstance(input_file, str) or not isinstance(output_file, str):
raise ValueError("input/output must be strings")
cmd = [
"ffmpeg",
"-y",
"-i", input_file,
"-vn",
"-acodec", "copy",
output_file
]
if ctx.dry_run:
return {"dry_run": True, "command": cmd}
result = run_command(cmd=cmd)
return {
"action": "extract_audio",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# TRIM
# =========================================================
def trim(self, payload: dict[str, Any], ctx: ToolContext):
input_file = payload.get("input")
output_file = payload.get("output")
start = payload.get("start", "00:00:00")
duration = payload.get("duration")
if not isinstance(input_file, str) or not isinstance(output_file, str):
raise ValueError("input/output must be strings")
cmd = [
"ffmpeg",
"-y",
"-i", input_file,
"-ss", str(start),
]
if duration:
cmd += ["-t", str(duration)]
cmd.append(output_file)
if ctx.dry_run:
return {"dry_run": True, "command": cmd}
result = run_command(cmd=cmd)
return {
"action": "trim",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# MERGE FILES
# =========================================================
def merge(self, payload: dict[str, Any], ctx: ToolContext):
inputs = payload.get("inputs")
output = payload.get("output")
if not isinstance(inputs, list) or not isinstance(output, str):
raise ValueError("inputs must be list, output must be string")
# ffmpeg concat demuxer style
file_list = "ffmpeg_concat.txt"
with open(file_list, "w", encoding="utf-8") as f:
for item in inputs:
f.write(f"file '{item}'\n")
cmd = [
"ffmpeg",
"-y",
"-f", "concat",
"-safe", "0",
"-i", file_list,
"-c", "copy",
output
]
if ctx.dry_run:
return {"dry_run": True, "command": cmd}
result = run_command(cmd=cmd)
return {
"action": "merge",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# PROBE
# =========================================================
def probe(self, payload: dict[str, Any]):
input_file = payload.get("input")
if not isinstance(input_file, str):
raise ValueError("input must be string")
cmd = [
"ffprobe",
"-v", "error",
"-show_format",
"-show_streams",
input_file
]
result = run_command(cmd=cmd)
return {
"action": "probe",
"data": result.get("stdout", ""),
"error": result.get("stderr", "")
}
# =========================================================
# THUMBNAIL
# =========================================================
def thumbnail(self, payload: dict[str, Any], ctx: ToolContext):
input_file = payload.get("input")
output_file = payload.get("output")
time = payload.get("time", "00:00:01")
if not isinstance(input_file, str) or not isinstance(output_file, str):
raise ValueError("input/output must be strings")
cmd = [
"ffmpeg",
"-y",
"-ss", str(time),
"-i", input_file,
"-vframes", "1",
output_file
]
if ctx.dry_run:
return {"dry_run": True, "command": cmd}
result = run_command(cmd=cmd)
return {
"action": "thumbnail",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# REGISTER
# =========================================================
registry.register(FFmpegTool())

View File

@@ -13,146 +13,110 @@ class FilesystemTool(BaseTool):
name = "filesystem"
description = "Safe filesystem operations"
MAX_READ_BYTES = 5_000_000
MAX_LIST_ENTRIES = 5000
# =========================
# EXECUTE
# EXECUTE ROUTER
# =========================
def execute(
self,
payload: dict[str, Any],
ctx: ToolContext
):
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"FILESYSTEM",
"filesystem_execute",
"INFO",
{
"action": action
}
{"action": action}
)
match action:
case "read_file":
return self.read_file(payload)
handlers = {
"read_file": self.read_file,
"write_file": self.write_file,
"list_dir": self.list_dir,
"exists": self.exists,
"mkdir": self.mkdir,
}
case "write_file":
return self.write_file(payload)
handler = handlers.get(action)
if not handler:
raise ValueError(f"Unknown filesystem action: {action}")
case "list_dir":
return self.list_dir(payload)
return handler(payload, ctx)
case "exists":
return self.exists(payload)
# =========================
# PATH HELPERS
# =========================
case "mkdir":
return self.mkdir(payload)
def _get_path(self, payload: dict[str, Any]) -> Path:
path_value = payload.get("path")
if not isinstance(path_value, str):
raise ValueError("path must be string")
case _:
raise ValueError(
f"Unknown filesystem action: {action}"
)
return safety.validate_path(path_value)
# =========================
# READ FILE
# =========================
def read_file(
self,
payload: dict[str, Any]
):
path_value = payload.get("path")
def read_file(self, payload: dict[str, Any], ctx: ToolContext):
path = self._get_path(payload)
if not isinstance(path_value, str):
raise ValueError("path must be string")
data = path.read_text(encoding="utf-8")
path = safety.validate_path(path_value)
if len(data.encode("utf-8")) > self.MAX_READ_BYTES:
raise ValueError("File exceeds read size limit")
return {
"path": str(path),
"content": path.read_text(
encoding="utf-8"
)
}
return {"path": str(path), "content": data}
# =========================
# WRITE FILE
# =========================
def write_file(
self,
payload: dict[str, Any]
):
path_value = payload.get("path")
content_value = payload.get("content")
def write_file(self, payload: dict[str, Any], ctx: ToolContext):
path = self._get_path(payload)
if not isinstance(path_value, str):
raise ValueError("path must be string")
content = payload.get("content")
if not isinstance(content, str):
raise ValueError("content must be string")
if not isinstance(content_value, str):
raise ValueError(
"content must be string"
)
safety.check_file_write(path, content)
path = safety.validate_path(path_value)
path.parent.mkdir(parents=True, exist_ok=True)
safety.check_file_write(
path,
content_value
)
if path.exists():
backup = path.with_suffix(path.suffix + ".bak")
try:
backup.write_text(path.read_text(encoding="utf-8"))
except Exception:
pass
path.parent.mkdir(
parents=True,
exist_ok=True
)
path.write_text(
content_value,
encoding="utf-8"
)
path.write_text(content, encoding="utf-8")
return {
"ok": True,
"path": str(path),
"bytes_written": len(
content_value.encode("utf-8")
)
"bytes_written": len(content.encode("utf-8"))
}
# =========================
# LIST DIRECTORY
# =========================
def list_dir(
self,
payload: dict[str, Any]
):
path_value = payload.get(
"path",
"."
)
def list_dir(self, payload: dict[str, Any], ctx: ToolContext):
path = self._get_path(payload)
if not isinstance(path_value, str):
raise ValueError(
"path must be string"
)
entries = []
for i, item in enumerate(path.iterdir()):
if i >= self.MAX_LIST_ENTRIES:
break
path = safety.validate_path(
path_value
)
entries: list[dict[str, Any]] = []
for item in path.iterdir():
entries.append(
{
"name": item.name,
"path": str(item),
"is_dir": item.is_dir(),
"is_file": item.is_file()
}
)
entries.append({
"name": item.name,
"path": str(item),
"is_dir": item.is_dir(),
"is_file": item.is_file()
})
return {
"path": str(path),
@@ -164,20 +128,8 @@ class FilesystemTool(BaseTool):
# EXISTS
# =========================
def exists(
self,
payload: dict[str, Any]
):
path_value = payload.get("path")
if not isinstance(path_value, str):
raise ValueError(
"path must be string"
)
path = safety.validate_path(
path_value
)
def exists(self, payload: dict[str, Any], ctx: ToolContext):
path = self._get_path(payload)
return {
"path": str(path),
@@ -190,25 +142,10 @@ class FilesystemTool(BaseTool):
# MKDIR
# =========================
def mkdir(
self,
payload: dict[str, Any]
):
path_value = payload.get("path")
def mkdir(self, payload: dict[str, Any], ctx: ToolContext):
path = self._get_path(payload)
if not isinstance(path_value, str):
raise ValueError(
"path must be string"
)
path = safety.validate_path(
path_value
)
path.mkdir(
parents=True,
exist_ok=True
)
path.mkdir(parents=True, exist_ok=True)
return {
"ok": True,
@@ -216,10 +153,4 @@ class FilesystemTool(BaseTool):
}
# =========================
# SELF REGISTER
# =========================
registry.register(
FilesystemTool()
)
registry.register(FilesystemTool())

View File

@@ -79,7 +79,7 @@ class GiteaTool(BaseTool):
}
try:
response = requests.post(url, json=payload_data, headers=self.headers)
response = requests.post(url, json=payload_data, headers=self.headers, timeout=(3, 10))
response.raise_for_status()
return {
"status": "success",
@@ -97,7 +97,7 @@ class GiteaTool(BaseTool):
url = f"{GITEA_URL}/api/v1/user/repos"
try:
response = requests.get(url, headers=self.headers)
response = requests.get(url, headers=self.headers, timeout=(3, 10))
response.raise_for_status()
repos = response.json()
return {
@@ -124,7 +124,7 @@ class GiteaTool(BaseTool):
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}"
try:
response = requests.get(url, headers=self.headers)
response = requests.get(url, headers=self.headers, timeout=(3, 10))
response.raise_for_status()
return {
"status": "success",
@@ -173,7 +173,7 @@ class GiteaTool(BaseTool):
}
try:
response = requests.post(url, json=payload_data, headers=self.headers)
response = requests.post(url, json=payload_data, headers=self.headers, timeout=(3, 10))
response.raise_for_status()
return {
"status": "success",
@@ -204,7 +204,7 @@ class GiteaTool(BaseTool):
url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/contents/{path}"
try:
response = requests.get(url, headers=self.headers)
response = requests.get(url, headers=self.headers, timeout=(3, 10))
response.raise_for_status()
return {
"status": "success",

175
tools/gpu.py Normal file
View File

@@ -0,0 +1,175 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.subprocess import run_command
class GPUTool(BaseTool):
"""
GPU introspection tool.
Uses nvidia-smi when available.
"""
name = "gpu"
description = "GPU usage, memory, and process inspection"
# =========================================================
# EXECUTE
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "info")).strip()
bus.log(
"GPU",
"gpu_execute",
"INFO",
{"action": action}
)
match action:
case "info":
return self.gpu_info()
case "usage":
return self.gpu_usage()
case "processes":
return self.gpu_processes()
case "full":
return self.full_snapshot()
case _:
raise ValueError(f"Unknown gpu action: {action}")
# =========================================================
# GPU INFO
# =========================================================
def gpu_info(self):
result = run_command(
cmd=[
"nvidia-smi",
"--query-gpu=name,driver_version,memory.total",
"--format=csv,noheader"
]
)
if result.get("return_code") != 0:
return {
"status": "error",
"error": result.get("stderr", "nvidia-smi not available")
}
lines = result.get("stdout", "").strip().splitlines()
gpus = []
for line in lines:
parts = [p.strip() for p in line.split(",")]
if len(parts) >= 3:
gpus.append({
"name": parts[0],
"driver": parts[1],
"memory_total": parts[2]
})
return {
"gpu_count": len(gpus),
"gpus": gpus
}
# =========================================================
# GPU USAGE
# =========================================================
def gpu_usage(self):
result = run_command(
cmd=[
"nvidia-smi",
"--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
"--format=csv,noheader,nounits"
]
)
if result.get("return_code") != 0:
return {
"status": "error",
"error": result.get("stderr", "")
}
lines = result.get("stdout", "").strip().splitlines()
usage = []
for line in lines:
parts = [p.strip() for p in line.split(",")]
if len(parts) >= 4:
usage.append({
"gpu_util_percent": parts[0],
"memory_used_mb": parts[1],
"memory_total_mb": parts[2],
"temperature_c": parts[3]
})
return {
"gpus": usage
}
# =========================================================
# GPU PROCESSES
# =========================================================
def gpu_processes(self):
result = run_command(
cmd=[
"nvidia-smi",
"--query-compute-apps=pid,process_name,used_memory",
"--format=csv,noheader"
]
)
if result.get("return_code") != 0:
return {
"status": "error",
"error": result.get("stderr", "")
}
lines = result.get("stdout", "").strip().splitlines()
processes = []
for line in lines:
parts = [p.strip() for p in line.split(",")]
if len(parts) >= 3:
processes.append({
"pid": parts[0],
"name": parts[1],
"memory": parts[2]
})
return {
"count": len(processes),
"processes": processes
}
# =========================================================
# FULL SNAPSHOT
# =========================================================
def full_snapshot(self):
return {
"info": self.gpu_info(),
"usage": self.gpu_usage(),
"processes": self.gpu_processes()
}
# =========================================================
# REGISTER
# =========================================================
registry.register(GPUTool())

190
tools/info.py Normal file
View File

@@ -0,0 +1,190 @@
from __future__ import annotations
from typing import Any
import inspect
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class InfoTool(BaseTool):
"""
Introspective tool for:
- tool discovery
- execution signature inspection
- structured payload hints
- example generation
"""
name = "info"
description = "Get tool schemas, args, and usage hints"
# =========================================================
# EXECUTE
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = payload.get("action", "list_tools")
bus.log(
"INFO",
"info_execute",
"INFO",
{"action": action}
)
match action:
case "list_tools":
return self.list_tools()
case "tool_schema":
return self.tool_schema(payload)
case "tool_catalog":
return self.tool_catalog()
case _:
raise ValueError(f"Unknown info action: {action}")
# =========================================================
# LIST TOOLS
# =========================================================
def list_tools(self) -> dict[str, Any]:
return {
"tools": [
{
"name": t.name,
"description": getattr(t, "description", "")
}
for t in registry.all_tools()
]
}
# =========================================================
# TOOL SCHEMA (CORE FEATURE)
# =========================================================
def tool_schema(self, payload: dict[str, Any]) -> dict[str, Any]:
name = payload.get("name")
if not isinstance(name, str):
raise ValueError("name must be string")
tool = next((t for t in registry.all_tools() if t.name == name), None)
if not tool:
return {"error": f"Tool not found: {name}"}
schema: dict[str, Any] = {
"name": tool.name,
"description": getattr(tool, "description", ""),
"class": tool.__class__.__name__,
"module": tool.__class__.__module__,
}
# =====================================================
# SAFE SIGNATURE INTROSPECTION
# =====================================================
try:
sig = inspect.signature(tool.execute)
params: dict[str, Any] = {}
for pname, param in sig.parameters.items():
if pname == "self":
continue
params[pname] = {
"required": param.default is inspect._empty,
"default": None if param.default is inspect._empty else param.default,
"kind": str(param.kind),
"type": (
param.annotation.__name__
if hasattr(param.annotation, "__name__")
else str(param.annotation)
)
}
schema["execute_signature"] = params
except Exception as e:
schema["execute_signature_error"] = str(e)
# =====================================================
# STRUCTURED HINTS (NOT STRING PARSING)
# =====================================================
schema["common_payload_patterns"] = self._infer_patterns(tool)
schema["example_payload"] = self._generate_example_payload(tool)
return schema
# =========================================================
# TOOL CATALOG
# =========================================================
def tool_catalog(self) -> dict[str, Any]:
return {
"tool_count": len(registry.all_tools()),
"tools": [
{
"name": t.name,
"description": getattr(t, "description", ""),
"patterns": self._infer_patterns(t),
}
for t in registry.all_tools()
]
}
# =========================================================
# PATTERN INFERENCE (CLEANED UP)
# =========================================================
def _infer_patterns(self, tool: BaseTool) -> list[str]:
name = tool.name.lower()
mapping = {
"git": ["repo", "branch", "remote", "message"],
"filesystem": ["path", "content", "directory"],
"gitea": ["owner", "repo", "path", "message"],
"subprocess": ["cmd", "cwd", "timeout"],
"memory": ["entry", "query"],
"reflection": ["action"],
}
return mapping.get(name, [])
# =========================================================
# EXAMPLES
# =========================================================
def _generate_example_payload(self, tool: BaseTool) -> dict[str, Any]:
name = tool.name.lower()
examples = {
"git": {
"action": "status",
"repo": "."
},
"filesystem": {
"action": "read_file",
"path": "./example.txt"
},
"gitea": {
"action": "list_repos"
},
"subprocess": {
"cmd": ["echo", "hello"],
"timeout": 60
},
"memory": {
"action": "add",
"entry": {"note": "example"}
},
"reflection": {
"action": "reflect"
}
}
return examples.get(name, {"action": "unknown"})

183
tools/intelligent_search.py Normal file
View File

@@ -0,0 +1,183 @@
from __future__ import annotations
from typing import Any
from urllib.parse import urlparse
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class IntelligentSearchTool(BaseTool):
"""
Intelligent wrapper over basic search results.
Enhances:
- ranking
- deduplication
- best-result selection
"""
name = "intelligent_search"
description = "Rerank and filter search results for best relevance"
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "rank")).strip()
bus.log(
"SEARCH",
"intelligent_search_execute",
"INFO",
{"action": action}
)
match action:
case "rank":
return self.rank(payload)
case "best":
return self.best(payload)
case _:
raise ValueError(f"Unknown action: {action}")
# =========================
# RANK RESULTS
# =========================
def rank(self, payload: dict[str, Any]):
results = payload.get("results")
query = payload.get("query", "")
if not isinstance(results, list):
raise ValueError("results must be list")
scored = []
for r in results:
if not isinstance(r, dict):
continue
title = r.get("title", "")
url = r.get("url", "")
score = self._score(query, title, url)
scored.append({
"title": title,
"url": url,
"score": score
})
scored.sort(key=lambda x: x["score"], reverse=True)
return {
"query": query,
"ranked": scored
}
# =========================
# BEST RESULT ONLY
# =========================
def best(self, payload: dict[str, Any]):
results = payload.get("results")
query = payload.get("query", "")
if not isinstance(results, list):
raise ValueError("results must be list")
best_item = None
best_score = -1
seen_domains = set()
for r in results:
if not isinstance(r, dict):
continue
title = r.get("title", "")
url = r.get("url", "")
domain = self._domain(url)
# simple dedupe
if domain in seen_domains:
continue
seen_domains.add(domain)
score = self._score(query, title, url)
if score > best_score:
best_score = score
best_item = {
"title": title,
"url": url,
"score": score
}
return {
"query": query,
"best": best_item
}
# =========================
# SCORING FUNCTION
# =========================
def _score(self, query: str, title: str, url: str) -> float:
"""
Lightweight heuristic ranking system.
Replace later with LLM scoring if desired.
"""
q = query.lower()
t = title.lower()
u = url.lower()
score = 0.0
# keyword overlap
for word in q.split():
if word in t:
score += 2.0
if word in u:
score += 1.0
# title boost
if q in t:
score += 5.0
# HTTPS boost
if url.startswith("https"):
score += 0.5
# domain quality heuristic
domain = self._domain(url)
if domain.endswith(".edu") or domain.endswith(".org"):
score += 1.5
return score
# =========================
# DOMAIN HELPERS
# =========================
def _domain(self, url: str) -> str:
try:
return urlparse(url).netloc.lower()
except Exception:
return ""
# =========================
# REGISTER
# =========================
registry.register(IntelligentSearchTool())

159
tools/memory.py Normal file
View File

@@ -0,0 +1,159 @@
from __future__ import annotations
from typing import Any
from pathlib import Path
import json
import time
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.config import WORKSPACE_ROOT
class MemoryTool(BaseTool):
"""
Persistent memory store for agent experiences.
Stores:
- research results
- tool outputs
- agent decisions
- arbitrary notes
"""
name = "memory"
description = "Persistent memory storage and retrieval"
def __init__(self):
self.memory_file = Path(WORKSPACE_ROOT) / "memory_store.json"
self._ensure_file()
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "add")).strip()
bus.log(
"MEMORY",
"memory_execute",
"INFO",
{"action": action}
)
match action:
case "add":
return self.add(payload)
case "search":
return self.search(payload)
case "list":
return self.list_all()
case "clear":
return self.clear()
case _:
raise ValueError(f"Unknown memory action: {action}")
# =========================
# ADD MEMORY
# =========================
def add(self, payload: dict[str, Any]):
entry = payload.get("entry")
if not isinstance(entry, dict):
raise ValueError("entry must be dict")
memory = self._load()
record = {
"id": len(memory) + 1,
"timestamp": time.time(),
"entry": entry
}
memory.append(record)
self._save(memory)
return {
"status": "ok",
"stored": record
}
# =========================
# SEARCH MEMORY
# =========================
def search(self, payload: dict[str, Any]):
query = payload.get("query", "")
if not isinstance(query, str):
raise ValueError("query must be string")
memory = self._load()
results = []
for item in memory:
entry = item.get("entry", {})
text_blob = json.dumps(entry).lower()
if query.lower() in text_blob:
results.append(item)
return {
"query": query,
"results": results,
"count": len(results)
}
# =========================
# LIST ALL
# =========================
def list_all(self):
return {
"memory": self._load()
}
# =========================
# CLEAR MEMORY
# =========================
def clear(self):
self._save([])
return {"status": "cleared"}
# =========================
# STORAGE LAYER
# =========================
def _ensure_file(self):
self.memory_file.parent.mkdir(parents=True, exist_ok=True)
if not self.memory_file.exists():
self.memory_file.write_text("[]", encoding="utf-8")
def _load(self) -> list[dict[str, Any]]:
try:
return json.loads(self.memory_file.read_text(encoding="utf-8"))
except Exception:
return []
def _save(self, data: list[dict[str, Any]]):
self.memory_file.write_text(
json.dumps(data, indent=2),
encoding="utf-8"
)
# =========================
# REGISTER
# =========================
registry.register(MemoryTool())

167
tools/net.py Normal file
View File

@@ -0,0 +1,167 @@
from __future__ import annotations
from typing import Any
import socket
import time
import urllib.request
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class NetTool(BaseTool):
"""
Network diagnostics and introspection tool.
Provides basic connectivity checks and resolution utilities.
"""
name = "net"
description = "Network diagnostics: DNS, ports, HTTP checks"
# =========================================================
# EXECUTE
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "dns")).strip()
bus.log(
"NET",
"net_execute",
"INFO",
{"action": action}
)
match action:
case "dns":
return self.dns_lookup(payload)
case "port":
return self.check_port(payload)
case "http":
return self.http_check(payload)
case "latency":
return self.latency_test(payload)
case _:
raise ValueError(f"Unknown net action: {action}")
# =========================================================
# DNS LOOKUP
# =========================================================
def dns_lookup(self, payload: dict[str, Any]):
host = payload.get("host")
if not isinstance(host, str):
raise ValueError("host must be string")
try:
ip = socket.gethostbyname(host)
return {
"host": host,
"ip": ip
}
except socket.gaierror as e:
return {
"host": host,
"error": str(e)
}
# =========================================================
# PORT CHECK
# =========================================================
def check_port(self, payload: dict[str, Any]):
host = payload.get("host", "127.0.0.1")
port = payload.get("port")
if not isinstance(host, str):
raise ValueError("host must be string")
if not isinstance(port, int):
raise ValueError("port must be int")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
try:
result = sock.connect_ex((host, port))
return {
"host": host,
"port": port,
"open": result == 0
}
finally:
sock.close()
# =========================================================
# HTTP CHECK
# =========================================================
def http_check(self, payload: dict[str, Any]):
url = payload.get("url")
if not isinstance(url, str):
raise ValueError("url must be string")
try:
start = time.time()
req = urllib.request.Request(
url,
method="GET"
)
with urllib.request.urlopen(req, timeout=5) as response:
status = response.getcode()
return {
"url": url,
"status_code": status,
"latency_ms": round((time.time() - start) * 1000, 2)
}
except Exception as e:
return {
"url": url,
"error": str(e)
}
# =========================================================
# LATENCY TEST
# =========================================================
def latency_test(self, payload: dict[str, Any]):
host = payload.get("host")
if not isinstance(host, str):
raise ValueError("host must be string")
try:
start = time.time()
socket.gethostbyname(host)
latency = (time.time() - start) * 1000
return {
"host": host,
"dns_latency_ms": round(latency, 2)
}
except Exception as e:
return {
"host": host,
"error": str(e)
}
# =========================================================
# REGISTER
# =========================================================
registry.register(NetTool())

158
tools/ollama.py Normal file
View File

@@ -0,0 +1,158 @@
from __future__ import annotations
from typing import Any
import requests
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.config import OLLAMA_URL
class OllamaTool(BaseTool):
"""
Local LLM interface via Ollama.
Enables the agent to call local models for reasoning,
summarization, and transformation tasks.
"""
name = "ollama"
description = "Local LLM inference via Ollama"
# =========================================================
# EXECUTE
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "generate")).strip()
bus.log(
"OLLAMA",
"ollama_execute",
"INFO",
{"action": action}
)
match action:
case "generate":
return self.generate(payload, ctx)
case "chat":
return self.chat(payload, ctx)
case "models":
return self.list_models()
case _:
raise ValueError(f"Unknown ollama action: {action}")
# =========================================================
# GENERATE (single prompt)
# =========================================================
def generate(self, payload: dict[str, Any], ctx: ToolContext):
model = payload.get("model", "llama3")
prompt = payload.get("prompt")
if not isinstance(prompt, str):
raise ValueError("prompt must be string")
url = f"{OLLAMA_URL}/api/generate"
data = {
"model": model,
"prompt": prompt,
"stream": False
}
if ctx.dry_run:
return {
"dry_run": True,
"model": model,
"prompt_preview": prompt[:200]
}
try:
response = requests.post(url, json=data, timeout=(5, 120))
response.raise_for_status()
return {
"model": model,
"response": response.json().get("response", ""),
}
except requests.exceptions.RequestException as e:
return {
"status": "error",
"error": str(e)
}
# =========================================================
# CHAT (multi-message style)
# =========================================================
def chat(self, payload: dict[str, Any], ctx: ToolContext):
model = payload.get("model", "llama3")
messages = payload.get("messages")
if not isinstance(messages, list):
raise ValueError("messages must be list[dict]")
url = f"{OLLAMA_URL}/api/chat"
data = {
"model": model,
"messages": messages,
"stream": False
}
if ctx.dry_run:
return {
"dry_run": True,
"model": model,
"message_count": len(messages)
}
try:
response = requests.post(url, json=data, timeout=(5, 180))
response.raise_for_status()
return {
"model": model,
"response": response.json().get("message", {}).get("content", "")
}
except requests.exceptions.RequestException as e:
return {
"status": "error",
"error": str(e)
}
# =========================================================
# LIST MODELS
# =========================================================
def list_models(self):
url = f"{OLLAMA_URL}/api/tags"
try:
response = requests.get(url, timeout=(3, 10))
response.raise_for_status()
return {
"models": response.json().get("models", [])
}
except requests.exceptions.RequestException as e:
return {
"status": "error",
"error": str(e)
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(OllamaTool())

166
tools/pid.py Normal file
View File

@@ -0,0 +1,166 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.subprocess import run_command
class PidTool(BaseTool):
"""
Process inspection and management tool.
Provides visibility into running system processes and optional control.
"""
name = "pid"
description = "Process listing, lookup, and management"
# =========================================================
# EXECUTE
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "list")).strip()
bus.log(
"PID",
"pid_execute",
"INFO",
{"action": action}
)
match action:
case "list":
return self.list_processes(payload)
case "find":
return self.find_process(payload)
case "details":
return self.process_details(payload)
case "kill":
return self.kill_process(payload, ctx)
case _:
raise ValueError(f"Unknown pid action: {action}")
# =========================================================
# LIST PROCESSES
# =========================================================
def list_processes(self, payload: dict[str, Any]):
limit = payload.get("limit", 50)
result = run_command(
cmd=["tasklist"],
)
if result.get("return_code") != 0:
return {
"status": "error",
"stderr": result.get("stderr", "")
}
lines = result.get("stdout", "").splitlines()
processes = []
for line in lines[3:]: # skip header rows
parts = line.split()
if len(parts) < 2:
continue
processes.append({
"name": parts[0],
"pid": parts[1]
})
if len(processes) >= limit:
break
return {
"count": len(processes),
"processes": processes
}
# =========================================================
# FIND PROCESS
# =========================================================
def find_process(self, payload: dict[str, Any]):
name = payload.get("name")
if not isinstance(name, str):
raise ValueError("name must be string")
result = run_command(
cmd=["tasklist", "/FI", f"IMAGENAME eq {name}"],
)
return {
"name": name,
"raw": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# PROCESS DETAILS
# =========================================================
def process_details(self, payload: dict[str, Any]):
pid = payload.get("pid")
if not isinstance(pid, int):
raise ValueError("pid must be int")
result = run_command(
cmd=["wmic", "process", "where", f"ProcessId={pid}", "get", "ProcessId,Name,CommandLine"],
)
return {
"pid": pid,
"raw": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# KILL PROCESS
# =========================================================
def kill_process(self, payload: dict[str, Any], ctx: ToolContext):
pid = payload.get("pid")
force = payload.get("force", False)
if not isinstance(pid, int):
raise ValueError("pid must be int")
if ctx.dry_run:
return {
"dry_run": True,
"pid": pid,
"force": force,
"message": "Would terminate process"
}
cmd = ["taskkill", "/PID", str(pid)]
if force:
cmd.append("/F")
result = run_command(cmd)
return {
"pid": pid,
"status": "success" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(PidTool())

View File

@@ -1,5 +1,8 @@
from __future__ import annotations
from typing import Any
import time
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
@@ -7,19 +10,13 @@ from core.events import bus
class PingTool(BaseTool):
name = "ping"
# optional metadata (future MCP auto-binding)
description = "Health check"
# -------------------------
# EXECUTE
# -------------------------
description = "Health check / liveness probe"
def execute(
self,
payload: dict[str, str],
payload: dict[str, Any],
ctx: ToolContext
):
) -> dict[str, Any]:
message = payload.get("message", "pong")
bus.log(
@@ -34,12 +31,9 @@ class PingTool(BaseTool):
return {
"status": "ok",
"echo": message,
"tool": self.name
"tool": self.name,
"timestamp": time.time()
}
# =========================
# SELF REGISTER
# =========================
registry.register(PingTool())

136
tools/pwsh.py Normal file
View File

@@ -0,0 +1,136 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from core.subprocess import run_command
class PwshTool(BaseTool):
"""
PowerShell execution tool.
Provides controlled execution of PowerShell commands and scripts.
"""
name = "pwsh"
description = "Execute PowerShell commands safely"
# =========================================================
# ROUTER
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "run")).strip()
bus.log(
"PWSH",
"pwsh_execute",
"INFO",
{"action": action}
)
match action:
case "run":
return self.run(payload, ctx)
case "script":
return self.run_script(payload, ctx)
case "check":
return self.check_pwsh(payload)
case _:
raise ValueError(f"Unknown pwsh action: {action}")
# =========================================================
# RUN COMMAND
# =========================================================
def run(self, payload: dict[str, Any], ctx: ToolContext):
command = payload.get("command")
cwd = payload.get("cwd")
if not isinstance(command, str):
raise ValueError("command must be string")
if cwd is not None and not isinstance(cwd, str):
raise ValueError("cwd must be string")
cmd = ["pwsh", "-NoProfile", "-Command", command]
if ctx.dry_run:
return {
"dry_run": True,
"command": cmd,
"cwd": cwd
}
result = run_command(
cmd=cmd,
cwd=cwd
)
return {
"action": "run",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# RUN SCRIPT
# =========================================================
def run_script(self, payload: dict[str, Any], ctx: ToolContext):
script = payload.get("script")
cwd = payload.get("cwd")
if not isinstance(script, str):
raise ValueError("script must be string")
cmd = ["pwsh", "-NoProfile", "-Command", script]
if ctx.dry_run:
return {
"dry_run": True,
"script_preview": script[:500]
}
result = run_command(
cmd=cmd,
cwd=cwd
)
return {
"action": "script",
"status": "ok" if result.get("return_code") == 0 else "error",
"stdout": result.get("stdout", ""),
"stderr": result.get("stderr", "")
}
# =========================================================
# CHECK POWERSHELL
# =========================================================
def check_pwsh(self, payload: dict[str, Any]):
"""Check if PowerShell is available."""
result = run_command(
cmd=["pwsh", "-Command", "$PSVersionTable.PSVersion"]
)
return {
"action": "check",
"available": result.get("return_code") == 0,
"version_output": result.get("stdout", ""),
"error": result.get("stderr", "")
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(PwshTool())

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
from typing import Any
import subprocess
import json
from pathlib import Path
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.safety import safety
from core.events import bus
VM_STATE_FILE = Path("./vm_state.json")
class QemuTool(BaseTool):
name = "qemu"
description = "QEMU VM lifecycle management"
# -------------------------
# EXECUTE ROUTER
# -------------------------
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"QEMU",
"qemu_execute",
"INFO",
{"action": action}
)
match action:
case "start":
return self.start_vm(payload, ctx)
case "stop":
return self.stop_vm(payload, ctx)
case "status":
return self.status(payload)
case "list":
return self.list_vms()
case _:
raise ValueError(f"Unknown qemu action: {action}")
# -------------------------
# STATE HELPERS
# -------------------------
def _load_state(self) -> dict[str, Any]:
if VM_STATE_FILE.exists():
return json.loads(VM_STATE_FILE.read_text())
return {}
def _save_state(self, state: dict[str, Any]):
VM_STATE_FILE.write_text(json.dumps(state, indent=2))
# -------------------------
# START VM
# -------------------------
def start_vm(self, payload: dict[str, Any], ctx: ToolContext):
name = payload.get("name")
image = payload.get("image")
if not isinstance(name, str):
raise ValueError("name must be string")
if not isinstance(image, str):
raise ValueError("image must be string")
image_path = safety.validate_path(image)
state = self._load_state()
if name in state and state[name].get("running"):
return {"status": "already_running", "name": name}
cmd = [
"qemu-system-x86_64",
"-m", "2048",
"-drive", f"file={image_path},format=qcow2",
"-nographic"
]
process = subprocess.Popen(cmd)
state[name] = {
"pid": process.pid,
"image": str(image_path),
"running": True
}
self._save_state(state)
return {
"status": "started",
"name": name,
"pid": process.pid
}
# -------------------------
# STOP VM
# -------------------------
def stop_vm(self, payload: dict[str, Any], ctx: ToolContext):
name = payload.get("name")
if not isinstance(name, str):
raise ValueError("name must be string")
state = self._load_state()
vm = state.get(name)
if not vm or not vm.get("running"):
return {"status": "not_running", "name": name}
pid = vm["pid"]
try:
subprocess.run(["kill", str(pid)], check=False)
except Exception:
pass
vm["running"] = False
self._save_state(state)
return {"status": "stopped", "name": name, "pid": pid}
# -------------------------
# STATUS
# -------------------------
def status(self, payload: dict[str, Any]):
name = payload.get("name")
if not isinstance(name, str):
raise ValueError("name must be string")
state = self._load_state()
return state.get(name, {"status": "unknown"})
# -------------------------
# LIST
# -------------------------
def list_vms(self):
return self._load_state()
registry.register(QemuTool())

177
tools/reflection.py Normal file
View File

@@ -0,0 +1,177 @@
from __future__ import annotations
from typing import Any
import time
import json
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
from tools.memory import MemoryTool
class ReflectionTool(BaseTool):
"""
Analyzes stored memory and extracts insights.
"""
name = "reflection"
description = "Analyze memory and extract insights or patterns"
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "reflect")).strip()
bus.log(
"REFLECTION",
"reflection_execute",
"INFO",
{"action": action}
)
match action:
case "reflect":
return self.reflect(payload, ctx)
case "summarize_failures":
return self.summarize_failures(payload, ctx)
case "summarize_successes":
return self.summarize_successes(payload, ctx)
case "detect_loops":
return self.detect_loops(payload, ctx)
case _:
raise ValueError(f"Unknown reflection action: {action}")
# =========================
# CORE REFLECTION
# =========================
def reflect(self, payload: dict[str, Any], ctx: ToolContext):
memory_tool = self._get_memory()
memory = memory_tool._load()
insights = []
for item in memory:
entry = item.get("entry", {})
text = json.dumps(entry).lower()
if "error" in text or "failed" in text:
insights.append({
"type": "failure_pattern",
"id": item.get("id"),
"note": "Failure-related memory detected"
})
if "success" in text or "ok" in text:
insights.append({
"type": "success_pattern",
"id": item.get("id"),
"note": "Success-related memory detected"
})
reflection = {
"timestamp": time.time(),
"insights": insights,
"total_memory": len(memory),
"insight_count": len(insights)
}
# store reflection back into memory
memory_tool.add({
"entry": {
"type": "reflection",
"data": reflection
}
}, ctx) # type: ignore
return reflection
# =========================
# FAILURE ANALYSIS
# =========================
def summarize_failures(self, payload: dict[str, Any], ctx: ToolContext):
memory_tool = self._get_memory()
memory = memory_tool._load()
failures = [
item for item in memory
if "error" in json.dumps(item.get("entry", {})).lower()
or "fail" in json.dumps(item.get("entry", {})).lower()
]
return {
"count": len(failures),
"failures": failures
}
# =========================
# SUCCESS ANALYSIS
# =========================
def summarize_successes(self, payload: dict[str, Any], ctx: ToolContext):
memory_tool = self._get_memory()
memory = memory_tool._load()
successes = [
item for item in memory
if "success" in json.dumps(item.get("entry", {})).lower()
or "ok" in json.dumps(item.get("entry", {})).lower()
]
return {
"count": len(successes),
"successes": successes
}
# =========================
# LOOP DETECTION
# =========================
def detect_loops(self, payload: dict[str, Any], ctx: ToolContext):
memory_tool = self._get_memory()
memory = memory_tool._load()
seen = {}
loops = []
for item in memory:
key = json.dumps(item.get("entry", {}), sort_keys=True)
if key in seen:
loops.append({
"original_id": seen[key],
"duplicate_id": item.get("id")
})
else:
seen[key] = item.get("id")
return {
"loop_count": len(loops),
"loops": loops
}
# =========================
# HELPERS
# =========================
def _get_memory(self) -> MemoryTool:
for tool in registry.all_tools():
if tool.name == "memory":
return tool # type: ignore
raise RuntimeError("Memory tool not found")
# =========================
# REGISTER
# =========================
registry.register(ReflectionTool())

149
tools/research.py Normal file
View File

@@ -0,0 +1,149 @@
from __future__ import annotations
from typing import Any
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class ResearchTool(BaseTool):
"""
High-level research orchestrator.
Combines:
- search
- intelligent ranking
- crawling
into a structured report.
"""
name = "research"
description = "Autonomous web research pipeline"
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
query = payload.get("query")
depth = payload.get("depth", 1)
max_sources = payload.get("max_sources", 3)
if not isinstance(query, str):
raise ValueError("query must be string")
bus.log(
"RESEARCH",
"research_execute",
"INFO",
{
"query": query,
"depth": depth,
"max_sources": max_sources
}
)
# Step 1: search
search_results = registry.run(
"search",
{"action": "search", "query": query, "limit": 10},
ctx
)
results = search_results.get("results", [])
if not results:
return {
"query": query,
"error": "No search results found"
}
# Step 2: intelligent ranking
ranked = registry.run(
"intelligent_search",
{
"action": "rank",
"query": query,
"results": results
},
ctx
)
ranked_list = ranked.get("ranked", [])[:max_sources]
# Step 3: crawl top sources
pages = []
for item in ranked_list:
url = item.get("url")
if not url:
continue
page = registry.run(
"crawler",
{
"action": "fetch",
"url": url
},
ctx
)
pages.append({
"url": url,
"title": item.get("title"),
"text": page.get("text", ""),
"score": item.get("score", 0)
})
# Step 4: synthesize structure
return {
"query": query,
"sources_used": len(pages),
"sources": pages,
"summary_hint": self._build_hint(pages)
}
# =========================
# SIMPLE SYNTHESIS HELPER
# =========================
def _build_hint(self, pages: list[dict[str, Any]]) -> str:
"""
Lightweight heuristic summary hint.
This is NOT a full LLM summary — just structure guidance.
"""
if not pages:
return "No data available."
topics = []
for p in pages:
text = p.get("text", "")
# crude keyword extraction (lightweight, no deps)
words = text.split()
keywords = [w for w in words if len(w) > 6][:10]
topics.append({
"url": p.get("url"),
"keywords": keywords
})
return (
"Key extracted themes per source:\n"
+ "\n".join(
f"- {t['url']}: {', '.join(t['keywords'][:5])}"
for t in topics
)
)
# =========================
# REGISTER
# =========================
registry.register(ResearchTool())

117
tools/search.py Normal file
View File

@@ -0,0 +1,117 @@
from __future__ import annotations
from typing import Any
import urllib.request
import urllib.parse
import re
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.events import bus
class SearchTool(BaseTool):
"""
Lightweight web search tool using DuckDuckGo HTML endpoint.
Designed for:
- query → results
- agent retrieval step before crawling
"""
name = "search"
description = "Web search (DuckDuckGo HTML scraping)"
# =========================
# EXECUTE
# =========================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "search")).strip()
bus.log(
"SEARCH",
"search_execute",
"INFO",
{"action": action}
)
match action:
case "search":
return self.search(payload)
case _:
raise ValueError(f"Unknown search action: {action}")
# =========================
# SEARCH
# =========================
def search(self, payload: dict[str, Any]):
query = payload.get("query")
limit = payload.get("limit", 5)
if not isinstance(query, str):
raise ValueError("query must be string")
if not isinstance(limit, int):
limit = 5
encoded = urllib.parse.quote(query)
url = f"https://duckduckgo.com/html/?q={encoded}"
req = urllib.request.Request(
url,
headers={
"User-Agent": "MCP-Search/1.0"
}
)
try:
with urllib.request.urlopen(req, timeout=6) as resp:
html = resp.read().decode("utf-8", errors="ignore")
results = self._parse_results(html)
return {
"query": query,
"results": results[:limit],
"count": len(results)
}
except Exception as e:
return {
"query": query,
"error": str(e)
}
# =========================
# PARSER
# =========================
def _parse_results(self, html: str) -> list[dict[str, Any]]:
"""
DuckDuckGo HTML parsing (lightweight heuristic).
"""
results = []
# Extract result blocks
links = re.findall(r'<a rel="nofollow" class="result__a" href="(.*?)".*?>(.*?)</a>', html)
for url, title in links:
clean_title = re.sub("<.*?>", "", title)
results.append({
"title": clean_title,
"url": url,
})
return results
# =========================
# REGISTER
# =========================
registry.register(SearchTool())

View File

@@ -5,50 +5,59 @@ from typing import Any
from core.subprocess import run_command
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.safety import safety
class SubprocessTool(BaseTool):
name = "subprocess"
description = "Run a subprocess command safely"
# =========================
# EXECUTE
# =========================
def execute(
self,
payload: dict[str, Any],
ctx: ToolContext
):
cmd = payload.get("cmd")
if not isinstance(cmd, list):
raise ValueError(
"cmd must be list[str]"
)
cwd = payload.get("cwd")
timeout = payload.get("timeout", 60)
if cwd is not None and not isinstance(
cwd,
str
):
raise ValueError(
"cwd must be string"
)
# -------------------------
# Validate command
# -------------------------
if not isinstance(cmd, list) or not all(isinstance(c, str) for c in cmd):
raise ValueError("cmd must be list[str]")
timeout = payload.get(
"timeout",
60
)
# Optional safety: block empty commands
if not cmd:
raise ValueError("cmd cannot be empty")
if not isinstance(
timeout,
int
):
raise ValueError(
"timeout must be int"
)
# -------------------------
# Validate cwd
# -------------------------
if cwd is not None:
if not isinstance(cwd, str):
raise ValueError("cwd must be string")
cwd_path = safety.validate_path(cwd)
cwd = str(cwd_path)
# -------------------------
# Validate timeout
# -------------------------
if not isinstance(timeout, int) or timeout <= 0:
raise ValueError("timeout must be positive int")
# -------------------------
# Dry-run support (future-proofing)
# -------------------------
if getattr(ctx, "dry_run", False):
return {
"dry_run": True,
"cmd": cmd,
"cwd": cwd,
"timeout": timeout,
"message": "Would execute subprocess"
}
return run_command(
cmd=cmd,
@@ -57,10 +66,4 @@ class SubprocessTool(BaseTool):
)
# =========================
# SELF REGISTER
# =========================
registry.register(
SubprocessTool()
)
registry.register(SubprocessTool())

View File

@@ -0,0 +1,188 @@
from __future__ import annotations
from typing import Any
import subprocess
import sys
from pathlib import Path
from core.tools.base import BaseTool, ToolContext
from core.tools.registry import registry
from core.safety import safety
from core.events import bus
class VenvTool(BaseTool):
name = "venv"
description = "Python virtual environment management (create, install, run, list packages)"
# =========================================================
# EXECUTE ROUTER (ONLY ENTRYPOINT WITH ctx)
# =========================================================
def execute(self, payload: dict[str, Any], ctx: ToolContext):
action = str(payload.get("action", "")).strip()
bus.log(
"VENV",
"venv_execute",
"INFO",
{"action": action}
)
match action:
case "create":
return self.create_venv(payload)
case "install":
return self.install_package(payload)
case "run":
return self.run_python(payload)
case "list":
return self.list_packages(payload)
case _:
raise ValueError(f"Unknown venv action: {action}")
# =========================================================
# PATH HELPERS
# =========================================================
def _venv_path(self, path: str) -> Path:
return safety.validate_path(path)
def _python_bin(self, venv: Path) -> Path:
"""Cross-platform python binary resolution."""
if (venv / "bin").exists():
return venv / "bin" / "python"
return venv / "Scripts" / "python.exe"
def _pip_bin(self, venv: Path) -> Path:
"""Cross-platform pip binary resolution."""
if (venv / "bin").exists():
return venv / "bin" / "pip"
return venv / "Scripts" / "pip.exe"
# =========================================================
# CREATE VENV
# =========================================================
def create_venv(self, payload: dict[str, Any]):
path = payload.get("path")
if not isinstance(path, str):
raise ValueError("path must be string")
venv_path = self._venv_path(path)
if venv_path.exists():
return {
"status": "exists",
"path": str(venv_path)
}
subprocess.run(
[sys.executable, "-m", "venv", str(venv_path)],
check=True
)
return {
"status": "created",
"path": str(venv_path)
}
# =========================================================
# INSTALL PACKAGE
# =========================================================
def install_package(self, payload: dict[str, Any]):
path = payload.get("path")
package = payload.get("package")
if not isinstance(path, str):
raise ValueError("path must be string")
if not isinstance(package, str):
raise ValueError("package must be string")
venv_path = self._venv_path(path)
pip = self._pip_bin(venv_path)
if not pip.exists():
raise ValueError("pip not found in virtual environment")
result = subprocess.run(
[str(pip), "install", package],
capture_output=True,
text=True
)
return {
"status": "ok" if result.returncode == 0 else "error",
"stdout": result.stdout,
"stderr": result.stderr
}
# =========================================================
# RUN PYTHON CODE
# =========================================================
def run_python(self, payload: dict[str, Any]):
path = payload.get("path")
code = payload.get("code")
if not isinstance(path, str):
raise ValueError("path must be string")
if not isinstance(code, str):
raise ValueError("code must be string")
venv_path = self._venv_path(path)
python_bin = self._python_bin(venv_path)
if not python_bin.exists():
raise ValueError("python executable not found in venv")
result = subprocess.run(
[str(python_bin), "-c", code],
capture_output=True,
text=True
)
return {
"status": "ok" if result.returncode == 0 else "error",
"stdout": result.stdout,
"stderr": result.stderr
}
# =========================================================
# LIST PACKAGES
# =========================================================
def list_packages(self, payload: dict[str, Any]):
path = payload.get("path")
if not isinstance(path, str):
raise ValueError("path must be string")
venv_path = self._venv_path(path)
pip = self._pip_bin(venv_path)
result = subprocess.run(
[str(pip), "list"],
capture_output=True,
text=True
)
return {
"status": "ok",
"output": result.stdout
}
# =========================================================
# REGISTER TOOL
# =========================================================
registry.register(VenvTool())