SLOPSHOPPER

usage-line

Linha de status compacta: modelo, branch, barras de contexto e limites 5h/7d, custo e estado do cache de prompt.

newstatusprocesstimer
v0.1.0no licenseupdated 2026-10-21osceiatech/skills-osceia/usage-line
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-line
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ usage-line: Opus 5.5 ⎇ feat/auth-refresh │ ctx ▰▰▰▰▱▱▱▱ 49% │ 5h ▰▰▱▱▱▱▱▱ 31% NaNdNaNh │ $0.42 │ ⚡93% 5m
README

usage-line

Mod do Claude Code que mostra o consumo da sessão numa linha de status compacta, abaixo do prompt:

Opus 5.5 ⎇ main │ ctx ▰▰▰▱▱▱▱▱ 42% │ 5h ▰▰▱▱▱▱▱▱ 23% 2h10 │ 7d ▰▰▰▰▰▱▱▱ 62% 3d4h │ $1.23 │ ⚡90% 4m
CampoO que é
Opus 5.5 ⎇ mainmodelo da sessão e branch do git
ctxjanela de contexto ocupada
5h / 7dlimites de uso da assinatura e tempo até resetar (vazio com API key)
$1.23custo da sessão em USD
⚡90% 4m / ❄90%quanto da entrada do último turno veio do cache; quente com tempo restante, ou frio

Atualiza a cada turno, a cada medição do Claude Code e a cada 15 s.

Instalar

No prompt do Claude Code (terminal):

/plugin install usage-line --marketplace osceiatech/skills-osceia

Ou no shell:

claude plugin install usage-line@skills-osceia --scope user

Responda y para adicionar o marketplace e escolha o escopo. O mod passa a valer na hora.

Repositório privado: precisa de acesso de leitura ao osceiatech/skills-osceia e credencial git para o github.com (chave SSH, ou gh auth login seguido de gh auth setup-git).

Configurar

/config → Prompt cache TTL (cacheTtl): 5m (padrão da API) ou 1h. Ajuste para o TTL da sua conta, senão o cache aparece frio antes da hora.

Desenvolver

claude --plugin-dir ./usage-line   # carrega da pasta, com recarga ao salvar
claude plugin test ./usage-line
claude plugin validate ./usage-line
Source 1 files
hooks/register.ts 122 lines
1import type { EngineInterface, Register, SessionRateLimit, SessionUsage, TurnUsage } from 'claude-code'
2
3const SEP = ' │ '
4const TICK_MS = 15_000
5
6type Seen = {
7  ttlMs: number
8  lastUsage?: TurnUsage
9  lastResponseAt?: number
10  measured?: Pick<SessionUsage, 'context' | 'rateLimits' | 'cost'>
11}
12
13const seen: Seen = { ttlMs: 5 * 60_000 }
14
15export const register: Register = (on, options) => {
16  seen.ttlMs = options.cacheTtl === '1h' ? 60 * 60_000 : 5 * 60_000
17
18  on('session.start', async ($, e, next) => {
19    const started = await next(e)
20    await paint($)
21    $.clock.every(TICK_MS, () => void paint($))
22
23    return started
24  })
25
26  on('session.measure', async ($, e, next) => {
27    seen.measured = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
28    await paint($)
29
30    return next(e)
31  })
32
33  on('turn.complete', async ($, e, next) => {
34    if (e.agentId === undefined && e.usage !== undefined) {
35      seen.lastUsage = e.usage
36      seen.lastResponseAt = await $.clock.now()
37      await paint($)
38    }
39
40    return next(e)
41  })
42}
43
44async function paint($: EngineInterface) {
45  const now = await $.clock.now()
46  const [model, branch] = await Promise.all([$.session.model(), gitBranch($)])
47  const usage = seen.measured ?? (await $.session.usage())
48
49  const parts = [
50    branch === undefined ? prettyModel(model) : `${prettyModel(model)} ⎇ ${branch}`,
51    context(usage.context),
52    ...usage.rateLimits.map(l => limit(l, now)),
53    usage.cost === undefined ? undefined : `$${usage.cost.usd.toFixed(2)}`,
54    cache(seen.lastUsage, seen.lastResponseAt, now, seen.ttlMs),
55  ]
56
57  $.ui.status(parts.filter(p => p !== undefined).join(SEP))
58}
59
60async function gitBranch($: EngineInterface) {
61  try {
62    const { exitCode, stdout } = await $.process.run(['git', 'branch', '--show-current'], {
63      timeoutMs: 3000,
64    })
65    const name = stdout.trim()
66
67    return exitCode === 0 && name !== '' ? name : undefined
68  } catch {
69    return undefined
70  }
71}
72
73// claude-opus-5-5[1m] -> Opus 5.5 (1M)
74const prettyModel = (id: string) => {
75  const wide = id.includes('[1m]') ? ' (1M)' : ''
76  const words = id.replace(/\[1m\]$/, '').replace(/^claude-/, '').replace(/-\d{8}$/, '').split('-')
77  const family = words.filter(w => !/^\d+$/.test(w))
78  const version = words.filter(w => /^\d+$/.test(w)).join('.')
79  const name = family.map(w => w[0]?.toUpperCase() + w.slice(1)).join(' ')
80
81  return `${name}${version === '' ? '' : ` ${version}`}${wide}`
82}
83
84const BAR_CELLS = 8
85
86const bar = (percent: number) => {
87  const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * BAR_CELLS)
88
89  return '▰'.repeat(filled) + '▱'.repeat(BAR_CELLS - filled)
90}
91
92const metric = (label: string, percent: number | undefined) =>
93  percent === undefined ? `${label} ${bar(0)} –` : `${label} ${bar(percent)} ${Math.round(percent)}%`
94
95const context = (c: SessionUsage['context']) => metric('ctx', c.percent)
96
97const duration = (ms: number) => {
98  const m = Math.max(0, Math.round(ms / 60_000))
99  if (m < 60) return `${m}m`
100  const h = Math.floor(m / 60)
101  if (h < 24) return `${h}h${String(m % 60).padStart(2, '0')}`
102
103  return `${Math.floor(h / 24)}d${h % 24}h`
104}
105
106const LIMIT_LABEL: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: '$lim' }
107
108const limit = (l: SessionRateLimit, now: number) => {
109  const reset = l.resetsAt === undefined ? '' : ` ${duration(Date.parse(l.resetsAt) - now)}`
110
111  return `${metric(LIMIT_LABEL[l.kind] ?? l.kind, l.percentUsed)}${reset}`
112}
113
114const cache = (u: TurnUsage | undefined, at: number | undefined, now: number, ttlMs: number) => {
115  if (u === undefined || at === undefined) return undefined
116  const input = u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens
117  const hit = input === 0 ? 0 : Math.round((u.cache_read_input_tokens / input) * 100)
118  const left = at + ttlMs - now
119
120  return left > 0 ? `⚡${hit}% ${duration(left)}` : `❄${hit}%`
121}
122