Every morning Claude asks for the three tasks you need to do today, then pins them in a side pane next to your revenue and key numbers.

Claude Code now has mods. Small add-ons that change how Claude itself looks and behaves. These three turn it from an assistant into something closer to a manager that works for you.
| Mod | What it does |
|---|---|
| Daily Command Center | Every morning from 9am, Claude asks for the three tasks you need to do today and stays locked until you type them. Then it pins them in a side pane next to your revenue and key numbers. |
| Focus Lock | Pick one task, start a timed sprint, and Claude holds any prompt that is not about that task until the timer ends. |
| Done Chime | Plays a sound and shows a toast when Claude finishes a long job, so you can walk away instead of watching the screen. |
Step 1. Update Claude Code. Mods need a recent version. These were built and tested on 2.1.289. Older versions reject them.
Step 2. Open Claude Code and paste this:
Install these mods for me: https://github.com/tenfoldmarc/claude-code-mods
Step 3. Start a new session. That's it.
claude plugin marketplace add tenfoldmarc/claude-code-mods
claude plugin install daily-command-center@tenfold-mods
claude plugin install focus-lock@tenfold-mods
claude plugin install done-chime@tenfold-mods
Install only the ones you want. Then start a new session, or run /reload-plugins in an open one.
1, 2 or 3 in the pane to check one off.| Command | What it does |
|---|---|
/today | Open the Today pane |
/today done 2 | Check off task 2 |
/today reset | Clear today's tasks and get asked again |
/today skip | No tasks today |
/today refresh | Refresh the numbers now |
/metric Followers 12,400 | Pin any number to the pane |
/metric Followers | Remove that number |
Your numbers. Three ways to get them in, all optional:
/metric. Zero setup./plugin configure daily-command-center@tenfold-mods and paste a Stripe key. It shows revenue today and month to date. Use a restricted, read-only key with read access to Balance only, never your full secret key. Create one in Stripe under Developers, API keys, Create restricted key. Claude Code keeps it in your system's secure storage.Label: value line per number. Email subscribers, followers, anything you can fetch.| Command | What it does |
|---|---|
/sprint 50 write the sales page | Start a 50 minute sprint on that task |
/sprint write the sales page | Same, with your default length |
/sprint | See the time left |
/sprint stop | End the sprint early |
!! to push it through anyway.Nothing to do. Any job that runs 20 seconds or longer ends with a chime and a toast showing how long it took. Errors get a different sound.
Run /plugin configure <mod>@tenfold-mods to change these.
| Mod | Setting | Default |
|---|---|---|
| Daily Command Center | Start hour | 9 |
| Daily Command Center | Stripe key | none |
| Daily Command Center | Metrics command | none |
| Daily Command Center | Refresh minutes | 5 |
| Focus Lock | Sprint length (minutes) | 50 |
| Focus Lock | Judge model | haiku |
| Done Chime | Minimum seconds | 20 |
| All three | Volume (0 to 4) | 1 |
claude plugin uninstall <mod>@tenfold-mods.claude -p scripts are never held or captured.Paste this into Claude Code:
Update the mods from https://github.com/tenfoldmarc/claude-code-mods
Or in a terminal:
claude plugin marketplace update tenfold-mods
claude plugin update daily-command-center@tenfold-mods
Repeat the second command for focus-lock and done-chime.
If a user asks you to install these mods, do this:
claude --version. If it is older than 2.1.289, tell the user to update Claude Code first and stop. If claude is not on the PATH, give the user the terminal commands from the Install section to run themselves.claude plugin marketplace add tenfoldmarc/claude-code-mods.claude plugin install for each of daily-command-center@tenfold-mods, focus-lock@tenfold-mods and done-chime@tenfold-mods, unless the user asked for only some of them./reload-plugins), and that after 9am the Daily Command Center will ask for their three tasks./plugin configure daily-command-center@tenfold-mods and a restricted, read-only key.To update: claude plugin marketplace update tenfold-mods, then claude plugin update <mod>@tenfold-mods for each installed mod.
MIT. The sounds are original and included under the same license.
Built by @tenfoldmarc. Follow for daily AI builds. Real systems, not theory.
hooks/register.tsx 501 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Day, Metric, Task } from '../types'
5
6const PANE = 'today'
7const TASKS = 3
8const MAX_TASK = 120
9const CHECK = 'sounds/check.wav'
10const ALL_DONE = 'sounds/all-done.wav'
11
12const day = atom({ plugin: 'daily-command-center', key: 'day' } as const, {
13 date: '',
14 tasks: [],
15 isSkipped: false,
16})
17const metrics = atom({ plugin: 'daily-command-center', key: 'metrics' } as const, [])
18const refreshedAt = atom({ plugin: 'daily-command-center', key: 'refreshedAt' } as const, 0)
19const minute = atom({ plugin: 'daily-command-center', key: 'minute' } as const, 0)
20const zone = atom({ plugin: 'daily-command-center', key: 'zone' } as const, 0)
21
22type Config = {
23 startHour: number
24 stripeKey: string
25 metricsCommand: string
26 refreshMs: number
27 gain: number
28}
29
30type Parts = { date: string; hour: number; dayStart: number; monthStart: number }
31
32// The person's own calendar day, from the clock and their offset from UTC.
33const localParts = (ms: number, offsetMinutes: number): Parts => {
34 const local = new Date(ms + offsetMinutes * 60_000)
35 const [year, month, date] = [local.getUTCFullYear(), local.getUTCMonth(), local.getUTCDate()]
36 const pad = (n: number) => String(n).padStart(2, '0')
37
38 return {
39 date: `${year}-${pad(month + 1)}-${pad(date)}`,
40 hour: local.getUTCHours(),
41 dayStart: Date.UTC(year, month, date) - offsetMinutes * 60_000,
42 monthStart: Date.UTC(year, month, 1) - offsetMinutes * 60_000,
43 }
44}
45
46const isTask = (value: unknown): value is Task =>
47 typeof value === 'object' &&
48 value !== null &&
49 typeof (value as Task).text === 'string' &&
50 typeof (value as Task).isDone === 'boolean'
51
52const asDay = (value: unknown): Day | undefined => {
53 const held = value as Day | null | undefined
54
55 return typeof held === 'object' &&
56 held !== null &&
57 typeof held.date === 'string' &&
58 Array.isArray(held.tasks) &&
59 held.tasks.every(isTask)
60 ? { date: held.date, tasks: held.tasks.slice(0, TASKS), isSkipped: held.isSkipped === true }
61 : undefined
62}
63
64const forToday = (held: Day, date: string): Day =>
65 held.date === date ? held : { date, tasks: [], isSkipped: false }
66
67const isAsking = (today: Day, hour: number, startHour: number): boolean =>
68 hour >= startHour && !today.isSkipped && today.tasks.length < TASKS
69
70const clip = (text: string, length: number): string => {
71 const line = text.replace(/\s+/g, ' ').trim()
72
73 return line.length <= length ? line : `${line.slice(0, Math.max(1, length - 3))}...`
74}
75
76const SYMBOLS: Record<string, string> = { usd: '$', cad: 'CA$', aud: 'A$', eur: '€', gbp: '£' }
77const NO_CENTS = new Set(['jpy', 'krw', 'vnd', 'clp'])
78
79const money = (minorUnits: number, currency: string): string => {
80 const amount = NO_CENTS.has(currency) ? minorUnits : Math.round(minorUnits) / 100
81 const whole = Math.trunc(Math.abs(amount)).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
82 const cents = NO_CENTS.has(currency) ? '' : `.${Math.abs(amount).toFixed(2).split('.')[1]}`
83 const symbol = SYMBOLS[currency] ?? `${currency.toUpperCase()} `
84
85 return `${amount < 0 ? '-' : ''}${symbol}${whole}${cents}`
86}
87
88const ago = (ms: number): string => {
89 const minutes = Math.floor(ms / 60_000)
90
91 return minutes < 1 ? 'just now' : minutes < 60 ? `${minutes}m ago` : `${Math.floor(minutes / 60)}h ago`
92}
93
94// What a person typed, at the terminal, in the desktop app or from their phone.
95// Never `sdk`: a headless `claude -p` run or a script is nobody's morning.
96const TYPED = new Set(['composer', 'bridge'])
97
98let timer: Timer | undefined
99let wasAsking: boolean | undefined
100
101// The store is the truth across sessions; the state is what this one draws.
102async function load($: EngineInterface): Promise<{ today: Day; parts: Parts; time: number }> {
103 const time = await $.clock.now()
104 const parts = localParts(time, await read($, zone))
105 const today = forToday(asDay(await $.store.get('day')) ?? (await read($, day)), parts.date)
106
107 if (JSON.stringify(await read($, day)) !== JSON.stringify(today)) {
108 await update($, day, () => today)
109 }
110
111 return { today, parts, time }
112}
113
114async function save($: EngineInterface, next: Day) {
115 await $.store.set('day', next)
116 await update($, day, () => next)
117}
118
119async function readZone($: EngineInterface) {
120 const time = await $.clock.now()
121 let offset = -new Date(time).getTimezoneOffset()
122
123 try {
124 // The host's own answer wins: the module's environment may run in UTC.
125 const { exitCode, stdout } = await $.process.run(['date', '+%z'], { timeoutMs: 5000 })
126 const found = /^([+-])(\d{2})(\d{2})/.exec(stdout.trim())
127
128 if (exitCode === 0 && found !== null) {
129 offset = (found[1] === '-' ? -1 : 1) * (Number(found[2]) * 60 + Number(found[3]))
130 }
131 } catch {
132 // No `date` here (Windows): the environment's own offset stands.
133 }
134
135 await update($, zone, () => offset)
136}
137
138type StripePage = {
139 data: { id: string; amount: number; currency: string; created: number; type: string }[]
140 has_more: boolean
141}
142
143async function stripeRevenue($: EngineInterface, key: string, parts: Parts): Promise<Metric[]> {
144 let today = 0
145 let month = 0
146 let currency = 'usd'
147 let after = ''
148 let hasMore = false
149
150 try {
151 for (let page = 0; page < 20; page += 1) {
152 const since = Math.floor(parts.monthStart / 1000)
153 const response = await $.http.fetch(
154 `https://api.stripe.com/v1/balance_transactions?limit=100&created%5Bgte%5D=${since}${after}`,
155 { headers: { Authorization: `Bearer ${key}` } },
156 )
157
158 if (!response.ok) {
159 return [{ label: 'Revenue', value: `Stripe said ${response.status}. Check the key.` }]
160 }
161
162 const body = JSON.parse(response.text) as StripePage
163
164 for (const row of body.data) {
165 if (row.type !== 'charge' && row.type !== 'payment') {
166 continue
167 }
168
169 currency = row.currency
170 month += row.amount
171 today += row.created * 1000 >= parts.dayStart ? row.amount : 0
172 }
173
174 const last = body.data.at(-1)
175 hasMore = body.has_more && last !== undefined
176
177 if (!hasMore || last === undefined) {
178 break
179 }
180
181 after = `&starting_after=${last.id}`
182 }
183 } catch {
184 return [{ label: 'Revenue', value: 'Stripe could not be reached.' }]
185 }
186
187 return [
188 { label: 'Revenue today', value: money(today, currency) },
189 { label: 'Month to date', value: `${money(month, currency)}${hasMore ? '+' : ''}` },
190 ]
191}
192
193async function commandMetrics($: EngineInterface, command: string): Promise<Metric[]> {
194 try {
195 const { exitCode, stdout } = await $.process.run(['sh', '-c', command], { timeoutMs: 20_000 })
196
197 if (exitCode !== 0) {
198 return [{ label: 'Metrics command', value: `exited ${exitCode}` }]
199 }
200
201 return stdout
202 .split('\n')
203 .map(line => /^([^:]{1,40}):\s*(.+)$/.exec(line.trim()))
204 .flatMap(found => (found === null ? [] : [{ label: String(found[1]).trim(), value: clip(String(found[2]), 40) }]))
205 .slice(0, 8)
206 } catch {
207 return [{ label: 'Metrics command', value: 'could not run' }]
208 }
209}
210
211async function manualMetrics($: EngineInterface): Promise<Metric[]> {
212 const held = await $.store.get('manual')
213
214 return typeof held === 'object' && held !== null
215 ? Object.entries(held as Record<string, unknown>).map(([label, value]) => ({ label, value: String(value) }))
216 : []
217}
218
219async function refresh($: EngineInterface, config: Config) {
220 const { parts, time } = await load($)
221 const fromStripe = config.stripeKey === '' ? [] : await stripeRevenue($, config.stripeKey, parts)
222 const fromCommand = config.metricsCommand === '' ? [] : await commandMetrics($, config.metricsCommand)
223 const all = [...fromStripe, ...fromCommand, ...(await manualMetrics($))]
224 await update($, metrics, () => all)
225 await update($, refreshedAt, () => time)
226}
227
228async function tick($: EngineInterface, config: Config) {
229 const { today, parts, time } = await load($)
230 await update($, minute, () => time)
231 const asking = isAsking(today, parts.hour, config.startHour)
232
233 if (asking && wasAsking === false) {
234 $.ui.toast('Time to set your 3 tasks for today. Type the first one.', { timeoutMs: 15_000 })
235 }
236
237 wasAsking = asking
238
239 if (time - (await read($, refreshedAt)) >= config.refreshMs) {
240 await refresh($, config)
241 }
242}
243
244async function toggle($: EngineInterface, index: number, config: Config) {
245 const { today } = await load($)
246 const tasks = today.tasks.map((task, at) => (at === index ? { ...task, isDone: !task.isDone } : task))
247 const flipped = tasks[index]
248
249 if (flipped === undefined) {
250 return
251 }
252
253 await save($, { ...today, tasks })
254 const isAllDone = tasks.length === TASKS && tasks.every(task => task.isDone)
255
256 if (isAllDone) {
257 $.ui.toast('All three tasks done. That is the day.', { timeoutMs: 10_000 })
258 }
259
260 if (flipped.isDone && config.gain > 0) {
261 void $.audio.play({ asset: isAllDone ? ALL_DONE : CHECK }, { gain: config.gain }).catch(() => {})
262 }
263}
264
265const board = (today: Day): string =>
266 today.tasks.length === 0
267 ? 'No tasks set for today yet.'
268 : today.tasks.map((task, at) => `${at + 1}. [${task.isDone ? 'x' : ' '}] ${task.text}`).join('\n')
269
270export const register: Register = (on, options) => {
271 const config: Config = {
272 startHour: Math.min(23, Math.max(0, Math.floor(Number(options.start_hour ?? 9)))),
273 stripeKey: String(options.stripe_key ?? '').trim(),
274 metricsCommand: String(options.metrics_command ?? '').trim(),
275 refreshMs: Math.max(1, Number(options.refresh_minutes ?? 5)) * 60_000,
276 gain: Math.min(4, Math.max(0, Number(options.volume ?? 1))),
277 }
278
279 on('session.start', async ($, e, next) => {
280 await $.command.register({
281 name: 'today',
282 description: "Show today's three tasks and your numbers in a side pane.",
283 argumentHint: '[done <1-3> | reset | skip | refresh]',
284 })
285 await $.command.register({
286 name: 'metric',
287 description: 'Pin a number to the Today pane, or remove one by giving only its label.',
288 argumentHint: '<label> <value>',
289 })
290 await readZone($)
291 const { today, parts } = await load($)
292 wasAsking = isAsking(today, parts.hour, config.startHour)
293
294 timer?.cancel()
295 timer = $.clock.every(60_000, () => void tick($, config))
296 // Not awaited: a slow Stripe never holds the first prompt.
297 void refresh($, config)
298
299 // The board is up from the start, tasks set or not.
300 void $.ui.open({ id: PANE, title: 'Today' })
301
302 return next(e)
303 })
304
305 on('prompt.submit', async ($, e, next) => {
306 const isTyped = TYPED.has(e.origin.kind)
307 const text = e.text.trim()
308
309 if (!isTyped || text === '' || text.startsWith('/')) {
310 return next(e)
311 }
312
313 const { today, parts } = await load($)
314
315 if (!isAsking(today, parts.hour, config.startHour)) {
316 return next(e)
317 }
318
319 // One task per line, so all three can go in at once.
320 const lines = text
321 .split('\n')
322 .map(line => line.replace(/^\s*(?:[-*]|\d+[.)])\s*/, '').trim())
323 .filter(line => line !== '')
324
325 // A long message or a question is meant for Claude, not the board: it goes
326 // through, with a reminder that the tasks are still due.
327 if (lines.length > TASKS || lines.some(line => line.length > MAX_TASK || line.endsWith('?'))) {
328 $.ui.toast(`Your ${TASKS} tasks for today are still due. Type a short one to set it.`, { timeoutMs: 8000 })
329
330 return next(e)
331 }
332
333 const added = lines.map(line => ({ text: line, isDone: false }))
334 const tasks = [...today.tasks, ...added].slice(0, TASKS)
335 await save($, { ...today, tasks })
336
337 if (tasks.length < TASKS) {
338 return { drop: `Task ${tasks.length} of ${TASKS} locked in. Now type task ${tasks.length + 1}.` }
339 }
340
341 wasAsking = false
342 await $.ui.open({ id: PANE, title: 'Today' })
343
344 return { drop: 'All three tasks are set and pinned on the right. Get to work.' }
345 })
346
347 on('command.run', { command: 'today' }, async ($, e) => {
348 const { today } = await load($)
349 const [verb = '', argument = ''] = e.args.trim().toLowerCase().split(/\s+/)
350
351 if (verb === 'reset') {
352 await save($, { ...today, tasks: [], isSkipped: false })
353
354 return { text: "Today's tasks are cleared. Your next three prompts set them again." }
355 }
356
357 if (verb === 'skip') {
358 await save($, { ...today, isSkipped: true })
359
360 return { text: 'No tasks asked today. They are asked again tomorrow.' }
361 }
362
363 if (verb === 'refresh') {
364 await refresh($, config)
365
366 return { text: 'Numbers refreshed.' }
367 }
368
369 if (verb === 'done') {
370 const index = Number(argument) - 1
371
372 if (today.tasks[index] === undefined) {
373 return { text: `There is no task ${argument}. Use /today done 1, 2 or 3.` }
374 }
375
376 await toggle($, index, config)
377 }
378
379 await $.ui.open({ id: PANE, title: 'Today' })
380
381 return { text: board((await load($)).today) }
382 })
383
384 on('command.run', { command: 'metric' }, async ($, e) => {
385 const text = e.args.trim()
386 const split = /^(.+?):\s*(.*)$/.exec(text) ?? /^(.+)\s+(\S+)$/.exec(text)
387 const label = clip(split?.[1] ?? text, 30)
388 const value = clip(split?.[2] ?? '', 40)
389
390 if (label === '') {
391 return { text: 'Pin a number with /metric Followers 12,400. Remove it with /metric Followers.' }
392 }
393
394 const held = await $.store.get('manual')
395 const manual: Record<string, unknown> = typeof held === 'object' && held !== null ? { ...held } : {}
396
397 if (value === '') {
398 delete manual[label]
399 } else {
400 manual[label] = value
401 }
402
403 await $.store.set('manual', manual)
404 await refresh($, config)
405
406 return { text: value === '' ? `Removed ${label}.` : `Pinned ${label}: ${value}` }
407 })
408
409 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
410 // Read for the redraw each minute; the time itself is the clock's.
411 await read($, minute)
412 const parts = localParts(await $.clock.now(), await read($, zone))
413 const today = forToday(await read($, day), parts.date)
414
415 if (e.props.hasSurvey || !isAsking(today, parts.hour, config.startHour)) {
416 return next(e)
417 }
418
419 const { Box, Button, Text } = $.ui.resolve(e)
420 const left = TASKS - today.tasks.length
421
422 return (
423 <Box flexDirection="column">
424 <Text bold>What are your {TASKS} tasks for today?</Text>
425 {today.tasks.map((task, at) => (
426 <Text dimColor>
427 {at + 1}. {task.text}
428 </Text>
429 ))}
430 <Box>
431 <Text>
432 Type task {today.tasks.length + 1} and press enter. Claude is locked until {left === TASKS ? 'all three are' : `${left} more ${left === 1 ? 'is' : 'are'}`} in.{' '}
433 </Text>
434 <Button
435 key="skip"
436 label="Skip today"
437 dimColor
438 onPress={async () => save($, { ...(await load($)).today, isSkipped: true })}
439 />
440 </Box>
441 </Box>
442 )
443 })
444
445 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
446 const { Box, Button, Text } = $.ui.resolve(e)
447 await read($, minute)
448 const time = await $.clock.now()
449 const parts = localParts(time, await read($, zone))
450 const today = forToday(await read($, day), parts.date)
451 const numbers = await read($, metrics)
452 const refreshed = await read($, refreshedAt)
453 const width = Math.max(24, e.props.bodyColumns)
454 const done = today.tasks.filter(task => task.isDone).length
455 const labelWidth = Math.min(20, Math.max(8, ...numbers.map(one => one.label.length)))
456
457 return (
458 <Box flexDirection="column">
459 <Text bold>
460 TODAY'S {TASKS} TASKS {today.tasks.length === TASKS ? `${done}/${TASKS}` : ''}
461 </Text>
462 {today.tasks.length === 0 && (
463 <Text dimColor>
464 {today.isSkipped ? 'Skipped for today.' : `Not set yet. Claude asks from ${config.startHour}:00.`}
465 </Text>
466 )}
467 {today.tasks.map((task, at) => (
468 <Button
469 key={`task-${at + 1}`}
470 plain
471 hotkey={String(at + 1)}
472 dimColor={task.isDone}
473 label={`${task.isDone ? '[x]' : '[ ]'} ${clip(task.text, width - 8)}`}
474 onPress={() => toggle($, at, config)}
475 />
476 ))}
477 <Text> </Text>
478 <Text bold>NUMBERS</Text>
479 {numbers.length === 0 && (
480 <Text dimColor wrap="wrap">
481 Nothing pinned yet. Add one with /metric Followers 12,400 or connect Stripe in /config.
482 </Text>
483 )}
484 {numbers.map(one => (
485 <Box>
486 <Text dimColor>{clip(one.label, labelWidth).padEnd(labelWidth + 2)}</Text>
487 <Text bold wrap="truncate-end">
488 {one.value}
489 </Text>
490 </Box>
491 ))}
492 <Text> </Text>
493 <Box>
494 <Button key="refresh" label="Refresh" hotkey="r" onPress={() => refresh($, config)} />
495 <Text dimColor> {refreshed === 0 ? '' : `updated ${ago(time - refreshed)}`}</Text>
496 </Box>
497 </Box>
498 )
499 })
500}
501types/index.d.ts 24 lines1export type Task = { text: string; isDone: boolean }
2
3/** One local day's three tasks. `date` is YYYY-MM-DD in the person's own time. */
4export type Day = { date: string; tasks: Task[]; isSkipped: boolean }
5
6export type Metric = { label: string; value: string }
7
8declare module 'claude-code' {
9 interface PluginState {
10 'daily-command-center': {
11 /** The day on the board; kept across sessions in the store too. */
12 day: Day
13 /** The numbers under the tasks: Stripe, the metrics command, then manual. */
14 metrics: Metric[]
15 /** When the numbers were last refreshed, ms since the epoch (0: never). */
16 refreshedAt: number
17 /** The clock as last ticked, once a minute, so the 9am ask redraws. */
18 minute: number
19 /** The person's offset from UTC in minutes (-240 for EDT). */
20 zone: number
21 }
22 }
23}
24