diff --git a/.dockerignore b/.dockerignore index 5e7b346a0..930b8efd9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,73 +1,73 @@ -# VCS -.git -.github -.github/* -!.github/plugin/ -!.github/plugin/** -.gitignore - -# Python artifacts -__pycache__ -*.pyc -*.pyo -*.egg-info -dist/ -build/ - -# Dev/test tooling -.pytest_cache -.coverage -.mypy_cache -.ruff_cache -.pre-commit-config.yaml -.venv -venv - -# Tests & docs (not needed in image) -tests/ -docs/ -mkdocs.yml -CHANGELOG.md -LICENSE -NOTICE - -# JS/TS artifacts (dashboard, SDK — not part of proxy image) -apps/ -sdk/ -!sdk/ -!sdk/typescript/ -!sdk/typescript/** -plugins/ -!plugins/ -!plugins/openclaw/ -!plugins/openclaw/** -!plugins/headroom-agent-hooks/ -!plugins/headroom-agent-hooks/** -node_modules/ -*.tgz - -# Secrets & local config -.env -.env.* -*.log - -# IDE -.vscode -.idea -*.swp - -# Docker -Dockerfile -docker-compose*.yml -.dockerignore - -# Misc -.pi-lens/ -.superpowers/ -examples/ -node-compile-cache/ -!e2e/ -!e2e/init/ -!e2e/init/** -!.claude-plugin/ -!.claude-plugin/** +# VCS +.git +.github +.github/* +!.github/plugin/ +!.github/plugin/** +.gitignore + +# Python artifacts +__pycache__ +*.pyc +*.pyo +*.egg-info +dist/ +build/ + +# Dev/test tooling +.pytest_cache +.coverage +.mypy_cache +.ruff_cache +.pre-commit-config.yaml +.venv +venv + +# Tests & docs (not needed in image) +tests/ +docs/ +mkdocs.yml +CHANGELOG.md +LICENSE +NOTICE + +# JS/TS artifacts (dashboard, SDK — not part of proxy image) +apps/ +sdk/ +!sdk/ +!sdk/typescript/ +!sdk/typescript/** +plugins/ +!plugins/ +!plugins/openclaw/ +!plugins/openclaw/** +!plugins/headroom-agent-hooks/ +!plugins/headroom-agent-hooks/** +node_modules/ +*.tgz + +# Secrets & local config +.env +.env.* +*.log + +# IDE +.vscode +.idea +*.swp + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Misc +.pi-lens/ +.superpowers/ +examples/ +node-compile-cache/ +!e2e/ +!e2e/init/ +!e2e/init/** +!.claude-plugin/ +!.claude-plugin/** diff --git a/.gitignore b/.gitignore index 122f5731f..4b03031f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,219 +1,219 @@ -# Private scripts (contain credentials). Allowlist checked-in helpers below. -scripts/ -!scripts/ -scripts/* -!scripts/install.sh -!scripts/install.ps1 -!scripts/version-sync.py -!scripts/sync-plugin-versions.py -!scripts/changelog-gen.py -!scripts/verify-versions.py -!scripts/tests/ -!scripts/README.md -!scripts/repro_codex_replay.py -!scripts/fixtures/ -!scripts/fixtures/*.json - -# Swift SDK (separate repo) -swift/ - -# Local planning docs (never commit) -ENTERPRISE_HARDENING.md - -# Audit/scan outputs (contain security findings — never commit) -bandit_result.txt -pip_audit_result.txt -ruff_result.txt -reqs.txt - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -pytest_cache/ - -# Translations -*.mo -*.pot - -# Environments -.env -.env.* -!.env.act.example -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -.python-version - -# Secrets and API keys - NEVER commit these -*.pem -*.key -secrets.json -credentials.json -.secrets -api_keys.txt -.anthropic -.openai - -# IDE and editors -.idea/ -.vscode/ -*.swp -*.swo -*~ -.project -.pydevproject -.settings/ -*.sublime-project -*.sublime-workspace -.spyproject -.spyderproject - -# Jupyter Notebook -.ipynb_checkpoints -*.ipynb - -# macOS -.DS_Store -.AppleDouble -.LSOverride -._* - -# Thumbnails -Icon? -._* - -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini - -# Linux -*~ - -# Local configuration -local_settings.py -*.local.py -*.local.json -*.local.yaml - -# Database files -*.db -*.sqlite -*.sqlite3 - -# Log files -*.log -logs/ -log/ - -# Temporary files -tmp/ -temp/ -*.tmp -*.bak -*.swp - -# Benchmark results (keep framework, not results) -.benchmarks/ -benchmark_results.json -benchmark_results/ - -# DeepEval cache -.deepeval/ - -# Headroom specific -headroom.db -headroom_*.db -*.jsonl -!tests/fixtures/*.jsonl - -# Documentation build -docs/_build/ -site/ - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Ruff -.ruff_cache/ - -# pyright -pyrightconfig.json - -# Editor backup files -*~ -\#*\# -.\#* - -# Local development configuration -CLAUDE.md - -# Vitals provenance data -.vitals/ - -# Superpowers working files (plans, specs, brainstorming) -# docs/spec/ (lives in git - git-versioned living specification) - -# Managed platform (separate private repo) -headroom-managed/ - -# Local act testing (never commit test tokens) -/.env.act -.actrc.local - -# Release metadata artifact -.releaseetadata - -# uv lockfile: regenerated locally; not committed -uv.lock +# Private scripts (contain credentials). Allowlist checked-in helpers below. +scripts/ +!scripts/ +scripts/* +!scripts/install.sh +!scripts/install.ps1 +!scripts/version-sync.py +!scripts/sync-plugin-versions.py +!scripts/changelog-gen.py +!scripts/verify-versions.py +!scripts/tests/ +!scripts/README.md +!scripts/repro_codex_replay.py +!scripts/fixtures/ +!scripts/fixtures/*.json + +# Swift SDK (separate repo) +swift/ + +# Local planning docs (never commit) +ENTERPRISE_HARDENING.md + +# Audit/scan outputs (contain security findings — never commit) +bandit_result.txt +pip_audit_result.txt +ruff_result.txt +reqs.txt + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytest_cache/ + +# Translations +*.mo +*.pot + +# Environments +.env +.env.* +!.env.act.example +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.python-version + +# Secrets and API keys - NEVER commit these +*.pem +*.key +secrets.json +credentials.json +.secrets +api_keys.txt +.anthropic +.openai + +# IDE and editors +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ +*.sublime-project +*.sublime-workspace +.spyproject +.spyderproject + +# Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Thumbnails +Icon? +._* + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Linux +*~ + +# Local configuration +local_settings.py +*.local.py +*.local.json +*.local.yaml + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Log files +*.log +logs/ +log/ + +# Temporary files +tmp/ +temp/ +*.tmp +*.bak +*.swp + +# Benchmark results (keep framework, not results) +.benchmarks/ +benchmark_results.json +benchmark_results/ + +# DeepEval cache +.deepeval/ + +# Headroom specific +headroom.db +headroom_*.db +*.jsonl +!tests/fixtures/*.jsonl + +# Documentation build +docs/_build/ +site/ + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Ruff +.ruff_cache/ + +# pyright +pyrightconfig.json + +# Editor backup files +*~ +\#*\# +.\#* + +# Local development configuration +CLAUDE.md + +# Vitals provenance data +.vitals/ + +# Superpowers working files (plans, specs, brainstorming) +# docs/spec/ (lives in git - git-versioned living specification) + +# Managed platform (separate private repo) +headroom-managed/ + +# Local act testing (never commit test tokens) +/.env.act +.actrc.local + +# Release metadata artifact +.releaseetadata + +# uv lockfile: regenerated locally; not committed +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42f7f867d..ecbcd2b1d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,25 @@ -repos: - - repo: local - hooks: - - id: sync-plugin-versions - name: Sync plugin versions - entry: python scripts/sync-plugin-versions.py - language: system - pass_filenames: false - always_run: true - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.4 - hooks: - - id: ruff - args: [--fix] - exclude: ^experiments/ - - id: ruff-format - exclude: ^experiments/ - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 - hooks: - - id: mypy - args: [--ignore-missing-imports] - exclude: ^experiments/ - pass_filenames: false - entry: mypy headroom +repos: + - repo: local + hooks: + - id: sync-plugin-versions + name: Sync plugin versions + entry: python scripts/sync-plugin-versions.py + language: system + pass_filenames: false + always_run: true + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.4 + hooks: + - id: ruff + args: [--fix] + exclude: ^experiments/ + - id: ruff-format + exclude: ^experiments/ + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.1 + hooks: + - id: mypy + args: [--ignore-missing-imports] + exclude: ^experiments/ + pass_filenames: false + entry: mypy headroom diff --git a/README.md b/README.md index e94cbb99e..e5afcdbfd 100644 --- a/README.md +++ b/README.md @@ -1,275 +1,275 @@ -
- -# Headroom - -**Compress everything your AI agent reads. Same answers, fraction of the tokens.** - -[![CI](https://github.com/chopratejas/headroom/actions/workflows/ci.yml/badge.svg)](https://github.com/chopratejas/headroom/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/chopratejas/headroom/graph/badge.svg)](https://app.codecov.io/gh/chopratejas/headroom) -[![PyPI](https://img.shields.io/pypi/v/headroom-ai.svg)](https://pypi.org/project/headroom-ai/) -[![npm](https://img.shields.io/npm/v/headroom-ai.svg)](https://www.npmjs.com/package/headroom-ai) -[![Model: Kompress-base](https://img.shields.io/badge/model-Kompress--base-yellow.svg)](https://huggingface.co/chopratejas/kompress-base) -[![Tokens saved: 60B+](https://img.shields.io/badge/tokens%20saved-60B%2B-2ea44f)](https://headroomlabs.ai/dashboard) -[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://headroom-docs.vercel.app/docs) - -Headroom in action - -
- ---- - -Every tool call, log line, DB read, RAG chunk, and file your agent injects into a prompt is mostly boilerplate. Headroom strips the noise and keeps the signal — **losslessly, locally, and without touching accuracy.** - -> **100 logs. One FATAL error buried at position 67. Both runs found it.** -> Baseline **10,144 tokens** → Headroom **1,260 tokens** — **87% fewer, identical answer.** -> `python examples/needle_in_haystack_test.py` - ---- - -## Quick start - -Works with Anthropic, OpenAI, Google, Bedrock, Vertex, Azure, OpenRouter, and 100+ models via LiteLLM. - -**Wrap your coding agent — one command:** - -```bash -pip install "headroom-ai[all]" - -headroom wrap claude # Claude Code -headroom wrap codex # Codex -headroom wrap cursor # Cursor -headroom wrap aider # Aider -headroom wrap copilot # GitHub Copilot CLI -``` - -**Prefer a one-time durable install instead of wrapping every launch:** - -```bash -headroom init -g # Detect installed user-scoped agents and wire them to Headroom -headroom init claude # Install repo-local Claude hooks for just this project -headroom init copilot -g # Install user-scoped Copilot hooks and provider routing -``` - -**Drop it into your own code — Python or TypeScript:** - -```python -from headroom import compress - -result = compress(messages, model="claude-sonnet-4-5") -response = client.messages.create(model="claude-sonnet-4-5", messages=result.messages) -print(f"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})") -``` - -```typescript -import { compress } from 'headroom-ai'; -const result = await compress(messages, { model: 'gpt-4o' }); -``` - -**Or run it as a proxy — zero code changes, any language:** - -```bash -headroom proxy --port 8787 -ANTHROPIC_BASE_URL=http://localhost:8787 your-app -OPENAI_BASE_URL=http://localhost:8787/v1 your-app -``` - ---- - -## Why Headroom - -- **Accuracy-preserving.** GSM8K **0.870 → 0.870** (±0.000). TruthfulQA **+0.030**. SQuAD v2 and BFCL both **97%** accuracy after compression. Validated on public OSS benchmarks you can rerun yourself. -- **Runs on your machine.** No cloud API, no data egress. Compression latency is milliseconds — faster end-to-end for Sonnet / Opus / GPT-4 class models than a hosted service round-trip. -- **[Kompress-base](https://huggingface.co/chopratejas/kompress-base) on HuggingFace.** Our open-source text compressor, fine-tuned on real agentic traces — tool outputs, logs, RAG chunks, code. Install with `pip install "headroom-ai[ml]"`. -- **Cross-agent memory and learning.** Claude Code saves a fact, Codex reads it back. `headroom learn` mines failed sessions and writes corrections straight to `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` — reliability compounds over time. -- **Reversible (CCR).** Compression is not deletion. The model can always call `headroom_retrieve` to pull the original bytes. Nothing is thrown away. - -Bundles the [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — full [attribution below](#compared-to). - ---- - -## How it fits - -``` - Your agent / app - (Claude Code, Cursor, Codex, LangChain, Agno, Strands, your own code…) - │ prompts · tool outputs · logs · RAG results · files - ▼ - ┌────────────────────────────────────────────────────┐ - │ Headroom (runs locally — your data stays here) │ - │ ─────────────────────────────────────────────── │ - │ CacheAligner → ContentRouter → CCR │ - │ ├─ SmartCrusher (JSON) │ - │ ├─ CodeCompressor (AST) │ - │ └─ Kompress-base (text, HF) │ - │ │ - │ Cross-agent memory · headroom learn · MCP │ - └────────────────────────────────────────────────────┘ - │ compressed prompt + retrieval tool - ▼ - LLM provider (Anthropic · OpenAI · Bedrock · …) -``` - -→ [Architecture](https://headroom-docs.vercel.app/docs/architecture) · [CCR reversible compression](https://headroom-docs.vercel.app/docs/ccr) · [Kompress-base model card](https://huggingface.co/chopratejas/kompress-base) - ---- - -## Proof - -**Savings on real agent workloads:** - -| Workload | Before | After | Savings | -|-------------------------------|-------:|-------:|--------:| -| Code search (100 results) | 17,765 | 1,408 | **92%** | -| SRE incident debugging | 65,694 | 5,118 | **92%** | -| GitHub issue triage | 54,174 | 14,761 | **73%** | -| Codebase exploration | 78,502 | 41,254 | **47%** | - -**Accuracy preserved on standard benchmarks:** - -| Benchmark | Category | N | Baseline | Headroom | Delta | -|------------|----------|----:|---------:|---------:|----------:| -| GSM8K | Math | 100 | 0.870 | 0.870 | **±0.000**| -| TruthfulQA | Factual | 100 | 0.530 | 0.560 | **+0.030**| -| SQuAD v2 | QA | 100 | — | **97%** | 19% compression | -| BFCL | Tools | 100 | — | **97%** | 32% compression | - -Reproduce: - -```bash -python -m headroom.evals suite --tier 1 -``` - -**Community, live:** - -
- - 60B+ tokens saved — community leaderboard - -

