Composable local dashboard for Claude Code

Install this Claude Code dashboard for me: https://github.com/coolthor/cc-dash-kit
Claude Code: follow INSTALL.md and ask which installation scope to use before installing.
Your Claude Code status, right alongside the chat. · 繁體中文

Paste the sentence above into Claude Code. It follows INSTALL.md, asks before changing your terminal layout, and offers to customize the dashboard after installation.
Install without customizing:
claude plugin marketplace add coolthor/cc-dash-kit
claude plugin install --scope user cc-dash-kit@cc-dash-kit
User scope works across projects; use --scope local to try it in a single project. Reload plugins after installation. The terminal sidebar requires /tui fullscreen; see INSTALL.md for the layout step and Desktop Code behavior.
Customize this dashboard around what I check while I work.
Claude follows ASSEMBLE.md, asks three questions, and writes a local dash.config.json. The default quota and session cards work out of the box.
| Before: ready without config | After: GPU and two local services |
|---|---|
![]() | ![]() |
| Card | Needs | When unavailable |
|---|---|---|
| Claude usage | Session usage data | Shows “Unavailable” until usage data arrives |
| Session | Current session metadata | Shows available fields |
| GPU | nvidia-smi | Hidden |
| Disk | df and a configured path | Shows a read error |
| Endpoints | Configured HTTP URLs | Shows each endpoint as offline |
| Dispatch pause | Configured local flag path | Hidden until configured |
A second mod that shows a thumbnail of each pasted image directly under the prompt you pasted it into, instead of a bare [Image #1]. Works in Ghostty and kitty; needs Python 3 with Pillow.
claude plugin install --scope user image-peek@cc-dash-kit
Requirements, behavior in other terminals, privacy, and troubleshooting: plugins/image-peek/README.md.
The mod runs with Claude Code permissions. Review its code before installing. It reads session metadata and only the local resources you configure. Endpoint checks have short timeouts. The pause button changes only its configured local flag file; pause_status is advisory. Never put credentials in the config.
Run claude plugin validate . and claude plugin test .. See dash.config.example.json for the config shape. Local dash.config.json is gitignored.
MIT © coolthor.
hooks/register.tsx 190 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3import { DEFAULT_CONFIG, parseConfig } from '../src/config'
4import type { DashConfig } from '../src/config'
5import type { CardData, SourceConfig } from '../src/sources/types'
6import { setPaused } from '../src/sources/pause-flag'
7import { collectSource } from '../src/sources/collect'
8import { Card, StatusDot } from '../src/ui/components'
9import { parseLocale, setLocale, t } from '../src/i18n'
10
11const PANE = 'cc-dash'
12const snapshot = atom({ plugin: 'cc-dash-kit', key: 'snapshot' } as const, {} as Record<string, CardData>)
13let config: DashConfig = DEFAULT_CONFIG
14let configError: string | null = null
15let started = false
16let pauseToolRegistered = false
17let detectedLocale: 'en' | 'zh-TW' = 'en'
18let paneCanDock = false
19let inlineHintShown = false
20let fullscreenFromEnv = false
21let autoOpened = false
22
23async function loadConfig($: any) {
24 const path = `${$.plugin.root}/dash.config.json`
25 if (!await $.fs.exists(path)) return DEFAULT_CONFIG
26 return parseConfig(JSON.parse(await $.fs.read(path)))
27}
28
29async function sampleOne($: any, card: SourceConfig): Promise<CardData | null> {
30 try {
31 return await collectSource({
32 session: {
33 usage: () => $.session.usage(),
34 id: () => $.session.id(),
35 cwd: () => $.session.cwd(),
36 },
37 process: { run: (argv: string[], options: any) => $.process.run(argv, options) },
38 fs: {
39 exists: (path: string) => $.fs.exists(path),
40 write: (path: string, content: string) => $.fs.write(path, content),
41 },
42 clock: { sleep: (ms: number) => $.clock.sleep(ms) },
43 }, card)
44 } catch (error) {
45 return { rows: [], error: `${t('loadFailed')}: ${String(error).slice(0, 100)}` }
46 }
47}
48
49async function refresh($: any) {
50 try { config = await loadConfig($); configError = null }
51 catch (error) {
52 config = DEFAULT_CONFIG
53 configError = `${t('configFailed')}: ${String(error).slice(0, 100)}`
54 }
55 setLocale(config.locale && config.locale !== 'auto' ? config.locale : detectedLocale)
56 if (config.cards.some(card => card.id === 'pause-flag') && !pauseToolRegistered) {
57 await registerPauseTool($)
58 pauseToolRegistered = true
59 }
60 const pairs = await Promise.all(config.cards.map(async card => [card.id, await sampleOne($, card)] as const))
61 await update($, snapshot, () => Object.fromEntries(pairs.filter(([, data]) => data !== null)))
62}
63
64async function detectLocale($: any): Promise<'en' | 'zh-TW'> {
65 for (const key of ['LC_ALL', 'LC_MESSAGES', 'LANG']) {
66 try {
67 const result = await $.process.run(['printenv', key], { timeoutMs: 1000 })
68 if (result.exitCode === 0 && result.stdout.trim()) return parseLocale(result.stdout.trim()) || 'en'
69 } catch {}
70 }
71 try {
72 const result = await $.process.run(['defaults', 'read', '-g', 'AppleLanguages'], { timeoutMs: 1000 })
73 if (result.exitCode === 0) return parseLocale(result.stdout.match(/["']?([a-z]{2}(?:[-_][A-Za-z]+)?)/i)?.[1]) || 'en'
74 } catch {}
75 return 'en'
76}
77
78async function envIsFullscreen($: any) {
79 try {
80 const [enabled, disabled] = await Promise.all([
81 $.process.run(['printenv', 'CLAUDE_CODE_NO_FLICKER'], { timeoutMs: 1000 }),
82 $.process.run(['printenv', 'CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN'], { timeoutMs: 1000 }),
83 ])
84 return enabled.stdout.trim() === '1' && disabled.stdout.trim() !== '1'
85 } catch { return false }
86}
87
88function canDock(e: any) {
89 if (e.surface === 'desktop') return true
90 if (e.viewport?.isFullscreen !== undefined) return e.viewport.isFullscreen === true
91 return e.surface === 'terminal' && fullscreenFromEnv
92}
93
94async function autoOpen($: any, e: any) {
95 if (autoOpened || !canDock(e)) return
96 autoOpened = true
97 paneCanDock = true
98 try { await $.ui.open({ id: PANE, title: t('dashboard') }) }
99 catch { autoOpened = false }
100}
101
102async function start($: any) {
103 if (started) return
104 started = true
105 detectedLocale = await detectLocale($)
106 setLocale(detectedLocale)
107 void refresh($)
108 $.clock.every(config.refreshMs, () => void refresh($))
109}
110
111export const register: Register = on => {
112 on('session.start', async ($, e, next) => {
113 fullscreenFromEnv = await envIsFullscreen($)
114 paneCanDock = canDock(e)
115 inlineHintShown = false
116 autoOpened = false
117 await $.command.register({ name: 'cc-dash', description: 'Open the dashboard' })
118 await start($)
119 void autoOpen($, e)
120 return next(e)
121 })
122
123 on('session.attach', async ($, e, next) => {
124 await start($)
125 void autoOpen($, e)
126 return next(e)
127 })
128
129 on('prompt.submit', async ($, e, next) => {
130 void autoOpen($, e)
131 return next(e)
132 })
133
134 on('command.run', { command: 'cc-dash' }, async $ => {
135 await start($)
136 await refresh($)
137 await $.ui.close({ id: PANE })
138 const result = await $.ui.open({ id: PANE, title: t('dashboard'), focus: true })
139 if (result.isPlaced) autoOpened = true
140 return { text: result.isPlaced ? t('opened') : `${t('waiting')}: ${result.reason || 'pane unavailable'}` }
141 })
142
143 on('tool.call', { tool: 'mcp__cc-dash-kit__pause_status' }, async $ => {
144 const flags = config.cards.filter(card => card.id === 'pause-flag')
145 const rows = await Promise.all(flags.map(async card => {
146 try { return `${card.title || card.path}: ${await $.fs.exists(card.path) ? 'paused' : 'available'}` }
147 catch { return `${card.title || card.path}: unknown; do not dispatch until checked` }
148 }))
149 return { result: rows.join('\n') || 'No pause flags configured.' }
150 })
151
152 on('turn.complete', async ($, e, next) => {
153 if (!e.agentId) void refresh($)
154 void autoOpen($, e)
155 return next(e)
156 })
157
158 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
159 paneCanDock = e.props.placement === 'dock'
160 if (e.props.placement === 'inline' && !inlineHintShown) {
161 inlineHintShown = true
162 $.ui.toast(t('fullscreenHint'))
163 }
164 const ui = $.ui.resolve(e)
165 const { Box, Text, Button } = ui
166 const Svg = e.surface === 'desktop' ? (ui as any).Svg : undefined
167 const data = await read($, snapshot)
168 const width = Math.max(16, e.props.bodyColumns || 40)
169 return <Box flexDirection="column">
170 {config.cards.map(card => {
171 const item = data[card.id]
172 if (!item) return null
173 const extra = card.id === 'pause-flag' ? <Button key={`toggle-${card.id}`} autoFocus label={item.rows[0]?.value === t('paused') ? t('resume') : t('pause')} onPress={async () => {
174 try { await setPaused({ fs: { write: (path: string, content: string) => $.fs.write(path, content) }, process: { run: (argv: string[], options: any) => $.process.run(argv, options) } }, card.path!, item.rows[0]?.value !== t('paused')); await refresh($) }
175 catch (error) { $.ui.toast(`${t('flagFailed')}: ${String(error)}`) }
176 }} /> : null
177 const titleKey = { quota: 'quota', session: 'session', gpu: 'gpu', disk: 'disk', 'http-probe': 'httpProbe', 'pause-flag': 'pauseFlag' }[card.id] as Parameters<typeof t>[0]
178 return Card(ui, card.id, card.title || t(titleKey), item, width, extra)
179 })}
180 {configError && Card(ui, 'config', t('config'), { rows: [], error: configError }, width)}
181 {Svg && <Box><Svg alt="Dashboard status" width={12} height={12} source={'<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12"><circle cx="6" cy="6" r="4" fill="#38b46a"/></svg>'} /><Text dimColor> cc-dash-kit</Text></Box>}
182 {e.surface !== 'desktop' && <Box>{StatusDot(ui, 'ok')}<Text dimColor>cc-dash-kit</Text></Box>}
183 </Box>
184 })
185}
186
187async function registerPauseTool($: any) {
188 await $.tool.register({ name: 'pause_status', description: 'Read configured pause flags before dispatching work to another AI. This is advisory, not a permission boundary.', inputSchema: { type: 'object', properties: {} } })
189}
190src/config.ts 31 lines1import type { Locale } from './i18n'
2import type { SourceConfig } from './sources/types'
3
4export type DashConfig = { cards: SourceConfig[]; refreshMs: number; locale?: Locale | 'auto' }
5export const DEFAULT_CONFIG: DashConfig = { cards: [{ id: 'quota' }, { id: 'session' }], refreshMs: 30000 }
6const IDS = new Set(['quota', 'session', 'gpu', 'disk', 'http-probe', 'pause-flag'])
7
8export function parseConfig(value: unknown): DashConfig {
9 if (value == null) return DEFAULT_CONFIG
10 if (typeof value !== 'object' || Array.isArray(value)) throw new Error('config must be an object')
11 const input = value as Record<string, unknown>
12 if (!Array.isArray(input.cards)) throw new Error('cards must be an array')
13 const cards = input.cards.map((raw, index) => {
14 if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`cards[${index}] must be an object`)
15 const card = raw as Record<string, unknown>
16 if (typeof card.id !== 'string' || !IDS.has(card.id)) throw new Error(`cards[${index}] has unknown source`)
17 if (card.title !== undefined && (typeof card.title !== 'string' || card.title.length > 60)) throw new Error(`cards[${index}].title invalid`)
18 if (card.path !== undefined && (typeof card.path !== 'string' || !card.path.startsWith('/'))) throw new Error(`cards[${index}].path must be absolute`)
19 if (card.id === 'pause-flag' && !card.path) throw new Error(`cards[${index}].path required`)
20 if (card.id === 'http-probe' && !card.url && !card.endpoints) throw new Error(`cards[${index}] needs url or endpoints`)
21 if (card.endpoints !== undefined && (!Array.isArray(card.endpoints) || !card.endpoints.length || card.endpoints.some((e: any) => !e || typeof e.name !== 'string' || !e.name || typeof e.url !== 'string' || !/^https?:\/\//.test(e.url)))) throw new Error(`cards[${index}].endpoints invalid`)
22 if (card.url !== undefined && (typeof card.url !== 'string' || !/^https?:\/\//.test(card.url))) throw new Error(`cards[${index}].url invalid`)
23 return { id: card.id, title: card.title as string | undefined, path: card.path as string | undefined, url: card.url as string | undefined, endpoints: card.endpoints as SourceConfig["endpoints"] }
24 })
25 if (new Set(cards.map(card => card.id)).size !== cards.length) throw new Error('duplicate source id')
26 const refreshMs = input.refreshMs === undefined ? 30000 : Number(input.refreshMs)
27 if (!Number.isInteger(refreshMs) || refreshMs < 5000 || refreshMs > 300000) throw new Error('refreshMs must be 5000..300000')
28 if (input.locale !== undefined && !['en', 'zh-TW', 'auto'].includes(String(input.locale))) throw new Error('locale invalid')
29 return { cards, refreshMs, locale: input.locale as DashConfig['locale'] }
30}
31src/sources/types.ts 17 lines1export type RowData = { label: string; value: string; percent?: number; status?: 'ok' | 'warn' | 'error' }
2export type CardData = { rows: RowData[]; note?: string; error?: string }
3export type SourceConfig = { id: string; title?: string; path?: string; url?: string; endpoints?: { name: string; url: string }[] }
4export type Source = {
5 id: string
6 title: string
7 detect: ($: any, config: SourceConfig) => Promise<boolean>
8 sample: ($: any, config: SourceConfig) => Promise<CardData>
9 card: (data: CardData) => CardData
10}
11export const identityCard = (data: CardData) => data
12export async function run($: any, argv: string[], timeoutMs = 3000) {
13 const result = await $.process.run(argv, { timeoutMs })
14 if (result.exitCode !== 0) throw new Error(`${argv[0]} exited ${result.exitCode}`)
15 return result.stdout as string
16}
17src/sources/pause-flag.ts 18 lines1import { t } from '../i18n'
2import type { Source } from './types'
3import { identityCard } from './types'
4
5export const pauseFlag: Source = {
6 id: 'pause-flag', title: t('pauseFlag'), detect: async (_, config) => Boolean(config.path),
7 sample: async ($, config) => ({ rows: [{ label: t('dispatch'), value: await $.fs.exists(config.path) ? t('paused') : t('available') }] }),
8 card: identityCard,
9}
10
11export async function setPaused($: any, path: string, paused: boolean) {
12 if (paused) await $.fs.write(path, 'paused\n')
13 else {
14 const result = await $.process.run(['rm', '-f', '--', path], { timeoutMs: 3000 })
15 if (result.exitCode !== 0) throw new Error(`rm exited ${result.exitCode}`)
16 }
17}
18src/sources/collect.ts 13 lines1import { sources } from './index'
2import type { CardData, SourceConfig } from './types'
3
4export async function collectSource($: any, card: SourceConfig): Promise<CardData | null> {
5 const source = sources[card.id]
6 if (!await source.detect($, card)) return null
7 const data = await Promise.race([
8 source.sample($, card),
9 $.clock.sleep(4000).then(() => { throw new Error('sample timed out') }),
10 ])
11 return source.card(data)
12}
13src/ui/components.tsx 41 lines1import type { CardData, RowData } from '../sources/types'
2
3export function Bar(ui: any, percent: number | undefined, bodyColumns: number) {
4 const { Text } = ui
5 if (percent === undefined) return null
6 const width = Math.max(2, Math.min(14, bodyColumns - 18))
7 const fill = Math.round(Math.max(0, Math.min(100, percent)) * width / 100)
8 return <Text><Text color={percent >= 90 ? 'red' : percent >= 75 ? 'yellow' : 'green'}>{'━'.repeat(fill)}</Text><Text color="gray">{'─'.repeat(width - fill)}</Text></Text>
9}
10
11export function StatusDot(ui: any, status: RowData['status']) {
12 const { Text } = ui
13 return <Text color={status === 'error' ? 'red' : status === 'warn' ? 'yellow' : 'green'}>● </Text>
14}
15
16export function Row(ui: any, row: RowData, bodyColumns: number, key: string) {
17 const { Box, Text } = ui
18 return <Box key={key} flexDirection="row">
19 <Box width={bodyColumns < 28 ? 7 : 10} flexShrink={0}><Text wrap="truncate-end">{row.label}</Text></Box>
20 {row.status && StatusDot(ui, row.status)}
21 <Box flexGrow={1} flexShrink={1}><Text wrap="truncate-end">{row.value}</Text></Box>
22 {bodyColumns >= 30 && Bar(ui, row.percent, bodyColumns)}
23 </Box>
24}
25
26export function List(ui: any, data: CardData, bodyColumns: number) {
27 const { Box, Text } = ui
28 return <Box flexDirection="column" width={Math.max(1, bodyColumns - 4)}>
29 {data.error ? <Text color="red">{data.error}</Text> : data.rows.map((row, i) => Row(ui, row, bodyColumns, String(i)))}
30 {data.note && bodyColumns >= 35 && <Text dimColor wrap="truncate-end">{data.note}</Text>}
31 </Box>
32}
33
34export function Card(ui: any, id: string, title: string, data: CardData, bodyColumns: number, extra?: any) {
35 const { Box, Text } = ui
36 return <Box key={`card-${id}`} width={Math.max(12, bodyColumns - 1)} flexDirection="column" borderStyle="round" borderColor={data.error ? 'red' : 'blue'} paddingX={1}>
37 <Box justifyContent="space-between"><Text bold>{title}</Text>{extra}</Box>
38 {List(ui, data, bodyColumns)}
39 </Box>
40}
41src/i18n.ts 24 lines1export const messages = {
2 en: {
3 dashboard: 'Dashboard', quota: 'Claude usage', session: 'Current session', gpu: 'GPU', disk: 'Disk', httpProbe: 'Endpoints', pauseFlag: 'Dispatch pause', config: 'Config',
4 fiveHour: '5 hours', weekly: 'Weekly', unavailable: 'Unavailable', quotaHint: 'Usage may appear after your first message', directory: 'Folder', context: 'Context', usage: 'Used', free: 'Free',
5 http: 'HTTP', dispatch: 'Dispatch', paused: 'Paused', available: 'Available', pause: 'Pause', resume: 'Resume', status: 'Status', offline: 'Offline', loadFailed: 'Read failed', configFailed: 'Config error', flagFailed: 'Flag update failed',
6 opened: 'Dashboard opened.', waiting: 'Dashboard waiting', fullscreenHint: 'Sidebar requires fullscreen layout. Run /tui fullscreen, then /cc-dash.', footer: 'cc-dash-kit',
7 },
8 'zh-TW': {
9 dashboard: '看板', quota: 'Claude 額度', session: '目前 Session', gpu: 'GPU', disk: '磁碟', httpProbe: '端點', pauseFlag: '派工暫停', config: '設定',
10 fiveHour: '5 小時', weekly: '每週', unavailable: '未提供', quotaHint: '訂閱額度資料可能要送出首則訊息後才有', directory: '目錄', context: 'Context', usage: '使用率', free: '可用',
11 http: 'HTTP', dispatch: '派工', paused: '暫停', available: '可用', pause: '暫停', resume: '恢復', status: '狀態', offline: '離線', loadFailed: '讀取失敗', configFailed: '設定錯誤', flagFailed: '旗標更新失敗',
12 opened: '看板已開啟。', waiting: '看板等待中', fullscreenHint: '側邊欄需要全螢幕版面。執行 /tui fullscreen,再執行 /cc-dash。', footer: 'cc-dash-kit',
13 },
14} as const
15export type Locale = keyof typeof messages
16export type MessageKey = keyof typeof messages.en
17let current: Locale = 'en'
18export const setLocale = (locale: Locale) => { current = locale }
19export const t = (key: MessageKey): string => messages[current][key]
20export function parseLocale(value: unknown): Locale | undefined {
21 if (typeof value !== 'string') return undefined
22 return /^zh(?:[_-]|$)/i.test(value) ? 'zh-TW' : value.toLowerCase().startsWith('en') ? 'en' : undefined
23}
24src/sources/index.ts 10 lines1import { quota } from './quota'
2import { session } from './session'
3import { gpu } from './gpu'
4import { disk } from './disk'
5import { httpProbe } from './http-probe'
6import { pauseFlag } from './pause-flag'
7import type { Source } from './types'
8
9export const sources: Record<string, Source> = { quota, session, gpu, disk, 'http-probe': httpProbe, 'pause-flag': pauseFlag }
10src/sources/quota.ts 22 lines1import { t } from '../i18n'
2import type { Source } from './types'
3import { identityCard } from './types'
4
5export function parseQuota(usage: any) {
6 const limits = Array.isArray(usage?.rateLimits) ? usage.rateLimits : []
7 return ['five_hour', 'seven_day'].map(kind => {
8 const item = limits.find((one: any) => one.kind === kind)
9 return { label: kind === 'five_hour' ? t('fiveHour') : t('weekly'), value: item ? `${Math.round(item.percentUsed)}%` : t('unavailable'), percent: item?.percentUsed, status: item?.percentUsed >= 90 ? 'warn' as const : 'ok' as const }
10 })
11}
12
13export const quota: Source = {
14 id: 'quota', title: t('quota'), detect: async () => true,
15 sample: async $ => {
16 let usage: any = null
17 try { usage = await $.session.usage() } catch {}
18 return { rows: parseQuota(usage), note: !Array.isArray(usage?.rateLimits) || usage.rateLimits.length === 0 ? t('quotaHint') : undefined }
19 },
20 card: identityCard,
21}
22src/sources/session.ts 22 lines1import { t } from '../i18n'
2import type { Source } from './types'
3import { identityCard } from './types'
4
5export const session: Source = {
6 id: 'session', title: t('session'), detect: async () => true,
7 sample: async $ => {
8 const [id, cwd, usage] = await Promise.all([
9 $.session.id().catch(() => t('unavailable')),
10 $.session.cwd().catch(() => '/'),
11 $.session.usage().catch(() => null),
12 ])
13 const name = String(cwd).split('/').filter(Boolean).pop() || '/'
14 const used = usage?.context?.percent
15 return { rows: [
16 { label: t('directory'), value: name },
17 { label: 'ID', value: String(id).slice(0, 12) },
18 { label: t('context'), value: typeof used === 'number' ? `${Math.round(used)}%` : t('unavailable'), percent: used },
19 ] }
20 }, card: identityCard,
21}
22src/sources/gpu.ts 18 lines1import { t } from '../i18n'
2import type { Source } from './types'
3import { identityCard, run } from './types'
4
5export function parseGpu(output: string) {
6 const [name, usedText, totalText] = output.trim().split(/\s*,\s*/)
7 const used = Number(usedText), total = Number(totalText)
8 if (!name || !Number.isFinite(used) || !Number.isFinite(total) || total <= 0) throw new Error('invalid nvidia-smi output')
9 return { rows: [{ label: name, value: `${used} / ${total} MiB`, percent: used / total * 100 }] }
10}
11
12export const gpu: Source = {
13 id: 'gpu', title: 'GPU',
14 detect: async $ => { try { await run($, ['nvidia-smi', '--help'], 1500); return true } catch { return false } },
15 sample: async $ => parseGpu(await run($, ['nvidia-smi', '--query-gpu=name,memory.used,memory.total', '--format=csv,noheader,nounits'], 3000)),
16 card: identityCard,
17}
18src/sources/disk.ts 19 lines1import { t } from '../i18n'
2import type { Source } from './types'
3import { identityCard, run } from './types'
4
5export function parseDisk(output: string) {
6 const line = output.trim().split('\n').at(-1) || ''
7 const parts = line.trim().split(/\s+/)
8 const text = parts.find(x => /^\d+%$/.test(x))
9 const pct = Number(text?.slice(0, -1))
10 if (!text || !Number.isFinite(pct)) throw new Error('invalid df output')
11 return { rows: [{ label: t('usage'), value: `${pct}%`, percent: pct }, { label: t('available'), value: parts[3] || '—' }] }
12}
13
14export const disk: Source = {
15 id: 'disk', title: t('disk'), detect: async () => true,
16 sample: async ($, config) => parseDisk(await run($, ['df', '-h', config.path || '/'], 3000)),
17 card: identityCard,
18}
19