Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "context-guru",
"description": "Run context-guru in front of your Claude Code sessions in one command.",
"owner": {
"name": "rossoctl",
"url": "https://github.com/rossoctl/context-guru"
},
"plugins": [
{
"name": "context-guru",
"source": "./context-guru-plugin",
"displayName": "context-guru",
"description": "Install, route, inspect and remove a local context-guru proxy for Claude Code. Recovers prompt-cache misses on long sessions; no API key needed on a Pro/Max subscription.",
"homepage": "https://github.com/rossoctl/context-guru",
"repository": "https://github.com/rossoctl/context-guru",
"license": "Apache-2.0",
"keywords": ["cache", "cost", "proxy", "tokens", "prompt-caching"],
"category": "productivity"
}
]
}
12 changes: 8 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
check-latest: true
# go.mod is the single source of truth for the toolchain. Pinning a literal here is
# what let CI validate on 1.25 while go.mod declared 1.26.4 and releases built on
# 1.26 (#152) — three numbers that have to agree and no mechanism making them.
go-version-file: go.mod
- name: Lint
run: make lint
- name: Test & coverage
Expand Down Expand Up @@ -54,8 +56,10 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
check-latest: true
# go.mod is the single source of truth for the toolchain. Pinning a literal here is
# what let CI validate on 1.25 while go.mod declared 1.26.4 and releases built on
# 1.26 (#152) — three numbers that have to agree and no mechanism making them.
go-version-file: go.mod
- name: Build with no C compiler available
run: |
go build -o /tmp/cg-purego ./cmd/context-guru-proxy
Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Release

# Tag-driven, so a release is something a maintainer does on purpose. The `workflow_dispatch`
# entry builds the same matrix WITHOUT publishing (snapshot mode), which is how the release
# path gets exercised before there is a tag to regret.
on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
permissions:
# Only the tag path publishes, and only this job needs the write.
contents: write
steps:
- uses: actions/checkout@v4
with:
# GoReleaser's changelog needs the history the default shallow clone does not have.
fetch-depth: 0
- uses: actions/setup-go@v5
with:
# Same source as CI (go.mod), deliberately: an artifact people download must be built
# with the toolchain CI validated, and a literal here is how that drifts apart.
go-version-file: go.mod

# The claim the release rests on, asserted in CI rather than trusted: the shipped
# binary needs no C toolchain. CGO_ENABLED=0 with no compiler on PATH would fail loudly
# here if a cgo dependency ever escaped the cg_skeleton build tag — which is exactly the
# regression that would otherwise be discovered by an evaluator, at install time.
- name: Assert the binary is pure Go
env:
CGO_ENABLED: "0"
CC: /nonexistent-c-compiler
run: |
go build -o /tmp/cg-purego ./cmd/context-guru-proxy
file /tmp/cg-purego | tee /dev/stderr | grep -q "statically linked"
# And it has to actually start, not just link.
/tmp/cg-purego --listen 127.0.0.1:4471 --preset cache &
for _ in $(seq 1 40); do
sleep 0.25
curl -fsS http://127.0.0.1:4471/healthz && break
done
curl -fsS http://127.0.0.1:4471/healthz | grep -q ok
# An installer asks the binary what it is; make sure it can answer.
/tmp/cg-purego --version | tee /dev/stderr | grep -q context-guru-proxy

# Nothing tested the configuration we actually SHIP.
#
# ci.yaml runs the suite only with CGO_ENABLED=1, and a tag push previously published
# without running any tests at all. So the one guard that matters most to a released
# artifact — TestEveryPresetBuilds, which catches a preset naming a component that is not
# registered in a CGO-free binary — was never executed in the CGO-free configuration. That
# is exactly the `preset: coding` / `unknown component "skeleton"` failure, in a build no
# developer runs locally.
#
# The race detector needs cgo, so this cannot be the whole suite; it is the packages whose
# behaviour depends on which components are compiled in.
- name: Test the shipped configuration (CGO off, no race detector)
env:
CGO_ENABLED: "0"
run: go test ./config/... ./components/... ./apply/... ./proxy/... ./store/...

# A tag must not publish something the full suite has not seen.
- name: Full test suite
env:
CGO_ENABLED: "1"
run: go test ./...

