Status line with the working tree's uncommitted files and unpushed commits, polled in the background.

Béla's Claude Code plugins: mods (function-hook plugins that draw panes, bands and status lines), the skill that builds them, and personal skills grouped by theme.
| Plugin | What it is | |
|---|---|---|
mods | /mods opens a keyboard-driven pane that lists your mods, shows what each one does, and switches it on/off now or at startup. | |
mod-builder | A skill that builds new mods to a fixed bar: four tested templates, a mandatory `/<mod> on\ | off, a bare /<mod>` that toggles what it opened, keyboard-first panes, and a validate + type-check + test gate. |
briefings | ai-, tech-, geo-, cyber-, econ-, space-briefing, and morning-briefing, which runs them all into one digest. | |
decks | marp-deck (Marp slides on a tech topic), feature-deck (researches a release and hands it to marp-deck), obsidian-canvas. Decks go to $MARP_DECKS_DIR, else ~/Documents/Presentations. | |
dev-tools | cleanup, deps-audit, docgen, explain, find-bug, justify, package-scout, quick-commit, review-pr, scaffold, summarize-changes. | |
skill-tools | skill-writer, skill-refactor, skill-guide, agent-retro. |
At the prompt of a terminal Claude Code session:
/plugin install mods --marketplace B314-N03/plugin-marketplace
/plugin install mod-builder --marketplace B314-N03/plugin-marketplace
/plugin install briefings --marketplace B314-N03/plugin-marketplace
/plugin install decks --marketplace B314-N03/plugin-marketplace
/plugin install dev-tools --marketplace B314-N03/plugin-marketplace
/plugin install skill-tools --marketplace B314-N03/plugin-marketplace
A plugin's skills are called by their namespaced name, e.g. /briefings:ai-briefing.
Answer y to add the marketplace the first time, then choose the user scope. Get updates with claude plugin update <name>, then /reload-plugins.
Add your clone as a folder marketplace instead. Installed plugins are then read straight from the folder, so an edit reaches a session after /reload-plugins, with no version bump:
claude plugin marketplace add ~/Coding/plugin-marketplace
claude plugin install mods@b314
claude plugin install mod-builder@b314
# and briefings@b314, decks@b314, dev-tools@b314, skill-tools@b314
Gate a plugin before committing (the types file path is the one the built-in plugin-authoring skill names when it loads):
bash plugins/mod-builder/skills/mod-builder/scripts/check.sh plugins/mods <types-file>
It must end with == PASS. claude plugin validate . checks the marketplace file.
.claude-plugin/marketplace.json the plugin list
plugins/mods/ hooks module, pure logic, tests, types contract
plugins/mod-builder/skills/mod-builder/
SKILL.md the build procedure
reference/conventions.md the rules (off switch, keyboard, tests)
templates/ status-line, band-pane, tool-hook, poller
scripts/ new-mod.sh, check.sh, promote.shhooks/register.ts 97 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { TplPollerSnapshot } from '../types'
5import { changesBetween, countLines, parseToggle, statusText } from './logic'
6
7const NAME = 'tpl-poller'
8const OFF_KEY = 'isOff'
9const POLL_MS = 30_000
10const MIN_GAP_MS = 10_000
11const snapshot = atom({ plugin: 'tpl-poller', key: 'snapshot' } as const, null)
12const isOff = atom({ plugin: 'tpl-poller', key: 'isOff' } as const, false)
13
14// Module values reset on a hot reload; anything a drawing or a toast decision
15// needs across reloads lives in $.state (session) or $.store (across sessions).
16// A refresh asked for while one runs joins it instead of being dropped, so a
17// "check now" during a background poll still waits for a fresh result.
18let inFlight: Promise<void> | null = null
19let lastRefresh = 0
20
21// `$` may only be handed to functions declared at the top of the module.
22async function git($: EngineInterface, args: string[]) {
23 const ran = await $.process.run(['git', ...args], { cwd: await $.session.cwd(), timeoutMs: 10_000 })
24 return { ok: ran.exitCode === 0, out: ran.stdout }
25}
26
27async function readSnapshot($: EngineInterface): Promise<TplPollerSnapshot | null> {
28 const branch = await git($, ['rev-parse', '--abbrev-ref', 'HEAD'])
29 if (!branch.ok) return null
30 const [status, unpushed] = await Promise.all([
31 git($, ['status', '--porcelain']),
32 git($, ['rev-list', '--count', '@{u}..HEAD']),
33 ])
34 return {
35 branch: branch.out.trim(),
36 dirty: countLines(status.out),
37 unpushed: unpushed.ok ? Number(unpushed.out.trim()) : null,
38 }
39}
40
41async function refresh($: EngineInterface): Promise<void> {
42 if (inFlight) return inFlight
43 if (await read($, isOff)) return
44 inFlight = poll($).finally(() => {
45 inFlight = null
46 })
47 return inFlight
48}
49
50async function poll($: EngineInterface) {
51 try {
52 lastRefresh = await $.clock.now()
53 const before = await read($, snapshot)
54 const after = await readSnapshot($)
55 if (after === null) return $.ui.status(undefined)
56 for (const change of changesBetween(before, after)) $.ui.toast(change)
57 await update($, snapshot, () => after)
58 $.ui.status(statusText(after))
59 } catch {
60 $.ui.status(undefined)
61 }
62}
63
64export const register: Register = on => {
65 on('session.start', async ($, e, next) => {
66 await $.command.register({
67 name: 'tpl-poller',
68 description: `Working-tree status line. /${NAME} toggles it, /${NAME} off hides it, /${NAME} on shows it again`,
69 argumentHint: '[on|off]',
70 })
71 const wasOff = (await $.store.get(OFF_KEY)) === true
72 await update($, isOff, () => wasOff)
73 // Timers started here run until the module reloads; never start one per turn.
74 $.clock.every(POLL_MS, () => void refresh($))
75 void refresh($)
76 return next(e)
77 })
78
79 on('turn.complete', async ($, e, next) => {
80 const result = await next(e)
81 if (e.agentId === undefined && (await $.clock.now()) - lastRefresh > MIN_GAP_MS) void refresh($)
82 return result
83 })
84
85 on('command.run', { command: 'tpl-poller' }, async ($, e) => {
86 const toggle = parseToggle(e.args)
87 if (toggle === 'invalid') return { text: `Use /${NAME} (toggles), /${NAME} on or /${NAME} off.` }
88 // No pane: a bare command toggles the status line, the mod's one surface.
89 const value = toggle === 'show' ? !(await read($, isOff)) : toggle === 'off'
90 await $.store.set(OFF_KEY, value)
91 await update($, isOff, () => value)
92 if (value) $.ui.status(undefined)
93 else void refresh($)
94 return { text: value ? `${NAME} off (also in new sessions). /${NAME} on to bring it back.` : `${NAME} on.` }
95 })
96}
97hooks/logic.ts 30 lines1import type { TplPollerSnapshot } from '../types'
2
3export const countLines = (stdout: string) => (stdout.trim() === '' ? 0 : stdout.trim().split('\n').length)
4
5export const statusText = (snap: TplPollerSnapshot) => {
6 const parts = [snap.branch]
7 if (snap.dirty > 0) parts.push(`${snap.dirty} uncommitted`)
8 if (snap.unpushed === null) parts.push('no upstream')
9 else if (snap.unpushed > 0) parts.push(`${snap.unpushed} unpushed`)
10 if (parts.length === 1) parts.push('clean')
11 return parts.join(' · ')
12}
13
14// Toasts fire on transitions only, never for the first snapshot of a session.
15export const changesBetween = (before: TplPollerSnapshot | null, after: TplPollerSnapshot) => {
16 if (before === null || before.branch !== after.branch) return []
17 const changes: string[] = []
18 if (before.unpushed !== null && before.unpushed > 0 && after.unpushed === 0) changes.push(`${after.branch} is pushed`)
19 if (before.dirty === 0 && after.dirty > 0) changes.push(`${after.branch} has uncommitted changes`)
20 return changes
21}
22
23export type Toggle = 'on' | 'off' | 'show' | 'invalid'
24
25export const parseToggle = (args: string): Toggle => {
26 const arg = args.trim().toLowerCase()
27 if (arg === '') return 'show'
28 return arg === 'on' || arg === 'off' ? arg : 'invalid'
29}
30types/index.d.ts 8 lines1export type TplPollerSnapshot = { dirty: number; unpushed: number | null; branch: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'tpl-poller': { snapshot: TplPollerSnapshot | null; isOff: boolean }
6 }
7}
8