Skip to content

feat(plugin): add a Claude Code status line for cache TTL and savings - #217

Closed
OsherElhadad wants to merge 2 commits into
feat/context-guru-pluginfrom
feat/plugin-statusline
Closed

feat(plugin): add a Claude Code status line for cache TTL and savings#217
OsherElhadad wants to merge 2 commits into
feat/context-guru-pluginfrom
feat/plugin-statusline

Conversation

@OsherElhadad

Copy link
Copy Markdown
Collaborator

Stacked on #160

This branch is feat/plugin-statusline, based on feat/context-guru-plugin (#160), and must
merge after #160. It adds nothing to that PR's own diff — everything here is new files plus
small, additive extensions to settings.py and start-proxy.sh.

What this adds

A Claude Code status line (context-guru-plugin/scripts/statusline.py) that renders, for any
project routed through context-guru's proxy:

cache 4:12 | $0.42/3.1k saved | ka 2p
  • cache 4:12 / cache cold — a countdown to the prompt-cache TTL going cold. Sourced from
    Claude Code's own client-side cache tracker (prompt_cache in the statusLine stdin payload),
    not from the proxy — zero extra network cost, and it reflects the actual wire bytes regardless
    of what any component rewrote.
  • $0.42/3.1k saved — running savings (total_saved_usd / saved_unique), one short-timeout,
    short-TTL-cached GET /api/stats. Omitted while exactly zero (a fresh install has nothing to
    report yet); not omitted when negative — an idle keep-alive spending more than it recovers
    is a real outcome this does not hide.
  • ka 2p — idle keep-alive pings that have actually fired. Omitted while zero, which is the
    default.
  • cg! — the proxy is unreachable. Nothing else about the line is silent, including every
    project that is not routed through context-guru at all.

Plus the explicit, opt-in way to turn the existing idle keep-alive mechanism on
(/context-guru:keepalive), and the settings-management extension the status line needed
(--statusline on settings.py).

Confirmed field names, not memory

Grepped the installed CLI binary directly for the statusLine contract (a past session learned the
hard way that trusting a docs summary over the binary was wrong):

  • Settings schema: "statusLine": {"type": "command", "command": "...", "padding": 0, "refreshInterval": <seconds>, "hideVimModeIndicator": false} — found the exact validator:
    type:C("command"),command:s(),padding:w().optional(),refreshInterval:w().min(1).optional()...
  • Stdin payload: reconstructed from the function that builds it (Elo) and its base (ba).
    The useful find: Claude Code already tracks prompt-cache warmth itself — prompt_cache: {warm, ttl, expires_at, requests, misses, hit_ratio, cache_write_tokens, ...} — omitted while
    requests === 0. That is the "stopper" this feature needed, for free.
  • Proxy routes: confirmed against a real running proxy (curl, no auth needed in this plugin's
    single-tenant mode) — /stats and /api/stats real field names quoted in the script's own
    comments, not invented. No /config route exists (confirmed 404). Keep-alive can only be turned
    on via --config <yaml>, which replaces --preset entirely (loadConfig in
    cmd/context-guru-proxy/main.go) — the one measurement trap this design works around by always
    writing the active preset into that config.

Real rendered lines, from a real Claude Code session through this worktree's built proxy

Driven via tmux (a real interactive session, CLAUDE_CONFIG_DIR pointed at a throwaway config
dir, chained behind this box's real gateway — never port 4000, never the box's production
service). Real numbers, cross-checked against /api/stats directly each time:

before any response:      cache –
after the first response: cache 4:57
after a second turn:       cache 4:49 | $0.01/0 saved      (api/stats: total_saved_usd=0.012949, saved_unique=0)

Then, with the keep-alive toggle turned on and the proxy restarted (confirmed via ps that
--config was passed and pipeline=[cachesplit] — the preset survived), a real idle ping fired
after the default 280s idle window:

cache cold | $-0.02/0 saved | ka 1p

/api/stats at that moment: keepalive_pings=1, keepalive_ping_usd=0.0169, keepalive_net_usd=-0.0169 (nothing had yet come back to recoup it — a real negative net, shown
rather than hidden). Worth calling out explicitly: cache cold here does not mean the
provider's own cached entry is actually cold — it means Claude Code's own tracker, which cannot
see a ping the proxy sent while nobody was typing, thinks it is. Documented as a caveat rather
than something the render tries to paper over.

Why the render path never pings

The only network call statusline.py makes is a GET. Turning keep-alive on is a separate skill
(/context-guru:keepalive) that writes a small YAML config, never something the status line
triggers — a display hook that fires on nearly every keystroke must not double as an unbounded
traffic generator billed to the user.

Tests

go test ./context-guru-plugin/... -v -count=1: 86 === RUN lines, all pass. New coverage:
self-gating (including the litellm-port-collision case), a stalling proxy reported as cg!
inside its own timeout, malformed stdin/stats survived gracefully, exact TTL boundary math (a
frozen-clock unit test isolates remaining == 0 precisely, since a subprocess always burns real
wall-clock time and a naive table test cannot reliably hit that instant), zero- and
negative-savings segments, response caching (one real HTTP call across 5 rapid renders),
settings.py's new statusLine key (round trip, conflict refusal + --force, statusline-only
install touching no routing), and the keep-alive config's write/refuse/remove/start-proxy.sh
pickup wiring.

