SLOPSHOPPER

standup

/standup: click Generate and get a written standup of everything you got done today, grouped by project, from your local Claude Code sessions.

newpanecommandtoastmodelprocess
★ 1v0.1.1no licenseupdated 2026-10-11TroyJLorents-GH/mod-squad/mods/standup
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · standup
│ ┃ Standup ✕ › fix the failing auth test and add an audit log call │ ┃ [ Generate ] [ Today ] [ Yesterday ] │ ┃ Pick a day, then Generate. ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /standup │ ⎿ standup: Standup: press Generate in the pane. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Standup
[ Generate ] [ Today ] [ Yesterday ] Pick a day, then Generate.
README

standup

Click Generate and get a written standup of what you got done, grouped by project, from your local Claude Code sessions.

/standup opens a pane with Generate, Today and Yesterday buttons. Generate reads the day's session transcripts, boils each session down to a digest (what you asked, the commits it made, the files it changed, and the assistant's closing message), has a small model write the "done" and "open" bullets, then merges every project's sessions into one markdown standup. The result is shown in the pane and saved as ~/standups/YYYY-MM-DD.md.

# Standup · 2026-10-10

## peptideiq-mobile-app
- Fixed the token refresh race in the auth client
- Added retry with backoff to the API wrapper

**Open**
- Wire up the push notification test

_2 sessions · 5 files changed · 3 commits_

What counts as "done": commits, files changed and the assistant's closing message. A request alone never counts as completed work. If nothing finished, the goal is listed under Open.

Cost: one small-model call per session, clipped to about 6 KB, so a busy day with 20 sessions costs a few cents on Haiku 5.5. Each session's summary is cached, so pressing Generate again only pays for sessions that changed.

Install

/plugin marketplace add troyjlorents-gh/mod-squad
/plugin install standup@mod-squad

Commands

  • /standup: opens the pane.
  • /standup today, /standup yesterday, /standup 2026-10-08: picks that day and generates it.

Settings

SettingDefaultMeaning
modelclaude-haiku-5-5Writes each session's summary. Haiku is the default because this is high-volume summarizing, about 20x cheaper than Sonnet. Use claude-sonnet-5-5 for sharper wording.
effortlowHow hard the model thinks: low, medium, high, xhigh or max. Low is plenty for short summaries.
folderblank (~/standups)Where each standup is saved. Type off to save nothing.
maxSessions40Most sessions summarized per standup, newest activity first.

Limits

  • It reads the transcripts on the machine it runs on. Cloud sessions, and sessions on your other computers, are not included.
  • Subagent transcripts are not listed separately; they belong to their parent session.
  • Transcripts over 4 MB are read from their last 3.5 MB, which covers recent days.
  • A session that crosses midnight is split: only the chosen day's activity counts.
