SLOPSHOPPER

cache-tax

A Claude Code mod that keeps the one-hour prompt cache warm during breaks and asks before a cold send, with its estimated rewrite cost and a Send anyway…

newbandcommandstatuspromptmodel
v2.2.1-ask.3MITupdated 2026-10-16grvermeulen/claude-mods/plugins/cache-tax
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-tax
› 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 › /keepwarm ⎿ cache-tax: keepwarm on for 6h00m, a ping 50m after each idle stretch keeps the cache read, not re-written ⟨Claude Code's own drawing⟩ ▣ [>] cache-tax · warm · keepwarm 6h00m left · ping in 50m ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ▣ [>] cache-tax · warm · keepwarm 6h00m left · ping in 50m
README

claude-mods

Claude Code mods: tweaked copies of community mods, as a plugin marketplace.

cache-tax, with a Send anyway button

A copy of cache-tax 2.2.1 by Karan Bansal. The original drops a prompt sent on a cold prompt cache and asks you to send it again to pay. This copy asks first, in Claude Code's own dialog:

  • Send anyway ($x) lets the same prompt through as your own message. keepwarm then holds the cache for 3 hours.
  • Don't send drops it, nothing paid.

The question says what each answer saves. Closing the dialog without an answer falls back to the original behaviour: refused once, the resend pays.

/cache-tax guard test asks the question on your next prompt without a cold cache, priced at the session's context, so you can see it. Nothing is scored.

Everything else (/keepwarm, /cache-tax, the band above the prompt) works as in the original README.

Install

Needs Claude Code 2.1.287 or later.

claude plugin marketplace add grvermeulen/claude-mods
claude plugin install cache-tax@guido-mods

If the original is installed, turn it off, or a cold send is guarded twice:

claude plugin disable cache-tax@claude-code-mods

Restart Claude Code afterwards.

live-voice: talk to Claude, hands free

Claude Code's own voice mode is hold-to-talk or tap-to-talk. live-voice keeps listening instead: say what you want, and after a pause of 1.5 seconds it is sent as your prompt. When Claude answers, the reply is read aloud (code and tables left out, up to 700 characters), then it listens again.

  • By default OpenAI hears and speaks: each utterance goes to gpt-4o-transcribe, and the reply is spoken by gpt-4o-mini-tts with the voice marin, streamed so it starts at once. Claude stays the one answering.
  • /listen engine local keeps everything on your machine instead: faster-whisper hears, a Windows voice speaks (Microsoft Frank for Dutch).
  • The microphone is paused while Claude works and while it talks, so it does not hear itself.
  • A band above the prompt shows what it is doing, with Stop talking and Off buttons.

Windows only for now. Needs Python 3.10 or later, and for OpenAI an API key in the environment variable OPENAI_API_KEY (or CHATGPT_API_KEY), set before Claude starts. OpenAI bills transcription and speech per minute of audio.

claude plugin install live-voice@guido-mods

Then, in Claude Code:

CommandWhat it does
/listen setupOne time: a Python environment in ~/.claude-live-voice (with engine local, also faster-whisper and the Whisper model, about 600 MB)
/listenStart or stop listening
/listen engine localWhisper and a Windows voice instead of OpenAI; engine openai switches back
/listen language enThe language you speak and hear (nl by default, auto to detect)
/listen voice cedarAnother voice: an OpenAI voice (marin, cedar, coral, sage, …), or a Windows voice with engine local
/listen speak offStop reading replies aloud
/listen device jabraUse another microphone, by part of its name or its number
/listen model mediumWith engine local: a larger, slower, more accurate Whisper model; run /listen setup again to download it

Anything near the microphone that sounds like speech becomes a prompt, with the permissions your session has. Turn it off when you are not talking to Claude.

openai-models: GPT in Claude Code, on your ChatGPT plan (experimental)

Signs in with your ChatGPT account through OpenAI's Sign in with ChatGPT for open-source and local apps, then lets an OpenAI model answer the turns of a Claude Code session in Claude's place. Usage counts against your ChatGPT plan, not an API key; ChatGPT's settings show it per app and can cap it.

claude plugin install openai-models@guido-mods
CommandWhat it does
/gpt loginOpens your browser: sign in and allow plan usage. The tokens stay in ~/.config/claude-openai-models/credentials.json
/gpt modelsThe models your account can use
/gpt pickerAdds them to the /model picker, after Claude's, through the modelPicker setting in your user settings; pick one and it answers the session, subagents included. /gpt picker off takes them out again
/gpt use <model>Without the picker: that model answers this session's main loop. A part of the ID will do (astra, 6.1-sol) when it names one model. It stays picked for the session across a plugin reload
/gpt offClaude answers again (/gpt on brings back the last model)
/gpt agents follow · /gpt agents claudeSubagents answer with whoever answers the loop that starts them (the default), or always run on Claude
/gpt usageGPT's requests and tokens in this session, and the last error from OpenAI
/gpt schemasReads Claude Code's tool schemas again (it also does at every session start)
/gpt status · /gpt logoutWho is signed in and who answers · forget the tokens

How it works:

  • Tools. GPT gets every tool the session can call. Claude Code does not hand a mod the tools' schemas, so the mod reads them from the types file Claude Code's plugin tooling writes (all 79 built-in tools on 2.1.295), refreshes them at session start, and ships a copy for when it finds none. A deferred MCP tool gets its schema the way Claude gets it: once ToolSearch has loaded it into the conversation, the mod takes the schema from there. A tool with no schema at all is reachable through one invoke_tool function by its name; Claude Code checks its arguments against the real schema. Names OpenAI does not accept (over 64 characters) are shortened and mapped back.
  • Permissions. Claude Code still runs every tool, with the same permission checks it gives Claude.
  • Subagents started while GPT answers run on GPT too (/gpt agents claude keeps them on Claude; a model the Agent call names stands). A subagent on GPT gets its own conversation and a role naming its type and task; for a plugin's, project's or your own agent, its definition file's instructions too.
  • Compaction of a conversation GPT answers is written by GPT. Another plugin's $.model.complete naming a ChatGPT model, and its $.model.fork while GPT answers, go to OpenAI too.
  • Context. Claude's thinking goes to GPT as text; PDFs as files, text documents as text; images in tool results as images. GPT's own reasoning is carried from step to step (encrypted, as OpenAI returns it), and Claude Code's effort setting becomes GPT's reasoning effort. A model that refuses one of these gets the request again without it, and the band says so.
  • Usage. The band and /gpt usage count GPT's requests and tokens; Claude Code's own cost line does not price GPT. Cache Tax leaves GPT's turns out of Claude's cache clock, size and cost.
  • Sign-in. The ID token's signature is checked against OpenAI's published keys, with its issuer, audience, nonce and expiry. No token is put in a URL or a log line, and on Windows the token folder is cut to your account alone.

