Coloured band above the prompt: context, room before auto-compact, 5h and 7d limits, estimated cost in MYR

My collection of Claude-related things: plugins, skills, MCP setups, mods and projects.
| path | what it is |
|---|---|
| plugins/session-meter | Claude Code plugin: a coloured band above the prompt showing context tokens, room left before auto-compact, 5-hour and 7-day usage limits with reset times, and session cost in MYR (estimated from a daily USD→MYR rate) |
| animation | Hand-painted cartoon kit for Claude Code (from ClaudeAnimationBase), with the short "The Last Leaf" |
The repo is also a Claude Code plugin marketplace (chooi-mods).
claude plugin marketplace add swchooi/claude-mods
claude plugin install session-meter@chooi-mods
Start a new session, in the terminal or the desktop app's Code tab, and the band appears after the first reply.
claude plugin marketplace update chooi-mods
claude plugin update session-meter@chooi-mods
claude plugin uninstall session-meter@chooi-mods
Open animation/ in Claude Code and ask for a video, for example: "Read ANIMATION_GUIDE.md, then make a 15-second video of Clawd …". It needs Node.js, Chrome or Edge, and ffmpeg. To render the current film:
npm install
node render.mjs --frames --workers=4
node render.mjs --encode --out=out/video.mp4hooks/register.tsx 156 lines1import type { EngineInterface as Engine, Register, SessionContextUsage, SessionRateLimit, SessionCost } from 'claude-code'
2
3import type { Limit, Meter } from '../types'
4
5type Figures = { context: SessionContextUsage; rateLimits: SessionRateLimit[]; cost?: SessionCost }
6
7const meter = { plugin: 'session-meter', key: 'meter' } as const
8
9const GREEN = 'green'
10const AMBER = 'yellow'
11const RED = 'red'
12
13// Colour for a % used: green, amber from 70, red from 90.
14const usedColor = (pct: number) => (pct >= 90 ? RED : pct >= 70 ? AMBER : GREEN)
15
16// Colour for a % of room left: red at 10 or under, amber at 25 or under.
17const leftColor = (pct: number) => (pct <= 10 ? RED : pct <= 25 ? AMBER : GREEN)
18
19const trim = (s: string) => s.replace(/\.0$/, '')
20
21// USD -> MYR: a live rate fetched at most once a day and kept in the store across
22// sessions; the last good rate (or a rough default) stands in when offline.
23const RATE_URL = 'https://open.er-api.com/v6/latest/USD'
24const RATE_KEY = 'usdMyr'
25const RATE_TTL = 24 * 60 * 60 * 1000
26const FALLBACK_RATE = 4.1
27
28type Rate = { rate: number; at: number }
29
30let rateCache: Rate | undefined
31
32async function usdToMyr($: Engine): Promise<number> {
33 const now = await $.clock.now()
34 if (rateCache && now - rateCache.at < RATE_TTL) return rateCache.rate
35
36 const stored = (await $.store.get(RATE_KEY)) as Rate | undefined
37 if (stored && now - stored.at < RATE_TTL) return (rateCache = stored).rate
38
39 try {
40 const res = await $.http.fetch(RATE_URL)
41 const rate = res.ok ? JSON.parse(res.text)?.rates?.MYR : undefined
42 if (typeof rate === 'number' && rate > 0) {
43 rateCache = { rate, at: now }
44 await $.store.set(RATE_KEY, rateCache)
45 return rate
46 }
47 } catch {
48 // offline or blocked: fall through to the last known rate
49 }
50 // Retry no sooner than an hour from now.
51 rateCache = { rate: stored?.rate ?? FALLBACK_RATE, at: now - RATE_TTL + 60 * 60 * 1000 }
52 return rateCache.rate
53}
54
55const k = (n: number) =>
56 n >= 1_000_000 ? `${trim((n / 1_000_000).toFixed(1))}M` : n >= 1000 ? `${trim((n / 1000).toFixed(1))}k` : `${n}`
57
58// One coarse unit only: "40m", "4h", "2d".
59const resetsIn = (iso: string | undefined, now: number) => {
60 const at = iso ? Date.parse(iso) : NaN
61 if (!Number.isFinite(at) || !Number.isFinite(now)) return ''
62 const mins = Math.max(0, (at - now) / 60000)
63 const span = mins < 60 ? `${Math.round(mins)}m` : mins < 1440 ? `${Math.round(mins / 60)}h` : `${Math.round(mins / 1440)}d`
64 return ` resets in ${span}`
65}
66
67async function measure($: Engine, f: Figures): Promise<Meter> {
68 const { tokens, window } = f.context
69 let compactLeft: number | null | undefined
70
71 try {
72 const b = (await $.session.usage({ breakdown: 'summary' })).context.breakdown
73 if (b && !b.isAutoCompactEnabled) compactLeft = null
74 else if (b?.autoCompactThreshold && tokens !== undefined)
75 compactLeft = Math.max(0, Math.round((1 - tokens / b.autoCompactThreshold) * 100))
76 } catch {
77 // breakdown unavailable: leave the auto-compact figure out
78 }
79
80 const byKind = (kind: string, label: string): Limit[] => {
81 const r = f.rateLimits.find(l => l.kind === kind)
82 return r ? [{ label, percentUsed: r.percentUsed, resetsAt: r.resetsAt }] : []
83 }
84
85 return {
86 ctxTokens: tokens,
87 window,
88 compactLeft,
89 limits: [...byKind('five_hour', '5h'), ...byKind('seven_day', '7d')],
90 costMyr: f.cost?.usd === undefined ? undefined : f.cost.usd * (await usdToMyr($)),
91 }
92}
93
94export const register: Register = on => {
95 on('session.start', async ($, e, next) => {
96 const r = await next(e)
97 $.ui.status(undefined)
98 await $.state.set(meter, await measure($, await $.session.usage()))
99 return r
100 })
101
102 on('session.measure', async ($, e, next) => {
103 await $.state.set(meter, await measure($, e))
104 return next(e)
105 })
106
107 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
108 const { value: m } = await $.state.get(meter)
109 if (e.props.hasSurvey || !m) return next(e)
110
111 const { Box, Text } = $.ui.resolve(e)
112 const now = await $.clock.now()
113 const sep = <Text dimColor>{' │ '}</Text>
114
115 return (
116 <Box>
117 <Text wrap="truncate">
118 <Text dimColor>ctx </Text>
119 <Text>{m.ctxTokens === undefined ? '–' : k(m.ctxTokens)}</Text>
120 <Text dimColor>/{k(m.window)}</Text>
121 {m.compactLeft === null ? (
122 <Text>
123 {sep}
124 <Text dimColor>auto-compact off</Text>
125 </Text>
126 ) : m.compactLeft !== undefined ? (
127 <Text>
128 {sep}
129 <Text color={leftColor(m.compactLeft)} bold>
130 {m.compactLeft}%
131 </Text>
132 <Text dimColor> until compact</Text>
133 </Text>
134 ) : null}
135 {m.limits.map(l => (
136 <Text key={l.label}>
137 {sep}
138 <Text dimColor>{l.label} </Text>
139 <Text color={usedColor(l.percentUsed)} bold>
140 {Math.round(l.percentUsed)}%
141 </Text>
142 <Text dimColor>{resetsIn(l.resetsAt, now)}</Text>
143 </Text>
144 ))}
145 {m.costMyr !== undefined ? (
146 <Text>
147 {sep}
148 <Text>≈RM{m.costMyr.toFixed(2)}</Text>
149 </Text>
150 ) : null}
151 </Text>
152 </Box>
153 )
154 })
155}
156types/index.d.ts 18 lines1export type Limit = { label: string; percentUsed: number; resetsAt?: string }
2
3export type Meter = {
4 ctxTokens?: number
5 window: number
6 // % of room left before auto-compact; null when auto-compact is off
7 compactLeft?: number | null
8 limits: Limit[]
9 // session cost converted to MYR at an estimated rate
10 costMyr?: number
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'session-meter': { meter: Meter | null }
16 }
17}
18