Every load-bearing check was revert-verified: the defect reintroduced in the source (never the
test), confirmed the test failed naming its own subject, then reverted. Examples:

  • self-gate accepting any loopback URL → TestStatuslineIsSilentWhereRoutingIsNotConfigured/another_local_proxy... failed with printed "cache –\n" for a project that is not routed through us
  • down-detection mislabeled as merely-unavailable → both TestStatuslineReportsADownProxyInThreeCharacters and TestStatuslineNeverFailsOnAConnectionRefusal failed, got "cache –", want the down indicator "cg!"
  • TTL boundary off-by-one (remaining < 0 instead of <= 0) → TestStatuslineCacheCountdownExactBoundary failed: got "cache 0:00", want "cache cold"
  • zero-savings omission removed → TestStatuslineOmitsZeroSavings failed: a zero-valued segment was shown: "cache – | $0.00/0 saved\n"
  • negative-savings folded into the same omission → TestStatuslineShowsANegativeNetHonestly failed: got "cache –\n", want a segment showing the negative net, not an omission
  • settings.py statusline conflict check disabled → TestSettingsStatuslineRefusesToStealAnExisting failed: expected a conflict (exit 2), got exit 0
  • apply_statusline call dropped from the fresh-install branch → TestSettingsStatuslineRoundTrips failed: statusLine = <nil>
  • statusline-only removal branch disabled → TestSettingsStatuslineOnlyInstallTouchesNoRouting failed: fell through to note:no env.ANTHROPIC_BASE_URL here
  • start-proxy.sh's --config auto-pickup removed → TestStartProxyPicksUpAKeepaliveConfig failed: wanted --config .../keepalive-....yaml in argv, got "--listen ..."

Isolation

Built and ran everything under /tmp/cgfix/pr160b and /tmp/cgfix/ccdir160b
(CLAUDE_CONFIG_DIR, verified the CLI honours it), proxies only on ports 4055/4056. sha256sum
of the real ~/.claude/settings.json is byte-identical before and after this entire session.
Ports 4050–4059 swept clean at the end; the throwaway state/dashboard DBs and config dirs are
under /tmp/cgfix/, outside any tracked repo path.

What was deliberately not built

  • No render-path ping — read-only, always; see above.
  • No second keep-alive scheduler — the toggle switches on the existing proxy/keepalive.go
    mechanism via the config file the binary already reads; nothing new schedules a ping.
  • No settings.json wiring for the keep-alive toggle. It lives in its own file under the
    plugin's existing state directory (alongside the pidfile and dashboard DB it already keeps
    there), picked up by start-proxy.sh on the next (re)start — simpler than a third settings.json
    key for a decision that is really "does this file exist."
  • No on-demand "ping now" button. The local single-tenant proxy has no route for that (the
    multi-tenant /api/me/keepalive/sessions control routes need a web session cookie this mode has
    no login flow for); the only real mechanism is the idle scheduler, armed for future gaps.

Osher-Elhadad added 2 commits September 6, 2026 15:18
Renders the prompt-cache TTL countdown, running savings and keep-alive
activity in the terminal's status line for any project routed through
context-guru's proxy, and adds the explicit opt-in to arm the existing
idle keep-alive mechanism that a status line must never trigger itself.

statusline.py self-gates exactly like the two hooks (matching the
configured port, not any loopback /anthropic URL), reads its cache
countdown from Claude Code's own client-side cache tracker at zero
network cost, and makes one short-timeout, short-TTL-cached call to
/api/stats for savings and keep-alive counts. It never writes anything;
turning keep-alive on writes a small YAML config that start-proxy.sh
picks up on its next start, preserving the active preset explicitly
since --config replaces --preset rather than layering over it.

settings.py is extended, not replaced, to manage the new top-level
statusLine key with the same conservatism as the existing routing key:
refuses to replace a foreign value without --force, records what it
replaces, and restores it on removal. A --statusline-only call (no
--url) installs the status line without implicitly starting routing.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
Adds a Status line section covering what each segment means, why the
render path never sends a keep-alive ping, and the honest nuance that
"cache cold" no longer proves the underlying entry is cold once
keep-alive is running — the countdown is Claude Code's own view of its
last request, which cannot see a ping the proxy sent in between turns.

Signed-off-by: Osher-Elhadad <Osher.Elhadad@ibm.com>
@OsherElhadad

Copy link
Copy Markdown
Collaborator Author

This PR's base branch (feat/context-guru-plugin) was deleted once #160 merged, which closed this PR and left it un-reopenable (GitHub refuses to reopen a PR whose base ref no longer exists). Reworked with the default line inverted (savings against session totals, cache/keep-alive now opt-in) and rebased onto main in #218.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants