SLOPSHOPPER

daily-command-center

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.

newpanebandcommandtoastprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · daily-command-center
│ ┃ Today ✕ › fix the failing auth test and add an audit log call │ ┃ TODAY'S 3 TASKS │ ┃ Not set yet. Claude asks from 9:00. ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ NUMBERS ⏺ Update(src/auth.ts) │ ┃ Nothing pinned yet. Add one with /metric ⎿ Added 2 lines, removed 1 line │ ┃ Followers 12,400 or connect Stripe in ⏺ Bash(bun test) │ ┃ /config. ⎿ 3 pass, 1 fail │ ┃ │ ┃ [ Refresh ] updated just now ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /today │ ⎿ daily-command-center: No tasks set for today yet. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Today
TODAY'S 3 TASKS Not set yet. Claude asks from 9:00. NUMBERS Nothing pinned yet. Add one with /metric Followers 12,400 or connect Stripe in /config. [ Refresh ] updated just now
README

Claude Code Mods: Run Your Workday

Three Claude Code mods by @tenfoldmarc

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.

ModWhat it does
Daily Command CenterEvery 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 LockPick one task, start a timed sprint, and Claude holds any prompt that is not about that task until the timer ends.
Done ChimePlays a sound and shows a toast when Claude finishes a long job, so you can walk away instead of watching the screen.

Install

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.

How to use them

Daily Command Center

  • From 9am, a band above the prompt asks for your three tasks. Type one per message, or all three at once on separate lines.
  • A long message or a question is not treated as a task. It goes to Claude as usual, with a reminder that your tasks are still due.
  • Once all three are in, the Today pane opens with your tasks and numbers. Press 1, 2 or 3 in the pane to check one off.
CommandWhat it does
/todayOpen the Today pane
/today done 2Check off task 2
/today resetClear today's tasks and get asked again
/today skipNo tasks today
/today refreshRefresh the numbers now
/metric Followers 12,400Pin any number to the pane
/metric FollowersRemove that number

Your numbers. Three ways to get them in, all optional:

  1. Pin by hand with /metric. Zero setup.
  2. Stripe revenue. Run /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.
  3. Your own script. Set the "Metrics command" option to any command that prints one Label: value line per number. Email subscribers, followers, anything you can fetch.

Focus Lock

CommandWhat it does
/sprint 50 write the sales pageStart a 50 minute sprint on that task
/sprint write the sales pageSame, with your default length
/sprintSee the time left
/sprint stopEnd the sprint early
  • A countdown sits in the status line while the sprint runs.
  • A small, fast model checks each prompt against your task. Follow-ups, corrections and anything unclear count as on task. Only a clear switch to different work gets held.
  • Start a prompt with !! to push it through anyway.
  • When the timer ends, a sound plays and the held prompts show up above the prompt with Send and Drop buttons.

Done Chime

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.

Settings

Run /plugin configure <mod>@tenfold-mods to change these.

ModSettingDefault
Daily Command CenterStart hour9
Daily Command CenterStripe keynone
Daily Command CenterMetrics commandnone
Daily Command CenterRefresh minutes5
Focus LockSprint length (minutes)50
Focus LockJudge modelhaiku
Done ChimeMinimum seconds20
All threeVolume (0 to 4)1

Good to know

  • Sounds play on Mac only. On Windows and Linux everything else works and the toasts still show.
  • Mods are early access in Claude Code. A Claude Code update can change how they behave. If one breaks, update the mods (below) or remove it with claude plugin uninstall <mod>@tenfold-mods.
  • Focus Lock makes one small model call per prompt during a sprint. If that call fails, the prompt goes through. It never blocks you because of an outage.
  • Nothing leaves your machine except the calls you turn on: Stripe (if you add a key) and whatever your own metrics command does.
  • The mods only act on prompts you type. Scheduled tasks, background agents and claude -p scripts are never held or captured.

Update

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.

Instructions for Claude

If a user asks you to install these mods, do this:

  1. Run 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.
  2. Run claude plugin marketplace add tenfoldmarc/claude-code-mods.
  3. Run 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.
  4. Tell the user to start a new session (or run /reload-plugins), and that after 9am the Daily Command Center will ask for their three tasks.
  5. Do not ask the user to paste a Stripe key into the chat. If they want revenue in the pane, point them to /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.

License

MIT. The sounds are original and included under the same license.


Built by @tenfoldmarc. Follow for daily AI builds. Real systems, not theory.

Source 2 files
hooks/register.tsx 501 lines
1import { 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}
501
types/index.d.ts 24 lines
1export 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