SLOPSHOPPER

todays-report

/report shows how many tasks Claude did, the hours it saved you and what that time is worth, by day, week, month and year

paneguardcommand
v1.0.0no licenseupdated 2026-10-18hussam8988/claude-mods/plugins/todays-report
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · todays-report
│ ┃ Today's Report ✕ › fix the failing auth test and add an audit log call │ ┃ Today's Report │ ┃ ⏺ Read(src/auth.ts) │ ┃ [ Today ] [ This week ] [ This month ] [ Thi ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ 1 tasks done ⎿ Added 2 lines, removed 1 line │ ┃ 35 min of your time saved (estimate) ⏺ Bash(bun test) │ ┃ $23 worth of time at $40/h ⎿ 3 pass, 1 fail │ ┃ 3 file edits · 9 tool actions · Claude │ ┃ worked 0 min ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ Last 7 days — time saved ✻ Worked for 42s · done 4:20 PM │ ┃ Fri · │ ┃ Sat · › /report │ ┃ Sun · │ ┃ Mon · │ ┃ Tue · │ ┃ Wed · │ ┃ Thu ██████████████████ 35 min │ ┃ │ ┃ Change your hourly value: /report rate 45 │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Today's Report
Today's Report [ Today ] [ This week ] [ This month ] [ This year ] 1 tasks done 35 min of your time saved (estimate) $23 worth of time at $40/h 3 file edits · 9 tool actions · Claude worked 0 min Last 7 days — time saved Fri · Sat · Sun · Mon · Tue · Wed · Thu ██████████████████ 35 min Change your hourly value: /report rate 45
README

Hussam's Claude Code mods

Three mods for the Code tab of the Claude desktop app (and terminal sessions):

ModWhat it doesHow to use
auto-modelReads your first prompt of a session and picks Haiku (quick questions), Sonnet (normal work) or Opus (big builds) for the whole session. It never switches mid-session, so the prompt cache stays intact and you don't pay to re-read the conversation. If you chose a model yourself with /model, it leaves it alone.Automatic. /auto-model shows the pick; /auto-model off / on; /auto-model opus before your first prompt forces one.
prompt-upgradeAsks you 3 quick questions, then rewrites a one-line prompt into a detailed one you can edit, run, put in the prompt box, or copy./boost make a landing page for my honey brand
todays-reportCounts the tasks Claude finished, estimates hours saved and what that time is worth — today, this week, month, year./report · /report rate 45 to set your hourly value (default $40)

Model use: auto-model makes one tiny Haiku call per new session; prompt-upgrade makes one Haiku call (questions) and one Sonnet call (the rewrite) each time you run it; todays-report makes none.

The "hours saved" figure is an estimate: 3 min for a question answered, otherwise 5 min + 2 min per tool action + 6 min per file edit, capped at 3 h per task.

Source 2 files
hooks/register.tsx 242 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { ReportRange } from '../types'
5
6// Today's Report — /report
7// Counts the tasks Claude finished for you, estimates the hours it saved you
8// and what that time is worth, by day, week, month and year. Kept across
9// sessions on this computer.
10//   /report            open the report
11//   /report rate 45    set your hourly value (default 40)
12
13type Day = { tasks: number; tools: number; edits: number; activeSec: number; savedMin: number }
14const ZERO: Day = { tasks: 0, tools: 0, edits: 0, activeSec: 0, savedMin: 0 }
15
16const PANE = 'todays-report'
17const range = atom({ plugin: 'todays-report', key: 'range' } as const, 'day' as ReportRange)
18const tick = atom({ plugin: 'todays-report', key: 'tick' } as const, 0)
19
20const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
21const LABELS: Record<ReportRange, string> = { day: 'Today', week: 'This week', month: 'This month', year: 'This year' }
22
23function dayKey(ms: number): string {
24  // YYYY-MM-DD in the computer's own time zone
25  const d = new Date(ms)
26  const y = d.getFullYear()
27  const m = String(d.getMonth() + 1).padStart(2, '0')
28  const day = String(d.getDate()).padStart(2, '0')
29  return y + '-' + m + '-' + day
30}
31
32// A conservative estimate of how long the same work would take a person by hand.
33function minutesSaved(tools: number, edits: number): number {
34  if (tools === 0) return 3
35  const other = Math.max(0, tools - edits)
36  return Math.min(180, 5 + 2 * Math.min(other, 30) + 6 * edits)
37}
38
39function inRange(key: string, r: ReportRange, now: number): boolean {
40  const today = dayKey(now)
41  if (r === 'day') return key === today
42  if (r === 'month') return key.slice(0, 7) === today.slice(0, 7)
43  if (r === 'year') return key.slice(0, 4) === today.slice(0, 4)
44  const weekAgo = dayKey(now - 6 * 86400000)
45  return key >= weekAgo && key <= today
46}
47
48async function loadRange($: any, r: ReportRange): Promise<{ sum: Day; days: number }> {
49  const now = await $.clock.now()
50  const keys: string[] = await $.store.keys()
51  const sum: Day = { ...ZERO }
52  let days = 0
53  for (const k of keys) {
54    if (!k.startsWith('d:')) continue
55    const key = k.slice(2)
56    if (!inRange(key, r, now)) continue
57    const d = ((await $.store.get(k)) as Day | undefined) ?? ZERO
58    sum.tasks += d.tasks
59    sum.tools += d.tools
60    sum.edits += d.edits
61    sum.activeSec += d.activeSec
62    sum.savedMin += d.savedMin
63    if (d.tasks > 0) days += 1
64  }
65  return { sum, days }
66}
67
68async function lastSevenDays($: any): Promise<{ label: string; tasks: number; savedMin: number }[]> {
69  const now = await $.clock.now()
70  const out: { label: string; tasks: number; savedMin: number }[] = []
71  for (let i = 6; i >= 0; i--) {
72    const ms = now - i * 86400000
73    const k = dayKey(ms)
74    const d = ((await $.store.get('d:' + k)) as Day | undefined) ?? ZERO
75    const label = new Date(ms).toLocaleDateString('en-CA', { weekday: 'short' })
76    out.push({ label, tasks: d.tasks, savedMin: d.savedMin })
77  }
78  return out
79}
80
81async function getRate($: any): Promise<number> {
82  const v = await $.store.get('rate')
83  return typeof v === 'number' && v > 0 ? v : 40
84}
85
86function hours(min: number): string {
87  if (min < 60) return Math.round(min) + ' min'
88  return (min / 60).toFixed(1) + ' h'
89}
90
91function money(n: number): string {
92  return '$' + n.toLocaleString('en-CA', { maximumFractionDigits: 0 })
93}
94
95async function summaryText($: any, r: ReportRange): Promise<string> {
96  const { sum } = await loadRange($, r)
97  const rate = await getRate($)
98  return (
99    LABELS[r] + ': ' + sum.tasks + ' tasks, ' + sum.edits + ' file edits. Saved about ' + hours(sum.savedMin) +
100    ' (worth ~' + money((sum.savedMin / 60) * rate) + ' at $' + rate + '/h). Claude worked ' + hours(sum.activeSec / 60) + '.'
101  )
102}
103
104export const register: Register = on => {
105  let startedAt = 0
106  let tools = 0
107  let edits = 0
108
109  on('session.start', async ($, e, next) => {
110    const out = await next(e)
111    try {
112      await $.command.register({
113        name: 'report',
114        description: "Today's Report: tasks done, hours saved and what it's worth",
115        argumentHint: '[rate <dollars per hour>]',
116        immediate: true,
117      })
118    } catch (err) {
119      $.ui.log('todays-report: could not add /report (' + String(err) + ')')
120    }
121    return out
122  })
123
124  on('turn.start', async ($, e, next) => {
125    if (e.agentId === undefined) {
126      startedAt = await $.clock.now()
127      tools = 0
128      edits = 0
129    }
130    return next(e)
131  })
132
133  on('tool.call', async ($, e, next) => {
134    // Subagents' tool calls count too: they are part of the same task
135    tools += 1
136    if (EDIT_TOOLS.has(String(e.tool))) edits += 1
137    return next(e)
138  })
139
140  on('turn.complete', async ($, e, next) => {
141    const out = await next(e)
142    if ((e as any).agentId !== undefined || e.reason !== 'answer' || startedAt === 0) return out
143    const now = await $.clock.now()
144    const key = 'd:' + dayKey(now)
145    const d = ((await $.store.get(key)) as Day | undefined) ?? ZERO
146    const updated: Day = {
147      tasks: d.tasks + 1,
148      tools: d.tools + tools,
149      edits: d.edits + edits,
150      activeSec: d.activeSec + Math.max(0, Math.round((now - startedAt) / 1000)),
151      savedMin: d.savedMin + minutesSaved(tools, edits),
152    }
153    await $.store.set(key, updated)
154    startedAt = 0
155    await update($, tick, n => n + 1)
156    return out
157  })
158
159  on('command.run', { command: 'report' }, async ($, e) => {
160    const args = (e.args || '').trim().split(/\s+/)
161    if (args[0] === 'rate') {
162      const n = Number((args[1] || '').replace(/[$,]/g, ''))
163      if (!(n > 0)) return { text: 'Usage: /report rate 45  (your hourly value in dollars)' }
164      await $.store.set('rate', n)
165      await update($, tick, x => x + 1)
166      return { text: 'Hourly value set to $' + n + '/h.' }
167    }
168    const opened = await $.ui.open({ id: PANE, title: "Today's Report" })
169    if (!opened.isPlaced) {
170      const lines = [
171        await summaryText($, 'day'),
172        await summaryText($, 'week'),
173        await summaryText($, 'month'),
174        await summaryText($, 'year'),
175      ]
176      return { text: lines.join('\n') }
177    }
178    return {}
179  })
180
181  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
182    const { Box, Text, Button } = $.ui.resolve(e) as any
183    const r = await read($, range)
184    await read($, tick)
185    const { sum, days } = await loadRange($, r)
186    const rate = await getRate($)
187    const value = (sum.savedMin / 60) * rate
188    const week = await lastSevenDays($)
189    const most = Math.max(1, ...week.map(d => d.savedMin))
190    const barWidth = 18
191
192    const tab = (key: ReportRange) => (
193      <Button
194        key={'tab-' + key}
195        label={LABELS[key]}
196        variant={key === r ? 'primary' : 'secondary'}
197        onPress={() => update($, range, () => key)}
198      />
199    )
200
201    return (
202      <Box flexDirection="column">
203        <Text bold>Today's Report</Text>
204        <Box marginTop={1}>
205          {tab('day')}
206          <Text> </Text>
207          {tab('week')}
208          <Text> </Text>
209          {tab('month')}
210          <Text> </Text>
211          {tab('year')}
212        </Box>
213        <Box flexDirection="column" marginTop={1}>
214          <Text>
215            <Text bold>{sum.tasks}</Text> tasks done{r !== 'day' && days > 0 ? ' over ' + days + ' active day' + (days === 1 ? '' : 's') : ''}
216          </Text>
217          <Text>
218            <Text bold>{hours(sum.savedMin)}</Text> of your time saved (estimate)
219          </Text>
220          <Text>
221            <Text bold color="green">{money(value)}</Text> worth of time at ${rate}/h
222          </Text>
223          <Text dimColor>
224            {sum.edits} file edits · {sum.tools} tool actions · Claude worked {hours(sum.activeSec / 60)}
225          </Text>
226        </Box>
227        <Box flexDirection="column" marginTop={1}>
228          <Text dimColor>Last 7 days — time saved</Text>
229          {week.map(d => (
230            <Text>
231              {d.label.padEnd(4)} {'█'.repeat(Math.round((d.savedMin / most) * barWidth)) || '·'} {d.savedMin > 0 ? hours(d.savedMin) : ''}
232            </Text>
233          ))}
234        </Box>
235        <Box marginTop={1}>
236          <Text dimColor>Change your hourly value: /report rate 45</Text>
237        </Box>
238      </Box>
239    )
240  })
241}
242
types/index.d.ts 8 lines
1export type ReportRange = 'day' | 'week' | 'month' | 'year'
2
3declare module 'claude-code' {
4  interface PluginState {
5    'todays-report': { range: ReportRange; tick: number }
6  }
7}
8