SLOPSHOPPER

ctx-keeper

Cache countdown, context, limits and cost band; past 350k it runs /re-fresh, copies it, compacts and pastes it back

newbandguardtoastprocesstimer
v0.1.0no licenseupdated 2026-10-05irfanfaraaz/claude-kit/mods/ctx-keeper
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ctx-keeper
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ● cache warm 60m | ctx 97k/350k | $0.97 | $0.05 | 5h 31% w: auto-warm: on c: compact h: hand-off now ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
● cache warm 60m | ctx 97k/350k | $0.97 | $0.05 | 5h 31% w: auto-warm: on c: compact h: hand-o ⟨Claude Code's own drawing⟩
README

claude-kit

My Claude Code setup: local mods, skills, and the plugins I run.

Mods

Local plugins built with function hooks (panes, bands, guards). Load one with:

claude --plugin-dir mods/<name>
ModWhat it does
carousel-shipAfter a carousel render: Caption, Queue Threads and Log ledger buttons; blocks Threads posts with a part over 500 characters
commit-guardNever commits docs/superpowers, never pushes unless you said push, asks before committing on main/staging/prod
ctx-keeperCache countdown, context, limits and cost band; past 350k it runs /re-fresh, copies it, compacts and pastes it back
env-bootstrapPer-project Node version on PATH and env file auto-loaded for Bash, so Claude stops typing nvm use / source .env
git-bandGit state above the prompt: branch, upstream, ahead/behind, Claude vs your changes, PR and CI, worktree, stash; toasts where each commit landed
hybrid-agentsSubagents default to Sonnet; pasting a plan path adds the hybrid rule (inline small tasks, Sonnet subagents for big ones)
rm-to-trashRewrites rm into macOS trash so deletes are recoverable and stop hitting the rm deny rule
tool-preloadKeeps context-mode, jcodemunch, claude-mem, WebSearch, Task and Supabase tools loaded so Claude stops calling ToolSearch for them
turn-coachOne fork per turn: a verdict on work turns (solved, gaps, shortcuts) plus up to 3 next prompts, in one compact block above the prompt. Merges next-steps and next-steps-supervisor.

Skills

102 skills in skills/. Copy any folder into ~/.claude/skills/ to use it.

