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

Three mods for the Code tab of the Claude desktop app (and terminal sessions):
| Mod | What it does | How to use |
|---|---|---|
| auto-model | Reads 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-upgrade | Asks 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-report | Counts 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.
hooks/register.tsx 242 lines1import { 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}
242types/index.d.ts 8 lines1export type ReportRange = 'day' | 'week' | 'month' | 'year'
2
3declare module 'claude-code' {
4 interface PluginState {
5 'todays-report': { range: ReportRange; tick: number }
6 }
7}
8