SLOPSHOPPER

weekly-usage

Shows the weekly allowance used and when it resets, above the prompt

newbandtoast
v0.1.2no licenseupdated 2026-10-15manualofideas/claude-weekly-usage
A shopper browsing a rack in a slop shop
README

weekly-usage

A Claude Code mod that shows your weekly allowance above the prompt, in the terminal and in the desktop app's Code tab:

Weekly allowance: 42% used · resets Fri Oct 16, 11:00 AM

The reset time is in your computer's local time. The line is dim, with a few parts in bold:

  • the percentage, at 80% used or more (a toast also appears once when you cross 80%);
  • the reset date and time, when the reset is within 48 hours;
  • only the reset time, when the reset is within 24 hours.

It reads the seven_day rate-limit window that Claude Code reports after each response, so the line appears after your first message in a session. It needs a Claude subscription; with API-key billing there is no weekly window and nothing is shown.

Install

In a terminal, start claude and type:

/plugin install weekly-usage --marketplace manualofideas/claude-weekly-usage

Answer y to add the marketplace, then press Enter to pick the user scope. Installed at the user scope, it also loads in the desktop app's Code tab (the /plugin command itself works only in a terminal).

Change the threshold

WARN_AT near the top of hooks/register.tsx.

Develop

claude plugin validate .
claude plugin test .
Source 2 files
hooks/register.tsx 102 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Weekly } from '../types'
5
6const weekly = atom({ plugin: 'weekly-usage', key: 'weekly' } as const, null)
7const warned = atom({ plugin: 'weekly-usage', key: 'warned' } as const, false)
8
9const WARN_AT = 80
10const HOUR = 60 * 60 * 1000
11const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
12const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
13
14// { day: "Thu Oct 16", clock: "9:00 AM" } in the machine's local time.
15const resetParts = (iso: string): { day: string; clock: string } | null => {
16  const d = new Date(iso)
17  if (Number.isNaN(d.getTime())) return null
18  const h = d.getHours()
19  const m = String(d.getMinutes()).padStart(2, '0')
20  return {
21    day: `${DAYS[d.getDay()]} ${MONTHS[d.getMonth()]} ${d.getDate()}`,
22    clock: `${h % 12 === 0 ? 12 : h % 12}:${m} ${h < 12 ? 'AM' : 'PM'}`,
23  }
24}
25
26// "Thu Oct 16, 9:00 AM" in the machine's local time.
27export const formatReset = (iso: string): string => {
28  const p = resetParts(iso)
29  return p ? `${p.day}, ${p.clock}` : iso
30}
31
32export const describe = (w: Weekly): string =>
33  `Weekly allowance: ${w.percentUsed}% used` +
34  (w.resetsAt ? ` · resets ${formatReset(w.resetsAt)}` : '')
35
36type Segment = { text: string; bold?: boolean }
37
38// The line in parts, the ones to stand out marked bold: the percentage at
39// WARN_AT and above; the reset's date and time within 48 hours of it, only
40// its time within 24 hours.
41export const segments = (w: Weekly, now: number): Segment[] => {
42  const out: Segment[] = [
43    { text: 'Weekly allowance: ' },
44    { text: `${w.percentUsed}%`, bold: w.percentUsed >= WARN_AT },
45    { text: ' used' },
46  ]
47  if (!w.resetsAt) return out
48
49  const p = resetParts(w.resetsAt)
50  if (!p) return [...out, { text: ` · resets ${w.resetsAt}` }]
51
52  const left = new Date(w.resetsAt).getTime() - now
53  out.push({ text: ' · resets ' })
54  if (left <= 24 * HOUR) {
55    out.push({ text: `${p.day}, ` }, { text: p.clock, bold: true })
56  } else if (left <= 48 * HOUR) {
57    out.push({ text: `${p.day}, ${p.clock}`, bold: true })
58  } else {
59    out.push({ text: `${p.day}, ${p.clock}` })
60  }
61  return out
62}
63
64export const register: Register = on => {
65  on('session.measure', async ($, e, next) => {
66    const window = e.rateLimits.find(r => r.kind === 'seven_day')
67    if (window) {
68      const w: Weekly = { percentUsed: window.percentUsed, resetsAt: window.resetsAt ?? null }
69      await update($, weekly, () => w)
70
71      // One toast per crossing: re-arms when usage drops back (a new week).
72      if (w.percentUsed < WARN_AT) {
73        await update($, warned, () => false)
74      } else if (!(await read($, warned))) {
75        await update($, warned, () => true)
76        $.ui.toast(`${describe(w)}`)
77      }
78    }
79
80    return next(e)
81  })
82
83  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
84    const w = await read($, weekly)
85    if (e.props.hasSurvey || w === null) {
86      return next(e)
87    }
88
89    const { Box, Text } = $.ui.resolve(e)
90    const now = await $.clock.now()
91
92    // Dim by default; a bold part is drawn in the normal font color.
93    return (
94      <Box>
95        <Text key="weekly">
96          {segments(w, now).map(s => (s.bold ? <Text bold>{s.text}</Text> : <Text dimColor>{s.text}</Text>))}
97        </Text>
98      </Box>
99    )
100  })
101}
102
types/index.d.ts 8 lines
1export type Weekly = { percentUsed: number; resetsAt: string | null }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'weekly-usage': { weekly: Weekly | null; warned: boolean }
6  }
7}
8