SLOPSHOPPER

cache-clock

A prompt-cache line under your status line, from Claude Code's own cache figures: time left while warm, hit rate and misses, and how many tokens the next…

newcommandprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cache-clock
› 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-clock ⎿ cache-clock: cache-clock is off. /cache-clock setup adds the cache line under your status line. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-code-mods-collection

A private reference collection of public Claude Code mods, with a security-oriented analysis.

  • ANALYSIS.md: what each mod can reach, which to install, which to avoid
  • SOURCES.md: upstream repo and pinned commit for every folder in mods/

Nothing here is installed or executed. All code is by its upstream authors under their own licences (MIT, one Apache-2.0); each folder keeps its original LICENSE.

Source 1 files
hooks/register.ts 231 lines
1// Cache Clock: a prompt-cache line under the status line, drawn from the
2// status line input's `prompt_cache`. Plugins cannot register a status line,
3// so /cache-clock setup points statusLine at a script that runs the person's
4// own status line first and adds the cache line under it.
5import type { EngineInterface, Register } from 'claude-code'
6
7// The scripts setup copies out of the plugin. They live in the config dir, not
8// in the plugin's folder, which changes with every version of the mod.
9const SCRIPTS = ['cache-clock.mjs', 'render.mjs']
10// What marks a statusLine command as this mod's.
11const MARK = 'cache-clock/cache-clock.mjs'
12const REFRESH_SECONDS = 30
13// The first Claude Code that sends `prompt_cache` to the status line.
14const MIN_VERSION = [2, 1, 251]
15
16type StatusLine = { type?: string; command?: string; refreshInterval?: number; [key: string]: unknown }
17// What setup leaves for remove and for the script: the settings file it wrote,
18// the statusLine that file had before, and the command the script runs first.
19// One per settings file (setup-<key>.json), so setups in several projects and
20// in the user settings each keep their own original.
21type Saved = { file: string; previous: StatusLine | null; wrapped: string | null }
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    const r = await next(e)
26    await $.command.register({
27      name: 'cache-clock',
28      description: 'Add a prompt-cache line under the status line; /cache-clock remove takes it out',
29      argumentHint: '[setup|remove]',
30    }).catch(() => {}) // a name Claude Code already has is refused: start anyway
31    void refreshScripts($).catch(() => {})
32    return r
33  })
34
35  on('command.run', { command: 'cache-clock' }, async ($, e) => {
36    const word = e.args.trim().toLowerCase()
37    try {
38      if (word === 'setup') return { text: await setup($) }
39      if (word === 'remove') return { text: await remove($) }
40      if (word === '') return { text: await status($) }
41      return { text: 'Usage: /cache-clock [setup|remove]' }
42    } catch (err) {
43      return { text: `cache-clock: ${err instanceof Error ? err.message : String(err)}` }
44    }
45  })
46}
47
48async function setup($: EngineInterface) {
49  const dir = await clockDir($)
50  const found = await inForce($)
51  if (found.source === 'policy' || found.source === 'flag') {
52    const where = found.source === 'policy' ? 'managed settings' : '--settings'
53    return `Your status line comes from ${where}, which cache-clock can't change. Nothing changed.`
54  }
55  if (isOurs(found.statusLine)) {
56    await copyScripts($, dir)
57    return 'cache-clock is already set up. Its scripts are up to date.'
58  }
59
60  const file = await targetFile($, found.source)
61  const settings = await readJson($, file)
62  const current = found.statusLine
63  const wrapped = typeof current?.command === 'string' && current.command.trim() ? current.command : null
64  const saved: Saved = { file, previous: asStatusLine(settings.statusLine), wrapped }
65  const key = keyFor(file)
66
67  await copyScripts($, dir)
68  await $.fs.write(`${dir}/setup-${key}.json`, JSON.stringify(saved, null, 2) + '\n')
69
70  const { command: _command, refreshInterval, ...kept } = current ?? {}
71  settings.statusLine = {
72    ...kept,
73    type: 'command',
74    command: `node "${shellQuoted(`${dir}/cache-clock.mjs`)}" ${key}`,
75    refreshInterval: typeof refreshInterval === 'number' && refreshInterval < REFRESH_SECONDS ? refreshInterval : REFRESH_SECONDS,
76  }
77  await $.fs.write(file, JSON.stringify(settings, null, 2) + '\n')
78
79  const lines = [
80    wrapped
81      ? `cache-clock set up in ${file}. Your status line stays as it is, with the cache line under it.`
82      : `cache-clock set up in ${file}. The cache line is your status line.`,
83    'It shows up after Claude\'s next reply. /cache-clock remove puts things back.',
84    ...(await warnings($)),
85  ]
86  return lines.join('\n')
87}
88
89// Takes out the setup in force here: the user settings', or this project's local one.
90async function remove($: EngineInterface) {
91  const dir = await clockDir($)
92  const found = await inForce($)
93  const file = await targetFile($, isOurs(found.statusLine) ? found.source : 'user')
94  const key = keyFor(file)
95  const saved = await readSaved($, `${dir}/setup-${key}.json`)
96  if (!saved) return 'cache-clock is not set up here. Nothing changed.'
97
98  const settings = await readJson($, saved.file)
99  if (!isOurs(asStatusLine(settings.statusLine))) {
100    await $.fs.write(`${dir}/setup-${key}.json`, '{}\n')
101    return `The status line in ${saved.file} was changed since setup, so it was left as it is.`
102  }
103  if (saved.previous) settings.statusLine = saved.previous
104  else delete settings.statusLine
105  await $.fs.write(saved.file, JSON.stringify(settings, null, 2) + '\n')
106  await $.fs.write(`${dir}/setup-${key}.json`, '{}\n')
107  return saved.previous
108    ? `cache-clock removed. Your status line in ${saved.file} is back as it was.`
109    : `cache-clock removed from ${saved.file}.`
110}
111
112async function status($: EngineInterface) {
113  const found = await inForce($)
114  if (isOurs(found.statusLine)) {
115    return ['cache-clock is on. /cache-clock remove takes it out.', ...(await warnings($))].join('\n')
116  }
117  return 'cache-clock is off. /cache-clock setup adds the cache line under your status line.'
118}
119
120// After an update of the mod, the copies setup made catch up with the new scripts.
121async function refreshScripts($: EngineInterface) {
122  if (!isOurs((await inForce($)).statusLine)) return
123  const dir = await clockDir($)
124  for (const name of SCRIPTS) {
125    const fresh = await $.fs.read(`${$.plugin.root}/statusline/${name}`)
126    const copy = await $.fs.read(`${dir}/${name}`).catch(() => null)
127    if (copy !== fresh) await $.fs.write(`${dir}/${name}`, fresh)
128  }
129}
130
131async function copyScripts($: EngineInterface, dir: string) {
132  for (const name of SCRIPTS) {
133    await $.fs.write(`${dir}/${name}`, await $.fs.read(`${$.plugin.root}/statusline/${name}`))
134  }
135}
136
137async function warnings($: EngineInterface) {
138  const out: string[] = []
139  const node = await $.process.run(['node', '--version'], { timeoutMs: 5000 }).catch(() => null)
140  if (!node || node.exitCode !== 0) out.push('Warning: node is not on your PATH. The cache line needs Node to run.')
141  const { base, version } = await $.session.version()
142  if (isOlder(base ?? version, MIN_VERSION)) {
143    out.push(`Warning: Claude Code ${version} is older than 2.1.251, so it sends no cache data. The line stays empty until you update.`)
144  }
145  return out
146}
147
148// The statusLine in force and the settings source it comes from, highest precedence first.
149async function inForce($: EngineInterface) {
150  for (const source of ['policy', 'flag', 'local', 'project', 'user'] as const) {
151    const settings = await $.settings.read({ source }).catch(() => ({}))
152    const statusLine = asStatusLine((settings as Record<string, unknown>).statusLine)
153    if (statusLine) return { source, statusLine }
154  }
155  return { source: null, statusLine: null }
156}
157
158// The file setup writes: where the status line lives, except that a project's
159// shared settings get a local override instead of a path from this machine.
160async function targetFile($: EngineInterface, source: string | null) {
161  const root = await $.session.root() // where Claude Code reads a project's settings from
162  if (source === 'local' || source === 'project') return `${root}/.claude/settings.local.json`
163  return `${await configDir($)}/settings.json`
164}
165
166async function configDir($: EngineInterface) {
167  const custom = await $.env.get('CLAUDE_CONFIG_DIR')
168  if (custom) return custom.replace(/\/+$/, '')
169  const home = await $.env.get('HOME')
170  if (!home) throw new Error('HOME is not set, so the Claude config folder is unknown.')
171  return `${home}/.claude`
172}
173
174async function clockDir($: EngineInterface) {
175  return `${await configDir($)}/cache-clock`
176}
177
178async function readJson($: EngineInterface, file: string): Promise<Record<string, unknown>> {
179  if (!(await $.fs.exists(file))) return {}
180  const text = await $.fs.read(file)
181  if (!text.trim()) return {}
182  let parsed: unknown
183  try {
184    parsed = JSON.parse(text)
185  } catch {
186    throw new Error(`${file} is not valid JSON, so it was left as it is.`)
187  }
188  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error(`${file} does not hold a settings object.`)
189  return parsed as Record<string, unknown>
190}
191
192async function readSaved($: EngineInterface, path: string): Promise<Saved | null> {
193  const text = await $.fs.read(path).catch(() => null)
194  if (!text) return null
195  try {
196    const saved = JSON.parse(text)
197    return typeof saved?.file === 'string' ? (saved as Saved) : null
198  } catch {
199    return null
200  }
201}
202
203// A short stable name for a settings file's saved state: FNV-1a of its path, in hex.
204function keyFor(file: string) {
205  let hash = 0x811c9dc5
206  for (let i = 0; i < file.length; i++) hash = Math.imul(hash ^ file.charCodeAt(i), 0x01000193) >>> 0
207  return hash.toString(16).padStart(8, '0')
208}
209
210// Safe inside double quotes for sh.
211function shellQuoted(path: string) {
212  return path.replace(/["\\$`]/g, c => `\\${c}`)
213}
214
215function asStatusLine(value: unknown): StatusLine | null {
216  return value && typeof value === 'object' && !Array.isArray(value) ? (value as StatusLine) : null
217}
218
219function isOurs(statusLine: StatusLine | null) {
220  return typeof statusLine?.command === 'string' && statusLine.command.includes(MARK)
221}
222
223function isOlder(version: string, min: number[]) {
224  const parts = version.split(/[.-]/).slice(0, 3).map(Number)
225  if (parts.some(Number.isNaN)) return false
226  for (let i = 0; i < min.length; i++) {
227    if (parts[i] !== min[i]) return parts[i] < min[i]
228  }
229  return false
230}
231