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.**
-
-[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
-[](https://app.codecov.io/gh/chopratejas/headroom)
-[](https://pypi.org/project/headroom-ai/)
-[](https://www.npmjs.com/package/headroom-ai)
-[](https://huggingface.co/chopratejas/kompress-base)
-[](https://headroomlabs.ai/dashboard)
-[](LICENSE)
-[](https://headroom-docs.vercel.app/docs)
-
-

-
-
-
----
-
-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:**
-
-
-
-→ [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.
-
-
-

-
-
----
-
-## 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.**
+
+[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
+[](https://app.codecov.io/gh/chopratejas/headroom)
+[](https://pypi.org/project/headroom-ai/)
+[](https://www.npmjs.com/package/headroom-ai)
+[](https://huggingface.co/chopratejas/kompress-base)
+[](https://headroomlabs.ai/dashboard)
+[](LICENSE)
+[](https://headroom-docs.vercel.app/docs)
+
+

+
+
+
+---
+
+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:**
+
+
+
+→ [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.
+
+
+

+
+
+---
+
+## 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()