From efd2ac1ca4d88d8f5990259c98b673c603902896 Mon Sep 17 00:00:00 2001 From: Garm Date: Fri, 24 Apr 2026 15:33:30 +0200 Subject: [PATCH] chore: renormalize line endings to LF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.gitattributes` declares `*.py text eol=lf` and `*.sh text eol=lf`, but 74 files (73 .py, 1 .sh) are stored in the index with CRLF line endings, violating that contract. Every macOS/Linux clone reports these files as "modified" on fresh checkout because git's diff engine sees the stored bytes don't match the attribute contract, even though the working tree and index match byte-for-byte. Running `git add --renormalize .` rewrites each affected blob so the stored form matches the attribute declaration. No semantic changes — every affected file's diff is "N insertions, N deletions" with inserts and deletes being the same lines modulo line endings. Follow-up commit adds `.git-blame-ignore-revs` so `git blame` / GitHub blame skip this mechanical commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- headroom/cli/wrap.py | 4654 ++++++++--------- headroom/compress.py | 694 +-- headroom/copilot_auth.py | 888 ++-- headroom/install/health.py | 56 +- headroom/install/providers.py | 348 +- headroom/install/runtime.py | 558 +- headroom/providers/aider/install.py | 24 +- headroom/providers/claude/install.py | 126 +- headroom/providers/codex/install.py | 136 +- headroom/providers/copilot/install.py | 50 +- headroom/providers/cursor/install.py | 30 +- headroom/providers/install_registry.py | 172 +- headroom/providers/openclaw/install.py | 100 +- headroom/proxy/handlers/openai.py | 5492 ++++++++++---------- headroom/proxy/server.py | 5240 +++++++++---------- headroom/release_version.py | 620 +-- headroom/subscription/__init__.py | 144 +- headroom/subscription/base.py | 460 +- headroom/subscription/client.py | 262 +- headroom/subscription/codex_rate_limits.py | 494 +- headroom/subscription/copilot_quota.py | 732 +-- headroom/subscription/models.py | 790 +-- headroom/subscription/session_tracking.py | 378 +- headroom/subscription/tracker.py | 928 ++-- headroom/transforms/content_router.py | 4260 +++++++-------- scripts/changelog-gen.py | 406 +- scripts/sync-plugin-versions.py | 110 +- scripts/tests/test_changelog_gen.py | 604 +-- scripts/tests/test_sync_plugin_versions.py | 136 +- tests/test_backend_anyllm.py | 768 +-- tests/test_ccr_batch_store.py | 252 +- tests/test_ccr_response_handler_extra.py | 744 +-- tests/test_cli/test_wrap_copilot.py | 670 +-- tests/test_cli_learn.py | 532 +- tests/test_codex_rate_limits.py | 454 +- tests/test_compress_api.py | 548 +- tests/test_compress_failure.py | 78 +- tests/test_copilot_quota.py | 662 +-- tests/test_evals_datasets.py | 1076 ++-- tests/test_evals_metrics.py | 264 +- tests/test_exceptions.py | 80 +- tests/test_graph.py | 704 +-- tests/test_install/test_paths.py | 136 +- tests/test_install/test_runtime.py | 936 ++-- tests/test_install/test_supervisors.py | 942 ++-- tests/test_plugin_manifests.py | 110 +- tests/test_pricing.py | 260 +- tests/test_pricing_litellm.py | 194 +- tests/test_provider_aider.py | 66 +- tests/test_provider_claude.py | 18 +- tests/test_provider_codex_runtime.py | 24 +- tests/test_provider_copilot_wrap.py | 248 +- tests/test_provider_openclaw_wrap.py | 238 +- tests/test_provider_package_init.py | 222 +- tests/test_provider_proxy_routes.py | 680 +-- tests/test_provider_registry_extended.py | 478 +- tests/test_proxy_copilot_auth_hooks.py | 398 +- tests/test_proxy_handler_helpers.py | 428 +- tests/test_proxy_pipeline_lifecycle.py | 368 +- tests/test_proxy_savings_history.py | 1422 ++--- tests/test_quota_registry.py | 526 +- tests/test_release_version.py | 372 +- tests/test_release_workflows.py | 124 +- tests/test_reporting.py | 610 +-- tests/test_subscription_base.py | 280 +- tests/test_subscription_client.py | 358 +- tests/test_subscription_tracker.py | 402 +- tests/test_tokenizer.py | 100 +- tests/test_transforms_content_detection.py | 404 +- tests/test_transforms_content_router.py | 588 +-- tests/test_transforms_log_compressor.py | 366 +- tests/test_transforms_package.py | 52 +- tests/test_transforms_search_compressor.py | 286 +- tests/test_utils.py | 244 +- 74 files changed, 23802 insertions(+), 23802 deletions(-) diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index a315e336e..fe8c697cb 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -1,2327 +1,2327 @@ -"""Wrap CLI commands to run through Headroom proxy. - -Usage: - headroom wrap claude # Start proxy + rtk + claude - headroom wrap copilot -- --model ... # Start proxy + launch GitHub Copilot CLI - 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, cast - -# 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 headroom.copilot_auth import DEFAULT_API_URL as COPILOT_API_URL -from headroom.copilot_auth import has_oauth_auth, resolve_client_bearer_token -from headroom.providers.aider import build_launch_env as _build_aider_launch_env -from headroom.providers.claude import proxy_base_url as _claude_proxy_base_url -from headroom.providers.codex import build_launch_env as _build_codex_launch_env -from headroom.providers.copilot import ( - build_launch_env as _build_copilot_launch_env, -) -from headroom.providers.copilot import ( - detect_running_proxy_backend as _copilot_detect_running_proxy_backend, -) -from headroom.providers.copilot import ( - model_configured as _copilot_model_configured_impl, -) -from headroom.providers.copilot import ( - provider_key_source as _copilot_provider_key_source, -) -from headroom.providers.copilot import ( - query_proxy_config as _copilot_query_proxy_config, -) -from headroom.providers.copilot import ( - resolve_provider_type as _copilot_resolve_provider_type, -) -from headroom.providers.copilot import ( - validate_configuration as _validate_copilot_configuration, -) -from headroom.providers.cursor import render_setup_lines as _render_cursor_setup_lines -from headroom.providers.openclaw import ( - build_plugin_entry as _build_openclaw_plugin_entry_impl, -) -from headroom.providers.openclaw import ( - build_unwrap_entry as _build_openclaw_unwrap_entry_impl, -) -from headroom.providers.openclaw import ( - decode_entry_json as _decode_openclaw_entry_json_impl, -) -from headroom.providers.openclaw import ( - normalize_gateway_provider_ids as _normalize_openclaw_gateway_provider_ids_impl, -) - -from .main import main - - -def _live_wrap_module() -> Any: - """Return the current live wrap module instance.""" - return cast(Any, sys.modules[__name__]) - - -def _print_telemetry_notice() -> None: - """Print a telemetry notice when anonymous telemetry is enabled. - - Respects the HEADROOM_TELEMETRY and HEADROOM_TELEMETRY_WARN feature flags. - Does nothing when telemetry or warnings are disabled. - """ - from headroom.telemetry.beacon import format_telemetry_notice - - notice = format_telemetry_notice(prefix=" ") - if notice: - click.echo(notice) - - -# 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.""" - from headroom import paths as _paths - - log_dir = _paths.log_dir() - log_dir.mkdir(parents=True, exist_ok=True) - return log_dir / "proxy.log" - - -def _start_proxy( - port: int, - *, - learn: bool = False, - memory: bool = False, - agent_type: str = "unknown", - code_graph: bool = False, - backend: str | None = None, - anyllm_provider: str | None = None, - region: str | None = None, - openai_api_url: 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 --memory flag to proxy subprocess - if memory: - cmd.append("--memory") - - # Forward --code-graph flag to proxy subprocess (live file watcher) - if code_graph: - cmd.append("--code-graph") - - # 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]) - - if openai_api_url: - cmd.extend(["--openai-api-url", openai_api_url]) - - 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" - - # Tell the proxy which agent is being wrapped (for traffic learning output) - if agent_type != "unknown": - proxy_env["HEADROOM_AGENT_TYPE"] = agent_type - proxy_env.setdefault("HEADROOM_STACK", f"wrap_{agent_type}") - - proc = subprocess.Popen( - cmd, - stdout=log_file, - stderr=log_file, - env=proxy_env, - ) - - # Wait for proxy to be ready (up to 45 seconds). - # ML components (Kompress, Magika, Tree-sitter) load synchronously before - # uvicorn binds the port. On slower machines this can take 20-30 seconds. - for _i in range(45): - 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 45 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 - - -_CBM_MCP_SERVER_NAME = "codebase-memory-mcp" - - -def _register_cbm_mcp_server(cbm_bin: str) -> None: - """Register codebase-memory-mcp as an MCP server in Claude Code. - - Uses ``claude mcp add`` so the tools appear in ``/mcp`` automatically. - Idempotent — skips if already registered. - """ - claude_cli = shutil.which("claude") - if not claude_cli: - return - - # Check if already registered - check = subprocess.run( - [claude_cli, "mcp", "get", _CBM_MCP_SERVER_NAME], - capture_output=True, - text=True, - ) - if check.returncode == 0: - return # Already registered - - result = subprocess.run( - [claude_cli, "mcp", "add", _CBM_MCP_SERVER_NAME, "-s", "user", "--", cbm_bin], - capture_output=True, - text=True, - ) - if result.returncode == 0: - click.echo(f" Code graph: registered {_CBM_MCP_SERVER_NAME} MCP server") - else: - pass # Non-critical — tools won't appear in /mcp but graph still works - - -def _setup_code_graph(verbose: bool = False) -> bool: - """Ensure codebase-memory-mcp is installed, registered as MCP server, and project is indexed. - - codebase-memory-mcp builds a knowledge graph of the codebase using - tree-sitter, enabling the LLM to query code structure (call chains, - function definitions, impact analysis) instead of reading entire files. - - Steps: - 1. Download the binary if not already present. - 2. Register as an MCP server in Claude Code (``claude mcp add``). - 3. Index the current project (fast, idempotent). - - With Claude Code's MCP Tool Search, the 14 graph tools add ~200 tokens - overhead per request (not the full ~1,915) — they're lazy-loaded. - - Returns True if graph is ready, False if setup failed. - """ - from headroom.graph.installer import ensure_cbm, get_cbm_path - - cbm_path = get_cbm_path() - if not cbm_path: - click.echo(" Code graph: downloading codebase-memory-mcp...") - cbm_path = ensure_cbm() - if cbm_path: - click.echo(f" Code graph: installed at {cbm_path}") - else: - click.echo(" Code graph: download failed — skipping") - return False - - cbm_bin = str(cbm_path) - - # Register as MCP server so tools appear in /mcp - _register_cbm_mcp_server(cbm_bin) - - # Index current project (fast — ~1s for most repos, idempotent) - project_dir = str(Path.cwd()) - try: - result = subprocess.run( - [ - cbm_bin, - "cli", - "index_repository", - json.dumps({"repo_path": project_dir, "mode": "fast"}), - ], - capture_output=True, - text=True, - timeout=30, - ) - if result.returncode == 0: - # Parse node/edge counts from output - for line in result.stdout.splitlines(): - if '"nodes"' in line: - try: - # Parse the JSON response to extract node/edge counts - import re - - m_nodes = re.search(r'"nodes":(\d+)', line) - m_edges = re.search(r'"edges":(\d+)', line) - if m_nodes and m_edges: - nodes = int(m_nodes.group(1)) - edges = int(m_edges.group(1)) - click.echo( - f" Code graph: indexed ({nodes:,} symbols, " - f"{edges:,} relationships)" - ) - else: - click.echo(" Code graph: indexed") - except (ValueError, AttributeError): - click.echo(" Code graph: indexed") - return True - click.echo(" Code graph: indexed") - return True - else: - if verbose: - click.echo(f" Code graph: indexing failed ({result.stderr[:100]})") - return False - except subprocess.TimeoutExpired: - click.echo(" Code graph: indexing timed out (will complete in background)") - return False - except Exception as e: - if verbose: - click.echo(f" Code graph: setup failed ({e})") - return False - - -# 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