mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
feat: add durable init command for agent hooks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
0d6ba9972a
commit
3a999d1562
11 changed files with 1409 additions and 344 deletions
30
.claude-plugin/marketplace.json
Normal file
30
.claude-plugin/marketplace.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "headroom-marketplace",
|
||||
"owner": {
|
||||
"name": "Headroom Contributors"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
"claude-code",
|
||||
"copilot-cli"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
30
.github/plugin/marketplace.json
vendored
Normal file
30
.github/plugin/marketplace.json
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "headroom-marketplace",
|
||||
"owner": {
|
||||
"name": "Headroom Contributors"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Headroom marketplace for Claude Code and GitHub Copilot CLI plugins.",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "headroom",
|
||||
"source": "./plugins/headroom-agent-hooks",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
"claude-code",
|
||||
"copilot-cli"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
540
README.md
540
README.md
|
|
@ -1,266 +1,274 @@
|
|||
<div align="center">
|
||||
|
||||
# Headroom
|
||||
|
||||
**Compress everything your AI agent reads. Same answers, fraction of the tokens.**
|
||||
|
||||
[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
|
||||
[](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)
|
||||
|
||||
<img src="HeadroomDemo-Fast.gif" alt="Headroom in action" width="820">
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**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:**
|
||||
|
||||
<div align="center">
|
||||
<a href="https://headroomlabs.ai/dashboard">
|
||||
<img src="headroom-savings.png" alt="60B+ tokens saved — community leaderboard" width="820">
|
||||
</a>
|
||||
<p><b><a href="https://headroomlabs.ai/dashboard">60B+ tokens saved by the community in the last 20 days — live leaderboard →</a></b></p>
|
||||
</div>
|
||||
|
||||
→ [Full benchmarks & methodology](https://headroom-docs.vercel.app/docs/benchmarks)
|
||||
|
||||
---
|
||||
|
||||
## Built for coding agents
|
||||
|
||||
| Agent | One-command wrap | Notes |
|
||||
|--------------------|------------------------------------|------------------------------------------------------------------|
|
||||
| **Claude Code** | `headroom wrap claude` | `--memory` for cross-agent memory, `--code-graph` for codebase intel |
|
||||
| **Codex** | `headroom wrap codex --memory` | Shares the same memory store as Claude |
|
||||
| **Cursor** | `headroom wrap cursor` | Prints Cursor config — paste once, done |
|
||||
| **Aider** | `headroom wrap aider` | Starts proxy, launches Aider |
|
||||
| **Copilot CLI** | `headroom wrap copilot` | Starts proxy, launches Copilot |
|
||||
| **OpenClaw** | `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.
|
||||
|
||||
<div align="center">
|
||||
<img src="headroom_learn.gif" alt="headroom learn in action" width="720">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Integrations
|
||||
|
||||
<details>
|
||||
<summary><b>Drop Headroom into any stack</b></summary>
|
||||
|
||||
| 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` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>What's inside</b></summary>
|
||||
|
||||
- **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.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
<div align="center">
|
||||
|
||||
# Headroom
|
||||
|
||||
**Compress everything your AI agent reads. Same answers, fraction of the tokens.**
|
||||
|
||||
[](https://github.com/chopratejas/headroom/actions/workflows/ci.yml)
|
||||
[](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)
|
||||
|
||||
<img src="HeadroomDemo-Fast.gif" alt="Headroom in action" width="820">
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
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:**
|
||||
|
||||
<div align="center">
|
||||
<a href="https://headroomlabs.ai/dashboard">
|
||||
<img src="headroom-savings.png" alt="60B+ tokens saved — community leaderboard" width="820">
|
||||
</a>
|
||||
<p><b><a href="https://headroomlabs.ai/dashboard">60B+ tokens saved by the community in the last 20 days — live leaderboard →</a></b></p>
|
||||
</div>
|
||||
|
||||
→ [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.
|
||||
|
||||
<div align="center">
|
||||
<img src="headroom_learn.gif" alt="headroom learn in action" width="720">
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Integrations
|
||||
|
||||
<details>
|
||||
<summary><b>Drop Headroom into any stack</b></summary>
|
||||
|
||||
| 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` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>What's inside</b></summary>
|
||||
|
||||
- **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.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
|
|
|||
679
headroom/cli/init.py
Normal file
679
headroom/cli/init.py
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
"""Durable agent initialization commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
from hashlib import sha1
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
from headroom.install.models import ConfigScope, InstallPreset, RuntimeKind, SupervisorKind
|
||||
from headroom.install.paths import claude_settings_path, codex_config_path, validate_profile_name
|
||||
from headroom.install.planner import build_manifest
|
||||
from headroom.install.providers import _apply_unix_env_scope, _apply_windows_env_scope
|
||||
from headroom.install.runtime import (
|
||||
resolve_headroom_command,
|
||||
start_detached_agent,
|
||||
start_persistent_docker,
|
||||
stop_runtime,
|
||||
wait_ready,
|
||||
)
|
||||
from headroom.install.state import load_manifest, save_manifest
|
||||
from headroom.install.supervisors import start_supervisor
|
||||
|
||||
from .main import main
|
||||
|
||||
_GLOBAL_PROFILE = "init-user"
|
||||
_CLAUDE_HOOK_MARKER = "headroom-init-claude"
|
||||
_COPILOT_HOOK_MARKER = "headroom-init-copilot"
|
||||
_CODEX_HOOK_MARKER = "headroom-init-codex"
|
||||
_CODEX_PROVIDER_MARKER_START = "# --- Headroom init provider ---"
|
||||
_CODEX_PROVIDER_MARKER_END = "# --- end Headroom init provider ---"
|
||||
_CODEX_FEATURE_MARKER_START = "# --- Headroom init features ---"
|
||||
_CODEX_FEATURE_MARKER_END = "# --- end Headroom init features ---"
|
||||
_SUPPORTED_TARGETS = ("claude", "copilot", "codex", "openclaw")
|
||||
_LOCAL_TARGETS = {"claude", "codex"}
|
||||
_GLOBAL_TARGETS = {"claude", "copilot", "codex", "openclaw"}
|
||||
|
||||
|
||||
def _command_string(parts: list[str]) -> str:
|
||||
if os.name == "nt":
|
||||
return subprocess.list2cmdline(parts)
|
||||
return shlex.join(parts)
|
||||
|
||||
|
||||
def _hook_command(*parts: str) -> str:
|
||||
return _command_string([*resolve_headroom_command(), "init", "hook", "ensure", *parts])
|
||||
|
||||
|
||||
def _powershell_matcher() -> str:
|
||||
return "Bash|PowerShell" if os.name == "nt" else "Bash"
|
||||
|
||||
|
||||
def _local_profile(cwd: Path | None = None) -> str:
|
||||
root = (cwd or Path.cwd()).resolve()
|
||||
slug = "".join(ch if ch.isalnum() or ch in "-._" else "-" for ch in root.name.lower()).strip(
|
||||
"-"
|
||||
)
|
||||
digest = sha1(str(root).encode("utf-8")).hexdigest()[:8]
|
||||
return validate_profile_name(f"init-{slug or 'repo'}-{digest}")
|
||||
|
||||
|
||||
def _runtime_profile(global_scope: bool, cwd: Path | None = None) -> str:
|
||||
return _GLOBAL_PROFILE if global_scope else _local_profile(cwd)
|
||||
|
||||
|
||||
def _copilot_config_path() -> Path:
|
||||
return Path.home() / ".copilot" / "config.json"
|
||||
|
||||
|
||||
def _codex_hooks_path(global_scope: bool) -> Path:
|
||||
return (Path.home() if global_scope else Path.cwd()) / ".codex" / "hooks.json"
|
||||
|
||||
|
||||
def _claude_scope_path(global_scope: bool) -> Path:
|
||||
if global_scope:
|
||||
return claude_settings_path()
|
||||
return Path.cwd() / ".claude" / "settings.local.json"
|
||||
|
||||
|
||||
def _codex_scope_path(global_scope: bool) -> Path:
|
||||
if global_scope:
|
||||
return codex_config_path()
|
||||
return Path.cwd() / ".codex" / "config.toml"
|
||||
|
||||
|
||||
def _json_file(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {}
|
||||
content = path.read_text(encoding="utf-8").strip()
|
||||
if not content:
|
||||
return {}
|
||||
payload = json.loads(content)
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _ensure_claude_hooks(path: Path, profile: str, port: int) -> None:
|
||||
payload = _json_file(path)
|
||||
env_map = dict(payload.get("env") or {}) if isinstance(payload.get("env"), dict) else {}
|
||||
env_map["ANTHROPIC_BASE_URL"] = f"http://127.0.0.1:{port}"
|
||||
payload["env"] = env_map
|
||||
|
||||
hooks = dict(payload.get("hooks") or {}) if isinstance(payload.get("hooks"), dict) else {}
|
||||
command = _hook_command("--profile", profile)
|
||||
for event, matcher in (
|
||||
("SessionStart", "startup|resume"),
|
||||
("PreToolUse", _powershell_matcher()),
|
||||
):
|
||||
entries = list(hooks.get(event) or []) if isinstance(hooks.get(event), list) else []
|
||||
retained: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
retained.append(entry)
|
||||
continue
|
||||
hook_items = entry.get("hooks")
|
||||
if not isinstance(hook_items, list):
|
||||
retained.append(entry)
|
||||
continue
|
||||
has_headroom = any(
|
||||
isinstance(item, dict)
|
||||
and item.get("command")
|
||||
and _CLAUDE_HOOK_MARKER in str(item.get("command"))
|
||||
for item in hook_items
|
||||
)
|
||||
if not has_headroom:
|
||||
retained.append(entry)
|
||||
retained.append(
|
||||
{
|
||||
"matcher": matcher,
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": f"{command} --marker {_CLAUDE_HOOK_MARKER}",
|
||||
"timeout": 15,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
hooks[event] = retained
|
||||
payload["hooks"] = hooks
|
||||
_write_json(path, payload)
|
||||
|
||||
|
||||
def _ensure_copilot_hooks(path: Path, profile: str) -> None:
|
||||
payload = _json_file(path)
|
||||
hooks = dict(payload.get("hooks") or {}) if isinstance(payload.get("hooks"), dict) else {}
|
||||
command = f"{_hook_command('--profile', profile)} --marker {_COPILOT_HOOK_MARKER}"
|
||||
for event in ("SessionStart", "PreToolUse"):
|
||||
entries = list(hooks.get(event) or []) if isinstance(hooks.get(event), list) else []
|
||||
retained = [
|
||||
entry
|
||||
for entry in entries
|
||||
if not (
|
||||
isinstance(entry, dict) and _COPILOT_HOOK_MARKER in str(entry.get("command", ""))
|
||||
)
|
||||
]
|
||||
retained.append({"type": "command", "command": command, "cwd": ".", "timeout": 15})
|
||||
hooks[event] = retained
|
||||
payload["hooks"] = hooks
|
||||
_write_json(path, payload)
|
||||
|
||||
|
||||
def _replace_marker_block(content: str, marker_start: str, marker_end: str, block: str) -> str:
|
||||
if marker_start in content and marker_end in content:
|
||||
start = content.index(marker_start)
|
||||
end = content.index(marker_end) + len(marker_end)
|
||||
content = content[:start].rstrip() + "\n\n" + content[end:].lstrip()
|
||||
return (content.rstrip() + "\n\n" + block.strip() + "\n").lstrip()
|
||||
|
||||
|
||||
def _ensure_codex_provider(path: Path, port: int) -> None:
|
||||
block = (
|
||||
f"{_CODEX_PROVIDER_MARKER_START}\n"
|
||||
'model_provider = "headroom"\n\n'
|
||||
"[model_providers.headroom]\n"
|
||||
'name = "Headroom init proxy"\n'
|
||||
f'base_url = "http://127.0.0.1:{port}/v1"\n'
|
||||
'env_key = "OPENAI_API_KEY"\n'
|
||||
"requires_openai_auth = true\n"
|
||||
"supports_websockets = true\n"
|
||||
f"{_CODEX_PROVIDER_MARKER_END}"
|
||||
)
|
||||
content = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
content = _replace_marker_block(
|
||||
content, _CODEX_PROVIDER_MARKER_START, _CODEX_PROVIDER_MARKER_END, block
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _ensure_codex_feature_flag(path: Path) -> None:
|
||||
content = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
if _CODEX_FEATURE_MARKER_START in content and _CODEX_FEATURE_MARKER_END in content:
|
||||
block = f"{_CODEX_FEATURE_MARKER_START}\ncodex_hooks = true\n{_CODEX_FEATURE_MARKER_END}"
|
||||
content = _replace_marker_block(
|
||||
content,
|
||||
_CODEX_FEATURE_MARKER_START,
|
||||
_CODEX_FEATURE_MARKER_END,
|
||||
block,
|
||||
)
|
||||
elif "[features]" in content:
|
||||
lines = content.splitlines()
|
||||
inserted = False
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip() != "[features]":
|
||||
continue
|
||||
section_end = index + 1
|
||||
while section_end < len(lines) and not (
|
||||
lines[section_end].startswith("[") and lines[section_end].endswith("]")
|
||||
):
|
||||
if "codex_hooks" in lines[section_end]:
|
||||
inserted = True
|
||||
break
|
||||
section_end += 1
|
||||
if not inserted:
|
||||
lines[index + 1 : index + 1] = [
|
||||
_CODEX_FEATURE_MARKER_START,
|
||||
"codex_hooks = true",
|
||||
_CODEX_FEATURE_MARKER_END,
|
||||
]
|
||||
inserted = True
|
||||
break
|
||||
content = "\n".join(lines).rstrip() + "\n"
|
||||
if not inserted:
|
||||
content = (
|
||||
content.rstrip()
|
||||
+ "\n\n[features]\n"
|
||||
+ _CODEX_FEATURE_MARKER_START
|
||||
+ "\n"
|
||||
+ "codex_hooks = true\n"
|
||||
+ _CODEX_FEATURE_MARKER_END
|
||||
+ "\n"
|
||||
)
|
||||
else:
|
||||
content = (
|
||||
content.rstrip()
|
||||
+ "\n\n[features]\n"
|
||||
+ _CODEX_FEATURE_MARKER_START
|
||||
+ "\n"
|
||||
+ "codex_hooks = true\n"
|
||||
+ _CODEX_FEATURE_MARKER_END
|
||||
+ "\n"
|
||||
).lstrip()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _ensure_codex_hooks(path: Path, profile: str) -> None:
|
||||
command = f"{_hook_command('--profile', profile)} --marker {_CODEX_HOOK_MARKER}"
|
||||
payload = {
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume",
|
||||
"hooks": [{"type": "command", "command": command, "timeout": 15}],
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [{"type": "command", "command": command, "timeout": 15}],
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _manifest_changed(
|
||||
existing: Any,
|
||||
*,
|
||||
port: int,
|
||||
backend: str,
|
||||
anyllm_provider: str | None,
|
||||
region: str | None,
|
||||
memory: bool,
|
||||
) -> bool:
|
||||
return any(
|
||||
[
|
||||
getattr(existing, "port", port) != port,
|
||||
getattr(existing, "backend", backend) != backend,
|
||||
getattr(existing, "anyllm_provider", anyllm_provider) != anyllm_provider,
|
||||
getattr(existing, "region", region) != region,
|
||||
getattr(existing, "memory_enabled", memory) != memory,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _ensure_runtime_manifest(
|
||||
*,
|
||||
global_scope: bool,
|
||||
targets: list[str],
|
||||
port: int,
|
||||
backend: str,
|
||||
anyllm_provider: str | None,
|
||||
region: str | None,
|
||||
memory: bool,
|
||||
) -> str:
|
||||
profile = _runtime_profile(global_scope)
|
||||
existing = load_manifest(profile)
|
||||
merged_targets = sorted(set(existing.targets if existing else []).union(targets))
|
||||
manifest = build_manifest(
|
||||
profile=profile,
|
||||
preset=InstallPreset.PERSISTENT_TASK.value,
|
||||
runtime_kind=RuntimeKind.PYTHON.value,
|
||||
scope=ConfigScope.USER.value,
|
||||
provider_mode="manual",
|
||||
targets=merged_targets,
|
||||
port=port,
|
||||
backend=backend,
|
||||
anyllm_provider=anyllm_provider,
|
||||
region=region,
|
||||
proxy_mode="token",
|
||||
memory_enabled=memory,
|
||||
telemetry_enabled=True,
|
||||
image="ghcr.io/chopratejas/headroom:latest",
|
||||
)
|
||||
manifest.supervisor_kind = SupervisorKind.NONE.value
|
||||
manifest.artifacts = []
|
||||
manifest.mutations = existing.mutations if existing else []
|
||||
if existing is not None and _manifest_changed(
|
||||
existing,
|
||||
port=port,
|
||||
backend=backend,
|
||||
anyllm_provider=anyllm_provider,
|
||||
region=region,
|
||||
memory=memory,
|
||||
):
|
||||
try:
|
||||
stop_runtime(existing)
|
||||
except Exception:
|
||||
pass
|
||||
save_manifest(manifest)
|
||||
return profile
|
||||
|
||||
|
||||
def _env_manifest(values: dict[str, str]) -> Any:
|
||||
return build_manifest(
|
||||
profile="init-env",
|
||||
preset=InstallPreset.PERSISTENT_TASK.value,
|
||||
runtime_kind=RuntimeKind.PYTHON.value,
|
||||
scope=ConfigScope.USER.value,
|
||||
provider_mode="manual",
|
||||
targets=["copilot"],
|
||||
port=8787,
|
||||
backend="anthropic",
|
||||
anyllm_provider=None,
|
||||
region=None,
|
||||
proxy_mode="token",
|
||||
memory_enabled=False,
|
||||
telemetry_enabled=True,
|
||||
image="ghcr.io/chopratejas/headroom:latest",
|
||||
)
|
||||
|
||||
|
||||
def _apply_user_env(values: dict[str, str]) -> None:
|
||||
manifest = _env_manifest(values)
|
||||
manifest.base_env = {}
|
||||
manifest.tool_envs = {"copilot": values}
|
||||
if os.name == "nt":
|
||||
_apply_windows_env_scope(manifest)
|
||||
else:
|
||||
_apply_unix_env_scope(manifest)
|
||||
|
||||
|
||||
def _resolve_copilot_env(port: int, backend: str) -> dict[str, str]:
|
||||
if backend == "anthropic":
|
||||
return {
|
||||
"COPILOT_PROVIDER_TYPE": "anthropic",
|
||||
"COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}",
|
||||
}
|
||||
return {
|
||||
"COPILOT_PROVIDER_TYPE": "openai",
|
||||
"COPILOT_PROVIDER_BASE_URL": f"http://127.0.0.1:{port}/v1",
|
||||
"COPILOT_PROVIDER_WIRE_API": "completions",
|
||||
}
|
||||
|
||||
|
||||
def _marketplace_source() -> str:
|
||||
override = os.environ.get("HEADROOM_MARKETPLACE_SOURCE")
|
||||
if override:
|
||||
return override
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
if (repo_root / ".claude-plugin" / "marketplace.json").exists():
|
||||
return str(repo_root)
|
||||
return "JerrettDavis/headroom"
|
||||
|
||||
|
||||
def _run_checked(command: list[str], *, action: str) -> None:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
detail = "\n".join(part for part in (result.stderr.strip(), result.stdout.strip()) if part)
|
||||
if "already" in detail.lower() or "exists" in detail.lower():
|
||||
return
|
||||
raise click.ClickException(f"{action} failed: {detail or result.returncode}")
|
||||
|
||||
|
||||
def _install_claude_marketplace(scope: str) -> None:
|
||||
claude_bin = shutil.which("claude")
|
||||
if not claude_bin:
|
||||
raise click.ClickException("'claude' not found in PATH. Install Claude Code first.")
|
||||
source = _marketplace_source()
|
||||
_run_checked(
|
||||
[claude_bin, "plugin", "marketplace", "add", source], action="claude marketplace add"
|
||||
)
|
||||
_run_checked(
|
||||
[claude_bin, "plugin", "install", "headroom@headroom-marketplace", "--scope", scope],
|
||||
action="claude plugin install",
|
||||
)
|
||||
|
||||
|
||||
def _install_copilot_marketplace() -> None:
|
||||
copilot_bin = shutil.which("copilot")
|
||||
if not copilot_bin:
|
||||
raise click.ClickException("'copilot' not found in PATH. Install GitHub Copilot CLI first.")
|
||||
source = _marketplace_source()
|
||||
_run_checked(
|
||||
[copilot_bin, "plugin", "marketplace", "add", source],
|
||||
action="copilot marketplace add",
|
||||
)
|
||||
_run_checked(
|
||||
[copilot_bin, "plugin", "install", "headroom@headroom-marketplace"],
|
||||
action="copilot plugin install",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_profile_running(profile: str) -> None:
|
||||
manifest = load_manifest(profile)
|
||||
if manifest is None:
|
||||
return
|
||||
if wait_ready(manifest, timeout_seconds=1):
|
||||
return
|
||||
try:
|
||||
if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value:
|
||||
start_persistent_docker(manifest)
|
||||
elif manifest.supervisor_kind == SupervisorKind.SERVICE.value:
|
||||
start_supervisor(manifest)
|
||||
else:
|
||||
start_detached_agent(manifest.profile)
|
||||
wait_ready(manifest, timeout_seconds=45)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def detect_init_targets(global_scope: bool) -> list[str]:
|
||||
allowed = _GLOBAL_TARGETS if global_scope else _LOCAL_TARGETS
|
||||
detected: list[str] = []
|
||||
for target in _SUPPORTED_TARGETS:
|
||||
if target not in allowed:
|
||||
continue
|
||||
if shutil.which(target):
|
||||
detected.append(target)
|
||||
return detected
|
||||
|
||||
|
||||
def _init_claude(*, global_scope: bool, profile: str, port: int) -> None:
|
||||
_ensure_claude_hooks(_claude_scope_path(global_scope), profile, port)
|
||||
_install_claude_marketplace("user" if global_scope else "local")
|
||||
click.echo(f"Configured Claude Code ({'user' if global_scope else 'local'} scope).")
|
||||
click.echo("Restart Claude Code to activate Headroom hooks and provider routing.")
|
||||
|
||||
|
||||
def _init_copilot(*, global_scope: bool, profile: str, port: int, backend: str) -> None:
|
||||
if not global_scope:
|
||||
raise click.ClickException(
|
||||
"Copilot durable init currently requires -g (current-user scope)."
|
||||
)
|
||||
_ensure_copilot_hooks(_copilot_config_path(), profile)
|
||||
_apply_user_env(_resolve_copilot_env(port, backend))
|
||||
_install_copilot_marketplace()
|
||||
click.echo("Configured GitHub Copilot CLI (user scope).")
|
||||
click.echo("Restart Copilot CLI to activate Headroom hooks and provider routing.")
|
||||
|
||||
|
||||
def _init_codex(*, global_scope: bool, profile: str, port: int) -> None:
|
||||
config_path = _codex_scope_path(global_scope)
|
||||
_ensure_codex_provider(config_path, port)
|
||||
_ensure_codex_feature_flag(config_path)
|
||||
_ensure_codex_hooks(_codex_hooks_path(global_scope), profile)
|
||||
click.echo(f"Configured Codex ({'user' if global_scope else 'local'} scope).")
|
||||
if os.name == "nt":
|
||||
click.echo(
|
||||
"Codex hooks are currently disabled upstream on Windows; provider routing was still installed."
|
||||
)
|
||||
click.echo("Restart Codex to activate Headroom configuration.")
|
||||
|
||||
|
||||
def _init_openclaw(*, global_scope: bool, port: int) -> None:
|
||||
if not global_scope:
|
||||
raise click.ClickException(
|
||||
"OpenClaw durable init currently requires -g (current-user scope)."
|
||||
)
|
||||
command = [*resolve_headroom_command(), "wrap", "openclaw", "--proxy-port", str(port)]
|
||||
result = subprocess.run(command)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(result.returncode)
|
||||
|
||||
|
||||
def _run_init_targets(
|
||||
*,
|
||||
targets: list[str],
|
||||
global_scope: bool,
|
||||
port: int,
|
||||
backend: str,
|
||||
anyllm_provider: str | None,
|
||||
region: str | None,
|
||||
memory: bool,
|
||||
) -> None:
|
||||
runtime_targets = [target for target in targets if target != "openclaw"]
|
||||
profile = _ensure_runtime_manifest(
|
||||
global_scope=global_scope,
|
||||
targets=runtime_targets,
|
||||
port=port,
|
||||
backend=backend,
|
||||
anyllm_provider=anyllm_provider,
|
||||
region=region,
|
||||
memory=memory,
|
||||
)
|
||||
for target in targets:
|
||||
if target == "claude":
|
||||
_init_claude(global_scope=global_scope, profile=profile, port=port)
|
||||
elif target == "copilot":
|
||||
_init_copilot(global_scope=global_scope, profile=profile, port=port, backend=backend)
|
||||
elif target == "codex":
|
||||
_init_codex(global_scope=global_scope, profile=profile, port=port)
|
||||
elif target == "openclaw":
|
||||
_init_openclaw(global_scope=global_scope, port=port)
|
||||
|
||||
|
||||
@main.group(invoke_without_command=True)
|
||||
@click.option("-g", "--global", "global_scope", is_flag=True, help="Install for the current user.")
|
||||
@click.option("--port", default=8787, type=int, show_default=True, help="Headroom proxy port.")
|
||||
@click.option("--backend", default="anthropic", show_default=True, help="Proxy backend.")
|
||||
@click.option("--anyllm-provider", default=None, help="Provider for any-llm backends.")
|
||||
@click.option("--region", default=None, help="Cloud region for Bedrock / Vertex style backends.")
|
||||
@click.option("--memory", is_flag=True, help="Enable persistent memory in the proxy runtime.")
|
||||
@click.pass_context
|
||||
def init(
|
||||
ctx: click.Context,
|
||||
global_scope: bool,
|
||||
port: int,
|
||||
backend: str,
|
||||
anyllm_provider: str | None,
|
||||
region: str | None,
|
||||
memory: bool,
|
||||
) -> None:
|
||||
"""Install durable Headroom integrations for supported agents."""
|
||||
if ctx.invoked_subcommand is not None:
|
||||
ctx.obj = {
|
||||
"global_scope": global_scope,
|
||||
"port": port,
|
||||
"backend": backend,
|
||||
"anyllm_provider": anyllm_provider,
|
||||
"region": region,
|
||||
"memory": memory,
|
||||
}
|
||||
return
|
||||
|
||||
targets = detect_init_targets(global_scope)
|
||||
if not targets:
|
||||
scope_label = "user" if global_scope else "local"
|
||||
raise click.ClickException(
|
||||
f"No supported {scope_label} init targets were auto-detected. Specify one explicitly."
|
||||
)
|
||||
_run_init_targets(
|
||||
targets=targets,
|
||||
global_scope=global_scope,
|
||||
port=port,
|
||||
backend=backend,
|
||||
anyllm_provider=anyllm_provider,
|
||||
region=region,
|
||||
memory=memory,
|
||||
)
|
||||
|
||||
|
||||
def _ctx_value(ctx: click.Context, key: str) -> Any:
|
||||
return (ctx.obj or {}).get(key)
|
||||
|
||||
|
||||
@init.command("claude")
|
||||
@click.pass_context
|
||||
def init_claude(ctx: click.Context) -> None:
|
||||
"""Install Claude Code durable hooks and provider routing."""
|
||||
_run_init_targets(
|
||||
targets=["claude"],
|
||||
global_scope=bool(_ctx_value(ctx, "global_scope")),
|
||||
port=int(_ctx_value(ctx, "port") or 8787),
|
||||
backend=str(_ctx_value(ctx, "backend") or "anthropic"),
|
||||
anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
|
||||
region=_ctx_value(ctx, "region"),
|
||||
memory=bool(_ctx_value(ctx, "memory")),
|
||||
)
|
||||
|
||||
|
||||
@init.command("copilot")
|
||||
@click.pass_context
|
||||
def init_copilot(ctx: click.Context) -> None:
|
||||
"""Install GitHub Copilot CLI durable hooks and provider routing."""
|
||||
_run_init_targets(
|
||||
targets=["copilot"],
|
||||
global_scope=bool(_ctx_value(ctx, "global_scope")),
|
||||
port=int(_ctx_value(ctx, "port") or 8787),
|
||||
backend=str(_ctx_value(ctx, "backend") or "anthropic"),
|
||||
anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
|
||||
region=_ctx_value(ctx, "region"),
|
||||
memory=bool(_ctx_value(ctx, "memory")),
|
||||
)
|
||||
|
||||
|
||||
@init.command("codex")
|
||||
@click.pass_context
|
||||
def init_codex(ctx: click.Context) -> None:
|
||||
"""Install Codex durable hooks and provider routing."""
|
||||
_run_init_targets(
|
||||
targets=["codex"],
|
||||
global_scope=bool(_ctx_value(ctx, "global_scope")),
|
||||
port=int(_ctx_value(ctx, "port") or 8787),
|
||||
backend=str(_ctx_value(ctx, "backend") or "anthropic"),
|
||||
anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
|
||||
region=_ctx_value(ctx, "region"),
|
||||
memory=bool(_ctx_value(ctx, "memory")),
|
||||
)
|
||||
|
||||
|
||||
@init.command("openclaw")
|
||||
@click.pass_context
|
||||
def init_openclaw(ctx: click.Context) -> None:
|
||||
"""Install the durable OpenClaw Headroom plugin."""
|
||||
_run_init_targets(
|
||||
targets=["openclaw"],
|
||||
global_scope=bool(_ctx_value(ctx, "global_scope")),
|
||||
port=int(_ctx_value(ctx, "port") or 8787),
|
||||
backend=str(_ctx_value(ctx, "backend") or "anthropic"),
|
||||
anyllm_provider=_ctx_value(ctx, "anyllm_provider"),
|
||||
region=_ctx_value(ctx, "region"),
|
||||
memory=bool(_ctx_value(ctx, "memory")),
|
||||
)
|
||||
|
||||
|
||||
@init.group("hook", hidden=True)
|
||||
def init_hook() -> None:
|
||||
"""Internal hook helpers."""
|
||||
|
||||
|
||||
@init_hook.command("ensure")
|
||||
@click.option("--profile", default=None, help="Explicit deployment profile to ensure.")
|
||||
@click.option("--marker", default=None, hidden=True)
|
||||
def init_hook_ensure(profile: str | None, marker: str | None) -> None:
|
||||
"""Best-effort ensure used by installed agent hooks."""
|
||||
del marker
|
||||
profiles: list[str] = []
|
||||
if profile:
|
||||
profiles.append(profile)
|
||||
else:
|
||||
local_profile = _local_profile()
|
||||
if load_manifest(local_profile) is not None:
|
||||
profiles.append(local_profile)
|
||||
elif load_manifest(_GLOBAL_PROFILE) is not None:
|
||||
profiles.append(_GLOBAL_PROFILE)
|
||||
for name in profiles:
|
||||
_ensure_profile_running(name)
|
||||
|
|
@ -1,78 +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
|
||||
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()
|
||||
|
|
|
|||
17
plugins/headroom-agent-hooks/.claude-plugin/plugin.json
Normal file
17
plugins/headroom-agent-hooks/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.1.0",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
"claude-code",
|
||||
"copilot-cli"
|
||||
]
|
||||
}
|
||||
18
plugins/headroom-agent-hooks/.github/plugin/plugin.json
vendored
Normal file
18
plugins/headroom-agent-hooks/.github/plugin/plugin.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "headroom",
|
||||
"version": "0.1.0",
|
||||
"description": "Headroom startup hooks for Claude Code and GitHub Copilot CLI.",
|
||||
"author": {
|
||||
"name": "Headroom Contributors",
|
||||
"url": "https://github.com/JerrettDavis/headroom"
|
||||
},
|
||||
"homepage": "https://github.com/JerrettDavis/headroom",
|
||||
"repository": "https://github.com/JerrettDavis/headroom",
|
||||
"keywords": [
|
||||
"headroom",
|
||||
"hooks",
|
||||
"claude-code",
|
||||
"copilot-cli"
|
||||
],
|
||||
"hooks": "./hooks"
|
||||
}
|
||||
11
plugins/headroom-agent-hooks/README.md
Normal file
11
plugins/headroom-agent-hooks/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Headroom agent hooks
|
||||
|
||||
This plugin exposes lightweight startup hooks for Claude Code and GitHub Copilot CLI.
|
||||
|
||||
The hooks call:
|
||||
|
||||
```bash
|
||||
headroom init hook ensure
|
||||
```
|
||||
|
||||
That hidden helper checks for a matching durable `headroom init` deployment and starts it if needed.
|
||||
29
plugins/headroom-agent-hooks/hooks/hooks.json
Normal file
29
plugins/headroom-agent-hooks/hooks/hooks.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"description": "Headroom plugin hooks — ensure the local Headroom runtime is available for initialized agents.",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "headroom init hook ensure",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash|PowerShell",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "headroom init hook ensure",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
202
tests/test_cli/test_init_cli.py
Normal file
202
tests/test_cli/test_init_cli.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
def _load_init_module(monkeypatch):
|
||||
monkeypatch.delitem(sys.modules, "headroom.cli.init", raising=False)
|
||||
monkeypatch.delitem(sys.modules, "headroom.cli.main", raising=False)
|
||||
fake_main_module = types.ModuleType("headroom.cli.main")
|
||||
|
||||
@click.group()
|
||||
def fake_main() -> None:
|
||||
pass
|
||||
|
||||
fake_main_module.main = fake_main
|
||||
monkeypatch.setitem(sys.modules, "headroom.cli.main", fake_main_module)
|
||||
importlib.invalidate_caches()
|
||||
init_cli = importlib.import_module("headroom.cli.init")
|
||||
monkeypatch.delitem(sys.modules, "headroom.cli.init", raising=False)
|
||||
return init_cli, fake_main
|
||||
|
||||
|
||||
def test_init_auto_detects_targets(monkeypatch) -> None:
|
||||
init_cli, fake_main = _load_init_module(monkeypatch)
|
||||
runner = CliRunner()
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(init_cli, "detect_init_targets", lambda global_scope: ["claude", "codex"])
|
||||
monkeypatch.setattr(init_cli, "_run_init_targets", lambda **kwargs: captured.update(kwargs))
|
||||
|
||||
result = runner.invoke(fake_main, ["init", "-g"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["targets"] == ["claude", "codex"]
|
||||
assert captured["global_scope"] is True
|
||||
|
||||
|
||||
def test_init_fails_when_auto_detection_empty(monkeypatch) -> None:
|
||||
init_cli, fake_main = _load_init_module(monkeypatch)
|
||||
runner = CliRunner()
|
||||
monkeypatch.setattr(init_cli, "detect_init_targets", lambda global_scope: [])
|
||||
|
||||
result = runner.invoke(fake_main, ["init"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "auto-detected" in result.output
|
||||
|
||||
|
||||
def test_init_copilot_requires_global(monkeypatch) -> None:
|
||||
init_cli, fake_main = _load_init_module(monkeypatch)
|
||||
runner = CliRunner()
|
||||
monkeypatch.setattr(init_cli, "_ensure_runtime_manifest", lambda **kwargs: "init-local-test")
|
||||
|
||||
result = runner.invoke(fake_main, ["init", "copilot"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "requires -g" in result.output
|
||||
|
||||
|
||||
def test_init_claude_local_writes_settings_and_installs_marketplace(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
init_cli, fake_main = _load_init_module(monkeypatch)
|
||||
runner = CliRunner()
|
||||
monkeypatch.chdir(tmp_path)
|
||||
marketplace_calls: list[str] = []
|
||||
monkeypatch.setattr(init_cli, "_ensure_runtime_manifest", lambda **kwargs: "init-local-demo")
|
||||
monkeypatch.setattr(
|
||||
init_cli,
|
||||
"_install_claude_marketplace",
|
||||
lambda scope: marketplace_calls.append(scope),
|
||||
)
|
||||
|
||||
result = runner.invoke(fake_main, ["init", "claude"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
settings_path = tmp_path / ".claude" / "settings.local.json"
|
||||
payload = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
|
||||
assert marketplace_calls == ["local"]
|
||||
assert any(
|
||||
"--profile init-local-demo" in hook["command"] and "init hook ensure" in hook["command"]
|
||||
for entry in payload["hooks"]["SessionStart"]
|
||||
for hook in entry["hooks"]
|
||||
)
|
||||
|
||||
|
||||
def test_init_codex_merges_feature_flag_into_existing_table(monkeypatch, tmp_path: Path) -> None:
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config_path = tmp_path / ".codex" / "config.toml"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path.write_text("[features]\nshell_tool = true\n", encoding="utf-8")
|
||||
|
||||
init_cli._init_codex(global_scope=False, profile="init-local-demo", port=9000)
|
||||
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
assert 'base_url = "http://127.0.0.1:9000/v1"' in content
|
||||
assert content.count("[features]") == 1
|
||||
assert "codex_hooks = true" in content
|
||||
hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text(encoding="utf-8"))
|
||||
assert "--profile init-local-demo" in hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||
assert "init hook ensure" in hooks["hooks"]["SessionStart"][0]["hooks"][0]["command"]
|
||||
|
||||
|
||||
def test_init_claude_uses_custom_port(monkeypatch, tmp_path: Path) -> None:
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr(init_cli, "_install_claude_marketplace", lambda scope: None)
|
||||
|
||||
init_cli._init_claude(global_scope=False, profile="init-local-demo", port=9011)
|
||||
|
||||
payload = json.loads((tmp_path / ".claude" / "settings.local.json").read_text(encoding="utf-8"))
|
||||
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9011"
|
||||
|
||||
|
||||
def test_init_copilot_global_writes_hooks_and_env(monkeypatch, tmp_path: Path) -> None:
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
captured_env: dict[str, str] = {}
|
||||
monkeypatch.setattr(init_cli, "_copilot_config_path", lambda: tmp_path / "copilot-config.json")
|
||||
monkeypatch.setattr(init_cli, "_apply_user_env", lambda values: captured_env.update(values))
|
||||
monkeypatch.setattr(init_cli, "_install_copilot_marketplace", lambda: None)
|
||||
|
||||
init_cli._init_copilot(global_scope=True, profile="init-user", port=9005, backend="openai")
|
||||
|
||||
payload = json.loads((tmp_path / "copilot-config.json").read_text(encoding="utf-8"))
|
||||
assert "SessionStart" in payload["hooks"]
|
||||
assert "PreToolUse" in payload["hooks"]
|
||||
assert "--profile init-user" in payload["hooks"]["SessionStart"][0]["command"]
|
||||
assert captured_env == {
|
||||
"COPILOT_PROVIDER_TYPE": "openai",
|
||||
"COPILOT_PROVIDER_BASE_URL": "http://127.0.0.1:9005/v1",
|
||||
"COPILOT_PROVIDER_WIRE_API": "completions",
|
||||
}
|
||||
|
||||
|
||||
def test_init_hook_ensure_prefers_local_profile(monkeypatch) -> None:
|
||||
init_cli, fake_main = _load_init_module(monkeypatch)
|
||||
ensured: list[str] = []
|
||||
|
||||
def fake_load(profile: str):
|
||||
return object() if profile == "init-repo-12345678" else None
|
||||
|
||||
monkeypatch.setattr(init_cli, "_local_profile", lambda cwd=None: "init-repo-12345678")
|
||||
monkeypatch.setattr(init_cli, "load_manifest", fake_load)
|
||||
monkeypatch.setattr(
|
||||
init_cli, "_ensure_profile_running", lambda profile: ensured.append(profile)
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(fake_main, ["init", "hook", "ensure"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert ensured == ["init-repo-12345678"]
|
||||
|
||||
|
||||
def test_init_openclaw_requires_global(monkeypatch) -> None:
|
||||
_, fake_main = _load_init_module(monkeypatch)
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(fake_main, ["init", "openclaw"])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "requires -g" in result.output
|
||||
|
||||
|
||||
def test_init_openclaw_delegates_to_wrap(monkeypatch) -> None:
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
calls: list[list[str]] = []
|
||||
|
||||
class _Result:
|
||||
returncode = 0
|
||||
|
||||
monkeypatch.setattr(init_cli, "resolve_headroom_command", lambda: ["headroom"])
|
||||
monkeypatch.setattr(
|
||||
init_cli.subprocess,
|
||||
"run",
|
||||
lambda cmd: calls.append(cmd) or _Result(),
|
||||
)
|
||||
|
||||
init_cli._init_openclaw(global_scope=True, port=9999)
|
||||
|
||||
assert calls == [["headroom", "wrap", "openclaw", "--proxy-port", "9999"]]
|
||||
|
||||
|
||||
def test_detect_init_targets_respects_scope(monkeypatch) -> None:
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
monkeypatch.setattr(
|
||||
init_cli.shutil,
|
||||
"which",
|
||||
lambda name: name if name in {"claude", "copilot", "codex", "openclaw"} else None,
|
||||
)
|
||||
|
||||
assert init_cli.detect_init_targets(False) == ["claude", "codex"]
|
||||
assert init_cli.detect_init_targets(True) == ["claude", "copilot", "codex", "openclaw"]
|
||||
40
tests/test_plugin_manifests.py
Normal file
40
tests/test_plugin_manifests.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _load_json(relative_path: str) -> object:
|
||||
return json.loads((REPO_ROOT / relative_path).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_marketplace_manifests_match() -> None:
|
||||
assert _load_json(".claude-plugin/marketplace.json") == _load_json(
|
||||
".github/plugin/marketplace.json"
|
||||
)
|
||||
|
||||
|
||||
def test_plugin_manifests_share_core_metadata() -> None:
|
||||
claude = _load_json("plugins/headroom-agent-hooks/.claude-plugin/plugin.json")
|
||||
copilot = _load_json("plugins/headroom-agent-hooks/.github/plugin/plugin.json")
|
||||
assert isinstance(claude, dict)
|
||||
assert isinstance(copilot, dict)
|
||||
for key in ("name", "version", "description", "author", "homepage", "repository", "keywords"):
|
||||
assert claude[key] == copilot[key]
|
||||
assert "hooks" not in claude
|
||||
assert copilot["hooks"] == "./hooks"
|
||||
|
||||
|
||||
def test_marketplace_entry_points_to_plugin_root() -> None:
|
||||
marketplace = _load_json(".claude-plugin/marketplace.json")
|
||||
assert isinstance(marketplace, dict)
|
||||
plugins = marketplace["plugins"]
|
||||
assert isinstance(plugins, list)
|
||||
plugin = plugins[0]
|
||||
assert plugin["name"] == "headroom"
|
||||
plugin_root = (REPO_ROOT / plugin["source"]).resolve()
|
||||
assert plugin_root.is_dir()
|
||||
assert (plugin_root / ".claude-plugin" / "plugin.json").is_file()
|
||||
assert (plugin_root / "hooks" / "hooks.json").is_file()
|
||||
Loading…
Add table
Add a link
Reference in a new issue