Disk, RAM, CPU and swap in one quiet row above the prompt, refreshed every 5 minutes by a local script on macOS, Linux and Windows. No model call, no tokens.

Your machine in one quiet row above the prompt: free disk, free RAM, CPU load and swap.
disk 116G/926G free · ram 5.5G/32G free · cpu 32% · swap 21.2G/22G used
/bin/sh on macOS and Linux, Windows PowerShell on Windows (started by its full path under %SystemRoot%, never from the project folder).| Command | |
|---|---|
/jr-sysinfo | Hide or show the band (it toggles); /jr-sysinfo hide and show set it |
/jr-sysinfo now | Read the machine again now |
/plugin install jr-sysinfo --marketplace jrlprost/claude-code-mods
Runs one read-only command every 5 minutes while a session is open: on macOS df, sysctl, vm_stat and top; on Linux df and /proc/meminfo and /proc/stat; on Windows PowerShell's Get-CimInstance. Disk is the system drive (/, or C: on Windows). Keeps only the band choice in the plugin's own store. Sends nothing.
hooks/register.tsx 133 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { colorAt, GRADIENTS } from './jr/gradient'
5import { parseStats, pressure, size, UNIX_SCRIPT, WINDOWS_SCRIPT } from './sysinfo'
6
7const EVERY_MS = 5 * 60_000
8const stats = atom({ plugin: 'jr-sysinfo', key: 'stats' } as const, null)
9const bandHidden = atom({ plugin: 'jr-sysinfo', key: 'isBandHidden' } as const, false)
10
11// The band can be hidden: /jr-sysinfo hide, show, or the bare command to toggle. One choice for
12// every session, kept across restarts.
13const band = { isLoaded: false }
14
15const bandVerb = (args: string) => {
16 const arg = args.trim().toLowerCase()
17 if (arg === 'hide' || arg === 'off') return 'hide' as const
18 if (arg === 'show' || arg === 'on') return 'show' as const
19 return arg === '' ? ('toggle' as const) : null
20}
21
22async function isBandHidden($: EngineInterface) {
23 if (!band.isLoaded) {
24 if ((await $.store.get('isBandHidden')) === true) await update($, bandHidden, () => true)
25 band.isLoaded = true
26 }
27 return read($, bandHidden)
28}
29
30async function setBand($: EngineInterface, verb: 'hide' | 'show' | 'toggle') {
31 const hide = verb === 'toggle' ? !(await isBandHidden($)) : verb === 'hide'
32 await $.store.set('isBandHidden', hide)
33 await update($, bandHidden, () => hide)
34 return { text: hide ? 'jr sysinfo · band hidden (/jr-sysinfo to bring it back)' : 'jr sysinfo · band shown' }
35}
36
37// Windows is told by its SystemRoot (C:\Windows): there it runs PowerShell, never sh, which
38// could be a Git Bash one. Both start by absolute path from a system folder, never from the
39// project: Windows looks for a bare name in the working folder first, where a cloned repo could
40// hold its own sh.exe or powershell.exe.
41const live = { isStarted: false }
42
43async function readMachine($: EngineInterface) {
44 const root = await $.env.get('SystemRoot')
45 const isWindows = root !== undefined && /^[A-Za-z]:\\/.test(root)
46 const argv = isWindows
47 ? [`${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, '-NoProfile', '-NonInteractive', '-Command', WINDOWS_SCRIPT]
48 : ['/bin/sh', '-c', UNIX_SCRIPT]
49 try {
50 const { stdout } = await $.process.run(argv, { cwd: isWindows ? root : '/', timeoutMs: 20_000 })
51 return parseStats(stdout)
52 } catch {
53 return null
54 }
55}
56
57async function refresh($: EngineInterface) {
58 const next = await readMachine($)
59 if (next) await update($, stats, () => next)
60}
61
62async function ensureStarted($: EngineInterface) {
63 if (live.isStarted) return
64 live.isStarted = true
65 await $.command.register({ name: 'jr-sysinfo', description: 'Show or hide the disk, RAM, CPU and swap band; /jr-sysinfo now reads them again' })
66 await refresh($)
67 $.clock.every(EVERY_MS, () => {
68 void refresh($)
69 })
70}
71
72export const register: Register = on => {
73 // A /clear starts a new conversation with empty state and no session.start: load again once it has begun.
74 on('session.end', async ($, e, next) => {
75 const ended = await next(e)
76 if (e.reason === 'clear') {
77 band.isLoaded = false
78 $.clock.after(1000, () => {
79 void isBandHidden($)
80 void refresh($)
81 })
82 }
83 return ended
84 })
85
86 on('session.start', async ($, e, next) => {
87 const started = await next(e)
88 await isBandHidden($)
89 await ensureStarted($)
90 return started
91 })
92
93 on('command.run', { command: 'jr-sysinfo' }, async ($, e) => {
94 await ensureStarted($)
95 if (e.args.trim().toLowerCase() === 'now') {
96 await refresh($)
97 return { text: (await read($, stats)) ? 'jr sysinfo · read again' : 'jr sysinfo · could not read this machine' }
98 }
99 const verb = bandVerb(e.args)
100 if (verb) return setBand($, verb)
101 return { text: 'jr sysinfo · /jr-sysinfo toggles the band, /jr-sysinfo hide, show, or now to read again' }
102 })
103
104 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
105 const below = await next(e)
106 if (await read($, bandHidden)) return below
107 const s = await read($, stats)
108 if (!s || e.props.hasSurvey) return below
109 const p = pressure(s)
110 const { Box, Text } = $.ui.resolve(e)
111 const figure = (label: string, value: string, ratio: number, first = false) => (
112 <Text>
113 {first ? '' : <Text dimColor>{' · '}</Text>}
114 <Text dimColor>{`${label} `}</Text>
115 <Text color={colorAt(GRADIENTS.heat, ratio)}>{value}</Text>
116 </Text>
117 )
118 return (
119 <Box flexDirection="column">
120 <Box justifyContent="flex-end">
121 <Text wrap="truncate-end">
122 {figure('disk', `${size(s.diskFree)}/${size(s.diskTotal)} free`, p.disk, true)}
123 {figure('ram', `${size(s.memAvailable)}/${size(s.memTotal)} free`, p.mem)}
124 {figure('cpu', `${Math.round(s.cpu)}%`, p.cpu)}
125 {s.swapTotal > 0 ? figure('swap', `${size(s.swapUsed)}/${size(s.swapTotal)} used`, p.swap) : figure('swap', 'off', 0)}
126 </Text>
127 </Box>
128 {below}
129 </Box>
130 )
131 })
132}
133hooks/jr/gradient.ts 75 lines1// Shared look of every jr mod: one palette, gradients drawn one cell at a time.
2// Synced into each plugin's hooks/jr/ by scripts/sync-shared.sh: edit it here only.
3
4export type Rgb = readonly [number, number, number]
5
6const hexToRgb = (hex: string): Rgb => {
7 const n = Number.parseInt(hex.replace('#', ''), 16)
8 return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
9}
10
11const rgbToHex = ([r, g, b]: Rgb) =>
12 `#${[r, g, b].map(v => Math.round(v).toString(16).padStart(2, '0')).join('')}`
13
14export const GRADIENTS = {
15 // The jr signature: violet to pink to amber.
16 brand: ['#7f5af0', '#ff5ca8', '#ffb86b'],
17 // Calm to alarm, for gauges where full is bad.
18 heat: ['#2cb67d', '#f4d35e', '#ff8c42', '#ef4565'],
19 // Cool, for neutral figures.
20 ice: ['#3da9fc', '#7f5af0'],
21 // Money and growth.
22 money: ['#2cb67d', '#a8e6cf'],
23} as const satisfies Record<string, readonly string[]>
24
25export type GradientName = keyof typeof GRADIENTS
26
27/** The color at `t` (0..1) along the stops. */
28export const colorAt = (stops: readonly string[], t: number): string => {
29 if (stops.length === 1) return stops[0]!
30 const clamped = Math.min(1, Math.max(0, t))
31 const span = clamped * (stops.length - 1)
32 const i = Math.min(stops.length - 2, Math.floor(span))
33 const a = hexToRgb(stops[i]!)
34 const b = hexToRgb(stops[i + 1]!)
35 const local = span - i
36 return rgbToHex([0, 1, 2].map(k => a[k]! + (b[k]! - a[k]!) * local) as unknown as Rgb)
37}
38
39/** One color per character of `text`, spread across the stops. */
40export const paint = (text: string, stops: readonly string[]) => {
41 const chars = [...text]
42 return chars.map((char, i) => ({ char, color: colorAt(stops, chars.length > 1 ? i / (chars.length - 1) : 0) }))
43}
44
45const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']
46
47/**
48 * A bar `width` cells wide filled to `ratio` (0..1), with eighth-cell precision.
49 * Each filled cell takes the gradient color of its position along the whole bar,
50 * so a nearly full heat bar ends red and a low one stays green.
51 */
52export const bar = (ratio: number, width: number, stops: readonly string[]) => {
53 const filled = Math.min(1, Math.max(0, ratio)) * width
54 const full = Math.floor(filled)
55 const partial = EIGHTHS[Math.round((filled - full) * 7)] ?? ''
56 const cells: { char: string; color: string; empty?: true }[] = []
57 for (let i = 0; i < width; i++) {
58 const color = colorAt(stops, width > 1 ? i / (width - 1) : 0)
59 if (i < full) cells.push({ char: '█', color })
60 else if (i === full && partial) cells.push({ char: partial, color })
61 else cells.push({ char: '·', color, empty: true })
62 }
63 return cells
64}
65
66export const formatTokens = (n: number) =>
67 n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 10_000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
68
69export const formatDuration = (ms: number) => {
70 const minutes = Math.max(0, Math.round(ms / 60_000))
71 const h = Math.floor(minutes / 60)
72 const m = minutes % 60
73 return h > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${m}min`
74}
75hooks/sysinfo.ts 78 lines1import type { JrSysinfoStats } from '../types'
2
3// Both scripts print the same four lines, in kilobytes, so one parser reads every system:
4// disk <total> <free>
5// mem <total> <available>
6// swap <total> <used>
7// cpu <percent>
8
9/** macOS and Linux, run by sh. Anything else (a Windows sh such as Git Bash) prints `os other`. */
10export const UNIX_SCRIPT = `case "$(uname -s)" in
11Darwin)
12 df -kP / | awk 'NR==2 {print "disk", $2, $4}'
13 ps=$(sysctl -n hw.pagesize)
14 vm_stat | awk -v ps="$ps" -v total="$(sysctl -n hw.memsize)" '/^Pages (free|inactive|speculative):/ {s += $NF} END {printf "mem %d %d\\n", total / 1024, s * ps / 1024}'
15 sysctl -n vm.swapusage | awk '{t = $3 + 0; u = $6 + 0; if ($3 ~ /G/) t *= 1024; if ($6 ~ /G/) u *= 1024; printf "swap %d %d\\n", t * 1024, u * 1024}'
16 top -l 2 -n 0 -s 1 | awk '/^CPU usage/ {idle = $7 + 0} END {printf "cpu %.0f\\n", 100 - idle}'
17 ;;
18Linux)
19 df -kP / | awk 'NR==2 {print "disk", $2, $4}'
20 awk '/^MemTotal:/ {t = $2} /^MemAvailable:/ {a = $2} /^SwapTotal:/ {st = $2} /^SwapFree:/ {sf = $2} END {print "mem", t, a; print "swap", st, st - sf}' /proc/meminfo
21 a=$(head -n 1 /proc/stat); sleep 1; b=$(head -n 1 /proc/stat)
22 printf '%s\\n%s\\n' "$a" "$b" | awk '{idle = $5 + $6; tot = 0; for (j = 2; j <= NF; j++) tot += $j; if (NR == 1) {i1 = idle; t1 = tot} else if (tot > t1) printf "cpu %.0f\\n", 100 * (1 - (idle - i1) / (tot - t1)); else print "cpu 0"}'
23 ;;
24*)
25 echo "os other"
26 ;;
27esac`
28
29/** Windows, run by Windows PowerShell (present on every Windows since 7). */
30export const WINDOWS_SCRIPT = `$ErrorActionPreference = 'Stop'
31$os = Get-CimInstance Win32_OperatingSystem
32$d = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'"
33$pf = Get-CimInstance Win32_PageFileUsage | Measure-Object -Property AllocatedBaseSize, CurrentUsage -Sum
34$cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
35"disk $([math]::Floor($d.Size / 1KB)) $([math]::Floor($d.FreeSpace / 1KB))"
36"mem $($os.TotalVisibleMemorySize) $($os.FreePhysicalMemory)"
37"swap $([int64]$pf[0].Sum * 1024) $([int64]$pf[1].Sum * 1024)"
38"cpu $([math]::Round([double]$cpu))"`
39
40/** The four lines, or null when one is missing or not a number. */
41export const parseStats = (out: string): JrSysinfoStats | null => {
42 const line = (key: string) => {
43 const m = out.match(new RegExp(`^${key} (.+)$`, 'm'))
44 return m ? m[1]!.trim().split(/\s+/).map(Number) : []
45 }
46 const [diskTotal, diskFree] = line('disk')
47 const [memTotal, memAvailable] = line('mem')
48 const [swapTotal, swapUsed] = line('swap')
49 const [cpu] = line('cpu')
50 const all = [diskTotal, diskFree, memTotal, memAvailable, swapTotal, swapUsed, cpu]
51 if (all.some(n => n === undefined || !Number.isFinite(n))) return null
52 return {
53 diskTotal: diskTotal!,
54 diskFree: diskFree!,
55 memTotal: memTotal!,
56 memAvailable: memAvailable!,
57 swapTotal: swapTotal!,
58 swapUsed: swapUsed!,
59 cpu: Math.min(100, Math.max(0, cpu!)),
60 }
61}
62
63/** Kilobytes as a short size: 494G, 16G, 6.1G, 512M. */
64export const size = (kb: number) => {
65 const gb = kb / 1024 / 1024
66 if (gb >= 100) return `${Math.round(gb)}G`
67 if (gb >= 1) return `${Number(gb.toFixed(1))}G`
68 return `${Math.round(kb / 1024)}M`
69}
70
71/** How full each figure is, 0 to 1, for the colour (more full is worse). */
72export const pressure = (s: JrSysinfoStats) => ({
73 disk: s.diskTotal > 0 ? 1 - s.diskFree / s.diskTotal : 0,
74 mem: s.memTotal > 0 ? 1 - s.memAvailable / s.memTotal : 0,
75 cpu: s.cpu / 100,
76 swap: s.swapTotal > 0 ? s.swapUsed / s.swapTotal : 0,
77})
78types/index.d.ts 17 lines1/** One reading, in kilobytes (cpu in percent, 0 to 100). */
2export type JrSysinfoStats = {
3 diskTotal: number
4 diskFree: number
5 memTotal: number
6 memAvailable: number
7 swapTotal: number
8 swapUsed: number
9 cpu: number
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'jr-sysinfo': { stats: JrSysinfoStats | null; isBandHidden: boolean }
15 }
16}
17