60B+ tokens saved by the community in the last 20 days — live leaderboard →

-
- -→ [Full benchmarks & methodology](https://headroom-docs.vercel.app/docs/benchmarks) - ---- - -## Built for coding agents - -| Agent | Durable init / one-shot wrap | Notes | -|--------------------|------------------------------------|------------------------------------------------------------------| -| **Claude Code** | `headroom init claude -g` / `headroom wrap claude` | `init` installs user or repo-local hooks; `wrap` is still useful for ad hoc sessions | -| **Codex** | `headroom init codex -g` / `headroom wrap codex --memory` | `init` installs provider config plus lifecycle hooks where supported | -| **Cursor** | `headroom wrap cursor` | Prints Cursor config — durable init not available yet | -| **Aider** | `headroom wrap aider` | Starts proxy, launches Aider | -| **Copilot CLI** | `headroom init copilot -g` / `headroom wrap copilot` | `init` installs hooks and BYOK provider routing for the current user | -| **OpenClaw** | `headroom init openclaw -g` / `headroom wrap openclaw` | Installs Headroom as ContextEngine plugin | - -MCP-native too — `headroom mcp install` exposes `headroom_compress`, `headroom_retrieve`, and `headroom_stats` to any MCP client. - -
- headroom learn in action -
- ---- - -## Integrations - -
-Drop Headroom into any stack - -| Your setup | Hook in with | -|-------------------------|------------------------------------------------------------------| -| Any Python app | `compress(messages, model=…)` | -| Any TypeScript app | `await compress(messages, { model })` | -| Anthropic / OpenAI SDK | `withHeadroom(new Anthropic())` · `withHeadroom(new OpenAI())` | -| Vercel AI SDK | `wrapLanguageModel({ model, middleware: headroomMiddleware() })` | -| LiteLLM | `litellm.callbacks = [HeadroomCallback()]` | -| LangChain | `HeadroomChatModel(your_llm)` | -| Agno | `HeadroomAgnoModel(your_model)` | -| Strands | [Strands guide](https://headroom-docs.vercel.app/docs/strands) | -| ASGI apps | `app.add_middleware(CompressionMiddleware)` | -| Multi-agent | `SharedContext().put / .get` | -| MCP clients | `headroom mcp install` | - -
- -
-What's inside - -- **SmartCrusher** — universal JSON: arrays of dicts, nested objects, mixed types. -- **CodeCompressor** — AST-aware for Python, JS, Go, Rust, Java, C++. -- **Kompress-base** — our HuggingFace model, trained on agentic traces. -- **Image compression** — 40–90% reduction via trained ML router. -- **CacheAligner** — stabilizes prefixes so Anthropic/OpenAI KV caches actually hit. -- **IntelligentContext** — score-based context fitting with learned importance. -- **CCR** — reversible compression; LLM retrieves originals on demand. -- **Cross-agent memory** — shared store, agent provenance, auto-dedup. -- **SharedContext** — compressed context passing across multi-agent workflows. -- **`headroom learn`** — plugin-based failure mining for Claude, Codex, Gemini. - -
- ---- - -## Install - -```bash -pip install "headroom-ai[all]" # Python, everything -npm install headroom-ai # TypeScript / Node -docker pull ghcr.io/chopratejas/headroom:latest -``` - -Granular extras: `[proxy]`, `[mcp]`, `[ml]` (Kompress-base), `[agno]`, `[langchain]`, `[evals]`. Requires **Python 3.10+**. - -→ [Installation guide](https://headroom-docs.vercel.app/docs/installation) — Docker tags, persistent service, PowerShell, devcontainers. - ---- - -## Documentation - -| Start here | Go deeper | -|-------------------------------------------------------------------------|------------------------------------------------------------------------| -| [Quickstart](https://headroom-docs.vercel.app/docs/quickstart) | [Architecture](https://headroom-docs.vercel.app/docs/architecture) | -| [Proxy](https://headroom-docs.vercel.app/docs/proxy) | [How compression works](https://headroom-docs.vercel.app/docs/how-compression-works) | -| [MCP tools](https://headroom-docs.vercel.app/docs/mcp) | [CCR — reversible compression](https://headroom-docs.vercel.app/docs/ccr) | -| [Memory](https://headroom-docs.vercel.app/docs/memory) | [Cache optimization](https://headroom-docs.vercel.app/docs/cache-optimization) | -| [Failure learning](https://headroom-docs.vercel.app/docs/failure-learning) | [Benchmarks](https://headroom-docs.vercel.app/docs/benchmarks) | -| [Configuration](https://headroom-docs.vercel.app/docs/configuration) | [Limitations](https://headroom-docs.vercel.app/docs/limitations) | - ---- - -## Compared to - -Headroom runs **locally**, covers **every** content type (not just CLI or text), works with every major framework, and is **reversible**. - -| | Scope | Deploy | Local | Reversible | -|----------------------------------|-------------------------------------------------|-------------------------------------|:-----:|:----------:| -| **Headroom** | All context — tools, RAG, logs, files, history | Proxy · library · middleware · MCP | Yes | Yes | -| [RTK](https://github.com/rtk-ai/rtk) | CLI command outputs | CLI wrapper | Yes | No | -| [Compresr](https://compresr.ai), [Token Co.](https://thetokencompany.ai) | Text sent to their API | Hosted API call | No | No | -| OpenAI Compaction | Conversation history | Provider-native | No | No | - -> **Attribution.** Headroom ships with the excellent [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — `git show` → `git show --short`, noisy `ls` → scoped, chatty installers → summarized. Huge thanks to the RTK team; their tool is a first-class part of our stack, and Headroom compresses everything downstream of it. - ---- - -## Contributing - -```bash -git clone https://github.com/chopratejas/headroom.git && cd headroom -pip install -e ".[dev]" && pytest -``` - -Devcontainers in `.devcontainer/` (default + `memory-stack` with Qdrant & Neo4j). See [CONTRIBUTING.md](CONTRIBUTING.md). - ---- - -## Community - -- **[Live leaderboard](https://headroomlabs.ai/dashboard)** — 60B+ tokens saved and counting. -- **[Discord](https://discord.gg/yRmaUNpsPJ)** — questions, feedback, war stories. -- **[Kompress-base on HuggingFace](https://huggingface.co/chopratejas/kompress-base)** — the model behind our text compression. - -## License - -Apache 2.0 — see [LICENSE](LICENSE). +
+ +# Headroom + +**Compress everything your AI agent reads. Same answers, fraction of the tokens.** + +[![CI](https://github.com/chopratejas/headroom/actions/workflows/ci.yml/badge.svg)](https://github.com/chopratejas/headroom/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/chopratejas/headroom/graph/badge.svg)](https://app.codecov.io/gh/chopratejas/headroom) +[![PyPI](https://img.shields.io/pypi/v/headroom-ai.svg)](https://pypi.org/project/headroom-ai/) +[![npm](https://img.shields.io/npm/v/headroom-ai.svg)](https://www.npmjs.com/package/headroom-ai) +[![Model: Kompress-base](https://img.shields.io/badge/model-Kompress--base-yellow.svg)](https://huggingface.co/chopratejas/kompress-base) +[![Tokens saved: 60B+](https://img.shields.io/badge/tokens%20saved-60B%2B-2ea44f)](https://headroomlabs.ai/dashboard) +[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://headroom-docs.vercel.app/docs) + +Headroom in action + +
+ +--- + +Every tool call, log line, DB read, RAG chunk, and file your agent injects into a prompt is mostly boilerplate. Headroom strips the noise and keeps the signal — **losslessly, locally, and without touching accuracy.** + +> **100 logs. One FATAL error buried at position 67. Both runs found it.** +> Baseline **10,144 tokens** → Headroom **1,260 tokens** — **87% fewer, identical answer.** +> `python examples/needle_in_haystack_test.py` + +--- + +## Quick start + +Works with Anthropic, OpenAI, Google, Bedrock, Vertex, Azure, OpenRouter, and 100+ models via LiteLLM. + +**Wrap your coding agent — one command:** + +```bash +pip install "headroom-ai[all]" + +headroom wrap claude # Claude Code +headroom wrap codex # Codex +headroom wrap cursor # Cursor +headroom wrap aider # Aider +headroom wrap copilot # GitHub Copilot CLI +``` + +**Prefer a one-time durable install instead of wrapping every launch:** + +```bash +headroom init -g # Detect installed user-scoped agents and wire them to Headroom +headroom init claude # Install repo-local Claude hooks for just this project +headroom init copilot -g # Install user-scoped Copilot hooks and provider routing +``` + +**Drop it into your own code — Python or TypeScript:** + +```python +from headroom import compress + +result = compress(messages, model="claude-sonnet-4-5") +response = client.messages.create(model="claude-sonnet-4-5", messages=result.messages) +print(f"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})") +``` + +```typescript +import { compress } from 'headroom-ai'; +const result = await compress(messages, { model: 'gpt-4o' }); +``` + +**Or run it as a proxy — zero code changes, any language:** + +```bash +headroom proxy --port 8787 +ANTHROPIC_BASE_URL=http://localhost:8787 your-app +OPENAI_BASE_URL=http://localhost:8787/v1 your-app +``` + +--- + +## Why Headroom + +- **Accuracy-preserving.** GSM8K **0.870 → 0.870** (±0.000). TruthfulQA **+0.030**. SQuAD v2 and BFCL both **97%** accuracy after compression. Validated on public OSS benchmarks you can rerun yourself. +- **Runs on your machine.** No cloud API, no data egress. Compression latency is milliseconds — faster end-to-end for Sonnet / Opus / GPT-4 class models than a hosted service round-trip. +- **[Kompress-base](https://huggingface.co/chopratejas/kompress-base) on HuggingFace.** Our open-source text compressor, fine-tuned on real agentic traces — tool outputs, logs, RAG chunks, code. Install with `pip install "headroom-ai[ml]"`. +- **Cross-agent memory and learning.** Claude Code saves a fact, Codex reads it back. `headroom learn` mines failed sessions and writes corrections straight to `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` — reliability compounds over time. +- **Reversible (CCR).** Compression is not deletion. The model can always call `headroom_retrieve` to pull the original bytes. Nothing is thrown away. + +Bundles the [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — full [attribution below](#compared-to). + +--- + +## How it fits + +``` + Your agent / app + (Claude Code, Cursor, Codex, LangChain, Agno, Strands, your own code…) + │ prompts · tool outputs · logs · RAG results · files + ▼ + ┌────────────────────────────────────────────────────┐ + │ Headroom (runs locally — your data stays here) │ + │ ─────────────────────────────────────────────── │ + │ CacheAligner → ContentRouter → CCR │ + │ ├─ SmartCrusher (JSON) │ + │ ├─ CodeCompressor (AST) │ + │ └─ Kompress-base (text, HF) │ + │ │ + │ Cross-agent memory · headroom learn · MCP │ + └────────────────────────────────────────────────────┘ + │ compressed prompt + retrieval tool + ▼ + LLM provider (Anthropic · OpenAI · Bedrock · …) +``` + +→ [Architecture](https://headroom-docs.vercel.app/docs/architecture) · [CCR reversible compression](https://headroom-docs.vercel.app/docs/ccr) · [Kompress-base model card](https://huggingface.co/chopratejas/kompress-base) + +--- + +## Proof + +**Savings on real agent workloads:** + +| Workload | Before | After | Savings | +|-------------------------------|-------:|-------:|--------:| +| Code search (100 results) | 17,765 | 1,408 | **92%** | +| SRE incident debugging | 65,694 | 5,118 | **92%** | +| GitHub issue triage | 54,174 | 14,761 | **73%** | +| Codebase exploration | 78,502 | 41,254 | **47%** | + +**Accuracy preserved on standard benchmarks:** + +| Benchmark | Category | N | Baseline | Headroom | Delta | +|------------|----------|----:|---------:|---------:|----------:| +| GSM8K | Math | 100 | 0.870 | 0.870 | **±0.000**| +| TruthfulQA | Factual | 100 | 0.530 | 0.560 | **+0.030**| +| SQuAD v2 | QA | 100 | — | **97%** | 19% compression | +| BFCL | Tools | 100 | — | **97%** | 32% compression | + +Reproduce: + +```bash +python -m headroom.evals suite --tier 1 +``` + +**Community, live:** + +
+ + 60B+ tokens saved — community leaderboard + +

60B+ tokens saved by the community in the last 20 days — live leaderboard →

+
+ +→ [Full benchmarks & methodology](https://headroom-docs.vercel.app/docs/benchmarks) + +--- + +## Built for coding agents + +| Agent | Durable init / one-shot wrap | Notes | +|--------------------|------------------------------------|------------------------------------------------------------------| +| **Claude Code** | `headroom init claude -g` / `headroom wrap claude` | `init` installs user or repo-local hooks; `wrap` is still useful for ad hoc sessions | +| **Codex** | `headroom init codex -g` / `headroom wrap codex --memory` | `init` installs provider config plus lifecycle hooks where supported | +| **Cursor** | `headroom wrap cursor` | Prints Cursor config — durable init not available yet | +| **Aider** | `headroom wrap aider` | Starts proxy, launches Aider | +| **Copilot CLI** | `headroom init copilot -g` / `headroom wrap copilot` | `init` installs hooks and BYOK provider routing for the current user | +| **OpenClaw** | `headroom init openclaw -g` / `headroom wrap openclaw` | Installs Headroom as ContextEngine plugin | + +MCP-native too — `headroom mcp install` exposes `headroom_compress`, `headroom_retrieve`, and `headroom_stats` to any MCP client. + +
+ headroom learn in action +
+ +--- + +## Integrations + +
+Drop Headroom into any stack + +| Your setup | Hook in with | +|-------------------------|------------------------------------------------------------------| +| Any Python app | `compress(messages, model=…)` | +| Any TypeScript app | `await compress(messages, { model })` | +| Anthropic / OpenAI SDK | `withHeadroom(new Anthropic())` · `withHeadroom(new OpenAI())` | +| Vercel AI SDK | `wrapLanguageModel({ model, middleware: headroomMiddleware() })` | +| LiteLLM | `litellm.callbacks = [HeadroomCallback()]` | +| LangChain | `HeadroomChatModel(your_llm)` | +| Agno | `HeadroomAgnoModel(your_model)` | +| Strands | [Strands guide](https://headroom-docs.vercel.app/docs/strands) | +| ASGI apps | `app.add_middleware(CompressionMiddleware)` | +| Multi-agent | `SharedContext().put / .get` | +| MCP clients | `headroom mcp install` | + +
+ +
+What's inside + +- **SmartCrusher** — universal JSON: arrays of dicts, nested objects, mixed types. +- **CodeCompressor** — AST-aware for Python, JS, Go, Rust, Java, C++. +- **Kompress-base** — our HuggingFace model, trained on agentic traces. +- **Image compression** — 40–90% reduction via trained ML router. +- **CacheAligner** — stabilizes prefixes so Anthropic/OpenAI KV caches actually hit. +- **IntelligentContext** — score-based context fitting with learned importance. +- **CCR** — reversible compression; LLM retrieves originals on demand. +- **Cross-agent memory** — shared store, agent provenance, auto-dedup. +- **SharedContext** — compressed context passing across multi-agent workflows. +- **`headroom learn`** — plugin-based failure mining for Claude, Codex, Gemini. + +
+ +--- + +## Install + +```bash +pip install "headroom-ai[all]" # Python, everything +npm install headroom-ai # TypeScript / Node +docker pull ghcr.io/chopratejas/headroom:latest +``` + +Granular extras: `[proxy]`, `[mcp]`, `[ml]` (Kompress-base), `[agno]`, `[langchain]`, `[evals]`. Requires **Python 3.10+**. + +→ [Installation guide](https://headroom-docs.vercel.app/docs/installation) — Docker tags, persistent service, PowerShell, devcontainers. + +--- + +## Documentation + +| Start here | Go deeper | +|-------------------------------------------------------------------------|------------------------------------------------------------------------| +| [Quickstart](https://headroom-docs.vercel.app/docs/quickstart) | [Architecture](https://headroom-docs.vercel.app/docs/architecture) | +| [Proxy](https://headroom-docs.vercel.app/docs/proxy) | [How compression works](https://headroom-docs.vercel.app/docs/how-compression-works) | +| [MCP tools](https://headroom-docs.vercel.app/docs/mcp) | [CCR — reversible compression](https://headroom-docs.vercel.app/docs/ccr) | +| [Memory](https://headroom-docs.vercel.app/docs/memory) | [Cache optimization](https://headroom-docs.vercel.app/docs/cache-optimization) | +| [Failure learning](https://headroom-docs.vercel.app/docs/failure-learning) | [Benchmarks](https://headroom-docs.vercel.app/docs/benchmarks) | +| [Configuration](https://headroom-docs.vercel.app/docs/configuration) | [Limitations](https://headroom-docs.vercel.app/docs/limitations) | + +--- + +## Compared to + +Headroom runs **locally**, covers **every** content type (not just CLI or text), works with every major framework, and is **reversible**. + +| | Scope | Deploy | Local | Reversible | +|----------------------------------|-------------------------------------------------|-------------------------------------|:-----:|:----------:| +| **Headroom** | All context — tools, RAG, logs, files, history | Proxy · library · middleware · MCP | Yes | Yes | +| [RTK](https://github.com/rtk-ai/rtk) | CLI command outputs | CLI wrapper | Yes | No | +| [Compresr](https://compresr.ai), [Token Co.](https://thetokencompany.ai) | Text sent to their API | Hosted API call | No | No | +| OpenAI Compaction | Conversation history | Provider-native | No | No | + +> **Attribution.** Headroom ships with the excellent [RTK](https://github.com/rtk-ai/rtk) binary for shell-output rewriting — `git show` → `git show --short`, noisy `ls` → scoped, chatty installers → summarized. Huge thanks to the RTK team; their tool is a first-class part of our stack, and Headroom compresses everything downstream of it. + +--- + +## Contributing + +```bash +git clone https://github.com/chopratejas/headroom.git && cd headroom +pip install -e ".[dev]" && pytest +``` + +Devcontainers in `.devcontainer/` (default + `memory-stack` with Qdrant & Neo4j). See [CONTRIBUTING.md](CONTRIBUTING.md). + +--- + +## Community + +- **[Live leaderboard](https://headroomlabs.ai/dashboard)** — 60B+ tokens saved and counting. +- **[Discord](https://discord.gg/yRmaUNpsPJ)** — questions, feedback, war stories. +- **[Kompress-base on HuggingFace](https://huggingface.co/chopratejas/kompress-base)** — the model behind our text compression. + +## License + +Apache 2.0 — see [LICENSE](LICENSE). diff --git a/headroom/cli/main.py b/headroom/cli/main.py index 3d8eef75b..e56d20362 100644 --- a/headroom/cli/main.py +++ b/headroom/cli/main.py @@ -1,79 +1,79 @@ -"""Main CLI entry point for Headroom.""" - -import click - -CLI_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-?"]} - - -def get_version() -> str: - """Get the current version.""" - try: - from headroom._version import __version__ - - return __version__ - except ImportError: - return "unknown" - - -@click.group(context_settings=CLI_CONTEXT_SETTINGS) -@click.version_option(get_version(), "--version", "-v", prog_name="headroom") -@click.pass_context -def main(ctx: click.Context) -> None: - """Headroom - The Context Optimization Layer for LLM Applications. - - Manage memories, run the optimization proxy, and analyze metrics. - - \b - Examples: - headroom proxy Start the optimization proxy - headroom memory list List stored memories - headroom memory stats Show memory statistics - """ - ctx.ensure_object(dict) - - -# Import subcommands - these register themselves with the main group -def _register_commands() -> None: - """Register all subcommand groups.""" - from . import ( - evals, # noqa: F401 - init, # noqa: F401 - install, # noqa: F401 - learn, # noqa: F401 - mcp, # noqa: F401 - perf, # noqa: F401 - proxy, # noqa: F401 - tools, # noqa: F401 - wrap, # noqa: F401 - ) - - # Memory CLI requires numpy/hnswlib — optional - try: - from . import memory # noqa: F401 - except ImportError: - pass - - -_register_commands() - - -def _apply_help_aliases(command: click.Command) -> None: - """Ensure `-?` works everywhere in the Click command tree.""" - context_settings = dict(command.context_settings or {}) - help_option_names = list(context_settings.get("help_option_names", [])) - if "--help" not in help_option_names: - help_option_names.append("--help") - if "-?" not in help_option_names: - help_option_names.append("-?") - context_settings["help_option_names"] = help_option_names - command.context_settings = context_settings - - if isinstance(command, click.Group): - for child in command.commands.values(): - _apply_help_aliases(child) - - -_apply_help_aliases(main) - -if __name__ == "__main__": - main() +"""Main CLI entry point for Headroom.""" + +import click + +CLI_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-?"]} + + +def get_version() -> str: + """Get the current version.""" + try: + from headroom._version import __version__ + + return __version__ + except ImportError: + return "unknown" + + +@click.group(context_settings=CLI_CONTEXT_SETTINGS) +@click.version_option(get_version(), "--version", "-v", prog_name="headroom") +@click.pass_context +def main(ctx: click.Context) -> None: + """Headroom - The Context Optimization Layer for LLM Applications. + + Manage memories, run the optimization proxy, and analyze metrics. + + \b + Examples: + headroom proxy Start the optimization proxy + headroom memory list List stored memories + headroom memory stats Show memory statistics + """ + ctx.ensure_object(dict) + + +# Import subcommands - these register themselves with the main group +def _register_commands() -> None: + """Register all subcommand groups.""" + from . import ( + evals, # noqa: F401 + init, # noqa: F401 + install, # noqa: F401 + learn, # noqa: F401 + mcp, # noqa: F401 + perf, # noqa: F401 + proxy, # noqa: F401 + tools, # noqa: F401 + wrap, # noqa: F401 + ) + + # Memory CLI requires numpy/hnswlib — optional + try: + from . import memory # noqa: F401 + except ImportError: + pass + + +_register_commands() + + +def _apply_help_aliases(command: click.Command) -> None: + """Ensure `-?` works everywhere in the Click command tree.""" + context_settings = dict(command.context_settings or {}) + help_option_names = list(context_settings.get("help_option_names", [])) + if "--help" not in help_option_names: + help_option_names.append("--help") + if "-?" not in help_option_names: + help_option_names.append("-?") + context_settings["help_option_names"] = help_option_names + command.context_settings = context_settings + + if isinstance(command, click.Group): + for child in command.commands.values(): + _apply_help_aliases(child) + + +_apply_help_aliases(main) + +if __name__ == "__main__": + main() diff --git a/scripts/tests/test_version_sync.py b/scripts/tests/test_version_sync.py index 7a4d72ea6..ebecb8439 100644 --- a/scripts/tests/test_version_sync.py +++ b/scripts/tests/test_version_sync.py @@ -1,291 +1,291 @@ -"""Tests for version-sync.py.""" - -import json -import subprocess -import sys -from pathlib import Path - -import pytest - - -@pytest.fixture -def temp_project(tmp_path: Path) -> dict[str, Path]: - """Create a temporary project with all versioned files.""" - # Create directory structure - root = tmp_path / "project" - headroom = root / "headroom" - headroom.mkdir(parents=True) - repo_claude_plugin = root / ".claude-plugin" - repo_claude_plugin.mkdir(parents=True) - repo_github_plugin = root / ".github" / "plugin" - repo_github_plugin.mkdir(parents=True) - plugins = root / "plugins" - openclaw = plugins / "openclaw" - openclaw.mkdir(parents=True) - agent_hooks_claude = plugins / "headroom-agent-hooks" / ".claude-plugin" - agent_hooks_claude.mkdir(parents=True) - agent_hooks_github = plugins / "headroom-agent-hooks" / ".github" / "plugin" - agent_hooks_github.mkdir(parents=True) - sdk = root / "sdk" - typescript = sdk / "typescript" - typescript.mkdir(parents=True) - - # pyproject.toml - pyproject = root / "pyproject.toml" - pyproject.write_text('[project]\nversion = "0.5.25"\n') - - # headroom/_version.py - version_py = headroom / "_version.py" - version_py.write_text('"""Package version metadata."""\n\n__version__ = "0.5.25"\n') - - # plugins/openclaw/package.json - openclaw_pkg = openclaw / "package.json" - openclaw_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"})) - - repo_claude_marketplace = repo_claude_plugin / "marketplace.json" - repo_claude_marketplace.write_text( - json.dumps( - { - "metadata": {"name": "claude-marketplace", "version": "0.1.0"}, - "plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}], - } - ) - ) - - repo_github_marketplace = repo_github_plugin / "marketplace.json" - repo_github_marketplace.write_text( - json.dumps( - { - "metadata": {"name": "copilot-marketplace", "version": "0.1.0"}, - "plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}], - } - ) - ) - - claude_plugin = agent_hooks_claude / "plugin.json" - claude_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"})) - - github_plugin = agent_hooks_github / "plugin.json" - github_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"})) - - # sdk/typescript/package.json - typescript_pkg = typescript / "package.json" - typescript_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"})) - - return { - "root": root, - "pyproject": pyproject, - "version_py": version_py, - "openclaw_pkg": openclaw_pkg, - "repo_claude_marketplace": repo_claude_marketplace, - "repo_github_marketplace": repo_github_marketplace, - "claude_plugin": claude_plugin, - "github_plugin": github_plugin, - "typescript_pkg": typescript_pkg, - } - - -def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None: - """Test --version flag updates all files.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [sys.executable, str(script), "--root", str(root), "--version", "0.7.0"], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - - # Verify pyproject.toml - pyproject_content = temp_project["pyproject"].read_text() - assert 'version = "0.7.0"' in pyproject_content - - # Verify headroom/_version.py - version_py_content = temp_project["version_py"].read_text() - assert '__version__ = "0.7.0"' in version_py_content - - # Verify plugins/openclaw/package.json - openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) - assert openclaw_pkg["version"] == "0.7.0" - - # Verify sdk/typescript/package.json - typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) - assert typescript_pkg["version"] == "0.7.0" - - repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text()) - assert repo_claude_marketplace["metadata"]["version"] == "0.7.0" - assert repo_claude_marketplace["plugins"][0]["version"] == "0.7.0" - - repo_github_marketplace = json.loads(temp_project["repo_github_marketplace"].read_text()) - assert repo_github_marketplace["metadata"]["version"] == "0.7.0" - assert repo_github_marketplace["plugins"][0]["version"] == "0.7.0" - - claude_plugin = json.loads(temp_project["claude_plugin"].read_text()) - assert claude_plugin["version"] == "0.7.0" - - github_plugin = json.loads(temp_project["github_plugin"].read_text()) - assert github_plugin["version"] == "0.7.0" - - # Verify .releaseetadata was created - release_metadata = root / ".releaseetadata" - assert release_metadata.exists() - metadata = json.loads(release_metadata.read_text()) - assert metadata["version"] == "0.7.0" - assert metadata["packages"]["pypi"] == "0.7.0" - assert metadata["packages"]["npm-sdk"] == "0.7.0" - assert metadata["packages"]["npm-openclaw"] == "0.7.0" - assert metadata["packages"]["agent-hooks-plugin"] == "0.7.0" - - -def test_bump_patch(temp_project: dict[str, Path]) -> None: - """Test --bump patch bumps 0.5.25 to 0.5.26.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [sys.executable, str(script), "--root", str(root), "--bump", "patch"], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - - # Verify all files updated to 0.5.26 - pyproject_content = temp_project["pyproject"].read_text() - assert 'version = "0.5.26"' in pyproject_content - - version_py_content = temp_project["version_py"].read_text() - assert '__version__ = "0.5.26"' in version_py_content - - openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) - assert openclaw_pkg["version"] == "0.5.26" - - typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) - assert typescript_pkg["version"] == "0.5.26" - - claude_plugin = json.loads(temp_project["claude_plugin"].read_text()) - assert claude_plugin["version"] == "0.5.26" - - -def test_bump_minor(temp_project: dict[str, Path]) -> None: - """Test --bump minor bumps 0.5.25 to 0.6.0.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [sys.executable, str(script), "--root", str(root), "--bump", "minor"], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - - # Verify all files updated to 0.6.0 - pyproject_content = temp_project["pyproject"].read_text() - assert 'version = "0.6.0"' in pyproject_content - - version_py_content = temp_project["version_py"].read_text() - assert '__version__ = "0.6.0"' in version_py_content - - openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) - assert openclaw_pkg["version"] == "0.6.0" - - typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) - assert typescript_pkg["version"] == "0.6.0" - - github_plugin = json.loads(temp_project["github_plugin"].read_text()) - assert github_plugin["version"] == "0.6.0" - - -def test_bump_major(temp_project: dict[str, Path]) -> None: - """Test --bump major bumps 0.5.25 to 1.0.0.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [sys.executable, str(script), "--root", str(root), "--bump", "major"], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - - # Verify all files updated to 1.0.0 - pyproject_content = temp_project["pyproject"].read_text() - assert 'version = "1.0.0"' in pyproject_content - - version_py_content = temp_project["version_py"].read_text() - assert '__version__ = "1.0.0"' in version_py_content - - openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) - assert openclaw_pkg["version"] == "1.0.0" - - typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) - assert typescript_pkg["version"] == "1.0.0" - - repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text()) - assert repo_claude_marketplace["metadata"]["version"] == "1.0.0" - - -def test_release_metadata_written(temp_project: dict[str, Path]) -> None: - """Test .releaseetadata is written correctly.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [sys.executable, str(script), "--root", str(root), "--version", "0.6.0"], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - - release_metadata = root / ".releaseetadata" - assert release_metadata.exists() - - metadata = json.loads(release_metadata.read_text()) - assert metadata == { - "version": "0.6.0", - "packages": { - "pypi": "0.6.0", - "npm-sdk": "0.6.0", - "npm-openclaw": "0.6.0", - "agent-hooks-plugin": "0.6.0", - }, - } - - -def test_plugin_manifests_only_leaves_package_versions_unchanged( - temp_project: dict[str, Path], -) -> None: - """Test plugin-only sync leaves canonical package versions alone.""" - root = temp_project["root"] - script = Path(__file__).parent.parent / "version-sync.py" - - result = subprocess.run( - [ - sys.executable, - str(script), - "--root", - str(root), - "--version", - "0.8.0", - "--plugin-manifests-only", - ], - capture_output=True, - text=True, - ) - - assert result.returncode == 0, f"Script failed: {result.stderr}" - assert 'version = "0.5.25"' in temp_project["pyproject"].read_text() - assert '__version__ = "0.5.25"' in temp_project["version_py"].read_text() - assert json.loads(temp_project["openclaw_pkg"].read_text())["version"] == "0.5.25" - assert json.loads(temp_project["typescript_pkg"].read_text())["version"] == "0.5.25" - assert json.loads(temp_project["claude_plugin"].read_text())["version"] == "0.8.0" - assert ( - json.loads(temp_project["repo_github_marketplace"].read_text())["metadata"]["version"] - == "0.8.0" - ) - assert not (root / ".releaseetadata").exists() +"""Tests for version-sync.py.""" + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + + +@pytest.fixture +def temp_project(tmp_path: Path) -> dict[str, Path]: + """Create a temporary project with all versioned files.""" + # Create directory structure + root = tmp_path / "project" + headroom = root / "headroom" + headroom.mkdir(parents=True) + repo_claude_plugin = root / ".claude-plugin" + repo_claude_plugin.mkdir(parents=True) + repo_github_plugin = root / ".github" / "plugin" + repo_github_plugin.mkdir(parents=True) + plugins = root / "plugins" + openclaw = plugins / "openclaw" + openclaw.mkdir(parents=True) + agent_hooks_claude = plugins / "headroom-agent-hooks" / ".claude-plugin" + agent_hooks_claude.mkdir(parents=True) + agent_hooks_github = plugins / "headroom-agent-hooks" / ".github" / "plugin" + agent_hooks_github.mkdir(parents=True) + sdk = root / "sdk" + typescript = sdk / "typescript" + typescript.mkdir(parents=True) + + # pyproject.toml + pyproject = root / "pyproject.toml" + pyproject.write_text('[project]\nversion = "0.5.25"\n') + + # headroom/_version.py + version_py = headroom / "_version.py" + version_py.write_text('"""Package version metadata."""\n\n__version__ = "0.5.25"\n') + + # plugins/openclaw/package.json + openclaw_pkg = openclaw / "package.json" + openclaw_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"})) + + repo_claude_marketplace = repo_claude_plugin / "marketplace.json" + repo_claude_marketplace.write_text( + json.dumps( + { + "metadata": {"name": "claude-marketplace", "version": "0.1.0"}, + "plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}], + } + ) + ) + + repo_github_marketplace = repo_github_plugin / "marketplace.json" + repo_github_marketplace.write_text( + json.dumps( + { + "metadata": {"name": "copilot-marketplace", "version": "0.1.0"}, + "plugins": [{"name": "headroom-agent-hooks", "version": "0.1.0"}], + } + ) + ) + + claude_plugin = agent_hooks_claude / "plugin.json" + claude_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"})) + + github_plugin = agent_hooks_github / "plugin.json" + github_plugin.write_text(json.dumps({"name": "headroom-agent-hooks", "version": "0.1.0"})) + + # sdk/typescript/package.json + typescript_pkg = typescript / "package.json" + typescript_pkg.write_text(json.dumps({"name": "test", "version": "0.5.25"})) + + return { + "root": root, + "pyproject": pyproject, + "version_py": version_py, + "openclaw_pkg": openclaw_pkg, + "repo_claude_marketplace": repo_claude_marketplace, + "repo_github_marketplace": repo_github_marketplace, + "claude_plugin": claude_plugin, + "github_plugin": github_plugin, + "typescript_pkg": typescript_pkg, + } + + +def test_version_sync_explicit_version(temp_project: dict[str, Path]) -> None: + """Test --version flag updates all files.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [sys.executable, str(script), "--root", str(root), "--version", "0.7.0"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + + # Verify pyproject.toml + pyproject_content = temp_project["pyproject"].read_text() + assert 'version = "0.7.0"' in pyproject_content + + # Verify headroom/_version.py + version_py_content = temp_project["version_py"].read_text() + assert '__version__ = "0.7.0"' in version_py_content + + # Verify plugins/openclaw/package.json + openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) + assert openclaw_pkg["version"] == "0.7.0" + + # Verify sdk/typescript/package.json + typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) + assert typescript_pkg["version"] == "0.7.0" + + repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text()) + assert repo_claude_marketplace["metadata"]["version"] == "0.7.0" + assert repo_claude_marketplace["plugins"][0]["version"] == "0.7.0" + + repo_github_marketplace = json.loads(temp_project["repo_github_marketplace"].read_text()) + assert repo_github_marketplace["metadata"]["version"] == "0.7.0" + assert repo_github_marketplace["plugins"][0]["version"] == "0.7.0" + + claude_plugin = json.loads(temp_project["claude_plugin"].read_text()) + assert claude_plugin["version"] == "0.7.0" + + github_plugin = json.loads(temp_project["github_plugin"].read_text()) + assert github_plugin["version"] == "0.7.0" + + # Verify .releaseetadata was created + release_metadata = root / ".releaseetadata" + assert release_metadata.exists() + metadata = json.loads(release_metadata.read_text()) + assert metadata["version"] == "0.7.0" + assert metadata["packages"]["pypi"] == "0.7.0" + assert metadata["packages"]["npm-sdk"] == "0.7.0" + assert metadata["packages"]["npm-openclaw"] == "0.7.0" + assert metadata["packages"]["agent-hooks-plugin"] == "0.7.0" + + +def test_bump_patch(temp_project: dict[str, Path]) -> None: + """Test --bump patch bumps 0.5.25 to 0.5.26.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [sys.executable, str(script), "--root", str(root), "--bump", "patch"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + + # Verify all files updated to 0.5.26 + pyproject_content = temp_project["pyproject"].read_text() + assert 'version = "0.5.26"' in pyproject_content + + version_py_content = temp_project["version_py"].read_text() + assert '__version__ = "0.5.26"' in version_py_content + + openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) + assert openclaw_pkg["version"] == "0.5.26" + + typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) + assert typescript_pkg["version"] == "0.5.26" + + claude_plugin = json.loads(temp_project["claude_plugin"].read_text()) + assert claude_plugin["version"] == "0.5.26" + + +def test_bump_minor(temp_project: dict[str, Path]) -> None: + """Test --bump minor bumps 0.5.25 to 0.6.0.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [sys.executable, str(script), "--root", str(root), "--bump", "minor"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + + # Verify all files updated to 0.6.0 + pyproject_content = temp_project["pyproject"].read_text() + assert 'version = "0.6.0"' in pyproject_content + + version_py_content = temp_project["version_py"].read_text() + assert '__version__ = "0.6.0"' in version_py_content + + openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) + assert openclaw_pkg["version"] == "0.6.0" + + typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) + assert typescript_pkg["version"] == "0.6.0" + + github_plugin = json.loads(temp_project["github_plugin"].read_text()) + assert github_plugin["version"] == "0.6.0" + + +def test_bump_major(temp_project: dict[str, Path]) -> None: + """Test --bump major bumps 0.5.25 to 1.0.0.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [sys.executable, str(script), "--root", str(root), "--bump", "major"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + + # Verify all files updated to 1.0.0 + pyproject_content = temp_project["pyproject"].read_text() + assert 'version = "1.0.0"' in pyproject_content + + version_py_content = temp_project["version_py"].read_text() + assert '__version__ = "1.0.0"' in version_py_content + + openclaw_pkg = json.loads(temp_project["openclaw_pkg"].read_text()) + assert openclaw_pkg["version"] == "1.0.0" + + typescript_pkg = json.loads(temp_project["typescript_pkg"].read_text()) + assert typescript_pkg["version"] == "1.0.0" + + repo_claude_marketplace = json.loads(temp_project["repo_claude_marketplace"].read_text()) + assert repo_claude_marketplace["metadata"]["version"] == "1.0.0" + + +def test_release_metadata_written(temp_project: dict[str, Path]) -> None: + """Test .releaseetadata is written correctly.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [sys.executable, str(script), "--root", str(root), "--version", "0.6.0"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + + release_metadata = root / ".releaseetadata" + assert release_metadata.exists() + + metadata = json.loads(release_metadata.read_text()) + assert metadata == { + "version": "0.6.0", + "packages": { + "pypi": "0.6.0", + "npm-sdk": "0.6.0", + "npm-openclaw": "0.6.0", + "agent-hooks-plugin": "0.6.0", + }, + } + + +def test_plugin_manifests_only_leaves_package_versions_unchanged( + temp_project: dict[str, Path], +) -> None: + """Test plugin-only sync leaves canonical package versions alone.""" + root = temp_project["root"] + script = Path(__file__).parent.parent / "version-sync.py" + + result = subprocess.run( + [ + sys.executable, + str(script), + "--root", + str(root), + "--version", + "0.8.0", + "--plugin-manifests-only", + ], + capture_output=True, + text=True, + ) + + assert result.returncode == 0, f"Script failed: {result.stderr}" + assert 'version = "0.5.25"' in temp_project["pyproject"].read_text() + assert '__version__ = "0.5.25"' in temp_project["version_py"].read_text() + assert json.loads(temp_project["openclaw_pkg"].read_text())["version"] == "0.5.25" + assert json.loads(temp_project["typescript_pkg"].read_text())["version"] == "0.5.25" + assert json.loads(temp_project["claude_plugin"].read_text())["version"] == "0.8.0" + assert ( + json.loads(temp_project["repo_github_marketplace"].read_text())["metadata"]["version"] + == "0.8.0" + ) + assert not (root / ".releaseetadata").exists() diff --git a/scripts/version-sync.py b/scripts/version-sync.py index 71f30f341..15aa9dcf9 100644 --- a/scripts/version-sync.py +++ b/scripts/version-sync.py @@ -1,190 +1,190 @@ -#!/usr/bin/env python3 -"""Synchronize version across all headroom packages.""" - -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path - -import tomllib - - -def get_version_from_pyproject(root: Path) -> str: - """Read version from pyproject.toml.""" - pyproject_path = root / "pyproject.toml" - with open(pyproject_path, "rb") as f: - data = tomllib.load(f) - return data["project"]["version"] - - -def bump_version(version: str, bump_type: str) -> str: - """Bump version according to bump_type (major, minor, patch).""" - major, minor, patch = map(int, version.split(".")) - if bump_type == "major": - major += 1 - minor = 0 - patch = 0 - elif bump_type == "minor": - minor += 1 - patch = 0 - elif bump_type == "patch": - patch += 1 - return f"{major}.{minor}.{patch}" - - -def update_version_py(root: Path, version: str) -> None: - """Update headroom/_version.py with new version.""" - version_py_path = root / "headroom" / "_version.py" - content = version_py_path.read_text(encoding="utf-8") - updated = re.sub( - r'__version__ = "[^"]+"', - f'__version__ = "{version}"', - content, - ) - version_py_path.write_text(updated, encoding="utf-8") - - -def update_package_json(file_path: Path, version: str) -> None: - """Update a package.json version field.""" - with open(file_path, encoding="utf-8") as f: - data = json.load(f) - data["version"] = version - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - f.write("\n") - - -def update_plugin_manifest(file_path: Path, version: str) -> None: - """Update a plugin.json version field.""" - with open(file_path, encoding="utf-8") as f: - data = json.load(f) - data["version"] = version - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - f.write("\n") - - -def update_marketplace_manifest(file_path: Path, version: str) -> None: - """Update marketplace metadata and plugin entry versions.""" - with open(file_path, encoding="utf-8") as f: - data = json.load(f) - metadata = data.get("metadata") - if isinstance(metadata, dict): - metadata["version"] = version - plugins = data.get("plugins") - if isinstance(plugins, list): - for plugin in plugins: - if isinstance(plugin, dict): - plugin["version"] = version - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - f.write("\n") - - -def update_plugin_versions(root: Path, version: str) -> None: - """Update marketplace and plugin manifest versions.""" - update_marketplace_manifest(root / ".claude-plugin" / "marketplace.json", version) - update_marketplace_manifest(root / ".github" / "plugin" / "marketplace.json", version) - update_plugin_manifest( - root / "plugins" / "headroom-agent-hooks" / ".claude-plugin" / "plugin.json", version - ) - update_plugin_manifest( - root / "plugins" / "headroom-agent-hooks" / ".github" / "plugin" / "plugin.json", - version, - ) - - -def update_openclaw_package_json(file_path: Path, version: str, sdk_version: str) -> None: - """Update openclaw package.json version and headroom-ai dependency range.""" - with open(file_path, encoding="utf-8") as f: - data = json.load(f) - data["version"] = version - if "dependencies" in data and "headroom-ai" in data["dependencies"]: - data["dependencies"]["headroom-ai"] = f"^{sdk_version}" - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - f.write("\n") - - -def update_pyproject_version(root: Path, version: str) -> None: - """Update pyproject.toml version.""" - pyproject_path = root / "pyproject.toml" - content = pyproject_path.read_text(encoding="utf-8") - updated = re.sub( - r'^version = "[^"]+"', - f'version = "{version}"', - content, - flags=re.MULTILINE, - ) - pyproject_path.write_text(updated, encoding="utf-8") - - -def write_release_metadata(root: Path, version: str) -> None: - """Write .releaseetadata JSON file.""" - metadata = { - "version": version, - "packages": { - "pypi": version, - "npm-sdk": version, - "npm-openclaw": version, - "agent-hooks-plugin": version, - }, - } - metadata_path = root / ".releaseetadata" - with open(metadata_path, "w", encoding="utf-8") as f: - json.dump(metadata, f, indent=2) - f.write("\n") - - -def main() -> None: - parser = argparse.ArgumentParser(description="Synchronize version across headroom packages") - parser.add_argument( - "--root", - type=Path, - default=Path(__file__).parent.parent, - help="Root directory of the project", - ) - group = parser.add_mutually_exclusive_group() - group.add_argument("--version", help="Explicit version to set (e.g., 0.6.0)") - group.add_argument( - "--bump", - choices=["major", "minor", "patch"], - help="Bump version from pyproject.toml", - ) - parser.add_argument( - "--plugin-manifests-only", - action="store_true", - help="Only update marketplace/plugin manifest versions", - ) - args = parser.parse_args() - - if args.version: - version = args.version - elif args.bump: - base_version = get_version_from_pyproject(args.root) - version = bump_version(base_version, args.bump) - else: - version = get_version_from_pyproject(args.root) - - if args.plugin_manifests_only: - update_plugin_versions(args.root, version) - print(f"Plugin versions synchronized to {version}") - return - - # Update all versioned files - update_pyproject_version(args.root, version) - update_version_py(args.root, version) - update_openclaw_package_json( - args.root / "plugins" / "openclaw" / "package.json", version, version - ) - update_package_json(args.root / "sdk" / "typescript" / "package.json", version) - update_plugin_versions(args.root, version) - write_release_metadata(args.root, version) - - print(f"Version synchronized to {version}") - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +"""Synchronize version across all headroom packages.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +import tomllib + + +def get_version_from_pyproject(root: Path) -> str: + """Read version from pyproject.toml.""" + pyproject_path = root / "pyproject.toml" + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + return data["project"]["version"] + + +def bump_version(version: str, bump_type: str) -> str: + """Bump version according to bump_type (major, minor, patch).""" + major, minor, patch = map(int, version.split(".")) + if bump_type == "major": + major += 1 + minor = 0 + patch = 0 + elif bump_type == "minor": + minor += 1 + patch = 0 + elif bump_type == "patch": + patch += 1 + return f"{major}.{minor}.{patch}" + + +def update_version_py(root: Path, version: str) -> None: + """Update headroom/_version.py with new version.""" + version_py_path = root / "headroom" / "_version.py" + content = version_py_path.read_text(encoding="utf-8") + updated = re.sub( + r'__version__ = "[^"]+"', + f'__version__ = "{version}"', + content, + ) + version_py_path.write_text(updated, encoding="utf-8") + + +def update_package_json(file_path: Path, version: str) -> None: + """Update a package.json version field.""" + with open(file_path, encoding="utf-8") as f: + data = json.load(f) + data["version"] = version + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def update_plugin_manifest(file_path: Path, version: str) -> None: + """Update a plugin.json version field.""" + with open(file_path, encoding="utf-8") as f: + data = json.load(f) + data["version"] = version + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def update_marketplace_manifest(file_path: Path, version: str) -> None: + """Update marketplace metadata and plugin entry versions.""" + with open(file_path, encoding="utf-8") as f: + data = json.load(f) + metadata = data.get("metadata") + if isinstance(metadata, dict): + metadata["version"] = version + plugins = data.get("plugins") + if isinstance(plugins, list): + for plugin in plugins: + if isinstance(plugin, dict): + plugin["version"] = version + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def update_plugin_versions(root: Path, version: str) -> None: + """Update marketplace and plugin manifest versions.""" + update_marketplace_manifest(root / ".claude-plugin" / "marketplace.json", version) + update_marketplace_manifest(root / ".github" / "plugin" / "marketplace.json", version) + update_plugin_manifest( + root / "plugins" / "headroom-agent-hooks" / ".claude-plugin" / "plugin.json", version + ) + update_plugin_manifest( + root / "plugins" / "headroom-agent-hooks" / ".github" / "plugin" / "plugin.json", + version, + ) + + +def update_openclaw_package_json(file_path: Path, version: str, sdk_version: str) -> None: + """Update openclaw package.json version and headroom-ai dependency range.""" + with open(file_path, encoding="utf-8") as f: + data = json.load(f) + data["version"] = version + if "dependencies" in data and "headroom-ai" in data["dependencies"]: + data["dependencies"]["headroom-ai"] = f"^{sdk_version}" + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + + +def update_pyproject_version(root: Path, version: str) -> None: + """Update pyproject.toml version.""" + pyproject_path = root / "pyproject.toml" + content = pyproject_path.read_text(encoding="utf-8") + updated = re.sub( + r'^version = "[^"]+"', + f'version = "{version}"', + content, + flags=re.MULTILINE, + ) + pyproject_path.write_text(updated, encoding="utf-8") + + +def write_release_metadata(root: Path, version: str) -> None: + """Write .releaseetadata JSON file.""" + metadata = { + "version": version, + "packages": { + "pypi": version, + "npm-sdk": version, + "npm-openclaw": version, + "agent-hooks-plugin": version, + }, + } + metadata_path = root / ".releaseetadata" + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(metadata, f, indent=2) + f.write("\n") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Synchronize version across headroom packages") + parser.add_argument( + "--root", + type=Path, + default=Path(__file__).parent.parent, + help="Root directory of the project", + ) + group = parser.add_mutually_exclusive_group() + group.add_argument("--version", help="Explicit version to set (e.g., 0.6.0)") + group.add_argument( + "--bump", + choices=["major", "minor", "patch"], + help="Bump version from pyproject.toml", + ) + parser.add_argument( + "--plugin-manifests-only", + action="store_true", + help="Only update marketplace/plugin manifest versions", + ) + args = parser.parse_args() + + if args.version: + version = args.version + elif args.bump: + base_version = get_version_from_pyproject(args.root) + version = bump_version(base_version, args.bump) + else: + version = get_version_from_pyproject(args.root) + + if args.plugin_manifests_only: + update_plugin_versions(args.root, version) + print(f"Plugin versions synchronized to {version}") + return + + # Update all versioned files + update_pyproject_version(args.root, version) + update_version_py(args.root, version) + update_openclaw_package_json( + args.root / "plugins" / "openclaw" / "package.json", version, version + ) + update_package_json(args.root / "sdk" / "typescript" / "package.json", version) + update_plugin_versions(args.root, version) + write_release_metadata(args.root, version) + + print(f"Version synchronized to {version}") + + +if __name__ == "__main__": + main()