SkillDescription
alibabacloud-ack-cli>
alibabacloud-cfw-internet-firewall-protect>
alibabacloud-cfw-status-overview/
alibabacloud-cli-guidance>
alibabacloud-ddos-security-monitor/
alibabacloud-domain-manage/
alibabacloud-ecs-diagnose/
alibabacloud-esa-pages-deployDeploy HTML pages, static directories, or custom edge functions to Alibaba Cloud ESA edge nodes. Manage Edge KV for distributed key-value st
alibabacloud-find-skills>
alibabacloud-network-health-inspection>
alibabacloud-network-reachability-analysis>
alibabacloud-nginx-ingress-to-api-gateway/
alibabacloud-oss-manage-cron-upload/
alibabacloud-resourcecenter-search/
alibabacloud-solution-deploy>
alibabacloud-tair-ai-assistant/
alibabacloud-tair-devtoolset/
alibabacloud-terraform-code-generation/
alibabacloud-waf-bot-management>
alibabacloud-waf-security-monitor/
api-design-principlesMaster REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designin
apply-grantPrepare an Agentic Engineering Grant application by gathering project data, git history, and context files, then presenting all fields neede
architecture-patternsImplement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use when arch
brand-designGenerate, preview, and apply a brand color palette (plus typography, gradients, and tone/voice) to a frontend project. Use when a user says
broRe-explain the previous assistant message in a much simpler way, for when the reply made you go 'bro what'. Use /bro to get a plain-languag
build-data-pipelineGuide a developer through building a Solana data pipeline or indexer. Use when a user says "build an indexer", "data pipeline", "analytics",
build-defi-protocolGuide a developer through building a DeFi protocol on Solana. Use when a user says "build a DEX", "AMM", "lending protocol", "vault", "yield
build-mobileGuide a developer through building a Solana mobile app. Use when a user says "build a mobile app", "React Native Solana", "Solana mobile", "
build-with-claudeGuide a developer through building their Solana MVP step by step using Claude Code. Use when a user says "help me build this", "start the MV
carousel-creatorTurns any content idea or Virlo trend data into a complete Instagram carousel post with hooks, slide by slide copy, and posting strategy. Ju
cinescrollBuild premium scroll-driven landing pages where scroll position controls what happens on screen, frame-scrubbed hero sequences, pinned narr
code-reviewReview code for quality, security, and best practices following team standards. Use when reviewing pull requests, examining code changes, or
coding-agentInvoke when ready to CODE a feature. Manages the entire feature development lifecycle through 8-step session cycles - reads project state, s
colosseum-copilotSearch and analyze 5,400+ Solana hackathon projects using Colosseum Copilot. Find similar projects, discover winner patterns, identify gaps,
communication-styleFollow expert-level communication preferences: be casual, terse, concise, anticipate needs, provide actual code/explanations not theory, giv
competitive-landscapeMap the competitive landscape for a crypto product idea. Use when a user says "who are my competitors", "map the competitive landscape", "wh
context7-documentationUse Context7 MCP server to reference up-to-date documentation for libraries and frameworks. Use when working with libraries mentioned by the
cqrs-implementationImplement Command Query Responsibility Segregation for scalable architectures. Use when separating read and write models, optimizing query p
create-pitch-deckCreate a structured pitch deck for a crypto project. Use when a user says "create a pitch deck", "help me pitch", "I need slides", "prepare
create-prdCreate a Product Requirements Document from conversation context. Use when planning new products, features, or when documenting requirements
cso/
data
database-migrationExecute database migrations across ORMs and platforms with zero-downtime strategies, data transformation, and rollback procedures. Use when
debug-programHelp a developer debug a failing Solana program or transaction. Use when a user says "debug my program", "program error", "transaction faile
defillama-researchResearch DeFi protocols and market opportunities using DefiLlama data. Use when a user says "show me TVL data", "which protocols are growing
deploy-to-mainnetGuide a Solana project from devnet to mainnet production deployment. Use when a user says "deploy to mainnet", "go to production", "deployme
ego-browserWhen you need a browser, read this Skill by default. Use it to open and operate websites, fill forms, click buttons, take screenshots, extra
elevenlabs-musicGenerate music, sound effects, and voice clips using the ElevenLabs API, then preview them in a web-based player. Use when asked to generate
event-store-designDesign and implement event stores for event-sourced systems. Use when building event sourcing infrastructure, choosing event store technolog
feature-verifyTest features after coding them. Invoke after implementing a feature to run tests and mark pass/fail status in features.json. Three modes -
find-next-crypto-ideaInterview users sharply to discover, rank, or validate what they should build in crypto. Use when a user asks what to build in crypto, wants
fluxwright-designDesign system skill for fluxwright. Activate when building UI components, pages, or any visual elements. Provides exact color tokens, typogr
frontend-design-guidelinesApply high-quality web interface design rules when building, reviewing, or styling frontend code. Use when the user says "build a frontend",
higgsfield-game-generation/
higgsfield-generate/
higgsfield-marketplace-cards/
higgsfield-product-photoshoot/
higgsfield-soul-id/
higgsfield-video-explainer/
higgsfield-websites/
launch-tokenGuide a developer through launching a token on Solana. Use when a user says "launch a token", "create a token", "pump.fun", "bonding curve",
learn/
marketing-videoCreate marketing videos for Solana projects using Remotion (code-driven) and Renoise (AI-generated). Use when a user says "marketing video",
mcp-builderGuide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-design
memory-managementUse mem0 memory tool to store and retrieve context across sessions. Use when storing project decisions, user preferences, technical patterns
merge-conflict-resolver[TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios,
microservices-patternsDesign microservices architectures with service boundaries, event-driven communication, and resilience patterns. Use when building distribut
monorepo-managementMaster monorepo management with Turborepo, Nx, and pnpm workspaces to build efficient, scalable multi-package repositories with optimized bu
navigate-skillsMeta skill, browse all installed solana-new skills, repos, and MCPs to find the right tool for any task
neuroarxivGrounds a coding agent's architecture decisions in real arXiv prior art before it builds something new. Reads arXiv category-wise via real H
plan-featureCreate comprehensive feature plans with deep codebase analysis and research. Use when planning new features, breaking down complex tasks, or
postgresqlDesign a PostgreSQL-specific schema. Covers best-practices, data types, indexing, constraints, performance patterns, and advanced features
product-reviewProduct quality review, UX flows, onboarding, feature completeness, and user value. Use when a user says "product review", "review my produ
project-initializerSet up the two-agent pattern for a new project. Run once at start with /project-initializer "project description". Creates features.json (yo
project-statusGet a quick project snapshot. Invoke at session start or anytime to see current progress - how many features are done/tested/blocked, recent
projection-patternsBuild read models and projections from event streams. Use when implementing CQRS read sides, building materialized views, or optimizing quer
re-fresh/
react-componentsConverts Stitch designs into modular Vite and React components using system-level networking and AST-based validation.
reddit-post-draft>
refactorRefactor code with specific focus (performance, readability, maintainability). Use when improving code quality, optimizing code structure, o
research-gateRoutes "how should I build this" research to the right depth so the cheap answer runs first and the expensive one only fires when it earns i
review-and-iterateReview Solana project code for quality, security, and production readiness. Use when a user says "review my code", "is this production ready
roast-my-productHarsh, honest product critique, find every weakness before users do. Use when a user says "roast my product", "harsh feedback", "be brutal"
scaffold-projectSet up a complete Solana project workspace from a validated idea. Use when a user says "scaffold my project", "set up my workspace", "what s
short-form-script-writerTurns any content idea into scroll stopping hooks and a complete short form script ready to film for Instagram Reels or TikTok. No prompting
skill-loaderDiscover and load symlinked skills from ~/.cursor/skills/ when needed. Use when a required skill wasn't auto-discovered by Cursor, or when c
solana-beginnerTeach Solana fundamentals to developers new to the ecosystem. Use when a user says "what is Solana", "why Solana", "new to Solana", "explain
static-visual-creatorTurns any script line or concept into a polished 4K PNG visual ready to drop straight into your video edit. No Photoshop or Canva needed. Ju
stock-fundamental-analyzerAnalyse a listed stock on fundamentals only, Indian (NSE/BSE) or US (NYSE/NASDAQ), with Sharia/halal compliance screening built into every
storm-researchUse when someone asks to run Storm Research, use the storm-research skill, run the STORM method on a topic, says \"storm research this\" / \
submit-to-hackathonPrepare and optimize a hackathon submission for a Solana project. Use when a user says "submit to hackathon", "prepare my submission", "hack
supabase-mcpUse Supabase MCP server tools for database operations, queries, and data retrieval. Use when reading from Supabase database, querying tables
superpowers
superpowers-gateUse when user gives a coding task (feature, fix, refactor, change). Runs BEFORE brainstorming, writing-plans, or TDD. Decides whether to imp
templateReplace with description of the skill and when Claude should use it.
thinking-toolUse thinking tools (mcp_think or sequential thinking) to organize reasoning after context changes. Use when examining files, running command
toggle-two-agentSet up the two-agent pattern for a new project. Run once at the start with /project-initializer to create features.json and init.sh structur
validateRun comprehensive validation of the project including linting, tests, and build. Use when validating code quality, checking for regressions,
validate-ideaRun a structured validation sprint on a crypto startup idea. Use when a user says "validate this idea", "is this worth building", "run a val
video-generatorAI video production workflow using Remotion. Use when creating promotional videos, product demos, social media videos, animated explainers,
virtual-solana-incubatorDeep technical Solana bootcamp, SVM architecture, Rust patterns, program development. Use when a user says "Solana incubator", "teach me Ru
website-builderBuild a cinematic, high-fidelity landing page from a brief description, then deploy it live on Netlify. Use when creating websites, building

Third-party skills

Installed from their authors' repos, not copied here. Install with npx skills add <source>.

SkillSource
3d-web-experiencedavila7/claude-code-templates
adaptpbakaus/impeccable
advise-project-approachAaravKashyap12/advise-project-approach
agent-browservercel-labs/agent-browser
alibabacloud-tech-solution-animation-creation-auto-deployaliyun/alibabacloud-aiops-skills
animatepbakaus/impeccable
archifytt-a1i/archify
architecture-decision-recordswshobson/agents
architecture-designerjeffallan/claude-skills
arrangepbakaus/impeccable
auditpbakaus/impeccable
aws-diagramseraserlabs/eraser-io
bencium-controlled-ux-designerbencium/bencium-claude-code-design-skill
bolderpbakaus/impeccable
clarifypbakaus/impeccable
cloud-architectjeffallan/claude-skills
cmuxmanaflow-ai/cmux
cmux-browsermanaflow-ai/cmux
colorizepbakaus/impeccable
copywritingcoreyhaines31/marketingskills
critiquepbakaus/impeccable
database-schema-designersoftaworks/agent-toolkit
delightpbakaus/impeccable
design-mdgoogle-labs-code/stitch-skills
design-taste-frontendLeonxlnx/taste-skill
devops-engineerjeffallan/claude-skills
distillpbakaus/impeccable
dogfoodvercel-labs/agent-browser
drawiobahayonghang/drawio-skills
enhance-promptgoogle-labs-code/stitch-skills
eraser-diagramseraserlabs/eraser-io
excalidrawdavila7/claude-code-templates
excalidraw-diagram-generatorgithub/awesome-copilot
extractpbakaus/impeccable
find-skillsvercel-labs/skills
fixfacebook/react
framer-motion-animatorpatricio0312rev/skills
frontend-designanthropics/skills
full-output-enforcementLeonxlnx/taste-skill
github-actions-templateswshobson/agents
gsapheygen-com/hyperframes
gsap-awwwards-websiteeng0ai/eng0-template-skills
gsap-reactbbeierle12/skill-mcp-claude
gsap-scrolltriggergreensock/gsap-skills
gsap-sequencingbbeierle12/skill-mcp-claude
hallmarknutlope/hallmark
hardenpbakaus/impeccable
high-end-visual-designLeonxlnx/taste-skill
hyperframesheygen-com/hyperframes
hyperframes-cliheygen-com/hyperframes
hyperframes-registryheygen-com/hyperframes
impeccablepbakaus/impeccable
industrial-brutalist-uiLeonxlnx/taste-skill
instagram-researchbradautomates/head-of-content
install-anti-slopdmmulroy/anti-slop
m01-ownershipzhanghandong/rust-skills
m06-error-handlingzhanghandong/rust-skills
m07-concurrencyzhanghandong/rust-skills
m10-performancezhanghandong/rust-skills
marketing-psychologycoreyhaines31/marketingskills
mermaid-diagramssoftaworks/agent-toolkit
minimalist-uiLeonxlnx/taste-skill
motionjezweb/claude-skills
next-cache-componentsvercel-labs/next-skills
normalizepbakaus/impeccable
onboardpbakaus/impeccable
optimizepbakaus/impeccable
overdrivepbakaus/impeccable
phaser-gamedevchongdashu/phaserjs-tinyswords
polishpbakaus/impeccable
presentation-buildersupercent-io/skills-template
qmdtobi/qmd
quieterpbakaus/impeccable
ralph-gpuvercel-labs/ralph-gpu
react:componentsgoogle-labs-code/stitch-skills
recipe-create-presentationgoogleworkspace/cli
redditresciencelab/opc-skills
reddit-marketingopenclaudia/openclaudia-skills
reddit-postskostja94/marketing-skills
redesign-existing-projectsLeonxlnx/taste-skill
remotiongoogle-labs-code/stitch-skills
remotion-best-practicesremotion-dev/skills
rust-async-patternswshobson/agents
rust-best-practicesapollographql/skills
rust-engineerjeffallan/claude-skills
script-writerailabs-393/ai-labs-claude-skills
scroll-experiencesickn33/antigravity-awesome-skills
scrollytellingdoodledood/claude-code-plugins
shadcn-uigoogle-labs-code/stitch-skills
shapepbakaus/impeccable
skill-creatoranthropics/skills
social-media-marketingdengineproblem/agents-monorepo
software-crypto-web3vasilyu1983/ai-agents-public
solana-devsolana-foundation/solana-dev-skill
solana-kitsendaifun/skills
stitch-designgoogle-labs-code/stitch-skills
stitch-design-tasteLeonxlnx/taste-skill
stitch-loopgoogle-labs-code/stitch-skills
system-designjwynia/agent-skills
threejsmrgoonie/claudekit-skills
threejs-postprocessingcloudai-x/threejs-skills
threejs-shaderscloudai-x/threejs-skills
tweet-writersanky369/vibe-building-skills

| `types

Source 2 files
hooks/register.tsx 308 lines
1// ctx-keeper: a band above the prompt with the prompt-cache countdown, context
2// size, cache rewrite cost, 5h/week limits and session cost (cache-keeper's
3// pills), plus Irfan's hand-off flow. When context passes 350k and nothing is
4// still running (no turn, no open tasks, no subagents), it runs /re-fresh,
5// copies the hand-off to the clipboard (and saves it), compacts, then sends the
6// hand-off back with "do nothing, reply ready".
7
8import { atom, read, update } from 'claude-code'
9import type { EngineInterface, Register, RenderElement } from 'claude-code'
10
11import type { Meter, Phase } from '../types'
12
13const THRESHOLD = 350_000
14const CACHE_MIN = 60 // settings.json promptCacheTtl "1h" (main chat); subagents stay 5m
15const WARN_MIN = 1
16const COOLDOWN_MS = 10 * 60_000
17// Auto keep-warm: from Irfan's idle gaps, pinging pays off up to ~20m; past that a rewrite is cheaper.
18// ponytail: with a 1h cache this never fires inside the 20m window; raise AUTO_WARM_MAX_MIN (~180) to bridge 1-3h breaks
19const PING_AT_MIN = CACHE_MIN - 1
20const AUTO_WARM_MAX_MIN = 20
21const LIMIT_STOP_PCT = 90
22
23const meter = atom({ plugin: 'ctx-keeper', key: 'meter' } as const, {
24  tokens: 0,
25  fiveHour: null,
26  week: null,
27  usd: null,
28  lastTurnAt: 0,
29} as Meter)
30const phase = atom({ plugin: 'ctx-keeper', key: 'phase' } as const, 'idle' as Phase)
31const autoWarm = atom({ plugin: 'ctx-keeper', key: 'autoWarm' } as const, true as boolean)
32
33const REFRESH_ARGS =
34  'full Goal: continue the current work in this same chat right after compaction. Stays here, nothing travels.'
35
36// ponytail: 1h cache-write price per Mtok (2x input) by model family; edit if pricing changes
37function usdPerMtok(model: string): number {
38  if (/haiku/i.test(model)) return 2
39  if (/sonnet/i.test(model)) return 6
40  return 10
41}
42
43function fmtK(n: number): string {
44  return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n)
45}
46
47function extractHandoff(answer: string): string {
48  const blocks = [...answer.matchAll(/```[a-z]*\n([\s\S]*?)```/g)].map(m => (m[1] ?? "").trim())
49  const longest = blocks.sort((a, b) => b.length - a.length)[0]
50  return longest !== undefined && longest.length > 200 ? longest : answer.trim()
51}
52
53// Module state: a hot reload resets it, which only forgets open tasks.
54const openTasks = new Set<string>()
55let isTurnRunning = false
56let lastAnswerOk = true
57let lastFlowAt = 0
58let pendingHandoff: string | null = null
59let warnedFor = 0
60let lastHumanAt = Date.now() // end of the last human turn; starts at load so a reload cannot ping forever
61let pingInFlight = false
62
63function autoWarmBlock(m: Meter, now: number): string | null {
64  if (m.fiveHour !== null && m.fiveHour >= LIMIT_STOP_PCT) return `5h ≥${LIMIT_STOP_PCT}%`
65  if (lastHumanAt > 0 && now - lastHumanAt >= AUTO_WARM_MAX_MIN * 60_000) return `idle ${AUTO_WARM_MAX_MIN}m+`
66  return null
67}
68
69function ping($: EngineInterface): void {
70  pingInFlight = true
71  void $.prompt.submit({ text: 'Cache keep-warm ping. Reply with only: ok' })
72}
73
74async function startFlow($: EngineInterface): Promise<void> {
75  if ((await read($, phase)) !== 'idle') return
76  lastFlowAt = await $.clock.now()
77  await update($, phase, () => 'refresh')
78  $.ui.toast('ctx-keeper: writing the hand-off with /re-fresh')
79  try {
80    await $.command.run({ command: 're-fresh', args: REFRESH_ARGS })
81  } catch (error) {
82    $.ui.toast(`ctx-keeper: /re-fresh failed: ${String(error)}`)
83    await update($, phase, () => 'idle')
84  }
85}
86
87async function finishFlow($: EngineInterface, answer: string): Promise<void> {
88  const handoff = extractHandoff(answer)
89  if (handoff.length < 100) {
90    $.ui.toast('ctx-keeper: /re-fresh gave no hand-off, stopped before compacting')
91    await update($, phase, () => 'idle')
92    return
93  }
94
95  // pbcopy is the Mac clipboard and works from the desktop app too; ui.copy is the fallback.
96  const pb = await $.process.run(['pbcopy'], { stdin: handoff, timeoutMs: 5000 }).catch(() => null)
97  const copied = pb?.exitCode === 0 ? { isCopied: true } : await $.ui.copy({ text: handoff })
98  const home = (await $.env.get('HOME')) ?? ''
99  const dir = `${home}/.claude/mods-data/ctx-keeper/handoffs`
100  const stamp = new Date(await $.clock.now()).toISOString().replace(/[:T]/g, '-').slice(0, 16)
101  await $.process.run(['mkdir', '-p', dir])
102  await $.fs.write(`${dir}/${stamp}.md`, handoff)
103  $.ui.toast(
104    `ctx-keeper: hand-off ${copied.isCopied ? 'copied' : 'not copied'} and saved to ${dir}/${stamp}.md. Compacting.`,
105  )
106
107  // Same path as typing /compact; the session.compact hook below sends the paste.
108  pendingHandoff = handoff
109  await update($, phase, () => 'compact')
110  await $.command.run({
111    command: 'compact',
112    args: 'A hand-off prompt is sent right after this. Keep the current task state and file paths.',
113  })
114}
115
116async function paste($: EngineInterface): Promise<void> {
117  const handoff = pendingHandoff
118  pendingHandoff = null
119  if (handoff === null) return void (await update($, phase, () => 'idle'))
120  await update($, phase, () => 'paste')
121  await $.prompt.submit({
122    text:
123      'Hand-off from before compaction is below. Read it and do nothing else: no tools, no work. ' +
124      'Reply with only the word: ready\n\n' +
125      handoff,
126  })
127}
128
129// The size is measured AFTER turn.complete, so the check runs on each measure
130// and after each turn, once nothing is running.
131async function maybeStart($: EngineInterface): Promise<void> {
132  if (isTurnRunning || !lastAnswerOk || openTasks.size > 0) return
133  if ((await read($, phase)) !== 'idle') return
134  const m = await read($, meter)
135  if (m.tokens < THRESHOLD || (await $.clock.now()) - lastFlowAt <= COOLDOWN_MS) return
136  const agents = await $.agent.list().catch(() => [])
137  if (agents.some(a => /run|pend|start|active/i.test(a.status))) return
138  $.clock.after(0, () => void startFlow($))
139}
140
141export const register: Register = on => {
142  on('session.compact', async ($, e, next) => {
143    const result = await next(e)
144    if ((await read($, phase)) !== 'compact') return result
145    if (result.skip !== undefined) {
146      pendingHandoff = null
147      $.ui.toast(`ctx-keeper: compact skipped (${result.skip}); hand-off is on the clipboard`)
148      await update($, phase, () => 'idle')
149    } else {
150      $.clock.after(0, () => void paste($))
151    }
152    return result
153  })
154
155  on('turn.start', async ($, e, next) => {
156    isTurnRunning = true
157    return next(e)
158  })
159
160  on('session.start', async ($, e, next) => {
161    // A reload kills any flow in flight, but $.state keeps its phase: reset it.
162    await update($, phase, () => 'idle')
163    // Every 30s: redraw, auto-ping before the cache goes cold, else warn.
164    $.clock.every(30_000, () => {
165      void (async () => {
166        $.ui.invalidate('ui.render')
167        const m = await read($, meter)
168        if (m.lastTurnAt === 0) return
169        const now = await $.clock.now()
170        const idleMin = (now - m.lastTurnAt) / 60_000
171        const left = CACHE_MIN - idleMin
172        const canPing =
173          (await read($, autoWarm)) &&
174          !isTurnRunning &&
175          !pingInFlight &&
176          (await read($, phase)) === 'idle' &&
177          autoWarmBlock(m, now) === null
178        if (canPing && left > 0 && idleMin >= PING_AT_MIN) return ping($)
179        if (!canPing && left > 0 && left <= WARN_MIN && warnedFor !== m.lastTurnAt) {
180          warnedFor = m.lastTurnAt
181          $.ui.toast(`ctx-keeper: cache goes cold in ${Math.ceil(left)}m.`)
182        }
183      })()
184    })
185    return next(e)
186  })
187
188  on('session.measure', async ($, e, next) => {
189    const five = e.rateLimits.find(r => r.kind === 'five_hour')
190    const week = e.rateLimits.find(r => r.kind === 'seven_day')
191    await update($, meter, m => ({
192      ...m,
193      tokens: e.context.tokens ?? m.tokens,
194      fiveHour: five?.percentUsed ?? m.fiveHour,
195      week: week?.percentUsed ?? m.week,
196      usd: e.cost?.usd ?? m.usd,
197    }))
198    void maybeStart($)
199    return next(e)
200  })
201
202  // Open tasks: a hand-off waits until the task list is clear.
203  // Task tools are not in every build, so the matchers are patterns.
204  on('tool.call', { tool: /^TaskCreate$/ }, async ($, e, next) => {
205    const ran = await next(e)
206    const id = /#?(\d+)/.exec(ran.text ?? '')?.[1]
207    if (id !== undefined) openTasks.add(id)
208    return ran
209  })
210  on('tool.call', { tool: /^TaskUpdate$/ }, async ($, e, next) => {
211    const input = e as unknown as { taskId?: string; status?: string }
212    if (input.taskId !== undefined && (input.status === 'completed' || input.status === 'deleted')) {
213      openTasks.delete(String(input.taskId))
214    }
215    return next(e)
216  })
217
218  on('turn.complete', async ($, e, next) => {
219    const result = await next(e)
220    if (e.agentId !== undefined) return result
221    isTurnRunning = false
222    lastAnswerOk = e.reason === 'answer'
223
224    const now = await $.clock.now()
225    await update($, meter, m => ({ ...m, lastTurnAt: now }))
226    if (!pingInFlight) lastHumanAt = now
227    pingInFlight = false
228    const current = await read($, phase)
229
230    if (current === 'refresh') {
231      const answer = e.answer
232      $.clock.after(0, () => void finishFlow($, answer))
233      return result
234    }
235    if (current === 'paste') {
236      await update($, phase, () => 'idle')
237      $.ui.toast('ctx-keeper: hand-off pasted, Claude is waiting for you')
238      return result
239    }
240
241    void maybeStart($)
242    return result
243  })
244
245  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next): Promise<RenderElement> => {
246    const below = await next(e)
247    if (e.props.hasSurvey) return below
248    const { Box, Text, Button } = $.ui.resolve(e)
249    const m = await read($, meter)
250    const current = await read($, phase)
251    const now = await $.clock.now()
252    const model = await $.session.model()
253    const isAutoOn = await read($, autoWarm)
254
255    const leftMin = m.lastTurnAt === 0 ? null : Math.round(CACHE_MIN - (now - m.lastTurnAt) / 60_000)
256    const isWarm = leftMin !== null && leftMin > 0
257    const cacheColor = !isWarm ? 'red' : leftMin <= WARN_MIN ? 'yellow' : 'green'
258    const ctxColor = m.tokens >= THRESHOLD ? 'red' : m.tokens >= THRESHOLD * 0.8 ? 'yellow' : 'green'
259    const rewriteUsd = (m.tokens / 1_000_000) * usdPerMtok(model)
260    const warmUsd = rewriteUsd / 20 // cache read is 0.1x input, 1h write is 2x
261    const limits = [
262      m.fiveHour === null ? null : `5h ${Math.round(m.fiveHour)}%`,
263    ]
264      .filter(Boolean)
265      .join(' · ')
266
267    return (
268      <Box flexDirection="column">
269        <Box flexWrap="wrap" marginY={1}>
270          <Text color={cacheColor}>● </Text>
271          <Text dimColor>
272            {leftMin === null ? 'cache n/a' : isWarm ? `cache warm ${leftMin}m` : 'cache cold'}
273            {' | '}
274          </Text>
275          <Text color={ctxColor}>ctx {fmtK(m.tokens)}/{fmtK(THRESHOLD)}</Text>
276          <Text dimColor>
277            {' | '}${rewriteUsd.toFixed(2)} | ${warmUsd.toFixed(2)}
278            {limits === '' ? '' : ` | ${limits}`}
279          </Text>
280          {isAutoOn && isWarm && autoWarmBlock(m, now) !== null && (
281            <Text color="yellow"> | auto-warm off ({autoWarmBlock(m, now)})</Text>
282          )}
283          {current !== 'idle' && <Text color="red"> ● {current}</Text>}
284          {!e.props.isWorking && current === 'idle' && <Text>   </Text>}
285          {!e.props.isWorking && current === 'idle' && (
286            <Button key="auto" plain hotkey="w" label={`auto-warm: ${isAutoOn ? 'on' : 'off'}`} onPress={() => {
287              void update($, autoWarm, v => !v).then(() => $.ui.invalidate('ui.render'))
288            }} />
289          )}
290          {!e.props.isWorking && current === 'idle' && <Text>   </Text>}
291          {!e.props.isWorking && current === 'idle' && (
292            <Button key="compact" plain hotkey="c" label="compact" onPress={() => {
293              void $.command.run({ command: 'compact', args: '' }).catch(err => $.ui.toast(`ctx-keeper: compact failed: ${String(err)}`))
294            }} />
295          )}
296          {!e.props.isWorking && current === 'idle' && <Text>   </Text>}
297          {!e.props.isWorking && current === 'idle' && (
298            <Button key="handoff" plain hotkey="h" label="hand-off now" onPress={() => {
299              void startFlow($)
300            }} />
301          )}
302        </Box>
303        {below}
304      </Box>
305    )
306  })
307}
308
types/index.d.ts 16 lines
1export type Meter = {
2  tokens: number
3  fiveHour: number | null
4  week: number | null
5  usd: number | null
6  lastTurnAt: number
7}
8
9export type Phase = 'idle' | 'refresh' | 'compact' | 'paste'
10
11declare module 'claude-code' {
12  interface PluginState {
13    'ctx-keeper': { meter: Meter; phase: Phase; autoWarm: boolean }
14  }
15}
16