- name: Release
uses: goreleaser/goreleaser-action@v6
with:
version: "~> v2"
# A tag publishes; a manual run builds the full matrix and publishes nothing.
args: ${{ startsWith(github.ref, 'refs/tags/v') && 'release --clean' || 'release --clean --snapshot' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload snapshot artifacts
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v4
with:
name: snapshot-dist
path: |
dist/*.tar.gz
dist/checksums.txt
retention-days: 7
98 changes: 98 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# GoReleaser: the release artifacts an evaluator downloads instead of installing a toolchain.
#
# The whole file is a plain GOOS/GOARCH matrix with no C cross-toolchains, no zig, and no
# libc coupling, because `CGO_ENABLED=0` builds the shipped binary. That is verified rather
# than assumed — the `Assert the binary is pure Go` step in .github/workflows/release.yaml fails
# the release if a cgo dependency ever escapes the cg_skeleton build tag. Measured directly on
# go 1.26.4:
# all four targets build, the artifact is 27–34 MB stripped, `file` reports "statically
# linked" and `ldd` "not a dynamic executable", and the resulting binary serves /healthz.
#
# `cg_skeleton` is the ONE thing that needs cgo (tree-sitter), and it is deliberately not
# built here: it is in no default preset, not in the cache story, and shipping it would mean
# per-platform C cross-compilation for a component this funnel never runs. Source build is
# documented in docs/components/skeleton.md.
#
# There is no `brews:` block yet — the tap repo and release signing are an open ownership
# question (spec §"Open questions", 3). Until it is answered the funnel installs from the
# release tarball, so nothing here depends on a repo that does not exist. Adding the tap
# later is additive and changes none of the below.
version: 2

project_name: context-guru

before:
hooks:
- go mod download

builds:
- id: context-guru-proxy
main: ./cmd/context-guru-proxy
binary: context-guru-proxy
env:
# The point of the whole file. Not inherited from the Makefile, which sets
# CGO_ENABLED=1 because `go test -race` needs it — a test-time requirement that was
# being read as a shipping requirement.
- CGO_ENABLED=0
flags:
# Reproducible paths in panics, and no VCS stamping (the checkout is shallow in CI).
- -trimpath
- -buildvcs=false
ldflags:
# Same two symbols the Makefile stamps, so `/stats` build_version is populated in a
# released binary exactly as it is in a locally built one.
- -s -w
- -X github.com/rossoctl/context-guru/internal/buildinfo.Version={{ .Version }}
- -X github.com/rossoctl/context-guru/internal/buildinfo.Commit={{ .ShortCommit }}
goos: [linux, darwin]
goarch: [amd64, arm64]

archives:
- id: default
ids: [context-guru-proxy]
# An evaluator untars this into ~/.local/bin, so the archive name is what they see and
# the binary inside must be the plain name with no version in it.
name_template: >-
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
formats: [tar.gz]
files:
- LICENSE
- README.md
- THIRD-PARTY-NOTICES

checksums:
# scripts/install.sh verifies the downloaded tarball against this file. macOS quarantines
# an unsigned download and the installer strips the attribute, so a checksum is the only
# integrity check left in that path — it is not optional decoration.
name_template: checksums.txt
algorithm: sha256

snapshot:
version_template: "{{ incpatch .Version }}-next"

changelog:
use: github
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- "^ci:"

release:
prerelease: auto
footer: |
## Install

No Go toolchain and no C compiler are needed — the binary is statically linked.

Download the tarball for your platform, untar it, and put `context-guru-proxy` on your
`PATH`:

```
tar xzf context-guru_*_darwin_arm64.tar.gz
install -m 755 context-guru-proxy ~/.local/bin/
```

Then see `docs/get-started/quickstart-proxy.md`.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ docker build -t context-guru:local .

## Quickstart (60 seconds)

**Claude Code users — two commands, no toolchain, and no API key needed on a Pro/Max
subscription** ([details](docs/how-to/install-plugin.md)):

```
/plugin marketplace add rossoctl/context-guru
/plugin install context-guru@context-guru
/context-guru:install
```

That installs a statically-linked binary (no Go, no C compiler), routes **this project only** by
default, starts the proxy on demand and lets it exit when idle. `/context-guru:uninstall` undoes it,
restoring any base URL it replaced. The plugin installs with `--preset cache` — the prompt-cache
split and nothing else. (The proxy's own default is `house`; `--preset` is how you change it.)

Or by hand — a release binary is statically linked, **no Go and no C compiler needed** — or build
from source:

```sh
# 1 — run the proxy (ships with the SWE-bench-winning cache-aware config by default)
./bin/context-guru-proxy # --preset house (the default); listens on :4000
Expand Down Expand Up @@ -144,6 +161,8 @@ See [docs/components.md](docs/components.md) and [docs/reference/presets.md](doc
| Flag / env | Default | Purpose |
|---|---|---|
| `--preset` / `PRESET` | `house` | pipeline preset when no `--config` |
| `--idle-exit` / `IDLE_EXIT` | `0` (never) | exit after this long unused; floor `max(2 × store.ttl_seconds, 1h)`, refused with `--upstreams` |
| `--version` | — | print version and commit, then exit |
| `--config` / `CONFIG` | — | YAML config (overrides preset) |
| `LISTEN_ADDR` | `:4000` | listen address |
| `--anthropic-upstream` / `ANTHROPIC_UPSTREAM` | `https://api.anthropic.com` | Anthropic upstream base |
Expand Down
Loading
Loading