Source 4 files
hooks/register.tsx 251 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Result, RunStatus } from '../types'
5import { SYSTEM, digestText, fallbackSummary, parseSummary, renderMarkdown } from './digest'
6import type { Entry, Summary } from './digest'
7import { dayKey, dayStart, parseSession } from './transcripts'
8import type { SessionDigest } from './transcripts'
9
10const PANE = 'standup'
11const FS_READ_MAX = 4_000_000
12/** Files over the read limit are read from their last ~3.5 MB, which holds the recent days. */
13const TAIL_BYTES = 3_500_000
14const BATCH = 4
15const DEFAULT_MODEL = 'claude-haiku-5-5'
16const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'] as const
17type Effort = (typeof EFFORTS)[number]
18// Higher effort spends output tokens thinking before the JSON, so the cap grows with it.
19const MAX_TOKENS: Record<Effort, number> = { low: 400, medium: 2000, high: 6000, xhigh: 12000, max: 24000 }
20
21const IDLE: RunStatus = { isRunning: false, done: 0, total: 0 }
22const resultRef = atom({ plugin: 'standup', key: 'result' } as const, null as Result | null)
23const statusRef = atom({ plugin: 'standup', key: 'status' } as const, IDLE)
24const pickRef = atom({ plugin: 'standup', key: 'pick' } as const, 'today')
25
26type Options = { model?: string; effort?: string; folder?: string; maxSessions?: number }
27type Cfg = { model: string; effort: Effort; maxTokens: number; folder: string; maxSessions: number }
28type Found = { path: string; size: number; mtime: number }
29
30let isBusy = false
31
32function config(options: unknown): Cfg {
33  const o = (options ?? {}) as Options
34  const n = Number(o.maxSessions)
35  return {
36    model: o.model?.trim() || DEFAULT_MODEL,
37    effort: EFFORTS.includes(o.effort as Effort) ? (o.effort as Effort) : 'low',
38    maxTokens: MAX_TOKENS[EFFORTS.includes(o.effort as Effort) ? (o.effort as Effort) : 'low'],
39    folder: (o.folder ?? '').trim(),
40    maxSessions: Number.isFinite(n) && n > 0 ? Math.floor(n) : 40,
41  }
42}
43
44async function tzOffset($: EngineInterface): Promise<number> {
45  try {
46    const r = await $.process.run(['date', '+%z'], { timeoutMs: 3000 })
47    const m = /^([+-])(\d{2})(\d{2})/.exec(r.stdout.trim())
48    if (m) return (m[1] === '-' ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3]))
49  } catch {
50    // No process available: use the environment's own offset.
51  }
52  return -new Date().getTimezoneOffset()
53}
54
55async function home($: EngineInterface): Promise<string> {
56  const h = (await $.env.get('HOME').catch(() => undefined)) ?? (await $.env.get('USERPROFILE').catch(() => undefined)) ?? ''
57  return h.replace(/[\\/]+$/, '')
58}
59
60async function configRoots($: EngineInterface): Promise<string[]> {
61  const roots: string[] = []
62  const configured = await $.env.get('CLAUDE_CONFIG_DIR').catch(() => undefined)
63  if (configured) for (const p of configured.split(',')) if (p.trim() !== '') roots.push(p.trim())
64  const h = await home($)
65  if (h !== '') roots.push(`${h}/.claude`)
66  return [...new Set(roots.map(r => r.replace(/[\\/]+$/, '')))]
67}
68
69async function walk($: EngineInterface, dir: string, depth: number, out: Found[]) {
70  const entries = await $.fs.list(dir).catch(() => [])
71  for (const en of entries) {
72    const path = `${dir}/${en.name}`
73    if (en.kind === 'file' && en.name.endsWith('.jsonl')) out.push({ path, size: en.size, mtime: en.mtimeMs })
74    // Subagent transcripts are part of their parent session's work, so they are not listed on their own.
75    else if (en.kind === 'dir' && depth > 0 && en.name !== 'subagents' && en.name !== 'tool-results') await walk($, path, depth - 1, out)
76  }
77}
78
79async function readTranscript($: EngineInterface, f: Found): Promise<string> {
80  if (f.size <= FS_READ_MAX) return $.fs.read(f.path)
81  const r = await $.process.run(['sh', '-c', 'tail -c "$1" "$2"', 'standup', String(TAIL_BYTES), f.path], { timeoutMs: 30_000 })
82  return r.stdout
83}
84
85/** Resolves `today`, `yesterday` or YYYY-MM-DD to a local day. Null for anything else. */
86function resolveDay(pick: string, now: number, tz: number): string | null {
87  if (pick === 'today' || pick === '') return dayKey(now, tz)
88  if (pick === 'yesterday') return dayKey(now - 86_400_000, tz)
89  return /^\d{4}-\d{2}-\d{2}$/.test(pick) ? pick : null
90}
91
92async function summarize($: EngineInterface, d: SessionDigest, day: string, tz: number, cfg: Cfg): Promise<Entry> {
93  const key = `s:${d.sid}:${day}:${d.last}`
94  const cached = (await $.store.get(key).catch(() => undefined)) as Summary | undefined
95  if (cached !== undefined) return { d, s: cached, isModel: true }
96  const r = await $.model
97    .complete({ model: cfg.model, system: SYSTEM, prompt: digestText(d, tz), effort: cfg.effort, maxTokens: cfg.maxTokens, timeoutMs: cfg.effort === 'low' ? 25_000 : 120_000 })
98    .catch(() => null)
99  const s = r?.isAnswered ? parseSummary(r.text) : null
100  if (s === null) return { d, s: fallbackSummary(d), isModel: false }
101  await $.store.set(key, s).catch(() => undefined)
102  return { d, s, isModel: true }
103}
104
105async function save($: EngineInterface, cfg: Cfg, day: string, markdown: string): Promise<string> {
106  if (cfg.folder.toLowerCase() === 'off') return ''
107  const dir = cfg.folder !== '' ? cfg.folder.replace(/[\\/]+$/, '') : `${await home($)}/standups`
108  if (dir === '/standups') return '' // no home folder to put it in
109  const path = `${dir}/${day}.md`
110  try {
111    await $.fs.write(path, markdown)
112    return path
113  } catch {
114    return ''
115  }
116}
117
118async function generate($: EngineInterface, cfg: Cfg) {
119  if (isBusy) return
120  isBusy = true
121  try {
122    await update($, statusRef, () => ({ isRunning: true, done: 0, total: 0 }))
123    const tz = await tzOffset($)
124    const day = resolveDay(await read($, pickRef), await $.clock.now(), tz)
125    if (day === null) throw new Error('pick today, yesterday or a YYYY-MM-DD date')
126    const since = dayStart(day, tz)
127
128    const found: Found[] = []
129    for (const root of await configRoots($)) await walk($, `${root}/projects`, 2, found)
130    const recent = [...new Map(found.filter(f => f.mtime >= since).map(f => [f.path, f])).values()].sort((a, b) => b.mtime - a.mtime)
131
132    const digests: SessionDigest[] = []
133    for (const f of recent) {
134      try {
135        const d = parseSession(await readTranscript($, f), day, tz)
136        if (d !== null && d.sid !== '') digests.push(d)
137      } catch {
138        // An unreadable file is skipped; the rest still make the standup.
139      }
140    }
141    // A session resumed from another file shows up twice; keep the busier one.
142    const bySid = new Map<string, SessionDigest>()
143    for (const d of digests) {
144      const prev = bySid.get(d.sid)
145      if (prev === undefined || d.prompts.length > prev.prompts.length) bySid.set(d.sid, d)
146    }
147    const picked = [...bySid.values()].sort((a, b) => b.last - a.last).slice(0, cfg.maxSessions)
148    await update($, statusRef, () => ({ isRunning: true, done: 0, total: picked.length }))
149
150    const entries: Entry[] = []
151    for (let i = 0; i < picked.length; i += BATCH) {
152      entries.push(...(await Promise.all(picked.slice(i, i + BATCH).map(d => summarize($, d, day, tz, cfg)))))
153      await update($, statusRef, () => ({ isRunning: true, done: Math.min(i + BATCH, picked.length), total: picked.length }))
154    }
155
156    const markdown = renderMarkdown(day, entries)
157    const path = await save($, cfg, day, markdown)
158    const result: Result = {
159      day,
160      markdown,
161      projects: new Set(entries.map(e => e.d.project)).size,
162      sessions: entries.length,
163      summarized: entries.filter(e => e.isModel).length,
164      fallback: entries.filter(e => !e.isModel).length,
165      path,
166    }
167    await update($, resultRef, () => result)
168    await $.store.set('last', result).catch(() => undefined)
169    await update($, statusRef, () => IDLE)
170    $.ui.toast(`standup: ${result.sessions} session${result.sessions === 1 ? '' : 's'}, ${result.projects} project${result.projects === 1 ? '' : 's'}${path !== '' ? `, saved to ${path}` : ''}`)
171  } catch (err) {
172    const error = err instanceof Error ? err.message : String(err)
173    await update($, statusRef, () => ({ isRunning: false, done: 0, total: 0, error })).catch(() => undefined)
174  } finally {
175    isBusy = false
176  }
177}
178
179/** Starts a run on the clock, so no hook waits for it. */
180function start($: EngineInterface, cfg: Cfg) {
181  $.clock.after(1, () => {
182    void generate($, cfg)
183  })
184}
185
186async function choose($: EngineInterface, pick: string) {
187  await update($, pickRef, () => pick)
188}
189
190const OPEN = { id: PANE, title: 'Standup', focus: true, closeOnEscape: true, rows: 40, columns: 100 } as const
191
192export const register: Register = (on, options) => {
193  const cfg = config(options)
194
195  on('session.start', async ($, e, next) => {
196    await $.command.register({
197      name: 'standup',
198      description: 'Standup: write up what you got done, from your local sessions',
199      argumentHint: '[today | yesterday | YYYY-MM-DD]',
200      immediate: true,
201    })
202    const last = (await $.store.get('last').catch(() => undefined)) as Result | undefined
203    if (last !== undefined) await update($, resultRef, () => last)
204    return next(e)
205  }).catch(($, e, next) => next(e))
206
207  on('command.run', { command: 'standup' }, async ($, e) => {
208    const arg = e.args.trim().toLowerCase()
209    await $.ui.open(OPEN).catch(() => undefined)
210    if (arg !== '') {
211      await choose($, arg)
212      start($, cfg)
213      return { text: `Standup for ${arg}: generating…` }
214    }
215    const r = await read($, resultRef)
216    return { text: r === null ? 'Standup: press Generate in the pane.' : `Standup (${r.day}): ${r.sessions} sessions, ${r.projects} projects${r.path !== '' ? `, saved to ${r.path}` : ''}.` }
217  })
218
219  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
220    const { Box, Text, Button, Markdown } = $.ui.resolve(e)
221    const result = await read($, resultRef)
222    const status = await read($, statusRef)
223    const pick = await read($, pickRef)
224    const cols = Math.max(20, e.props.bodyColumns)
225
226    const note = status.isRunning
227      ? `working… ${status.total > 0 ? `${status.done}/${status.total} sessions` : 'reading transcripts'}`
228      : status.error !== undefined
229        ? `failed: ${status.error}`
230        : result !== null
231          ? `${result.day} · ${result.sessions} sessions · ${result.projects} projects${result.fallback > 0 ? ` · ${result.fallback} without a model summary` : ''}${result.path !== '' ? ` · saved to ${result.path}` : ''}`
232          : 'Pick a day, then Generate.'
233
234    return (
235      <Box flexDirection="column" width={cols}>
236        <Box flexDirection="row" flexWrap="wrap" columnGap={1} width={cols}>
237          <Button key="gen" label="Generate" hotkey="g" variant="primary" onPress={() => start($, cfg)} />
238          <Button key="today" label="Today" hotkey="t" variant={pick === 'today' ? 'primary' : 'secondary'} onPress={() => choose($, 'today')} />
239          <Button key="yday" label="Yesterday" hotkey="y" variant={pick === 'yesterday' ? 'primary' : 'secondary'} onPress={() => choose($, 'yesterday')} />
240        </Box>
241        <Text dimColor wrap="wrap">
242          {note}
243        </Text>
244        <Box marginTop={1} flexDirection="column" width={cols}>
245          {result !== null && <Markdown text={result.markdown} />}
246        </Box>
247      </Box>
248    )
249  })
250}
251
hooks/digest.ts 77 lines
1import { clip } from './transcripts'
2import type { SessionDigest } from './transcripts'
3
4export type Summary = { title: string; done: string[]; open: string[] }
5
6export const SYSTEM = `You write standup notes from a digest of one coding-assistant session.
7Reply with JSON only: {"title":"<= 8 words","done":["past-tense bullet, <= 14 words", ... at most 5],"open":["unfinished item, <= 14 words", ... at most 3]}
8"done" lists only what the digest shows was completed: commits, files changed, the assistant's closing message. Do not infer success from a request alone; if nothing was finished, leave "done" empty and put the goal in "open".
9Be specific (name the feature, bug or file), not generic. No markdown, no preamble.`
10
11const hhmm = (ms: number, tz: number) => new Date(ms + tz * 60_000).toISOString().slice(11, 16)
12
13/** The text sent to the model for one session. About 6 KB at most. */
14export function digestText(d: SessionDigest, tz: number): string {
15  const lines = [`Project: ${d.project || 'unknown'}`, `Active: ${hhmm(d.first, tz)}-${hhmm(d.last, tz)}`]
16  lines.push('Requests, in order:', ...d.prompts.slice(0, 15).map(p => `- ${clip(p, 300)}`))
17  if (d.prompts.length > 15) lines.push(`- (${d.prompts.length - 15} more)`)
18  if (d.commits.length > 0) lines.push('Commits:', ...d.commits.slice(0, 10).map(c => `- ${c}`))
19  if (d.files.length > 0) lines.push(`Files changed (${d.files.length}):`, ...d.files.slice(0, 20).map(f => `- ${f}`))
20  if (d.answer !== '') lines.push('Assistant\'s last message:', clip(d.answer, 700))
21  return clip(lines.join('\n'), 6000)
22}
23
24export function parseSummary(reply: string): Summary | null {
25  const json = reply.match(/\{[\s\S]*\}/)?.[0]
26  if (!json) return null
27  try {
28    const v = JSON.parse(json) as Partial<Summary>
29    const list = (x: unknown, n: number) =>
30      Array.isArray(x) ? x.filter((s): s is string => typeof s === 'string' && s.trim() !== '').map(s => clip(s.trim(), 140)).slice(0, n) : []
31    const title = typeof v.title === 'string' ? clip(v.title.trim(), 80) : ''
32    const done = list(v.done, 5)
33    const open = list(v.open, 3)
34    return title === '' && done.length === 0 && open.length === 0 ? null : { title, done, open }
35  } catch {
36    return null
37  }
38}
39
40/** No model: commits when there are any, else the files touched; the first request as the goal. */
41export function fallbackSummary(d: SessionDigest): Summary {
42  const goal = clip(d.prompts[0] ?? 'Session', 80)
43  if (d.commits.length > 0) return { title: goal, done: d.commits.slice(0, 5), open: [] }
44  if (d.files.length > 0) {
45    const names = d.files.slice(0, 3).map(f => f.split(/[\\/]/).pop() ?? f)
46    return { title: goal, done: [`Edited ${d.files.length} file${d.files.length === 1 ? '' : 's'}: ${names.join(', ')}${d.files.length > 3 ? '…' : ''}`], open: [] }
47  }
48  return { title: goal, done: [], open: [goal] }
49}
50
51export type Entry = { d: SessionDigest; s: Summary; isModel: boolean }
52
53/** Standup markdown: one section per project, bullets merged across that project's sessions. */
54export function renderMarkdown(day: string, entries: Entry[]): string {
55  const byProject = new Map<string, Entry[]>()
56  for (const e of entries) byProject.set(e.d.project || 'unknown', [...(byProject.get(e.d.project || 'unknown') ?? []), e])
57  const out = [`# Standup · ${day}`, '']
58  if (entries.length === 0) return `${out.join('\n')}Nothing to report: no Claude Code activity found for this day.\n`
59
60  const dedupe = (xs: string[]) => {
61    const seen = new Set<string>()
62    return xs.filter(x => (seen.has(x.toLowerCase()) ? false : (seen.add(x.toLowerCase()), true)))
63  }
64  for (const [project, es] of [...byProject].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))) {
65    out.push(`## ${project}`)
66    const done = dedupe(es.flatMap(e => e.s.done))
67    const open = dedupe(es.flatMap(e => e.s.open))
68    if (done.length === 0) out.push('- _No completed work found; see open items._')
69    for (const x of done) out.push(`- ${x}`)
70    if (open.length > 0) out.push('', '**Open**', ...open.map(x => `- ${x}`))
71    const files = new Set(es.flatMap(e => e.d.files)).size
72    const commits = es.reduce((n, e) => n + e.d.commits.length, 0)
73    out.push('', `_${es.length} session${es.length === 1 ? '' : 's'} · ${files} file${files === 1 ? '' : 's'} changed${commits > 0 ? ` · ${commits} commit${commits === 1 ? '' : 's'}` : ''}_`, '')
74  }
75  return `${out.join('\n').trimEnd()}\n`
76}
77
hooks/transcripts.ts 102 lines
1// Reads one Claude Code transcript (JSONL) and boils a single local day of it down to a digest.
2
3export type SessionDigest = {
4  sid: string
5  project: string
6  cwd: string
7  first: number
8  last: number
9  prompts: string[]
10  files: string[]
11  commits: string[]
12  commands: number
13  answer: string
14}
15
16const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
17// Notices and caveats the harness injects as "user" rows are not things the person typed.
18const NOT_TYPED = /^(<|\[Request interrupted|Caveat:)/
19
20/** YYYY-MM-DD of `ms` shifted by `tzOffsetMin` (minutes east of UTC). */
21export function dayKey(ms: number, tzOffsetMin: number): string {
22  return new Date(ms + tzOffsetMin * 60_000).toISOString().slice(0, 10)
23}
24
25/** Epoch ms of local midnight at the start of `day`. */
26export function dayStart(day: string, tzOffsetMin: number): number {
27  return Date.parse(`${day}T00:00:00Z`) - tzOffsetMin * 60_000
28}
29
30/** `/home/me/src/app` → `app`; `C:\\work\\api\\` → `api`. */
31export function shortProject(cwd: string): string {
32  const parts = cwd.split(/[\\/]+/).filter(Boolean)
33  return parts[parts.length - 1] ?? cwd
34}
35
36export const clip = (s: string, n: number) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`)
37
38function userText(content: unknown): string {
39  if (typeof content === 'string') return content
40  if (!Array.isArray(content)) return ''
41  return content
42    .filter((b): b is { type: string; text: string } => typeof b === 'object' && b !== null && (b as { type?: string }).type === 'text')
43    .map(b => b.text)
44    .join('\n')
45}
46
47/** First line of the message of a `git commit`, quoted (`-m "…"`) or heredoc'd, or null. */
48export function commitMessage(command: string): string | null {
49  if (!/\bgit\s+commit\b/.test(command)) return null
50  const heredoc = /<<-?\s*['"]?(\w+)['"]?\s*\n\s*([^\n]+)/.exec(command)
51  if (heredoc?.[2]) return clip(heredoc[2].trim(), 120)
52  const quoted = /-m\s+(?:"([^"]+)"|'([^']+)')/.exec(command)
53  const msg = quoted?.[1] ?? quoted?.[2]
54  return msg ? clip(msg.split('\n')[0]?.trim() ?? msg, 120) : null
55}
56
57/** What happened in one transcript on `day`, or null when the person did nothing that day. */
58export function parseSession(text: string, day: string, tz: number): SessionDigest | null {
59  const d: SessionDigest = { sid: '', project: '', cwd: '', first: 0, last: 0, prompts: [], files: [], commits: [], commands: 0, answer: '' }
60  const files = new Set<string>()
61  for (const line of text.split('\n')) {
62    if (line.length < 2) continue
63    let o: Record<string, any>
64    try {
65      o = JSON.parse(line)
66    } catch {
67      continue // a cut first line (tail read) or one still being written
68    }
69    if (o.isSidechain === true) continue
70    const ts = Date.parse(String(o.timestamp ?? ''))
71    if (Number.isNaN(ts) || dayKey(ts, tz) !== day) continue
72    if (typeof o.sessionId === 'string') d.sid = o.sessionId
73    if (typeof o.cwd === 'string' && o.cwd !== '') d.cwd = o.cwd
74    d.first = d.first === 0 ? ts : Math.min(d.first, ts)
75    d.last = Math.max(d.last, ts)
76
77    if (o.type === 'user' && o.isMeta !== true) {
78      const t = userText(o.message?.content).trim()
79      if (t !== '' && !NOT_TYPED.test(t)) d.prompts.push(clip(t.replace(/\s+/g, ' '), 400))
80    } else if (o.type === 'assistant' && Array.isArray(o.message?.content)) {
81      for (const b of o.message.content) {
82        if (b?.type === 'text' && typeof b.text === 'string' && b.text.trim() !== '') d.answer = b.text.trim()
83        else if (b?.type === 'tool_use') {
84          const input = b.input ?? {}
85          if (EDIT_TOOLS.has(b.name)) {
86            const f = input.file_path ?? input.notebook_path
87            if (typeof f === 'string') files.add(f)
88          } else if (b.name === 'Bash' && typeof input.command === 'string') {
89            d.commands++
90            const msg = commitMessage(input.command)
91            if (msg !== null) d.commits.push(msg)
92          }
93        }
94      }
95    }
96  }
97  if (d.prompts.length === 0 && files.size === 0) return null
98  d.project = shortProject(d.cwd)
99  d.files = [...files].map(f => (d.cwd !== '' && f.startsWith(d.cwd) ? f.slice(d.cwd.length).replace(/^[\\/]+/, '') : f))
100  return d
101}
102
types/index.d.ts 25 lines
1export type Result = {
2  day: string
3  markdown: string
4  projects: number
5  sessions: number
6  /** Sessions the model summarized / sessions that fell back to a plain digest. */
7  summarized: number
8  fallback: number
9  /** Where the markdown was saved, or '' when saving is off or failed. */
10  path: string
11}
12
13export type RunStatus = { isRunning: boolean; done: number; total: number; error?: string }
14
15declare module 'claude-code' {
16  interface PluginState {
17    standup: {
18      result: Result | null
19      status: RunStatus
20      /** The day picked in the pane: today | yesterday | YYYY-MM-DD. */
21      pick: string
22    }
23  }
24}
25