feat(plugin): add a Claude Code status line for cache TTL and savings - #217
Closed
OsherElhadad wants to merge 2 commits into
Closed
feat(plugin): add a Claude Code status line for cache TTL and savings#217OsherElhadad wants to merge 2 commits into
OsherElhadad wants to merge 2 commits into
Conversation
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>
Collaborator
Author
|
This PR's base branch ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #160
This branch is
feat/plugin-statusline, based onfeat/context-guru-plugin(#160), and mustmerge after #160. It adds nothing to that PR's own diff — everything here is new files plus
small, additive extensions to
settings.pyandstart-proxy.sh.What this adds
A Claude Code status line (
context-guru-plugin/scripts/statusline.py) that renders, for anyproject routed through context-guru's proxy:
cache 4:12/cache cold— a countdown to the prompt-cache TTL going cold. Sourced fromClaude Code's own client-side cache tracker (
prompt_cachein 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 toreport 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 thedefault.
cg!— the proxy is unreachable. Nothing else about the line is silent, including everyproject 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(
--statuslineonsettings.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):
"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()...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 whilerequests === 0. That is the "stopper" this feature needed, for free.single-tenant mode) —
/statsand/api/statsreal field names quoted in the script's owncomments, not invented. No
/configroute exists (confirmed 404). Keep-alive can only be turnedon via
--config <yaml>, which replaces--presetentirely (loadConfigincmd/context-guru-proxy/main.go) — the one measurement trap this design works around by alwayswriting 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_DIRpointed at a throwaway configdir, chained behind this box's real gateway — never port 4000, never the box's production
service). Real numbers, cross-checked against
/api/statsdirectly each time:Then, with the keep-alive toggle turned on and the proxy restarted (confirmed via
psthat--configwas passed andpipeline=[cachesplit]— the preset survived), a real idle ping firedafter the default 280s idle window:
/api/statsat 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, shownrather than hidden). Worth calling out explicitly:
cache coldhere does not mean theprovider'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.pymakes is aGET. Turning keep-alive on is a separate skill(
/context-guru:keepalive) that writes a small YAML config, never something the status linetriggers — 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=== RUNlines, 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 == 0precisely, since a subprocess always burns realwall-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 newstatusLinekey (round trip, conflict refusal +--force, statusline-onlyinstall touching no routing), and the keep-alive config's write/refuse/remove/
start-proxy.shpickup 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:
TestStatuslineIsSilentWhereRoutingIsNotConfigured/another_local_proxy...failed withprinted "cache –\n" for a project that is not routed through usTestStatuslineReportsADownProxyInThreeCharactersandTestStatuslineNeverFailsOnAConnectionRefusalfailed,got "cache –", want the down indicator "cg!"remaining < 0instead of<= 0) →TestStatuslineCacheCountdownExactBoundaryfailed:got "cache 0:00", want "cache cold"TestStatuslineOmitsZeroSavingsfailed:a zero-valued segment was shown: "cache – | $0.00/0 saved\n"TestStatuslineShowsANegativeNetHonestlyfailed:got "cache –\n", want a segment showing the negative net, not an omissionsettings.pystatusline conflict check disabled →TestSettingsStatuslineRefusesToStealAnExistingfailed:expected a conflict (exit 2), got exit 0apply_statuslinecall dropped from the fresh-install branch →TestSettingsStatuslineRoundTripsfailed:statusLine = <nil>TestSettingsStatuslineOnlyInstallTouchesNoRoutingfailed: fell through tonote:no env.ANTHROPIC_BASE_URL herestart-proxy.sh's--configauto-pickup removed →TestStartProxyPicksUpAKeepaliveConfigfailed:wanted --config .../keepalive-....yaml in argv, got "--listen ..."Isolation
Built and ran everything under
/tmp/cgfix/pr160band/tmp/cgfix/ccdir160b(
CLAUDE_CONFIG_DIR, verified the CLI honours it), proxies only on ports 4055/4056.sha256sumof the real
~/.claude/settings.jsonis 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
proxy/keepalive.gomechanism via the config file the binary already reads; nothing new schedules a ping.
plugin's existing state directory (alongside the pidfile and dashboard DB it already keeps
there), picked up by
start-proxy.shon the next (re)start — simpler than a third settings.jsonkey for a decision that is really "does this file exist."
multi-tenant
/api/me/keepalive/sessionscontrol routes need a web session cookie this mode hasno login flow for); the only real mechanism is the idle scheduler, armed for future gaps.