Opens a CloudWatch metric widget as a live chart pane with range, pan, refresh and click-to-ask

Claude Code mods: plugins of function hooks that draw inside a terminal session.
| Mod | What it does |
|---|---|
cw-chart | /cw-chart <widget.json> opens a CloudWatch metric widget in a pane. The pane has 1h/6h/24h ranges, pan, refresh, and a strip you click to put a timestamp in the prompt. A widget with a fixed start/end opens as that window. Without them, or after you pick a range, it is live and re-fetches every minute. |
image-mirror | Draws each PNG that Claude reads inline in the transcript, under its Read row. |
At the prompt of a terminal session:
/plugin install cw-chart --marketplace voitta-ai/mods
/plugin install image-mirror --marketplace voitta-ai/mods
Answer y to add the marketplace, then choose a scope.
cw-chart runs aws cloudwatch get-metric-widget-image with your default AWS credentials. They need cloudwatch:GetMetricWidgetImage.Run a mod from its folder with claude --plugin-dir <mod folder>, or link it into the session's dev-mods folder for hot reload. Check a mod with claude plugin validate <mod folder>. CI runs the same check on every push and pull request.
A mod's version in its .claude-plugin/plugin.json is the cache key that installs use. A PR that changes a mod must bump that mod's version, or CI fails the PR. When the PR merges, CI tags <mod>-v<version> and creates a GitHub release. The release notes are the titles of the merged PRs, so write each PR title as a release note.
MIT
hooks/register.tsx 314 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderElement } from 'claude-code'
3
4import type { Chart } from '../types'
5
6type Widget = Record<string, unknown>
7
8const PANE = 'cw-chart'
9const chart = atom({ plugin: 'cw-chart', key: 'chart' } as const, null)
10
11// The widget size asked of CloudWatch when the spec names none.
12const DEFAULT_WIDTH = 1200
13const DEFAULT_HEIGHT = 420
14// A terminal cell is about twice as tall as it is wide.
15const CELL_ASPECT = 2
16const HOUR = 3_600_000
17const LIVE_REFRESH_MS = 60_000
18// ponytail: guessed plot area of a CloudWatch PNG, as fractions of its width,
19// so a click on the strip maps to an approximate time. Exact times need the
20// data itself (drawing the chart ourselves).
21const PLOT_LEFT = 0.05
22const PLOT_RIGHT = 0.95
23
24const iso = (ms: number) => {
25 const retval = new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z')
26 return retval
27}
28
29const hhmm = (ms: number) => {
30 const retval = `${iso(ms).slice(11, 16)}Z`
31 return retval
32}
33
34const numberOr = (value: unknown, fallback: number) => {
35 const retval = typeof value === 'number' ? value : fallback
36 return retval
37}
38
39const firstLine = (text: string) => {
40 const retval = text.trim().split('\n')[0] ?? ''
41 return retval
42}
43
44const messageOf = (thrown: unknown) => {
45 const retval = thrown instanceof Error ? thrown.message : String(thrown)
46 return retval
47}
48
49// Width and height from the PNG's IHDR chunk: bytes 16-23 of the file, the
50// first 32 base64 characters.
51const sizeOf = (base64: string) => {
52 const head = atob(base64.slice(0, 32))
53 const u32 = (at: number) => {
54 const value =
55 ((head.charCodeAt(at) << 24) |
56 (head.charCodeAt(at + 1) << 16) |
57 (head.charCodeAt(at + 2) << 8) |
58 head.charCodeAt(at + 3)) >>>
59 0
60 return value
61 }
62 const retval = { width: u32(16), height: u32(20) }
63 return retval
64}
65
66// A chart from a widget spec: an absolute start and end give a fixed window,
67// anything else the last three hours, live.
68const chartOf = (widget: Widget, fallbackTitle: string) => {
69 const start = typeof widget.start === 'string' ? Date.parse(widget.start) : Number.NaN
70 const end = typeof widget.end === 'string' ? Date.parse(widget.end) : Number.NaN
71 const isFixed = Number.isFinite(start) && Number.isFinite(end) && end > start
72 const sized = {
73 ...widget,
74 width: numberOr(widget.width, DEFAULT_WIDTH),
75 height: numberOr(widget.height, DEFAULT_HEIGHT),
76 }
77 const retval: Chart = {
78 title: typeof widget.title === 'string' ? widget.title : fallbackTitle,
79 widget: JSON.stringify(sized),
80 rangeMs: isFixed ? end - start : 3 * HOUR,
81 endMs: isFixed ? end : null,
82 png: null,
83 renderedAt: null,
84 error: null,
85 isRendering: false,
86 }
87 return retval
88}
89
90// Asks CloudWatch for the chart's window as a PNG. A PNG of another size than
91// asked is refused: a 200 with the wrong picture is the failure to catch.
92const fetchPng = async ($: EngineInterface, asked: Chart, now: number) => {
93 const end = asked.endMs ?? now
94 const widget: Widget = {
95 ...(JSON.parse(asked.widget) as Widget),
96 start: iso(end - asked.rangeMs),
97 end: iso(end),
98 }
99 const region = typeof widget.region === 'string' ? widget.region : 'us-east-1'
100 let retval: { png: string | null; error: string | null }
101 try {
102 const ran = await $.process.run(
103 [
104 'aws',
105 '--no-cli-pager',
106 'cloudwatch',
107 'get-metric-widget-image',
108 '--region',
109 region,
110 '--metric-widget',
111 JSON.stringify(widget),
112 '--output-format',
113 'png',
114 '--query',
115 'MetricWidgetImage',
116 '--output',
117 'text',
118 ],
119 { timeoutMs: 60_000 },
120 )
121 const png = ran.stdout.trim()
122 const size = ran.exitCode === 0 ? sizeOf(png) : null
123 const isExpected =
124 size !== null && size.width === widget.width && size.height === widget.height
125 if (isExpected) {
126 retval = { png, error: null }
127 } else if (size === null) {
128 retval = { png: null, error: firstLine(ran.stderr) || `aws exited ${ran.exitCode}` }
129 } else {
130 retval = {
131 png: null,
132 error: `CloudWatch returned ${size.width}x${size.height}, not ${String(widget.width)}x${String(widget.height)}`,
133 }
134 }
135 } catch (thrown) {
136 retval = { png: null, error: messageOf(thrown) }
137 }
138 return retval
139}
140
141// Renders the chart's current window and stores the picture, or why it
142// failed, keeping the last good one. A render whose window changed meanwhile
143// is dropped; the render the change started lands instead.
144const renderChart = async ($: EngineInterface) => {
145 const asked = await read($, chart)
146 if (asked === null) {
147 return
148 }
149 await update($, chart, c => {
150 const retval = c === null ? c : { ...c, isRendering: true }
151 return retval
152 })
153 const now = await $.clock.now()
154 const { png, error } = await fetchPng($, asked, now)
155 await update($, chart, c => {
156 const isStale =
157 c === null ||
158 c.widget !== asked.widget ||
159 c.rangeMs !== asked.rangeMs ||
160 c.endMs !== asked.endMs
161 const retval = isStale
162 ? c
163 : { ...c, isRendering: false, renderedAt: now, error, png: png ?? c.png }
164 return retval
165 })
166}
167
168const setWindow = async (
169 $: EngineInterface,
170 change: (c: Chart, now: number) => Pick<Chart, 'rangeMs' | 'endMs'>,
171) => {
172 const now = await $.clock.now()
173 await update($, chart, c => {
174 const retval = c === null ? c : { ...c, ...change(c, now) }
175 return retval
176 })
177 await renderChart($)
178}
179
180const live = (rangeMs: number) => () => {
181 const retval = { rangeMs, endMs: null }
182 return retval
183}
184
185// Moves the window by half its length; reaching now goes live again.
186const shift = (sign: number) => (c: Chart, now: number) => {
187 const end = (c.endMs ?? now) + (sign * c.rangeMs) / 2
188 const retval = { rangeMs: c.rangeMs, endMs: end >= now ? null : end }
189 return retval
190}
191
192export const register: Register = on => {
193 on('session.start', async ($, e, next) => {
194 await $.command.register({
195 name: 'cw-chart',
196 description: 'Open a CloudWatch metric widget JSON file as a live chart pane',
197 argumentHint: '<widget.json>',
198 })
199 $.clock.every(LIVE_REFRESH_MS, () => {
200 void (async () => {
201 const current = await read($, chart)
202 if (current !== null && current.endMs === null) {
203 await renderChart($)
204 }
205 })()
206 })
207 const retval = await next(e)
208 return retval
209 })
210
211 on('command.run', { command: 'cw-chart' }, async ($, e) => {
212 const path = e.args.trim()
213 let retval = { text: 'Usage: /cw-chart <path to a CloudWatch metric widget JSON file>' }
214 if (path !== '') {
215 try {
216 const widget = JSON.parse(await $.fs.read(path)) as Widget
217 const opened = chartOf(widget, path.split('/').pop() ?? path)
218 await update($, chart, () => opened)
219 await $.ui.open({ id: PANE, title: opened.title })
220 await renderChart($)
221 retval = { text: `Opened ${opened.title} in the chart pane.` }
222 } catch (thrown) {
223 retval = { text: `cw-chart: ${messageOf(thrown)}` }
224 }
225 }
226 return retval
227 })
228
229 // Closing the pane drops the chart, which stops its refresh.
230 on('ui.close', { id: PANE }, async ($, e, next) => {
231 await update($, chart, () => null)
232 const retval = await next(e)
233 return retval
234 })
235
236 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
237 const c = await read($, chart)
238 let retval: RenderElement
239 if (e.surface !== 'terminal' || c === null) {
240 const { Text } = $.ui.resolve(e)
241 retval = (
242 <Text dimColor>
243 {c === null ? 'No chart. Run /cw-chart <widget.json>.' : 'The chart draws in the terminal only.'}
244 </Text>
245 )
246 } else {
247 const { Box, Button, Image, Text } = $.ui.resolve(e)
248 const widget = JSON.parse(c.widget) as Widget
249 const width = numberOr(widget.width, DEFAULT_WIDTH)
250 const height = numberOr(widget.height, DEFAULT_HEIGHT)
251 const columns = Math.max(20, Math.min(255, e.props.bodyColumns))
252 const rows = Math.max(4, Math.min(255, Math.round((columns * height) / width / CELL_ASPECT)))
253 const end = c.endMs ?? c.renderedAt ?? (await $.clock.now())
254 const start = end - c.rangeMs
255 const segments = Math.floor(columns / 2)
256 const timeAt = (i: number) => {
257 const fraction = ((i + 0.5) / segments - PLOT_LEFT) / (PLOT_RIGHT - PLOT_LEFT)
258 const at = fraction < 0 || fraction > 1 ? null : start + fraction * c.rangeMs
259 return at
260 }
261 const status = [
262 c.endMs === null ? 'live, every 60s' : 'fixed window',
263 `${hhmm(start)}-${hhmm(end)}`,
264 c.renderedAt === null ? 'not rendered yet' : `rendered ${iso(c.renderedAt).slice(11)}`,
265 ...(c.isRendering ? ['rendering...'] : []),
266 ].join(' | ')
267 retval = (
268 <Box flexDirection="column">
269 {c.png === null ? (
270 <Text dimColor>{c.isRendering ? 'Rendering...' : 'No picture yet.'}</Text>
271 ) : (
272 <Image key="chart" source={{ png: c.png }} columns={columns} rows={rows} alt={c.title} />
273 )}
274 <Box>
275 {Array.from({ length: segments }, (_, i) => {
276 const at = timeAt(i)
277 const cell =
278 at === null ? (
279 <Text>{' '}</Text>
280 ) : (
281 <Box key={`s${i}`}>
282 <Button
283 key={`t${i}`}
284 label=".."
285 plain
286 onPress={async () => {
287 await $.prompt.fill({ text: `[${c.title} @ ${iso(at)}] `, mode: 'insert' })
288 }}
289 />
290 <Box position="absolute" top={1} left={0} display="none" hover={{ display: 'flex' }}>
291 <Text inverse>{` ${hhmm(at)} `}</Text>
292 </Box>
293 </Box>
294 )
295 return cell
296 })}
297 </Box>
298 <Box gap={1}>
299 <Button key="r1" label="1h" hotkey="1" onPress={() => setWindow($, live(HOUR))} />
300 <Button key="r6" label="6h" hotkey="6" onPress={() => setWindow($, live(6 * HOUR))} />
301 <Button key="r24" label="24h" hotkey="d" onPress={() => setWindow($, live(24 * HOUR))} />
302 <Button key="back" label="<" hotkey="h" onPress={() => setWindow($, shift(-1))} />
303 <Button key="fwd" label=">" hotkey="l" onPress={() => setWindow($, shift(1))} />
304 <Button key="refresh" label="refresh" hotkey="r" onPress={() => renderChart($)} />
305 </Box>
306 <Text dimColor>{status}</Text>
307 {c.error !== null && <Text color="red">{c.error}</Text>}
308 </Box>
309 )
310 }
311 return retval
312 })
313}
314types/index.d.ts 21 lines1export type Chart = {
2 title: string
3 /** The CloudWatch metric widget as JSON text; each render sets its start and end. */
4 widget: string
5 rangeMs: number
6 /** The window's end, ms since the epoch; null follows now and refreshes every minute. */
7 endMs: number | null
8 /** The last good render, a base64 PNG. */
9 png: string | null
10 renderedAt: number | null
11 /** Why the last render failed. */
12 error: string | null
13 isRendering: boolean
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'cw-chart': { chart: Chart | null }
19 }
20}
21