A pane of this repo's latest GitHub Actions runs, refreshed while open

Small Claude Code mods: TypeScript hook modules that run before, after or instead of what Claude Code does.
| Mod | What it does |
|---|---|
turn-timestamp | Ends every turn with ✻ 2026-10-03 19:05 CDT · 3s · 1 tool call, so scrollback in multi-day sessions shows when each turn finished. In the terminal it replaces Claude Code's own end-of-turn line (✻ Churned for 1s); in the desktop Code tab, which draws no such line, it goes under Claude's last reply block. Display only. |
ci-status | /ci opens a pane listing the repo's latest GitHub Actions runs (via gh), refreshes every 30s, shows CI: N running in the status line and a toast when a run finishes. |
ai-meter-agents | Classic command hooks (no TypeScript) that record each session's state (working and on which tool, needs you, done) to ~/.config/claude-usage-monitor/agents/, one file per session. The ai-meter daemon shows them on the desk display's Agents page. The same script feeds Codex, Grok and Antigravity sessions: python3 mods/ai-meter-agents/install_other_tools.py adds it to their hook configs (--uninstall removes it). Silent: never blocks a tool call. |
Installs every mod in setup/mods.conf (mine and the community ones I use) and restores their saved settings, such as the neon-cyber skin:
git clone https://github.com/shissncg/claude-mods ~/Dev/claude-mods
~/Dev/claude-mods/setup.sh
./setup.sh --dev registers this checkout as the marketplace instead of GitHub, so edits under mods/ load without pushing../setup.sh save copies this machine's mod settings into setup/store/; commit them to carry them to other machines.setup/mods.conf. Running the script again only installs what's missing.Plugins turned on at claude.ai sync by themselves and don't need to be listed.
To install just one of mine:
claude plugin marketplace add shissncg/claude-mods
claude plugin install turn-timestamp@shissncg-mods
Or try one for a session without installing:
claude --plugin-dir mods/turn-timestamp --plugin-dir mods/ci-status
Check one with claude plugin validate mods/<name>. Mods need a recent Claude Code build: older CLIs report "session.start" is not an event.
hooks/register.tsx 115 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Run, Snapshot } from '../types'
5
6const PANE = 'ci-status'
7const POLL_MS = 30_000
8const FIELDS = 'databaseId,displayTitle,headBranch,status,conclusion,createdAt'
9
10const snapshot = atom({ plugin: 'ci-status', key: 'snapshot' } as const, null)
11
12function mark(run: Run): [string, string] {
13 if (run.status !== 'completed') return ['●', 'yellow']
14 if (run.conclusion === 'success') return ['✔', 'green']
15 if (run.conclusion === 'skipped' || run.conclusion === 'cancelled') return ['○', 'gray']
16 return ['✘', 'red']
17}
18
19function age(createdAt: string, now: number): string {
20 const minutes = Math.max(0, Math.round((now - Date.parse(createdAt)) / 60_000))
21 if (minutes < 60) return `${minutes}m`
22 if (minutes < 60 * 24) return `${Math.round(minutes / 60)}h`
23 return `${Math.round(minutes / 60 / 24)}d`
24}
25
26export const register: Register = on => {
27 let refresh: (() => Promise<void>) | undefined
28 let stopPolling: (() => void) | undefined
29
30 on('session.start', async ($, e, next) => {
31 await $.command.register({
32 name: 'ci',
33 description: 'Show the latest GitHub Actions runs for this repo in a pane',
34 })
35
36 refresh = async () => {
37 const now = await $.clock.now()
38 let fresh: Snapshot
39 try {
40 const out = await $.process.run(['gh', 'run', 'list', '--limit', '15', '--json', FIELDS])
41 if (out.exitCode !== 0) {
42 fresh = { runs: [], error: out.stderr.trim() || `gh exited ${out.exitCode}`, checkedAt: now }
43 } else {
44 const raw = JSON.parse(out.stdout) as Array<Record<string, string | number>>
45 const runs: Run[] = raw.map(r => ({
46 id: Number(r.databaseId),
47 name: String(r.displayTitle),
48 branch: String(r.headBranch),
49 status: String(r.status),
50 conclusion: String(r.conclusion ?? ''),
51 createdAt: String(r.createdAt),
52 }))
53 fresh = { runs, error: null, checkedAt: now }
54 }
55 } catch (err) {
56 fresh = { runs: [], error: `could not run gh: ${String(err)}`, checkedAt: now }
57 }
58
59 const previous = await read($, snapshot)
60 await update($, snapshot, () => fresh)
61
62 // Toast when a run that was in progress has finished.
63 for (const run of fresh.runs) {
64 const before = previous?.runs.find(one => one.id === run.id)
65 if (before !== undefined && before.status !== 'completed' && run.status === 'completed') {
66 $.ui.toast(`CI ${run.conclusion}: ${run.name} (${run.branch})`)
67 }
68 }
69 const running = fresh.runs.filter(run => run.status !== 'completed').length
70 $.ui.status(running > 0 ? `CI: ${running} running` : undefined)
71 }
72
73 return next(e)
74 })
75
76 on('command.run', { command: 'ci' }, async $ => {
77 await $.ui.open({ id: PANE, title: 'CI runs' })
78 void refresh?.()
79 stopPolling ??= $.clock.every(POLL_MS, () => void refresh?.())
80
81 return { text: 'CI pane opened; it refreshes every 30s.' }
82 })
83
84 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
85 const { Box, Text, Button } = $.ui.resolve(e)
86 const snap = await read($, snapshot)
87 const now = await $.clock.now()
88 const room = Math.max(1, (e.viewport?.rows ?? 24) - 5)
89
90 return (
91 <Box flexDirection="column">
92 {snap === null && <Text dimColor>Loading…</Text>}
93 {snap?.error != null && <Text color="red">{snap.error}</Text>}
94 {snap !== null && snap.error === null && snap.runs.length === 0 && (
95 <Text dimColor>No workflow runs in this repo yet.</Text>
96 )}
97 {snap?.runs.slice(0, room).map(run => {
98 const [icon, color] = mark(run)
99 return (
100 <Box key={String(run.id)}>
101 <Text color={color}>{icon} </Text>
102 <Text wrap="truncate-end">
103 {run.name} <Text dimColor>{run.branch} · {age(run.createdAt, now)}</Text>
104 </Text>
105 </Box>
106 )
107 })}
108 <Box marginTop={1}>
109 <Button key="refresh" label="Refresh" onPress={() => void refresh?.()} />
110 </Box>
111 </Box>
112 )
113 })
114}
115types/index.d.ts 17 lines1export type Run = {
2 id: number
3 name: string
4 branch: string
5 status: string
6 conclusion: string
7 createdAt: string
8}
9
10export type Snapshot = { runs: Run[]; error: string | null; checkedAt: number }
11
12declare module 'claude-code' {
13 interface PluginState {
14 'ci-status': { snapshot: Snapshot | null }
15 }
16}
17