Files
yvgude--lean-ctx/docs/reference/05-advanced.md
T
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

761 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Journey 5 — Advanced & Integrations
> You've mastered daily use and want more: compress the LLM API stream itself,
> pull in GitHub/GitLab/Jira context, share context across repos or agents, and
> govern rules across your team. This journey covers the power-user surface.
Source files referenced here:
- `rust/src/cli/dispatch/network.rs``serve`, `proxy`, `daemon`, `provider`, `team`
- `rust/src/cli/profile_cmd.rs` — context `profile`
- `rust/src/cli/plugin_cmd.rs`, `rules_cmd.rs`, `pack_cmd.rs`
- `rust/src/tools/registered/ctx_provider.rs`, `ctx_pack.rs`, `ctx_multi_repo.rs`,
`ctx_agent.rs`, `ctx_handoff.rs`
- `rust/src/core/gateway/` (`client.rs`, `catalog.rs`, `router.rs`, `config.rs`),
`rust/src/tools/ctx_tools.rs` — the MCP Tool-Catalog Gateway
---
## 1. The proxy — compress the LLM stream itself
**What it does:** Everything so far compresses *before* your AI calls a tool. The
proxy goes one level deeper: it sits between your AI client and the LLM API and
compresses `tool_results` in-flight, before they reach the model.
```bash
lean-ctx proxy enable # set up env + autostart (writes RC + LaunchAgent)
lean-ctx proxy status
lean-ctx proxy start # start now
lean-ctx proxy stop
lean-ctx proxy disable # remove env + autostart
lean-ctx proxy cleanup # clear proxy state
```
**Golden output — `lean-ctx proxy status`** tells you, at a glance, whether the
proxy is configured, on which port, and whether the process is currently up:
```text
lean-ctx proxy:
Config: enabled
Port: 4444
Process: not running
```
`Config: enabled` with `Process: not running` means it is wired up but not
started — run `lean-ctx proxy start` (or rely on the LaunchAgent/systemd unit).
**Under the hood:** runs on `LEAN_CTX_PROXY_PORT` (default 4444), auth via
`session_token`. `proxy enable` writes `*_BASE_URL` exports into your shell RC,
`~/.claude/settings.json` (`ANTHROPIC_BASE_URL`), and Codex `config.toml`
(`OPENAI_BASE_URL`), and installs `com.leanctx.proxy.plist` (macOS) or a systemd
user unit (Linux). Upstreams are configurable in `[proxy]`.
**Plays nice with provider prompt caching.** Anthropic's `cache_control` and
OpenAI's automatic prompt caching bill cached prefix tokens at a fraction of
the base rate — but only for *byte-identical* prefixes. The proxy therefore
mutates history exclusively in cache-stable ways: tool-result compression is
content-deterministic (the same result compresses identically on every turn),
and old tool results are summarized only at **frozen compaction boundaries**
that advance in large deterministic strides instead of a per-turn rolling
window. Between boundary jumps your request prefix stays byte-identical, so
cache reads keep hitting; a jump costs one re-write and then caching resumes
on the smaller history. Tune via `[proxy].history_mode` (or
`LEAN_CTX_PROXY_HISTORY_MODE`):
| Mode | Behaviour | Use when |
|------|-----------|----------|
| `cache-aware` *(default)* | Prune at frozen 16-message strides, ≥8 recent messages always intact | You use prompt caching (Claude Code, Cursor, most clients) |
| `rolling` | Legacy: summarize everything older than the last 6 messages, every turn | Maximum raw-token reduction, no prompt caching in play |
| `off` | Never prune history (compression still applies) | Debugging, or the client manages history itself |
> **Heads-up (community-reported):** `proxy enable` modifies your shell RC. If a
> base URL "defaults to the wrong provider," check the exported `*_BASE_URL`
> values in your RC and `lean-ctx proxy status`. The unmodified RC is preserved
> as a `*.lean-ctx.bak` backup.
> **Claude Pro/Max subscriptions need an API key for the proxy.** The proxy
> forwards your credential upstream but never *injects* one. A Claude Pro/Max
> subscription authenticates via OAuth directly against `api.anthropic.com`, and
> that token is rejected by any custom `ANTHROPIC_BASE_URL` — routing it through
> the proxy produces a login loop / 401. Therefore `proxy enable` **skips the
> Claude redirect when no `ANTHROPIC_API_KEY` is detected** (env or
> `~/.claude/settings.json`) and leaves Claude Code talking to Anthropic directly.
> `lean-ctx doctor` flags the conflict if a stale redirect remains.
>
> - **On a subscription?** Keep the proxy disabled for Claude and get savings from
> the lean-ctx MCP tools instead (`ctx_read` / `ctx_search` / `ctx_shell`).
> Other providers (OpenAI/Codex, Gemini, Ollama) are still routed through the
> proxy.
> - **Pay-as-you-go?** Export `ANTHROPIC_API_KEY=<your-key>`, then run
> `lean-ctx proxy enable` (or `--force` to override detection). Claude traffic is
> then compressed by the proxy.
### Codex in front of the proxy (native WebSocket + HTTP/SSE)
The proxy serves the OpenAI Responses API on both `/v1/responses` and the bare
`/responses` path over **two transports**: native **WebSocket**
(`ws://127.0.0.1:4444/responses`) — Codex's default — and **HTTP/SSE** for clients
that prefer it ([#440](https://github.com/yvgude/lean-ctx/issues/440)). Point Codex
at the proxy and it connects over WebSockets out of the box; the proxy bridges the
WS frames to the upstream and compresses them like any other request:
```toml
# ~/.codex/config.toml — point Codex at the proxy (WebSockets work as-is)
[model_providers.lean-ctx]
name = "lean-ctx"
base_url = "http://127.0.0.1:4444/v1"
```
> Prefer HTTP/SSE instead? Set `supports_websockets = false` in the provider block
> to force Codex onto the `/v1/responses` HTTP transport.
**Non-loopback HTTP upstreams (e.g. `codex-lb`).** By default an upstream must be
HTTPS unless it is loopback (`127.0.0.1` / `localhost` / `[::1]`). To put the proxy
in front of a *trusted local-network* plaintext service such as
`http://host.docker.internal:2455`, opt in deliberately — otherwise the upstream is
rejected:
```bash
# env (any value) — wins over config.toml
export LEAN_CTX_ALLOW_INSECURE_HTTP_UPSTREAM=1
export LEAN_CTX_OPENAI_UPSTREAM="http://host.docker.internal:2455"
```
```toml
# or persist it in config.toml
[proxy]
openai_upstream = "http://host.docker.internal:2455"
allow_insecure_http_upstream = true
```
> ⚠ This downgrades the upstream hop to plaintext HTTP. Use it **only** on a trusted
> local network (loopback, a container host, a private LAN service you control) —
> never for traffic that crosses an untrusted network. The proxy prints a warning at
> startup whenever a non-loopback HTTP upstream is active.
**Custom HTTPS upstream hosts (e.g. a corporate gateway).** By default the upstream
host must be one of the provider defaults (`api.anthropic.com`, `api.openai.com`,
`chatgpt.com`, `generativelanguage.googleapis.com`). To route through a custom HTTPS
host you control — such as `https://gw.corp.example/anthropic` — opt in deliberately
([#590](https://github.com/yvgude/lean-ctx/issues/590)):
```bash
# env (any value) — works for a foreground `lean-ctx proxy start`
export LEAN_CTX_ALLOW_CUSTOM_UPSTREAM=1
```
```toml
# persist it in config.toml — REQUIRED for the service-managed proxy
[proxy]
anthropic_upstream = "https://gw.corp.example/anthropic"
allow_custom_upstream = true
```
> The env var only reaches a proxy you start **in the foreground** (`proxy start`),
> because it inherits your shell. A proxy started by `lean-ctx proxy enable` /
> `restart` runs as a LaunchAgent / systemd service that never sees your shell env,
> so it would otherwise fall back to the provider default. `enable`/`restart`
> therefore **auto-persist** `allow_custom_upstream = true` when you run them with
> `LEAN_CTX_ALLOW_CUSTOM_UPSTREAM` set and a custom upstream configured — or set the
> flag yourself with `lean-ctx config set proxy.allow_custom_upstream true`.
**Universal provider registry — `[[proxy.providers]]`.** Beyond the four built-in
provider routes, any OpenAI/Anthropic/Gemini-*compatible* endpoint (Azure AI
Foundry, OpenRouter, Groq, vLLM, Ollama, a corporate gateway…) can be declared as
data — no code change. Each entry is served under `/providers/{id}/...` with full
compression, introspection and usage metering for its wire shape:
```toml
[[proxy.providers]]
id = "foundry" # route: /providers/foundry/...
shape = "openai" # anthropic | openai | gemini
base_url = "https://acme.services.ai.azure.com"
api_key_env = "FOUNDRY_API_KEY" # optional: gateway-held key
[[proxy.providers]]
id = "openrouter"
shape = "openai"
base_url = "https://openrouter.ai/api"
[[proxy.providers]]
id = "local"
shape = "openai"
base_url = "http://host.docker.internal:11434" # gateway container → host Ollama
local = true # bill at the shadow rate
```
- **Shape ≠ identity.** The proxy speaks three wire dialects; any number of
provider identities map onto them. A declared HTTPS entry is itself the
custom-host opt-in (no separate `allow_custom_upstream` needed); plaintext HTTP
still requires loopback or `allow_insecure_http_upstream`.
- **`api_key_env` set** → the gateway holds the upstream credential: every caller
credential header is stripped and replaced (callers authenticate with the
lean-ctx Bearer token and never see the provider key). Unset → the caller's own
credentials are forwarded verbatim, exactly like the built-ins.
- **`local`** marks the endpoint as local inference for metering: usage is booked
at the transparent `local_shadow_rate` instead of cloud list prices. Unset, it
is derived from the URL (loopback hosts count as local) — declare it explicitly
when the endpoint is local but not loopback, e.g. the containerized gateway
reaching the host's Ollama via `host.docker.internal`, or an in-cluster vLLM
service. `local = false` likewise pins a loopback-tunneled cloud endpoint to
list-price billing.
- Invalid entries are logged and skipped; the registry is hot-reloaded from
`config.toml` like every upstream. Active entries appear on `/status` under
`providers`.
**Gateway mode — serving a whole org from one host** (`proxy_bind_host`). By
default the proxy binds `127.0.0.1` (nothing changes for local installs). Binding
a non-loopback address turns on gateway hardening **by construction**:
```toml
proxy_bind_host = "0.0.0.0" # env: LEAN_CTX_PROXY_BIND_HOST
proxy_allowed_hosts = ["ai-gateway.example.com"] # Host-header allowlist (DNS rebinding)
proxy_max_rps = 100 # optional; gateway default: 50 rps
```
- The provider-API-key auth fallback is **hard-disabled** (its justification is
strictly "loopback only") — every caller must send the lean-ctx Bearer
token regardless of `proxy_require_token`.
- The Host allowlist extends the loopback-only guard; loopback names always pass.
- A token-bucket rate limit activates (default 50 rps, burst 100; `proxy_max_rps`
overrides, `0` disables). `/health` is exempt for orchestrator liveness probes.
- An unparseable bind value falls back to `127.0.0.1` — a typo can only ever
narrow exposure, never open the listener.
**Per-person gateway keys — metering identity** (`gateway-keys.toml`). An org
gateway can issue one bearer key per person instead of sharing the proxy token.
The file lives at `<config_dir>/gateway-keys.toml` (override:
`LEAN_CTX_GATEWAY_KEYS`), holds **only SHA-256 hashes** of the keys, and is
loaded at proxy startup (rotation = restart, the standard secret-mount flow; a
malformed file fails the start loudly):
```toml
[[keys]]
sha256_hex = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
person = "alice@example.com"
team = "platform" # optional
default_project = "billing" # optional
```
- A request whose Bearer token hash matches an entry
authenticates **and** tags the turn's measured usage with
`person`/`team`/`project` — the basis for per-person/per-project metering.
- The `x-leanctx-project: <name>` request header overrides the key's
`default_project` per request (also works without a key, for solo setups). It
is an internal gateway header and is never forwarded upstream.
- Compute a key's hash with `shasum -a 256` (or `sha256sum`):
`printf '%s' "gk-alice-secret" | shasum -a 256`.
**Active routing — `[proxy.routing]`.** The gateway can rewrite the requested
model in-flight: exact **aliases** (stable org names, transparent swaps) and
intent-based **tier downgrades** (the last user message is classified
`fast|standard|premium`; the tier picks a target). Targets are `"model"` (same
upstream) or `"provider:model"` (re-target to a `[[proxy.providers]]` entry or a
built-in `anthropic|openai|gemini` — same wire shape only; cross-shape
translation is not in M1):
```toml
[proxy.routing]
enabled = true
[proxy.routing.aliases]
"acme/fast" = "foundry:Phi-4-mini-instruct" # stable org-level model name
"claude-opus-4-5" = "claude-sonnet-4-5" # transparent downgrade, same upstream
[proxy.routing.tiers]
fast = "foundry:Phi-4-mini-instruct" # explore/debug-style requests
standard = "" # "" / absent = keep requested model
premium = "" # premium work is never auto-downgraded
```
- **Fail-open by construction:** any miss (rule/classification/unknown provider/
shape mismatch) forwards the request unchanged — a routing bug can cost
savings, never availability. Aliases win over tiers.
- Routed usage events carry `routed_from` (the originally requested model) and
the serving provider, so the savings ledger can prove what the router did.
- Gemini (model in URL path) and the ChatGPT/Codex OAuth route stay passthrough
in M1.
**Loopback-open mode** (`proxy_loopback_open`). When enabled, the proxy skips
ALL authentication on loopback-bound listeners. MCP clients, browser dashboards,
and CLI tools work without setting up tokens. Ignored on non-loopback binds
(gateway mode always requires auth):
```toml
proxy_loopback_open = true # env: LEAN_CTX_PROXY_LOOPBACK_OPEN
```
Retrieve the current proxy token for manual use:
```bash
lean-ctx proxy token # prints token to stdout
lean-ctx proxy token --quiet # no trailing newline (for scripts)
```
### Agent CLI aliases (`skip_agent_aliases`)
`lean-ctx onboard` / `setup` installs shell aliases (`claude`, `codex`, `gemini`,
`codebuddy`) that set `LEAN_CTX_AGENT=1` and `BASH_ENV` so compression activates
automatically in agent sessions. If these aliases conflict with external launchers,
GUI wrappers, or WSL agent detection, disable them:
```toml
skip_agent_aliases = true
```
Or at install time:
```bash
lean-ctx onboard --no-agent-aliases
lean-ctx setup --no-agent-aliases
```
When toggled on, existing alias blocks are removed from `~/.zshrc` and `~/.bashrc`
on the next `setup` / `onboard` run.
This does **not** affect the shell compression hook (`_lc()`) — use
`shell_hook_disabled` to disable that. The `shell_activation` setting controls
*when* aliases activate, `skip_agent_aliases` controls *whether* they are installed.
**Counterfactual baseline — `[proxy.baseline]`.** The parameters that make
avoided-cost claims auditable. Frozen per deployment (contract annex), not
tunable at runtime by the vendor:
```toml
[proxy.baseline]
reference_model = "claude-opus-4.5" # what the org would have used without lean-ctx
local_shadow_rate_per_mtok = 0.25 # USD/MTok booked for local/loopback inference
```
- Every usage event stores `reference_cost_usd` = the request's **uncompressed**
input tokens priced at the reference model's input rate — the counterfactual
the ledger settles against. Unset `reference_model` = no counterfactual is
claimed.
- `is_local` traffic books the **shadow rate** as its actual cost (default 0.25
USD/MTok, never 0): local compute is free of provider fees, not of hardware
and power — keeping "savings vs. local" honest instead of infinite.
**Provider-verified savings — `proxy.counterfactual_metering` (#701).** The
baseline above still *estimates* the uncompressed side (bytes/4). Opt in to
replace the estimate with a receipt: for every request the proxy actually
rewrote, it fires Anthropic's **free** `count_tokens` endpoint with the
original, uncompressed body — concurrently with the real forward, never
delaying or mutating it — and pairs the provider-counted answer with the same
response's billed usage. `/status` then carries a `verified_savings` block
(`counterfactual_input_tokens`, `billed_input_tokens`, signed
`verified_saved_tokens`) and `lean-ctx proxy status` prints a `Verified:` line
next to the estimate. Same-request pairing means no traffic-mix confounds; a
net-negative result (stub overhead exceeding the squeeze) is reported honestly,
never clamped. Anthropic only — OpenAI/Gemini have no free counting endpoint.
```bash
lean-ctx config set proxy.counterfactual_metering true
```
**Self-hosted org gateway — `lean-ctx gateway serve`** (build with
`--features gateway-server`). One process bundling the hardened proxy, the
Postgres usage store and an admin listener:
```bash
DATABASE_URL="postgres://gateway@db/leanctx" \
LEAN_CTX_GATEWAY_ADMIN_TOKEN="$(openssl rand -hex 24)" \
lean-ctx gateway serve --port=8484 --admin-port=8485
```
- **Proxy** (`--port`): the exposed surface — all `proxy_bind_host` /
allowlist / Bearer / rate-limit rules above apply unchanged.
- **Usage store** (`DATABASE_URL`): every measured turn becomes a
`usage_events` row (person/team/project × provider/model × tokens/cost +
baseline fields). Schema is applied idempotently at start. **Fail-open:** an
unreachable Postgres degrades metering (events dropped and counted), never
live LLM traffic; without `DATABASE_URL` the store is simply off.
`?sslmode=require` in the URL activates TLS (rustls, webpki roots —
certificate *and* hostname always verified); required for managed Postgres
(Azure/AWS/GCP). Plain TCP stays available for in-cluster databases.
- **Admin listener** (`--admin-port`, default proxy port + 1): binds
`127.0.0.1` by default — widening is an explicit decision via
`[gateway_server].admin_bind_host` (or `LEAN_CTX_GATEWAY_ADMIN_BIND_HOST`);
invalid values fall back to loopback. Keep it cluster-internal (no ingress).
Requires `LEAN_CTX_GATEWAY_ADMIN_TOKEN` (env-only, like all tokens); without
it only the proxy runs. Every response carries hardened headers (CSP
`default-src 'self'`, `frame-ancestors 'none'`, `nosniff`, `no-referrer`;
`Cache-Control: no-store` on APIs); failed auth is throttled per source IP
(10/min → 429) and audit-logged (IP + path, SIEM-collectable).
- `GET /` — the **Gateway Console**: an embedded admin dashboard (login with
the admin token; kept in `sessionStorage` only). Org overview, spend/savings
trend, sortable breakdowns by person/project/model/provider with one-click
CSV export, provider credential status, drop counter, seat projection,
live "last updated" indicator. No CDN, no build step — served from the
binary.
- `GET /api/admin/usage?from=<ISO>&to=<ISO>` — person × project × model ×
provider breakdown with cost/savings sums, totals and the seat projection
(window defaults to the last 30 days).
- `GET /api/admin/timeseries?from=<ISO>&to=<ISO>` — per-UTC-day
requests/cost/saved/reference series (gapless; empty days are explicit
zeros) for trend charts.
- `GET /api/admin/status` — live health/config card: version, uptime, store
connectivity (probed per request), drop counter, provider registry with
credential presence, routing/baseline posture.
- `GET /metrics` — Prometheus text: per-model requests/tokens/cost, verified
ledger savings (total + per mechanism), dropped-event counter.
- `GET /healthz` — unauthenticated liveness.
```toml
[gateway_server]
seats = 800 # projection divisor ("if all seats saved like active users")
org_label = "Acme AI Gateway" # display name on cockpit + reports
# Admin listener bind (default 127.0.0.1 — secure by default). Containers set
# "0.0.0.0" so the pod/compose port mapping reaches it; exposure then stays
# governed by the mapping/Service, not the bind.
admin_bind_host = "127.0.0.1"
# admin_url: set on *client* machines to show the org-wide breakdown in their
# cockpit (ROI view) via GET /api/usage-breakdown; without it the cockpit shows
# the local snapshot of this machine only.
admin_url = "https://gateway.internal:8485"
```
**Gateway lifecycle CLI** (all under `lean-ctx gateway …`, `gateway-server`
builds):
```bash
lean-ctx gateway init pilot --org="Acme AG" --seats=800 \
--reference-model=claude-opus-4.5 --person=alice@acme.com # plug-and-play instance
cd pilot && docker compose up -d # gateway + Postgres 17
lean-ctx gateway doctor --dir . # go-live preflight (exit≠0 on FAIL)
lean-ctx gateway keys add --person=bob@acme.com --team=core # key shown once, hash stored
lean-ctx gateway keys list && lean-ctx gateway keys revoke --person=bob@acme.com
lean-ctx gateway report --out=q3.html # printable value report (usage_events)
```
- `init` generates `config.toml`, `.env` (0600; proxy/admin tokens + Postgres
password + `DATABASE_URL`), `docker-compose.yml` (healthchecks, restart
policies, admin port bound to `127.0.0.1`), `gateway-keys.toml`, `.gitignore`
and a README — and never overwrites an existing instance.
- `doctor` checks config posture (open bind without required tokens = FAIL),
security posture (admin exposure, upstream-TLS and Postgres-TLS stance),
key-set validity, token presence/strength, Postgres connectivity
(`SELECT 1`), provider `api_key_env` presence and live ports — each line with
a concrete fix command.
- **Upstream resilience:** the proxy retries exactly once (150350 ms jittered
backoff) on connect errors and on 429/502/503 — statuses where the upstream
provably did not process the request. 500/504 and mid-stream failures are
never retried. On a failed retry the *original* upstream response is passed
through. On SIGTERM the gateway finishes in-flight requests and drains the
usage-event queue (bounded, 5 s) before exit.
**Live upstream — `config.toml` is the source of truth for a running proxy**
([#449](https://github.com/yvgude/lean-ctx/issues/449)). A long-lived proxy
(LaunchAgent / systemd / IDE-spawned) re-reads its upstreams from `config.toml`
every ~2s, so a change takes effect **without a restart**:
```bash
lean-ctx config set proxy.openai_upstream https://api.openai.com # live in ≤2s
lean-ctx proxy status # shows the active upstreams
```
- **`LEAN_CTX_*_UPSTREAM` env vars are a *start-time* override only.** An
environment variable cannot reach a process that is already running, so for a
service-managed proxy use `config.toml` (or `lean-ctx proxy restart`, which
re-reads `config.toml` and drops any start-time env override). This is the
common trap with MCP hosts: **Codex (and other MCP clients) launch the lean-ctx
MCP server with a stripped, allowlisted environment** that omits
`LEAN_CTX_*_UPSTREAM`, so the proxy that server spawns never sees it — even
though `lean-ctx` *invoked directly as a CLI* does. Put the upstream in
`config.toml` and it applies to every proxy regardless of how it was started.
- An **invalid** value (typo, unreachable scheme) keeps the last good upstream —
a live proxy is never silently rerouted to the provider default.
- `lean-ctx doctor` warns when the running proxy's live upstream **drifts** from
what `config.toml` resolves to (typically an env override masking a later edit)
and points you at `lean-ctx proxy restart`.
- Tune the reload cadence with `LEAN_CTX_PROXY_RELOAD_SECS` (default `2`).
---
## 2. HTTP MCP & multi-repo — `lean-ctx serve`
For clients that speak Streamable HTTP instead of stdio, or to serve several
repos at once:
```bash
lean-ctx serve --daemon # background HTTP MCP server
lean-ctx serve --root ~/work/api:api \
--root ~/work/web:web # multi-repo, with aliases
lean-ctx serve --status
lean-ctx serve --stop
```
Multi-repo search fuses results across roots with Reciprocal Rank Fusion
(`--rrf-k`). The MCP equivalent is `ctx_multi_repo` (`add_root`, `list_roots`,
`search`, `save_config`).
The **daemon** (`lean-ctx daemon`) is the local IPC service (Unix socket in
`~/.local/share/lean-ctx/`); most users never touch it directly.
---
## 3. External context providers — `ctx_provider`
**What it does:** Brings issues, PRs/MRs, pipelines, tickets, and DB schema into
context so `ctx_semantic_search` and `ctx_knowledge` can find them.
Supported: GitHub, GitLab, Jira, Postgres, and arbitrary MCP bridges.
```text
ctx_provider action=list
ctx_provider action=gitlab_issues state=opened labels=bug
ctx_provider action=gitlab_mrs
ctx_provider action=query provider=jira resource=PROJ-123
```
**Auth:** via env tokens — `GITHUB_TOKEN`/`GH_TOKEN`, `GITLAB_TOKEN`/`CI_JOB_TOKEN`,
`JIRA_URL`+`JIRA_EMAIL`+`JIRA_TOKEN`, `DATABASE_URL`. Jira also supports OAuth via
`lean-ctx provider auth jira`. Configure under `[providers]` in `config.toml`.
**The pipeline:** provider data flows through the same consolidation path as
everything else — `execute()``consolidate()` → BM25 chunks + graph edges +
knowledge facts. That's why a GitHub issue can show up as a cross-source hint
when you read a related file.
---
## 4. Context profiles — `lean-ctx profile`
> Not to be confused with **tool profiles** (`lean-ctx tools`, Journey 2). Tool
> profiles pick *which MCP tools* exist. **Context profiles** tune *compression
> and read-mode behavior*.
```bash
lean-ctx profile list
lean-ctx profile show [name]
lean-ctx profile active
lean-ctx profile diff A B
lean-ctx profile set <name>
```
Set the active profile with `LEAN_CTX_PROFILE`; project overrides live in
`<repo>/.lean-ctx/profiles/`.
---
## 5. Packaging & sharing context — `lean-ctx pack` / `ctx_pack`
**Context packages** bundle curated context (and PR-specific "PR packs") so it
can be installed elsewhere or shared with teammates.
```bash
lean-ctx pack pr # build a PR pack for the current diff
lean-ctx pack create --name my-context
lean-ctx pack list
lean-ctx pack install <name>
lean-ctx pack export / import
```
Packages live under `packages/` with a `package-index.json`. `ctx_pack` exposes
the same actions to your AI.
---
## 6. Multi-agent coordination — `ctx_agent`, `ctx_handoff`, `ctx_share`
For workflows where several AI agents collaborate:
| Tool | Purpose |
|------|---------|
| `ctx_agent` | Register agents, post/read messages, `handoff`, `sync`, shared diaries |
| `ctx_handoff` | Deterministic handoff bundles (Context Ledger Protocol) |
| `ctx_share` | Push/pull cached file contexts between agents |
| `ctx_task` | A2A task orchestration (create/update/cancel) |
State lives under `agents/` (registry, diaries, shared knowledge) with per-agent
identity keys in `keys/`. Handoff bundles are written to `handoffs/`.
---
## 7. Governing rules — `lean-ctx rules` / `ctx_rules`
Keeps the lean-ctx rule blocks in sync across every agent's rule file
(`.cursor/rules`, `AGENTS.md`, `CLAUDE.md`, …).
```bash
lean-ctx rules status # what's installed where
lean-ctx rules sync # re-sync all agents
lean-ctx rules diff # show drift
lean-ctx rules lint # validate
```
Scope via `rules_scope` (`both`/`global`/`project`). Promote high-confidence
knowledge into rules with `lean-ctx export-rules`.
---
## 8. Plugins — `lean-ctx plugin`
```bash
lean-ctx plugin list
lean-ctx plugin enable <name>
lean-ctx plugin info <name>
lean-ctx plugin init # scaffold a new plugin
lean-ctx plugin hooks # show hook points
```
Plugins live under `<config-dir>/lean-ctx/plugins/`. `ctx_plugins` exposes
list/enable/disable/info/hooks to your AI.
---
## 9. Client integration internals — `instructions` & `hook`
These are the low-level building blocks `setup`/`init` (Journey 1) wire up for
you. You rarely call them by hand, but they're documented for anyone integrating
a new client or debugging an integration:
```bash
lean-ctx instructions --client cursor # compile guidance for one client
lean-ctx instructions --client claude --profile standard --crp tdd
lean-ctx instructions --client codex --json --include-rules
lean-ctx instructions --list-clients # which client IDs are supported
```
`instructions` renders the system-prompt/tool-instruction block a given client
should receive — useful when adding support for an editor `setup` doesn't know
yet, or to inspect exactly what guidance lean-ctx injects. `--client <id>` selects
the target (see `--list-clients`); `--profile` and `--crp off|compact|tdd` tune
the tool surface and output style; `--unified` emits one combined block; `--json`
adds metadata and, with `--include-rules`, the rules-file contents. Output is
**deterministic** for the same inputs, which is what lets the docs-drift CI gate
diff it reliably.
```bash
lean-ctx hook <rewrite|redirect|observe|copilot|codex-pretooluse|codex-session-start|rewrite-inline>
```
`hook` exposes the agent hook entry points that editors call automatically
(Cursor/Claude/Copilot/Codex). They are invoked by the editor's hook mechanism,
not typed manually — listed here so the integration surface is fully accounted
for.
**Portable hook binary — `hook_binary` / `LEAN_CTX_HOOK_BINARY` (#708).**
Generated hook commands normally bake the machine-absolute binary path
(agent hosts run hooks under a minimal shell without your `PATH`, #367). If
you sync agent settings such as `~/.claude/settings.json` across machines
with different usernames, that absolute path is wrong everywhere else — and
each machine's `init`/`doctor --fix` rewrites it, turning your settings sync
into permanent ping-pong. Set a verbatim, env-based expression instead:
```bash
lean-ctx config set hook_binary '$HOME/.local/bin/lean-ctx'
# or per-invocation: LEAN_CTX_HOOK_BINARY='$HOME/.local/bin/lean-ctx' lean-ctx init
```
Every *shell-executed* hook command then emits the expression verbatim — the
hook host's shell expands `$HOME` at run time — and `doctor` accepts it as
current. MCP server registrations and launchd/systemd autostart units are
deliberately unaffected: nothing expands shell variables there, so they keep
the real absolute path.
---
## 10. MCP Tool-Catalog Gateway — `ctx_tools` (downstream MCP servers)
**The problem it solves:** every MCP server you connect injects its *entire* tool
catalog into the system prompt — on every request. Ten servers can mean dozens of
tool schemas the model must read and disambiguate before it does anything. More
tools measurably *lowers* tool-selection accuracy and raises cost. lean-ctx only
ever shrank its **own** surface; the gateway extends that to *external* catalogs.
**What it does:** lean-ctx becomes an **MCP gateway** in front of any number of
downstream MCP servers. Instead of registering all their tools, it exposes one
meta-tool, `ctx_tools`:
| Action | What it does |
|--------|--------------|
| `find` | Rank the aggregated downstream catalog against your query (BM25, the same engine as `ctx_search`) and return the top-N as compact **ChoiceCards** |
| `call` | Proxy a `server::tool` call to its owning server and return the result |
| `list` | Show configured servers + how many tools each contributes |
| `refresh` | Drop the catalog cache and re-aggregate |
Net effect: **unlimited downstream tools at roughly constant context cost** — the
model only ever sees the handful that matter for the task in front of it.
**How to use it (config is global-only, off by default):**
```toml
# ~/.lean-ctx/config.toml
[gateway]
enabled = true
top_n = 5 # tools returned per `find`
cache_ttl_secs = 300 # catalog cache lifetime
call_timeout_secs = 30
[[gateway.servers]]
name = "fs" # becomes the namespace: fs::read_file
transport = "stdio" # spawn a local server as a child process
command = "mcp-server-filesystem"
args = ["/path/to/project"]
[[gateway.servers]]
name = "linear"
transport = "http" # connect to a remote server
url = "https://mcp.linear.app/mcp"
headers = { Authorization = "Bearer ${LINEAR_TOKEN}" }
```
Then, from the agent:
```jsonc
// 1) Discover — "what can touch issues?"
ctx_tools {"action":"find","query":"create an issue with a title and assignee"}
// 2) Invoke the chosen handle
ctx_tools {"action":"call","tool":"linear::create_issue",
"arguments":{"title":"Fix login","assignee":"me"}}
```
**Golden output — `ctx_tools find`** returns a ranked, citation-style shortlist
plus the size of the full catalog it is shielding you from:
```text
gateway: 3 tool(s) for "create an issue" (catalog: 47 tool(s) across 4 server(s))
1. linear::create_issue — Create a Linear issue
params: title*, assignee, team
2. linear::update_issue — Update fields on an existing issue
params: id*, title, state
3. github::create_issue — Open a GitHub issue
params: repo*, title*, body
Invoke one with:
ctx_tools {"action":"call","tool":"<server::tool>","arguments":{ ... }}
```
**What happens under the hood:**
- `rust/src/core/gateway/client.rs` — a real MCP client built on the official
`rmcp` SDK. `stdio` spawns the server as a child process; `http` uses the
streamable-HTTP transport with custom headers. Every connect/list/call is
bounded by `call_timeout_secs`; sessions are opened per operation and shut down
cleanly (no stale child processes).
- `rust/src/core/gateway/catalog.rs` — aggregates each enabled server's tools
into a namespaced `server::tool` catalog behind an in-process **TTL cache**.
Per-server fetch errors are *surfaced*, never hidden, so a misconfigured server
is visible to the agent.
- `rust/src/core/gateway/router.rs` — builds an **ephemeral BM25 index** over the
catalog per query and returns the top-N. Deterministic for a fixed catalog.
- `rust/src/tools/ctx_tools.rs` — gates on config, routes the action, and proxies
the call; downstream results flow back through the same ephemeral firewall and
sensitivity floor as native tools.
**Security:** `[gateway]` is **global-only** — it is never merged from a
project-local `.lean-ctx.toml`, so cloning an untrusted repo can never point the
gateway at an arbitrary command or endpoint. It is a complete no-op until you set
`enabled = true`.
---
## UX notes captured during this walkthrough
- The proxy is the most powerful and the most invasive feature (it edits RC files
and redirects API base URLs). The community-reported "defaults to wrong
provider" issue is called out inline with the recovery path (check `*_BASE_URL`,
`proxy status`, `.bak` backup).
- "profile" is overloaded: tool profile (Journey 2) vs. context profile (here).
Both journeys cross-reference each other to defuse the confusion.
--- lean-ctx: ctx_compose bundles search+read+symbols in one call ---
--- lean-ctx: ctx_compose bundles search+read+symbols in one call ---