Files
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

35 KiB
Raw Permalink Blame History

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.rsserve, 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.

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:

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). 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:

# ~/.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:

# 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"
# 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):

# env (any value) — works for a foreground `lean-ctx proxy start`
export LEAN_CTX_ALLOW_CUSTOM_UPSTREAM=1
# 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:

[[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:

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):

[[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):

[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):

proxy_loopback_open = true   # env: LEAN_CTX_PROXY_LOOPBACK_OPEN

Retrieve the current proxy token for manual use:

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:

skip_agent_aliases = true

Or at install time:

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:

[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.

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:

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.
[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):

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). 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:

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:

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.

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.

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.

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, …).

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

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:

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.

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:

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):

# ~/.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:

// 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:

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 ---