Where it stops:

  • Auto mode. Claude Code's auto mode approves actions with Anthropic's server-side classifier, which judges only Claude's own requests. An action GPT asks for that would need that approval is refused, with the reason; the band warns about it. While GPT answers, use the default or accept-edits mode (Shift+Tab).
  • Built-in subagent types (Explore, Plan, general-purpose) on GPT get their role but not Claude Code's internal prompt for that type, which a mod cannot read.
  • Calls with no hook. A call Claude Code makes for itself without a hook a mod can answer still goes to Anthropic with the picked model's name, if Claude Code makes one.
  • Needs Python 3 on the PATH (/gpt python <path> for another); scripts/gpt.py uses only its standard library.

License

MIT. cache-tax is © 2026 Karan Bansal; see plugins/cache-tax/LICENSE. live-voice is © 2026 Guido Vermeulen.

Source 2 files
hooks/register.ts 600 lines
1import type { EngineInterface, Register } from 'claude-code'
2import { statusIcons } from './status-icons'
3
4const TTL_MS = 60 * 60 * 1000
5const PING_AFTER_MS = 50 * 60 * 1000
6const MIN_PING_MS = 60 * 1000
7const AUTO_WARM_MS = 3 * 60 * 60 * 1000
8const DEFAULT_WINDOW_MS = 6 * 60 * 60 * 1000
9const BIG_TOKENS = 50000
10const PING_PROMPT = 'Reply with the single word: warm'
11const KEY_DEADLINE = 'deadline'
12const KEY_EVERY = 'every'
13const KEY_GUARD = 'guard'
14const KEY_ALWAYS = 'always'
15const KEEP_LABEL = "Don't send"
16const SEND_LABEL = 'Send anyway'
17
18// $ per million tokens, [cache read, 1h cache write, output], list prices September 2026.
19// Longer family names first: a model id matches the first row it contains.
20const PRICES: Array<[string, number, number, number]> = [
21  ['fable-5-1', 0.25, 20, 50],
22  ['fable-5', 1, 20, 50],
23  ['opus-5', 0.5, 10, 25],
24  ['opus-4', 0.5, 10, 25],
25  ['sonnet-5', 0.2, 4, 10],
26  ['sonnet', 0.3, 6, 15],
27  ['haiku', 0.1, 2, 5],
28]
29
30type PingRecord = { at: number; read: number; write: number; usd: number | null; warm: boolean }
31type Miss = { at: number; tokens: number; usd: number | null }
32type GuardMode = 'refuse' | 'warn'
33
34export type State = {
35  hasBand: boolean
36  sid: string
37  deadline: number
38  every: number
39  always: boolean
40  lastRequestAt: number
41  lastModel: string | null
42  ctx: number
43  compacted: boolean
44  guard: GuardMode
45  ackedAt: number
46  coldWritePending: boolean
47  misses: Miss[]
48  pending: { cancel: () => void } | null
49  last: PingRecord | null
50  stopped: string | null
51  /** `/cache-tax guard test`: the next prompt asks as if the cache were cold, and nothing is scored. */
52  testOnce: boolean
53  /** The model that answered the main loop's last step; another model's steps leave Claude's cache alone. */
54  lastAnswerModel: string | null
55}
56
57/**
58 * Whether a model is Claude, whose prompt cache this mod tracks. An unknown model counts as
59 * Claude; another provider's model (an OpenAI one through openai-models, say) does not.
60 */
61export function isClaude(model: string | null | undefined): boolean {
62  return !/^(gpt|o\d|chatgpt|codex|gemini|grok|llama|mistral|deepseek|qwen)\b/i.test(model ?? '')
63}
64
65function priceOf(model: string | null): [number, number, number] | null {
66  const m = (model ?? '').toLowerCase().replace(/[\s.]+/g, '-')
67  for (const [family, read, write, output] of PRICES) if (m.includes(family)) return [read, write, output]
68  return null
69}
70
71export function parseDuration(text: string): number | null {
72  const m = /^(?:(\d+)h)?(?:(\d+)m)?$/.exec(text.trim())
73  if (!m || (m[1] === undefined && m[2] === undefined)) return null
74  return (Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0)) * 60 * 1000
75}
76
77export function fmtDuration(ms: number): string {
78  const total = Math.max(0, Math.round(ms / 60000))
79  const h = Math.floor(total / 60)
80  const m = total % 60
81  if (h >= 48) return `${Math.floor(h / 24)}d ${h % 24}h`
82  return h > 0 ? `${h}h${String(m).padStart(2, '0')}m` : `${m}m`
83}
84
85function fmtUsd(usd: number | null): string {
86  return usd == null ? 'n/a' : '$' + (usd >= 100 ? usd.toFixed(0) : usd.toFixed(2))
87}
88
89function fmtTok(n: number): string {
90  return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1000 ? Math.round(n / 1000) + 'k' : String(n)
91}
92
93function coldUsd(s: State): number | null {
94  const price = priceOf(s.lastModel)
95  return price ? s.ctx * price[1] / 1e6 : null
96}
97
98function warmUsd(s: State): number | null {
99  const price = priceOf(s.lastModel)
100  return price ? s.ctx * price[0] / 1e6 : null
101}
102
103/** Everything a fork bills: the cache read, any cache write, uncached input at the base rate (half the 1h write rate), and the output. */
104function pingUsd(u: { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }, price: [number, number, number]): number {
105  return (u.cache_read_input_tokens * price[0] + u.cache_creation_input_tokens * price[1] + u.input_tokens * price[1] / 2 + u.output_tokens * price[2]) / 1e6
106}
107
108/** The read-only upper bound: pings at the cache-read rate that cost as much as one cold write of the same context. */
109function breakEvenPings(s: State): number | null {
110  const price = priceOf(s.lastModel)
111  if (!price || s.ctx <= 0) return null
112  return Math.floor(price[1] / price[0])
113}
114
115function isCold(s: State, now: number): boolean {
116  return s.lastRequestAt > 0 && !s.compacted && now - s.lastRequestAt >= TTL_MS
117}
118
119function costText(s: State): string {
120  const price = priceOf(s.lastModel)
121  const rate = price ? `$${price[1]}/MTok` : 'the cache-write rate'
122  const warm = warmUsd(s)
123  return `Sending this re-writes up to ${s.ctx.toLocaleString('en-US')} tokens at ${rate} = ${fmtUsd(coldUsd(s))}` +
124    (warm == null ? '' : ` (a warm turn would have cost ${fmtUsd(warm)})`) + '.'
125}
126
127function guardText(s: State, now: number): string {
128  return `the prompt cache went cold ${fmtDuration(now - s.lastRequestAt - TTL_MS)} ago. ${costText(s)}`
129}
130
131/** What each answer saves. The dialog's options are bare labels, so the question carries it. */
132function saveText(s: State): string {
133  const cold = coldUsd(s)
134  const warm = warmUsd(s)
135  if (cold == null || warm == null) return `${KEEP_LABEL} pays nothing; after ${SEND_LABEL}, keepwarm holds the cache for ${fmtDuration(AUTO_WARM_MS)}.`
136  return `${KEEP_LABEL} saves ${fmtUsd(cold)}; after ${SEND_LABEL}, keepwarm holds the cache for ${fmtDuration(AUTO_WARM_MS)} ` +
137    `at about ${fmtUsd(warm)} a ping, so the next break does not cost ${fmtUsd(cold)} again.`
138}
139
140function sendLabel(s: State): string {
141  const cold = coldUsd(s)
142  return cold == null ? SEND_LABEL : `${SEND_LABEL} (${fmtUsd(cold)})`
143}
144
145/** Asks in the engine's own dialog, Send anyway first; null when it is dismissed or no one can be asked. */
146async function ask($: EngineInterface, question: string, send: string): Promise<string | null> {
147  try {
148    return await $.ui.ask(question, { header: 'Cache Tax', options: [send, KEEP_LABEL] })
149  } catch {
150    return null
151  }
152}
153
154export type ResumeFields = {
155  source: string
156  model?: string
157  context_tokens?: number
158  seconds_since_last_response?: number
159  prompt_cache_likely_expired?: boolean
160  estimated_cache_write_usd?: number
161}
162
163/** /clear starts a new conversation in the same process; nothing priced before it still exists. */
164export function resetForClear(s: State) {
165  s.ctx = 0
166  s.lastRequestAt = 0
167  s.compacted = false
168  s.ackedAt = 0
169  s.coldWritePending = false
170  s.misses = []
171  disarm(s)
172}
173
174/** Applies a resumed session's fields to the state; returns the line to log, if any. */
175export function seedFromResume(s: State, e: ResumeFields, now: number): string | null {
176  if (e.source !== 'resume' && e.source !== 'fork') return null
177  if (typeof e.context_tokens === 'number' && e.context_tokens > 0) s.ctx = e.context_tokens
178  if (typeof e.seconds_since_last_response === 'number') s.lastRequestAt = now - e.seconds_since_last_response * 1000
179  if (typeof e.model === 'string') s.lastModel = e.model
180  s.compacted = false
181  if (e.prompt_cache_likely_expired !== true || s.ctx < BIG_TOKENS) return null
182  const usd = typeof e.estimated_cache_write_usd === 'number' ? fmtUsd(e.estimated_cache_write_usd) : fmtUsd(coldUsd(s))
183  return `resuming cold. The first message re-writes ${s.ctx.toLocaleString('en-US')} tokens, about ${usd}. /clear and paste a summary if you only need the conclusions.`
184}
185
186function statusText(s: State, now: number): string | undefined {
187  if (s.stopped) return `keepwarm stopped: ${s.stopped}`
188  if (!s.deadline) return undefined
189  const pingText = s.last ? ` · last ping read ${fmtTok(s.last.read)} ${fmtUsd(s.last.usd)}` : ''
190  const nextText = !s.lastRequestAt ? ' · waiting for the first turn'
191    : s.compacted ? ' · waiting for the first turn after compaction'
192    : isCold(s, now) ? ` · cold now, first ping ${fmtDuration(s.every)} after the next turn`
193    : ` · ping in ${fmtDuration(s.lastRequestAt + s.every - now)}`
194  return `keepwarm ${fmtDuration(s.deadline - now)} left${nextText}${pingText}`
195}
196
197function updateStatus($: EngineInterface, s: State, now: number) {
198  if (s.hasBand) $.ui.invalidate('ui.render')
199  else $.ui.status(statusText(s, now))
200}
201
202function disarm(s: State) {
203  if (s.pending) s.pending.cancel()
204  s.pending = null
205}
206
207// The window and its ping period belong to the session that armed them, so a
208// second session, or one resumed from another transcript, never inherits them
209// and cannot turn them off. The always switch and the guard mode stay global.
210function deadlineKey(s: State): string {
211  return `${KEY_DEADLINE}:${s.sid}`
212}
213
214function everyKey(s: State): string {
215  return `${KEY_EVERY}:${s.sid}`
216}
217
218/** Clears this session's own dead window and the bare keys a store written before 2.1.1 still holds. Other sessions' keys are never touched: a read followed by a delete cannot be made atomic against their renewal. */
219async function prune($: EngineInterface, s: State, now: number) {
220  for (const key of [KEY_DEADLINE, deadlineKey(s)]) {
221    const deadline = await $.store.get(key)
222    if (deadline === undefined) continue
223    if (typeof deadline === 'number' && deadline > now) continue
224    await $.store.delete(key)
225    await $.store.delete(KEY_EVERY + key.slice(KEY_DEADLINE.length))
226  }
227}
228
229async function stop($: EngineInterface, s: State, why: string | null, forgetAlways = false) {
230  s.deadline = 0
231  s.every = PING_AFTER_MS
232  s.stopped = why
233  disarm(s)
234  await $.store.delete(deadlineKey(s))
235  await $.store.delete(everyKey(s))
236  if (forgetAlways) {
237    s.always = false
238    await $.store.delete(KEY_ALWAYS)
239  }
240  updateStatus($, s, await $.clock.now())
241}
242
243async function arm($: EngineInterface, s: State) {
244  disarm(s)
245  if (!s.deadline) return
246  const now = await $.clock.now()
247  if (now >= s.deadline) return stop($, s, null)
248  // A cold window still needs expiry cleanup, but must not send a model request.
249  if (s.lastRequestAt && !s.compacted && !isCold(s, now)) {
250    const untilCold = s.lastRequestAt + TTL_MS - now
251    const delay = Math.min(s.deadline - now, untilCold, Math.max(1000, s.lastRequestAt + s.every - now))
252    s.pending = $.clock.after(delay, () => { void ping($, s) })
253  } else {
254    s.pending = $.clock.after(s.deadline - now, () => { void arm($, s) })
255  }
256  updateStatus($, s, now)
257}
258
259async function ping($: EngineInterface, s: State) {
260  s.pending = null
261  if (!s.deadline) return
262  const now = await $.clock.now()
263  if (now >= s.deadline) return arm($, s)
264  // A turn in the meantime re-armed the timer; this callback is stale.
265  if (isCold(s, now)) return arm($, s)
266  // A fork goes to the model answering the main thread; while that is not Claude, it warms nothing of Claude's.
267  if (!isClaude(s.lastAnswerModel)) return stop($, s, `${s.lastAnswerModel} answers this session, so there is no Claude cache to keep warm`)
268  if (now - s.lastRequestAt < s.every - 1000) return
269  let reply
270  try {
271    reply = await $.model.fork({ prompt: PING_PROMPT })
272  } catch (err) {
273    return stop($, s, `the ping failed, ${err instanceof Error ? err.message : String(err)}`)
274  }
275  if (reply === null) return stop($, s, 'the engine did not send the ping, either the snapshot was cold or the API call failed')
276  if (reply.isAnswered === false) {
277    const reason = reply.reason === 'nothing-to-fork' ? 'no conversation to warm yet'
278      : reply.reason === 'api-error' ? `the API call failed${reply.status === null ? '' : ` (${reply.status})`}`
279      : reply.reason === 'aborted' ? 'the ping was interrupted'
280      : 'the ping returned no text'
281    return stop($, s, reason)
282  }
283  const u = reply.usage
284  const price = priceOf(s.lastModel)
285  // A warm ping reads the prefix and writes only its own message; a write of a tenth of the read or more means the prefix broke.
286  const warm = u.cache_read_input_tokens > 0 && u.cache_creation_input_tokens < 0.1 * u.cache_read_input_tokens
287  const usd = price ? pingUsd(u, price) : null
288  s.last = { at: now, read: u.cache_read_input_tokens, write: u.cache_creation_input_tokens, usd, warm }
289  if (!warm) return stop($, s, `the ping read ${fmtTok(u.cache_read_input_tokens)} and wrote ${fmtTok(u.cache_creation_input_tokens)} tokens (${fmtUsd(usd)}), the cache was already gone`)
290  s.lastRequestAt = now
291  await arm($, s)
292}
293
294/** The reply to an arming command; on a cold cache it says when the first ping can come. */
295function armedText(s: State, now: number, windowMs: number): string {
296  if (isCold(s, now)) return `keepwarm on for ${fmtDuration(windowMs)}. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn`
297  return `keepwarm on for ${fmtDuration(windowMs)}, a ping ${fmtDuration(s.every)} after each idle stretch keeps the cache read, not re-written`
298}
299
300async function startWindow($: EngineInterface, s: State, windowMs: number, every: number) {
301  const now = await $.clock.now()
302  s.every = every
303  if (every === PING_AFTER_MS) await $.store.delete(everyKey(s))
304  else await $.store.set(everyKey(s), every)
305  s.deadline = now + windowMs
306  s.stopped = null
307  await $.store.set(deadlineKey(s), s.deadline)
308  await arm($, s)
309}
310
311function card(s: State, now: number): string {
312  const lines: string[] = []
313  lines.push(`${s.lastModel ?? 'model not seen yet'}`)
314  if (s.compacted) lines.push('state       reset by compaction, waiting for the first turn')
315  else if (!s.lastRequestAt) lines.push('state       no request yet this session')
316  else if (isCold(s, now)) lines.push(`state       COLD, last request ${fmtDuration(now - s.lastRequestAt)} ago`)
317  else lines.push(`state       warm, ${fmtDuration(s.lastRequestAt + TTL_MS - now)} left`)
318  lines.push(`context     ${s.ctx.toLocaleString('en-US')} tokens`)
319  lines.push(`cold cost   ${fmtUsd(coldUsd(s))} to re-write it (warm turn ${fmtUsd(warmUsd(s))})`)
320  const always = s.always ? ' (always)' : ''
321  const idle = s.always ? 'off until the next session start, which arms 6h00m (always)' : 'off (/keepwarm to arm it for 6h00m)'
322  lines.push(`keepwarm    ${s.deadline ? (statusText(s, now) ?? '').replace(/^keepwarm /, 'on, ') + always : s.stopped ? `stopped, ${s.stopped}${always}` : idle}`)
323  const pings = breakEvenPings(s)
324  if (pings != null) lines.push(`break-even  up to ${pings} pings at the read rate cost one cold write, about ${fmtDuration(pings * s.every)} of idle at one ping per ${fmtDuration(s.every)}`)
325  lines.push(`guard       ${s.guard === 'refuse' ? 'ask first (/cache-tax guard warn to only show the price)' : 'warn only (/cache-tax guard refuse to be asked first)'}`)
326  const paid = s.misses.reduce((a, m) => a + (m.usd ?? 0), 0)
327  lines.push(`session     ${s.misses.length} cold write${s.misses.length === 1 ? '' : 's'} paid, ${fmtUsd(paid)}`)
328  return lines.join('\n')
329}
330
331export function freshState(): State {
332  return {
333    hasBand: false, sid: '', deadline: 0, every: PING_AFTER_MS, always: false, lastRequestAt: 0, lastModel: null, ctx: 0, compacted: false,
334    guard: 'refuse', ackedAt: 0, coldWritePending: false, misses: [], pending: null, last: null, stopped: null, testOnce: false,
335    lastAnswerModel: null,
336  }
337}
338
339export const register: Register = on => {
340  const s = freshState()
341  let light = false
342  let noColor = false
343
344  on('config.set', { key: 'theme' }, async ($, e, next) => {
345    const result = await next(e)
346    if (result.deny === undefined) {
347      light = typeof result.value === 'string' && result.value.startsWith('light')
348      if (s.hasBand) $.ui.invalidate('ui.render')
349    }
350    return result
351  })
352
353  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
354    const rest = await next(e)
355    if (!s.hasBand || e.props.hasSurvey) return rest
356    const now = await $.clock.now()
357    const text = statusText(s, now)
358    if (!text || (s.deadline && now >= s.deadline)) return rest
359    const state = s.stopped ? 'stopped' : !s.lastRequestAt || s.compacted ? 'unknown' : isCold(s, now) ? 'cold' : 'warm'
360    const color = noColor ? undefined : state === 'warm' ? 'success' : state === 'cold' ? (light ? '#c15f3c' : '#d97757') : undefined
361    const { Box, Text } = $.ui.resolve(e)
362    const asset = statusIcons[light ? 'light' : 'dark'][state === 'warm' || state === 'cold' ? state : 'neutral']
363    const icon = noColor ? Text({ bold: true, children: ['[>]'] })
364      : e.surface === 'terminal' ? $.ui.resolve(e).Image({ source: { png: asset.png }, columns: 3, rows: 1, alt: '[>]' })
365      : e.surface === 'desktop' ? $.ui.resolve(e).Svg({ source: asset.svg, width: 18, height: 18, alt: 'Cache Tax' })
366      : Text({ color, bold: true, children: ['[>]'] })
367    return Box({ flexDirection: 'column', children: [
368      rest,
369      Box({ flexDirection: 'row', alignItems: 'center', children: [
370        Box({ flexShrink: 0, children: [icon] }),
371        Text({ children: [
372          ' cache-tax · ',
373          Text({ color, bold: true, children: [state] }),
374          ` · ${text}`,
375        ] }),
376      ] }),
377    ] })
378  })
379
380  on('session.start', async ($, e, next) => {
381    const r = await next(e)
382    s.hasBand = e.surface === 'terminal' || e.surface === 'desktop'
383    if (s.hasBand) {
384      const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
385      light = typeof theme === 'string' && theme.startsWith('light')
386      noColor = Boolean(await $.env.get('NO_COLOR'))
387      $.ui.status(undefined)
388    }
389    s.sid = await $.session.id()
390    const now = await $.clock.now()
391    await prune($, s, now)
392    const saved = await $.store.get(deadlineKey(s))
393    const savedEvery = await $.store.get(everyKey(s))
394    const savedGuard = await $.store.get(KEY_GUARD)
395    s.deadline = typeof saved === 'number' && saved > now ? saved : 0
396    s.every = typeof savedEvery === 'number' && savedEvery >= MIN_PING_MS ? savedEvery : PING_AFTER_MS
397    s.guard = savedGuard === 'warn' ? 'warn' : 'refuse'
398    s.always = (await $.store.get(KEY_ALWAYS)) === true
399    // Always means a fresh default window every session, whatever the last one left behind.
400    if (s.always) await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
401    const usage = await $.session.usage()
402    if (usage.context.tokens) s.ctx = usage.context.tokens
403    await $.command.register({
404      name: 'keepwarm',
405      description: 'Keep the prompt cache warm: bare for 6h, a window such as 90m, always, off, or status (cache-tax)',
406      argumentHint: '[6h | always | off | status]',
407      immediate: true,
408    })
409    await $.command.register({
410      name: 'cache-tax',
411      description: 'Prompt cache state, cold price and this session\'s cold writes; guard warn|refuse|test (cache-tax, the Mod)',
412      argumentHint: '[status | guard warn | guard refuse | guard test]',
413      immediate: true,
414    })
415    // The hook form of cache-tax ships a /cache-tax:status skill; both installed means two guards.
416    const commands = await $.command.list()
417    if (commands.some(c => c.name === 'cache-tax:status')) {
418      $.ui.log('the hook form (cache-tax@claude-code-hooks) is also installed, so a cold send is warned about or refused twice. Uninstall it, or /cache-tax guard warn here.')
419    }
420    updateStatus($, s, now)
421    return r
422  })
423
424  // Resume fields seed the guard before any turn of the resumed session has run.
425  on('classic.SessionStart', async ($, e, next) => {
426    const r = await next(e)
427    if (e.source === 'clear') {
428      await stop($, s, null)
429      s.stopped = null
430      resetForClear(s)
431      s.sid = await $.session.id()
432      updateStatus($, s, await $.clock.now())
433      return r
434    }
435    const line = seedFromResume(s, e, await $.clock.now())
436    // The resume payload may omit the model; without it the guard cannot price the cold write.
437    if (!s.lastModel) s.lastModel = await $.session.model()
438    if (line) $.ui.log(line)
439    // The seeded clock decides whether a restored or always window pings before the first turn: never when it is cold.
440    await arm($, s)
441    return r
442  })
443
444  on('command.run', { command: 'keepwarm' }, async ($, e) => {
445    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
446    const now = await $.clock.now()
447    if (words[0] === 'off') {
448      const wasAlways = s.always
449      await stop($, s, null, true)
450      return { text: wasAlways ? 'keepwarm is off, and no longer arms itself at session start' : 'keepwarm is off' }
451    }
452    if (words[0] === 'always') {
453      s.always = true
454      await $.store.set(KEY_ALWAYS, true)
455      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
456      const cold = isCold(s, now) ? `. The cache is cold now, so the first ping comes ${fmtDuration(s.every)} after the next turn` : ''
457      return { text: `keepwarm always on: every session starts with a ${fmtDuration(DEFAULT_WINDOW_MS)} window; /keepwarm off turns it off for good${cold}` }
458    }
459    if (!words.length) {
460      await startWindow($, s, DEFAULT_WINDOW_MS, PING_AFTER_MS)
461      return { text: armedText(s, now, DEFAULT_WINDOW_MS) }
462    }
463    if (words[0] !== 'status') {
464      const window = parseDuration(words[0])
465      if (window == null) return { text: 'keepwarm takes a window such as 6h or 90m, or always, off, or status' }
466      // "every 2m" is a testing knob and lasts only for the window it was given with.
467      let every = PING_AFTER_MS
468      if (words[1] === 'every') {
469        const period = parseDuration(words[2] ?? '')
470        if (period == null || period < MIN_PING_MS) return { text: 'every takes a period of at least 1m' }
471        every = period
472      }
473      await startWindow($, s, window, every)
474      return { text: armedText(s, now, window) }
475    }
476    return { text: statusText(s, now) ?? 'keepwarm is off' }
477  })
478
479  on('command.run', { command: 'cache-tax' }, async ($, e) => {
480    const words = String(e.args ?? '').trim().split(/\s+/).filter(Boolean)
481    const now = await $.clock.now()
482    if (words[0] === 'guard') {
483      if (words[1] === 'test') {
484        s.testOnce = true
485        return { text: 'the next prompt asks as if the cache were cold, priced at this session\'s context; nothing is scored' }
486      }
487      if (words[1] !== 'warn' && words[1] !== 'refuse') return { text: '/cache-tax guard takes warn, refuse or test' }
488      s.guard = words[1]
489      await $.store.set(KEY_GUARD, s.guard)
490      return { text: s.guard === 'refuse' ? `guard set to ask: a cold send waits for ${SEND_LABEL} or ${KEEP_LABEL}` : 'guard set to warn: a cold send goes through with its price shown' }
491    }
492    return { text: card(s, now) }
493  })
494
495  // The message that pays. Only its first character is read.
496  on('prompt.submit', async ($, e, next) => {
497    if (e.origin.kind === 'plugin') return next(e)
498    if (typeof e.text !== 'string' || e.text.trimStart().startsWith('/')) return next(e)
499    const now = await $.clock.now()
500    if (s.testOnce) {
501      s.testOnce = false
502      const send = sendLabel(s)
503      const answer = await ask($, `cache-tax test, as if the cache were cold: ${costText(s)} ${saveText(s)} Send it anyway?`, send)
504      return answer === send ? next(e) : { drop: 'cache-tax test: not sent.' }
505    }
506    // Another model answers this prompt (picked in /model, or the last step's): Claude's cold cache costs nothing.
507    if (!isClaude(s.lastAnswerModel) || !isClaude(await $.session.model())) return next(e)
508    if (!isCold(s, now) || s.ctx < BIG_TOKENS) return next(e)
509    if (s.guard === 'warn') {
510      $.ui.log(`${guardText(s, now)} Sending anyway; keepwarm will hold the cache for ${fmtDuration(AUTO_WARM_MS)} once it lands.`)
511      s.coldWritePending = true
512      return next(e)
513    }
514    if (s.ackedAt === s.lastRequestAt) {
515      s.ackedAt = 0
516      s.coldWritePending = true
517      return next(e)
518    }
519    // Ask before the turn starts: the button lets this very prompt through, still the user's own.
520    const send = sendLabel(s)
521    const answer = await ask($, `cache-tax: ${guardText(s, now)} ${saveText(s)} Send it anyway?`, send)
522    if (answer === send) {
523      s.coldWritePending = true
524      return next(e)
525    }
526    const cold = coldUsd(s)
527    if (answer !== null) return { drop: `cache-tax: not sent, nothing paid${cold == null ? '' : ` (${fmtUsd(cold)} saved)`}. Send it again to be asked again, or /clear and start from a note.` }
528    // No dialog to answer (dismissed, or a run with no one to ask): refuse once, the resend pays.
529    s.ackedAt = s.lastRequestAt
530    return { drop: `cache-tax: ${guardText(s, now)} Send it again to pay it, and keepwarm will then hold the cache for ${fmtDuration(AUTO_WARM_MS)}. Or /clear and start from a note.` }
531  })
532
533  // Only Claude's own requests read and write its prompt cache: a step another model answered
534  // gives the clock back the time it had.
535  on('turn.step', async function* ($, e, next) {
536    const before = s.lastRequestAt
537    if (!e.agentId) s.lastRequestAt = await $.clock.now()
538    const result = yield* next(e)
539    if (!e.agentId) {
540      s.lastAnswerModel = result?.usage?.model ?? e.model
541      if (!isClaude(s.lastAnswerModel)) s.lastRequestAt = before
542    }
543    return result
544  })
545
546  on('turn.complete', async ($, e, next) => {
547    const r = await next(e)
548    if (e.agentId) return r
549    // A turn another model answered changed nothing in Claude's cache, its size or its cost.
550    if (!isClaude(e.usage?.model ?? s.lastAnswerModel)) {
551      s.coldWritePending = false
552      s.ackedAt = 0
553      return r
554    }
555    const now = await $.clock.now()
556    // A sleeping host may deliver this turn before the expired window's timer.
557    if (s.deadline && now >= s.deadline) await stop($, s, null)
558    // turn.step stamps the exact request time; when no step of this turn did, the turn's end is the floor.
559    if (now - s.lastRequestAt > e.durationMs) s.lastRequestAt = now
560    s.compacted = false
561    s.ackedAt = 0
562    const u = e.usage
563    if (u) {
564      if (u.model) s.lastModel = u.model
565      const prev = s.ctx
566      // A turn's usage is its responses summed, so a ten-step turn reports ten
567      // reads of the context. The live window is the engine's figure; the sum
568      // is only the fallback for a host that reports no tokens.
569      const write = u.cache_creation_input_tokens
570      const live = (await $.session.usage()).context.tokens
571      s.ctx = live && live > 0 ? live : u.input_tokens + u.cache_read_input_tokens + write
572      const full = prev > 20000 && write >= 0.5 * prev
573      if (full || s.coldWritePending) {
574        const price = priceOf(s.lastModel)
575        const usd = price ? write * price[1] / 1e6 : null
576        s.misses.push({ at: now, tokens: write, usd })
577        if (s.deadline < now + AUTO_WARM_MS) {
578          await startWindow($, s, AUTO_WARM_MS, s.every)
579          $.ui.log(`cold write of ${fmtTok(write)} tokens paid (${fmtUsd(usd)}). Keeping the cache warm for ${fmtDuration(AUTO_WARM_MS)} so it is not paid again today; /keepwarm off to stop.`)
580        }
581      }
582    }
583    s.coldWritePending = false
584    await arm($, s)
585    return r
586  })
587
588  on('session.compact', async ($, e, next) => {
589    const r = await next(e)
590    if (!e.agentId) {
591      s.compacted = true
592      s.ctx = 0
593      s.ackedAt = 0
594      disarm(s)
595      await arm($, s)
596    }
597    return r
598  })
599}
600
hooks/status-icons.ts 32 lines
1// Generated by tools/generate-status-icons.py from the official SVG assets.
2export const statusIcons = {
3  "dark": {
4    "warm": {
5      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#78b85a\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
6      "png": "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"
7    },
8    "cold": {
9      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#d97757\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
10      "png": "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"
11    },
12    "neutral": {
13      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#faf9f5\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#92928b\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
14      "png": "iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAYAAADlNHIOAAAABmJLR0QA/wD/AP+gvaeTAAAMsklEQVR4nO2ceXAUVR7Hv2+GhOEIJP1ez0xCFkw2BFCwCsxBKMQr4MV6Y8S1MLu6iLFYgwdHiNcKIaJk5ajaCIKilCssZMsti9qVoLBCCFESQZCIcoUhu5npmRwTkpBk5rd/kHaHpEMO5shEP1X9x/R7/fr3+3379Zv+vX4N/EJAYYE2oDcIIW5gjJYSIRUAGEMhEVuuKMqhQNvWU4JKgIiIiJF6PVsKsCcB6NoVE8C2M8aybDbbj4Gwrze0d6JPEhYWxmWZ5+r1uhMAm2swGHTPZMxDWdnXKCv7Gs9kzIPBYGAAzSJyfyeE9I7RaDQF2u6gJyoqajDnfJEQvEYITkajTOnpj9PJH49T88ULl21nz/xImZnzyWQykhCchOD1ssxzJUkaFmg/ghEd53yOELyyLZj0wP330jdlX3UIfPvt2NHDlJ7+uCoCCSFssiw9C2BAoJ0KCjjnqZzzI2oAZ8xIpb17dncZ+Pbb3j27acaMVA8heLksS7MQZOOe35AkabIQ0l41YImJCfT3gm10saleM8CNDbVUUrKfSkr2U2NDrWadi0319PeCbZSYmOApxEEhxE2B9lcl4FeDJEnX6nRsBYB72n5j3rynMPcPTyA0NLRDfSLC6dMncaC4GE5nHQBgyJChuGFSAsaNGwfGOrrU2tqKjz/ehlWr8lBltaq7/+F20xKHw/Gdr3zrDgETgHM+gjH2GkDpAPSDBw/G73/3OObPn4+wsKGax1jOW1BcXARFUTTLIyIikJiQiNjYOM3yhoYGbHpvM9atW4e6OicAuAD2PhG9Yrfbz3vFsR7idwFkWR7qdrufYQzZAIaGhIQgLW0WXnjheRhlWfMYq9WKgwcP4Hxl92JkNpuRnJSCyMhIzXKHoxr5+flYv2EjmpubAaCRMaxpaXHl1tTU1PTOs97hTwFChRDpAL0OwMgYw913340li19ETEyM5gH19U6UfFWCEye+79UJR426BlNSpmD48HDNcovFgjVr1+Gjjz6G2+0GAAcRVoaFha0+c+ZMU69O2kP8IQCTZekhIuQCLBYAEhIS8FL2EiQmJmoe0NjYhCNHynDk2yNwuVxXdXKdToexY8ciISEJgwcN1qxz+PARLF+eg337i9Rd5wC2TFGUjQCuzoAu8KkAnPNUxvAGgEkAEB8/Gi88vwAzZ87UrN/S0oKjx46irOxrNDe3eNWWkJAQjL9uPCZOTEBoaIhmnX9/uQ+vv74Mx479NC4fY4xes9kcf/OqMR74RACj0TidyLWFCEYAiIyMxILMZzF7dhr0en2H+m63G+Xl5Th0qAQXGhp8YdJPDBpkwPXXT8T1E67v1JadO3di2fJcVFRUAAAYg5Ux/WNWq3WXt+3xugBGo3GG2+36JwAWHj4cGRkZePKJ38FgMGjWP1txBkVFRait9evYh6FDw5CUmIz4+HjN8ubmZmzbth0rV74JxW4HANLp9HdYrdbPvGmH1wUQgp8HEPX8gkw893wmGNPO91VWnsfBkmJUVVV524QeYTKZkJw0GVFRIzTLidzIW/U2Vv35bQCoVBS7dsVe4gsB3ACY5dxp6HQdu7jD4cDBgwdwtuKst099VYwaOQrJySmQJKlDmdvtQvSvYgCAFMXu1QyyL9LRDIBm8IMVD1+8fsH6PUMoSRLuvPPuoLkF+Rpf3IIIACrPV3SrvuW8BUVF++BwOLxtyhUZPjwcyUlJnaYt2hM1YiQAQFHsXo2Z32bEtmz5SPOhKnpENB568GFMu/FmDBms/aDkTQYNMiA5OQUPz0rTDL7b7cann37qcztU/NYDokaMxOjRcXjxhee68SB2SM3ReI2ePohp2Q94vwf4VQCVhIQEZC9djKSkJM02mpqacPiw/1IR33xzGDk5KzxTEf1aACtw6Ql5+vRUvPrKS1dMxpWWHcLx48dBRD22JTY2DslJSVdMxr351ips316gtu8AIF3J/qAXgDFdWFs6eimAsO6ko202K4qLe5aOnpycArO5s3S0A/n573imoxsYw9qWFlfugAH66ivZH/QCqA4EckJm7dq1cDrrAY0Jme7a7y0CJoBKb6ckiw8Wo66ue1OSLS0t2Lr1b92akvzZCaAiSdJknQ5vAGwaAMTExCBryULcddddmkF1uVrx7dGjAIAJ48dDr+/4TElE2LlzJ3JWrMTp06fV3SUAW6goyl5v2t9b+owAKpzzVAB5jGECAEyaNBFLs5YgJWVyj+w4cKAYy3NWoLS0TN1FAD4JDR3428rKyk5z3v4WoM+9mmi32wvtdvskgM0D8J/S0jI8+NDDmDMnHeXlXU9Nlpd/jzlz0pGf/w42vrseZaVfIT19DnQ6HQNwX3Nz07dGo3S7zx0JFOr7N+3f0VH396Stq3k1seLsycvKiw/so2nTpnq8HyR9JMuy2Zf2d4c+dwvSIiwsjBsMoS8S4VkABoPBgFMnT1xWJ/bX8Whq+v88eumhEpjNl8e3tbUVG97dhFWr8tBwaeatBmCLFUXZAMDtK/uvRJ+7BWnhdDrtNpt9scvlHgPgskCrqPsYw2cAsGhxlvruz08MGDAAT8+biz1fFCL1tlsBIBygfM75l0KIMT52Q5OgEEClurq6yxQrEZsPoHrXrkJMm3Yztm8v6FAnOjoaH3zwPjZvfg9RUVFgDFMAOiCE0J6f9CFBJUB3UBTlBMAmE6HIarPhj89mYs6cdFgslg51p6fehs9371J7QwRjtMLf9vY7AYBLItjt9hvb/knVFO7+HDffkoq/5K9Ha2vrZXWHDQvDypW5AAB1yZM/6ZcCBBP9ctGCECKeiN4DaAoApN52K3JyliE6OrpD3bo6JxYuXAxAXeyHB/xpa7/rAZcGUipmDFOMsow1q9/GBx+8rxn8XYW7cett01G4+3MAqCZiS/xtb7/rAYzRCiJETJ+eirVrVmPYsLAOdSwWC7KystXAgwhFjLHfK4pyQgjuV3v7nQDqQPpGbk6H4Hf2IGa3//9BzN/0OwE6o7S0DAsXLcZ33x1v20N/ZUz/nM1m+28g7fL7GGAymWJ92T5jKASARYuWoKqqClVVVchamo177r2/Lfh0SqejOxTF8Wj74PvaNi381gMMBgOamprgcrWWCyG9p9MNeNlqtXr9raxLAyndsqtwd8TESZetP2hhDKsGDhz8J4vF0uhZoOaaXK7WTNVWf+G3HrB/31489tij0Ov1IQCb63a7TvpiIbX6JMwYCgA4ATgZQ4HL5Z5ks9mXeAZfzbYOHBh6kgiLdDrdwJkzZ2LvnkJvmuRfhOC1WulgdTtyuJQeSZvluWy0UgjxFLrZG72ULh4ghHjKcyH4I2mz6MjhUk2bK86eVNuv7X1k/IQs8x1CcJo9O42sVZU9WUh9nHN+f1ftX60AnPP7heDHu7sQ3FpVSbNnp5EQnGSZ7/BOlHyIECJeCO4QglNcXCzl5b1FdbX2Th0sLPwXTZ065acAyjIvkuWIqZ233zsBZDliqizzIrVeUlIiFezY2ulC8HpnNW3auIHGjBmttu0IRLa0V7SJUKg6O2HCeNq0cQM1NtRpOttwoZY2bdxA1107zrNHfCJJ0rUd2+6ZAJIkXSsE/0Qtj48fTXl5b1G9s1rTlqZGJxXs2EoTJ070tKUwaILvCec8VQh+SHUkJSWZCnZs7bQ31FTbKC/vLYqNvUZ13CWE2GY2h1+jttldATjnI4SQ3hGCtwjBaeTIX9Grr2STXam6Ym+86aYbPQN/tO37EkENk2VplhD8R9WxO+64nfZ9+UWngfjvfyz06ivZFBUVqQaiQZZ5bnh4eHhXAsiyPLRtHtkpBKfISDNlZs4ni+VMp+crOVhE9937G8/AVwgh5gLoP6tM4uLiBgohLZBlrqiOzp6dRt+XH+00MKdOfk+ZmfPJaJTV8UHpWoBLdWRZUHr64122//TTc0mWhcex0oK4uLiBgY6Xz/C4Quu6e4V+VXKg/RXaqQBqD9u/b88Veti59j3sgtrDAh0fv3HpHi3eFYK39uYerSXAlCmTuzXGxMSMUgPfKoR4l3MemPVJfYH2/1LGjR1DG9bn04X6mk7/pWz5cHOH/Vs+3ExNjU7NYy7U19CG9fk0buyYLv9l/Wzp6QeburMFwweb+hy/fLKsb9CjXI3ndrW5pl/wwGQyDZFlni3aEnwmk5EyMubRmdM/dAj8mdM/UEbGPM/PVtbKMs82mUxDAu1H0GM2m2XO+Woh+EUhOEVHj6CXX8qicxWn6FzFKXr5pSyKjh6hBv4i53y12WzWXu/Uxwiq+6HJZIp1uVqWAewRdLSdAPpYrw/JrqqqOhUI+3pDUAmg0vbx7iwiTAcAxrCLiOUE48e7fyHA/A+cbDv3qiAY4gAAAABJRU5ErkJggg=="
15    }
16  },
17  "light": {
18    "warm": {
19      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#397527\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
20      "png": "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"
21    },
22    "cold": {
23      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#c15f3c\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
24      "png": "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"
25    },
26    "neutral": {
27      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\" fill=\"none\">\n  <title>Cache Tax</title>\n  <g stroke=\"#141413\" stroke-width=\"4\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path fill=\"#686861\" d=\"M8 18 32 5 56 18 32 31Z\"/>\n    <path d=\"M8 18v28l24 13 24-13V18M32 31v28\"/>\n    <path d=\"m40 32 8 5-8 5\"/>\n  </g>\n</svg>\n",
28      "png": "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"
29    }
30  }
31} as const
32