Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

A chart of how full Claude's memory of the conversation got, turn by turn.
Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.
Claude can only keep a limited amount of text in mind at once (the context window). The context-band mod shows the current number; this one shows the whole story as a chart, so you can see which turns made it grow fast, for example a large file being read.
/context-chart and press Enter. The panel opens.Esc to close.| Key | What it does |
|---|---|
Esc | Close the panel |
| Type | What it does |
|---|---|
/context-chart | Open the chart |
Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.
| Setting | What it means | Default |
|---|---|---|
language (Language) | Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese). | en |
You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.
_claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.claude --plugin-dir path/to/context-chart
To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").
From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 5 automated tests of this mod:
node scripts/check-mod.mjs examples/context-chart
The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.
For developers: the examples table lists the hooks and API calls this mod demonstrates.
hooks/register.tsx 75 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Point } from '../types'
5import { HISTORY, short, svgChart, textBars } from './chart'
6import { strings } from './i18n'
7
8const PANE = 'context-chart'
9const points = atom({ plugin: 'context-chart', key: 'points' } as const, [])
10
11export const register: Register = (on, options) => {
12 const t = strings(options.language)
13
14 // Pushed after each main-thread turn: no timer, no polling. Readings without tokens
15 // (before the first reply, right after a compaction) are skipped, never drawn as 0.
16 on('session.measure', async ($, e, next) => {
17 const { tokens, window, percent } = e.context
18 if (e.changed.includes('context') && tokens !== undefined && window > 0) {
19 const point: Point = { tokens, window, percent: Math.round(percent ?? (tokens / window) * 100) }
20 await update($, points, list => [...list, point].slice(-HISTORY))
21 }
22 return next(e)
23 })
24
25 on('session.start', async ($, e, next) => {
26 try {
27 await $.command.register({ name: 'context-chart', description: t.commandDescription, immediate: true })
28 } catch {
29 // name taken
30 }
31 return next(e)
32 })
33
34 on('command.run', { command: 'context-chart' }, async $ => {
35 const opened = await $.ui.open({ id: PANE, title: t.paneTitle, focus: true, closeOnEscape: true })
36 if (opened.isPlaced) return {}
37 const list = await read($, points)
38 return { text: list.length === 0 ? t.noReading : textBars(list, 60).join('\n') }
39 })
40
41 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
42 const ui = $.ui.resolve(e)
43 const { Box, Text } = ui
44 const list = await read($, points)
45 const now = list[list.length - 1]
46
47 let chart
48 if (now === undefined) {
49 chart = <Text dimColor>{t.noReading}</Text>
50 } else if (e.surface !== 'terminal' && 'Svg' in ui) {
51 // Decide by e.surface: at run time every surface's table lists every element, so
52 // `'Svg' in ui` alone is true on the terminal too (it only narrows the type).
53 const width = Math.max(120, e.props.bodyColumns * 8)
54 chart = <ui.Svg key="chart" source={svgChart(list, width, 120)} alt={t.alt(list.length, now.percent)} />
55 } else {
56 chart = <Text color="claude">{textBars(list, e.props.bodyColumns).join('\n')}</Text>
57 }
58
59 return (
60 <Box flexDirection="column" paddingX={1} gap={1}>
61 <Box justifyContent="space-between">
62 <Text bold color="claude">{t.heading}</Text>
63 {now !== undefined && (
64 <Text color={now.percent < 75 ? 'success' : now.percent < 90 ? 'warning' : 'error'}>
65 {`${now.percent}% · ${short(now.tokens)} / ${short(now.window)}`}
66 </Text>
67 )}
68 </Box>
69 {chart}
70 <Text dimColor>{t.footer}</Text>
71 </Box>
72 )
73 })
74}
75hooks/chart.ts 33 lines1// Pure drawing helpers: one data layer, two renderings. No $ here.
2import type { Point } from '../types'
3
4export const HISTORY = 40
5
6// A self-contained SVG line chart (desktop). Colours use currentColor so it follows the theme.
7export function svgChart(points: readonly Point[], width: number, height: number): string {
8 const pad = 6
9 const w = Math.max(60, width)
10 const h = Math.max(40, height)
11 const x = (i: number) => pad + (points.length <= 1 ? 0 : (i * (w - 2 * pad)) / (points.length - 1))
12 const y = (p: number) => Number((h - pad - (Math.max(0, Math.min(100, p)) / 100) * (h - 2 * pad)).toFixed(1))
13 const line = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${x(i).toFixed(1)},${y(p.percent).toFixed(1)}`).join(' ')
14 const guide = (p: number) => `<line x1="${pad}" x2="${w - pad}" y1="${y(p)}" y2="${y(p)}" stroke="currentColor" stroke-opacity="0.15" stroke-dasharray="3 3"/>`
15 const last = points[points.length - 1]
16 const dot = last === undefined ? '' : `<circle cx="${x(points.length - 1)}" cy="${y(last.percent)}" r="3" fill="currentColor"/>`
17 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">${guide(50)}${guide(90)}<path d="${line}" fill="none" stroke="currentColor" stroke-width="2"/>${dot}</svg>`
18}
19
20// The same data as text bars (terminal and anywhere Svg is missing).
21export function textBars(points: readonly Point[], columns: number): string[] {
22 const width = Math.max(8, columns - 12)
23 return points.slice(-12).map((p, i, all) => {
24 const filled = Math.round((Math.max(0, Math.min(100, p.percent)) / 100) * width)
25 const n = points.length - all.length + i + 1
26 return `${String(n).padStart(3)} ${'█'.repeat(filled)}${'░'.repeat(width - filled)} ${String(p.percent).padStart(3)}%`
27 })
28}
29
30export function short(n: number): string {
31 return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
32}
33hooks/i18n.ts 30 lines1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7 commandDescription: 'Chart context-window use per turn',
8 paneTitle: 'Context chart',
9 noReading: 'No reading yet: the chart starts after Claude’s first reply.',
10 alt: (turns: number, percent: number) => `Context use over ${turns} turns, now ${percent}%`,
11 heading: '◆ Context',
12 footer: 'one point per turn · dashed lines at 50% and 90% · Esc closes',
13}
14
15const ptBR: typeof en = {
16 commandDescription: 'Gráfico do uso de contexto por turno',
17 paneTitle: 'Gráfico de contexto',
18 noReading: 'Nenhuma leitura ainda: o gráfico começa depois da primeira resposta do Claude.',
19 alt: (turns: number, percent: number) => `Uso de contexto em ${turns} turnos, agora ${percent}%`,
20 heading: '◆ Contexto',
21 footer: 'um ponto por turno · linhas tracejadas em 50% e 90% · Esc fecha',
22}
23
24export type Strings = typeof en
25const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
26
27export function strings(language: unknown): Strings {
28 return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
29}
30types/index.d.ts 8 lines1export type Point = { percent: number; tokens: number; window: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'context-chart': { points: Point[] }
6 }
7}
8