"""Wrap CLI commands to run through Headroom proxy. Usage: headroom wrap claude # Start proxy + rtk + claude headroom wrap codex # Start proxy + OpenAI Codex CLI headroom wrap aider # Start proxy + aider headroom wrap cursor # Start proxy + print Cursor config instructions headroom wrap openclaw # Install + configure OpenClaw plugin headroom wrap claude --no-rtk # Without rtk hooks headroom wrap claude --port 9999 # Custom proxy port headroom wrap claude -- --model opus # Pass args to claude """ from __future__ import annotations import io import json import os import shutil import signal import socket import subprocess import sys import time from pathlib import Path from typing import Any # Fix Windows cp1252 encoding — box-drawing characters require UTF-8 if sys.platform == "win32" and hasattr(sys.stdout, "buffer"): if sys.stdout.encoding and sys.stdout.encoding.lower().replace("-", "") != "utf8": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") import click from .main import main # Proxy health check (reused from evals/suite_runner.py pattern) def _check_proxy(port: int) -> bool: """Check if Headroom proxy is running on given port.""" try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(1) s.connect(("127.0.0.1", port)) return True except (TimeoutError, ConnectionRefusedError, OSError): return False def _get_log_path() -> Path: """Get path for proxy log file.""" log_dir = Path.home() / ".headroom" / "logs" log_dir.mkdir(parents=True, exist_ok=True) return log_dir / "proxy.log" def _start_proxy( port: int, *, learn: bool = False, backend: str | None = None, anyllm_provider: str | None = None, region: str | None = None, ) -> subprocess.Popen: """Start Headroom proxy as a background subprocess. Logs are written to ~/.headroom/logs/proxy.log to avoid pipe buffer deadlocks (macOS pipe buffer is ~64KB — a busy proxy fills it quickly, blocking the process). """ cmd = [sys.executable, "-m", "headroom.cli", "proxy", "--port", str(port)] # Forward HEADROOM_MODE env var so the proxy respects the user's mode choice headroom_mode = os.environ.get("HEADROOM_MODE") if headroom_mode: cmd.extend(["--mode", headroom_mode]) # Forward --learn flag to proxy subprocess if learn: cmd.append("--learn") # Forward backend configuration to proxy subprocess _backend = backend or os.environ.get("HEADROOM_BACKEND") if _backend: cmd.extend(["--backend", _backend]) _anyllm = anyllm_provider or os.environ.get("HEADROOM_ANYLLM_PROVIDER") if _anyllm: cmd.extend(["--anyllm-provider", _anyllm]) _region = region or os.environ.get("HEADROOM_REGION") if _region: cmd.extend(["--region", _region]) log_path = _get_log_path() log_file = open(log_path, "a") # noqa: SIM115 # Ensure proxy subprocess uses UTF-8 (Windows defaults to cp1252) proxy_env = os.environ.copy() proxy_env["PYTHONIOENCODING"] = "utf-8" proc = subprocess.Popen( cmd, stdout=log_file, stderr=log_file, env=proxy_env, ) # Wait for proxy to be ready (up to 15 seconds) for _i in range(15): time.sleep(1) if _check_proxy(port): click.echo(f" Logs: {log_path}") return proc # Check if process died if proc.poll() is not None: log_file.close() # Read last few lines of log for error context try: tail = log_path.read_text()[-500:] except Exception: tail = "(no log output)" raise RuntimeError(f"Proxy exited with code {proc.returncode}: {tail}") proc.kill() log_file.close() raise RuntimeError(f"Proxy failed to start on port {port} within 15 seconds") def _setup_rtk(verbose: bool = False) -> Path | None: """Ensure rtk is installed and hooks are registered.""" from headroom.rtk import get_rtk_path from headroom.rtk.installer import ensure_rtk, register_claude_hooks rtk_path = get_rtk_path() if rtk_path: if verbose: click.echo(f" rtk found at {rtk_path}") else: click.echo(" Downloading rtk (Rust Token Killer)...") rtk_path = ensure_rtk() if rtk_path: click.echo(f" rtk installed at {rtk_path}") else: click.echo(" rtk download failed — continuing without it") return None # Register hooks (idempotent) if register_claude_hooks(rtk_path): if verbose: click.echo(" rtk hooks registered in Claude Code") else: click.echo(" rtk hook registration failed — continuing without it") return rtk_path # rtk instructions for tools without hook support (Codex, Cursor, Aider). # These get injected into AGENTS.md / .cursorrules so the LLM voluntarily # uses rtk-prefixed commands. Kept concise to minimize instruction overhead. RTK_INSTRUCTIONS_BLOCK = """\ # RTK (Rust Token Killer) - Token-Optimized Commands When running shell commands, **always prefix with `rtk`**. This reduces context usage by 60-90% with zero behavior change. If rtk has no filter for a command, it passes through unchanged — so it is always safe to use. ## Key Commands ```bash # Git (59-80% savings) rtk git status rtk git diff rtk git log # Files & Search (60-75% savings) rtk ls rtk read rtk grep rtk find rtk diff # Test (90-99% savings) — shows failures only rtk pytest tests/ rtk cargo test rtk test # Build & Lint (80-90% savings) — shows errors only rtk tsc rtk lint rtk cargo build rtk prettier --check rtk mypy rtk ruff check # Analysis (70-90% savings) rtk err rtk log rtk json rtk summary rtk deps rtk env # GitHub (26-87% savings) rtk gh pr view rtk gh run list rtk gh issue list # Infrastructure (85% savings) rtk docker ps rtk kubectl get rtk docker logs # Package managers (70-90% savings) rtk pip list rtk pnpm install rtk npm run