""" System tools — local machine operations. These tools affect the host system directly. Use with care. """ import asyncio import logging import os logger = logging.getLogger(__name__) ALLOW_SCRIPT = "/home/scott/.local/bin/claude-allow-dir" async def claude_allow_dir(path: str, mode: str = "rw") -> str: """Add Read/Edit allow rules to ~/.claude/settings.json for a directory. Calls the claude-allow-dir script, which edits settings.json directly. Changes take effect in the next Claude Code session (or after /hooks reload). """ if mode not in ("r", "w", "rw"): return f"Error: mode must be r, w, or rw (got '{mode}')" try: proc = await asyncio.create_subprocess_exec( "python3", ALLOW_SCRIPT, path, mode, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10) output = stdout.decode().strip() err = stderr.decode().strip() if proc.returncode != 0: logger.warning("claude-allow-dir failed (rc=%d): %s", proc.returncode, err) return f"Failed (exit {proc.returncode}): {err or output}" return output or "Done." except asyncio.TimeoutError: return "Error: script timed out" except Exception as e: logger.error("claude_allow_dir error: %s", e) return f"Error: {e}" async def shell_exec(command: str, working_dir: str | None = None, timeout: int = 30) -> str: """Execute a shell command on the Cortex host and return combined stdout/stderr.""" timeout = min(max(timeout, 1), 120) cwd = None if working_dir: cwd = os.path.expanduser(working_dir) if not os.path.isdir(cwd): return f"Error: working_dir '{working_dir}' does not exist or is not a directory" try: proc = await asyncio.create_subprocess_shell( command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd, ) stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) out = stdout.decode(errors="replace").strip() err = stderr.decode(errors="replace").strip() parts = [] if out: parts.append(out) if err: parts.append(f"[stderr]\n{err}") combined = "\n".join(parts) if parts else "(no output)" if proc.returncode != 0: return f"Exit {proc.returncode}:\n{combined}" return combined except asyncio.TimeoutError: return f"Error: command timed out after {timeout}s" except Exception as e: logger.error("shell_exec error: %s", e) return f"Error: {e}"