Countdown of the 1 hour prompt-cache window, counted from the last reply.

Small Claude Code mods, shipped as plugins from one marketplace.
| Plugin | What it does |
|---|---|
status-hud | A bar above the prompt: current activity, 5-hour limit, weekly limit and context window. |
Cache | A status-line countdown (Cache ⏱ 52:10) of the 1 hour prompt-cache window, counted from the last reply. |
In Claude Code:
/plugin marketplace add hymleong/claude-mods
/plugin install status-hud@claude-mods
/plugin install Cache@claude-mods
/reload-plugins
Each plugin installs and updates on its own. Mods need a recent Claude Code build that supports hooks modules.
Cache keeps its data on your machine only (in Claude Code's plugin store). It does not send anything anywhere.claude plugin marketplace update claude-mods, then claude plugin update <name>@claude-mods.MIT, see LICENSE.
hooks/register.ts 95 lines1import type { Register } from 'claude-code'
2
3const TTL_MS = 60 * 60 * 1000
4
5let deadline = 0
6let ticking = false
7
8function pad(n: number) {
9 return String(n).padStart(2, '0')
10}
11
12function format(ms: number) {
13 if (ms <= 0) return '⏱ expired'
14 const s = Math.ceil(ms / 1000)
15 return '⏱ ' + pad(Math.floor(s / 60)) + ':' + pad(s % 60)
16}
17
18// Deadlines per session id, kept across restarts so a resumed chat counts from its own last reply.
19async function remember($: any) {
20 const id = await $.session.id()
21 const now = await $.clock.now()
22 const saved = ((await $.store.get('deadlines')) ?? {}) as Record<string, number>
23 const kept: Record<string, number> = {}
24 for (const k of Object.keys(saved)) if (saved[k] > now) kept[k] = saved[k]
25 kept[id] = deadline
26 await $.store.set('deadlines', kept)
27}
28
29// Timestamp of the chat's last assistant message, read from its transcript; 0 when unknown.
30// (The file's own mtime is no good: opening a chat rewrites bookkeeping lines at its end.)
31async function transcriptTime($: any) {
32 const id = await $.session.id()
33 const root = String($.plugin.root).split(String.fromCharCode(92)).join('/')
34 const cut = root.indexOf('/plugins/')
35 if (cut < 0) return 0
36 const projects = root.slice(0, cut) + '/projects'
37 const cwd = String(await $.session.cwd())
38 let file = projects + '/' + cwd.replace(/[^A-Za-z0-9]/g, '-') + '/' + id + '.jsonl'
39 if (!(await $.fs.exists(file))) {
40 file = ''
41 for (const dir of await $.fs.list(projects)) {
42 const candidate = projects + '/' + dir.name + '/' + id + '.jsonl'
43 if (await $.fs.exists(candidate)) {
44 file = candidate
45 break
46 }
47 }
48 }
49 if (file === '') return 0
50 const lines = String(await $.fs.read(file)).split(String.fromCharCode(10))
51 for (let i = lines.length - 1; i >= 0; i--) {
52 if (lines[i].indexOf('"type":"assistant"') < 0) continue
53 const at = Date.parse(JSON.parse(lines[i]).timestamp)
54 if (at > 0) return at
55 }
56 return 0
57}
58
59async function show($: any) {
60 $.ui.status(format(deadline - (await $.clock.now())))
61 if (ticking) return
62 ticking = true
63 $.clock.every(1000, async () => {
64 $.ui.status(format(deadline - (await $.clock.now())))
65 })
66}
67
68export const register: Register = on => {
69 // A resumed chat counts from its own last reply; a chat with none shows nothing.
70 on('session.start', async ($, e, next) => {
71 const id = await $.session.id()
72 const saved = ((await $.store.get('deadlines')) ?? {}) as Record<string, number>
73 let last = 0
74 if (typeof saved[id] !== 'number') {
75 try {
76 last = await transcriptTime($)
77 } catch (err) {
78 last = 0
79 }
80 }
81 if (typeof saved[id] === 'number') deadline = saved[id]
82 else if (last > 0) deadline = last + TTL_MS
83 if (deadline > 0) await show($)
84 return next(e)
85 })
86
87 // The window restarts from each finished reply.
88 on('turn.complete', async ($, e, next) => {
89 deadline = (await $.clock.now()) + TTL_MS
90 await remember($)
91 await show($)
92 return next(e)
93 })
94}
95