SLOPSHOPPER

kb-health

KB health in the status line: the kb-intelligence snapshot's verdicts, and whether kb-serve runs the code on disk

newstatusprocesstimer
A shopper browsing a rack in a slop shop
README

claude-mods

Private Claude Code marketplace for Tuncer's mods: function-hook plugins that run inside every Claude Code session on his machines (MacBook Pro, MacBook Air, therig, the 5090 PC, the 4090 work PC).

PluginWhat it does
memory-guardsRefuses the footgun commands Claude's memory notes warn about (for example impeccable update --help, routing shiftyeyegames.com DNS through cloudflared, /logout before cswap add) and says why, with the safe alternative.
kb-healthShows KB health in the status line on the machine that runs the KB. It stays quiet on machines without the KB snapshot job.
machine-tagPrefixes session titles with the machine's emoji tag (💻 MBP, 🪶 Air, 🎮 Rig, 🔥 5090, ⚡ 4090), so pinned sessions show which computer they run on. /machine-tag <emoji> <word> overrides it per machine.
pin-everywherePinning a session turns on its Remote Control once machine-tag has put the machine icon in the title, so the session shows by name and icon on every other computer and the phone. Unpinning turns it back off when this mod turned it on. A title is published when Remote Control connects, so after renaming a pinned session, unpin and pin it again to publish the new name.

Install on a machine

claude plugin marketplace add Shifty-Eye-Games/claude-mods
claude plugin install memory-guards@claude-mods
claude plugin install kb-health@claude-mods
claude plugin install machine-tag@claude-mods
claude plugin install pin-everywhere@claude-mods

Then turn on auto-update: in /plugin, open Marketplaces, pick claude-mods and enable auto-update, or set it in ~/.claude/settings.json:

"extraKnownMarketplaces": {
  "claude-mods": {
    "source": { "source": "github", "repo": "Shifty-Eye-Games/claude-mods" },
    "autoUpdate": true
  }
}

Each session start then pulls main. No versions are set, so every commit is an update.

The repo is private, so each machine needs GitHub access without a prompt: an SSH key GitHub knows (Claude Code tries ssh -T git@github.com first), or stored HTTPS credentials (gh auth login then gh auth setup-git, or Git Credential Manager on Windows). Without them the clone and the updates fail silently and the machine keeps its last copy.

Machines that loaded these mods from local folders before must drop those folders from CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json, or each mod loads twice.

Change a mod

  1. Edit it under plugins/<name>/.
  2. Check it: claude plugin validate plugins/<name> and claude plugin test plugins/<name>.
  3. Push to main. Versions are left unset on purpose, so each commit reaches every machine at its next session start.

How mods work, and the engine rules that bite, are in Claude's memory note on Claude Code mods.

Security

Auto-update is on, so whatever lands on main runs in every session on all four machines. Only org admins can push here (the org's default member permission is read). Keep it that way: never give anyone else write access.

Source 1 files
hooks/register.ts 143 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// KB health in the status line: the verdicts of the 30-minute kb-intelligence snapshot, whether the hourly
4// kb-refresh still succeeds, and whether kb-serve runs the code on disk. It only reads files and asks lsof and
5// ps (from the home folder): an untracked file in ~/.hermes would stop the hourly kb-refresh cycle.
6const HOME = '/Users/tuncer'
7const HERMES = `${HOME}/.hermes`
8const SNAPSHOT_JOB = `${HOME}/Library/LaunchAgents/com.leela.kb-intelligence-snapshot.plist`
9const SNAPSHOT = `${HERMES}/profiles/leela/wiki/kb-site/data/kb-intelligence.json`
10const REFRESH_STATUS = `${HERMES}/state/kb-refresh/status.json`
11// kb-serve's code: what serve_kb_site.py imports at start, and the chat_service it loads lazily.
12const CODE_DIRS = [`${HERMES}/scripts/api`, `${HERMES}/scripts/lib`]
13const CODE_FILES = [`${HERMES}/scripts/serve_kb_site.py`, `${HERMES}/scripts/chat_service.py`]
14const PORT = '3901'
15const CHECK_MS = 5 * 60_000
16const SNAPSHOT_MAX_AGE_MS = 90 * 60_000 // the snapshot job runs every 30 minutes
17const REFRESH_MAX_AGE_MS = 150 * 60_000 // kb-refresh runs hourly at :30 and skips 02:30
18// kb-intelligence-status.py's healthy verdicts; every other status shows, UNKNOWN and new ones included.
19const GOOD = new Set(['FRESH', 'IN_SYNC', 'INFO', 'OK'])
20
21type Snapshot = { generated_at?: unknown; workflows?: { id: string; status: string }[] }
22
23const state = { timer: undefined as { cancel: () => void } | undefined, stale: new Set<string>() }
24
25// `ps -o etime=` prints [[dd-]hh:]mm:ss.
26function etimeSeconds(etime: string): number | undefined {
27  const m = etime.trim().match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/)
28  if (!m) {
29    return undefined
30  }
31  return ((Number(m[1] ?? 0) * 24 + Number(m[2] ?? 0)) * 60 + Number(m[3])) * 60 + Number(m[4])
32}
33
34// Python writes microseconds; keep milliseconds so Date.parse reads it everywhere.
35const parseIso = (text: unknown) => Date.parse(String(text).replace(/(\.\d{3})\d+/, '$1'))
36
37function ago(ms: number): string {
38  const minutes = Math.max(0, Math.round(ms / 60_000))
39  return minutes < 120 ? `${minutes} min` : `${Math.round(minutes / 60)} h`
40}
41
42// Staleness counts on the second check in a row, so the minutes after the Mac wakes don't raise an alarm.
43function confirmed(key: string, isStale: boolean): boolean {
44  const wasStale = state.stale.has(key)
45  if (isStale) {
46    state.stale.add(key)
47  } else {
48    state.stale.delete(key)
49  }
50  return isStale && wasStale
51}
52
53async function snapshotProblems($: EngineInterface, now: number): Promise<string[]> {
54  let snap: Snapshot
55  try {
56    snap = JSON.parse(String(await $.fs.read(SNAPSHOT))) as Snapshot
57  } catch {
58    return ['snapshot missing']
59  }
60  const workflows = Array.isArray(snap.workflows) ? snap.workflows : []
61  if (workflows.length === 0) {
62    return ['snapshot empty']
63  }
64  const problems = workflows
65    .filter(w => !GOOD.has(w.status))
66    .map(w => `${w.id} ${String(w.status).toLowerCase().replace(/_/g, ' ')}`)
67  const age = now - parseIso(snap.generated_at)
68  if (confirmed('snapshot', !(age <= SNAPSHOT_MAX_AGE_MS))) {
69    problems.push(Number.isNaN(age) ? 'snapshot undated' : `snapshot ${ago(age)} old`)
70  }
71  return problems
72}
73
74async function refreshProblem($: EngineInterface, now: number): Promise<string | undefined> {
75  let age = NaN
76  try {
77    age = now - parseIso((JSON.parse(String(await $.fs.read(REFRESH_STATUS))) as { last_successful_at?: unknown }).last_successful_at)
78  } catch {
79    age = NaN
80  }
81  if (!confirmed('refresh', !(age <= REFRESH_MAX_AGE_MS))) {
82    return undefined
83  }
84  return Number.isNaN(age) ? 'kb-refresh status unreadable' : `kb-refresh last ok ${ago(age)} ago`
85}
86
87// The process listening on :3901 is kb-serve (launchd's PID can be a relaunch that never bound the port).
88// ponytail: imports beyond CODE_DIRS/CODE_FILES are missed; add them if the server grows new ones.
89async function kbServeProblem($: EngineInterface, now: number): Promise<string | undefined> {
90  const listening = await $.process.run(['/usr/sbin/lsof', '-nP', '-t', `-iTCP:${PORT}`, '-sTCP:LISTEN'], { cwd: HOME }).catch(() => undefined)
91  const pid = listening?.stdout.trim().split('\n')[0]
92  if (!pid) {
93    return 'kb-serve down'
94  }
95  const ps = await $.process.run(['/bin/ps', '-o', 'etime=', '-p', pid], { cwd: HOME }).catch(() => undefined)
96  const up = ps === undefined ? undefined : etimeSeconds(ps.stdout)
97  if (up === undefined) {
98    return 'kb-serve ?'
99  }
100  let newest = 0
101  try {
102    for (const dir of CODE_DIRS) {
103      for (const entry of await $.fs.list(dir)) {
104        if (entry.kind === 'file' && entry.name.endsWith('.py')) {
105          newest = Math.max(newest, entry.mtimeMs)
106        }
107      }
108    }
109    for (const file of CODE_FILES) {
110      newest = Math.max(newest, (await $.fs.stat(file)).mtimeMs)
111    }
112  } catch {
113    return 'kb-serve ?'
114  }
115  return newest > now - up * 1000 + 2_000 ? 'kb-serve restart needed' : undefined
116}
117
118async function check($: EngineInterface) {
119  const now = await $.clock.now()
120  const problems = await snapshotProblems($, now)
121  for (const problem of [await refreshProblem($, now), await kbServeProblem($, now)]) {
122    if (problem) {
123      problems.push(problem)
124    }
125  }
126  const shown = problems.length > 4 ? [...problems.slice(0, 4), `+${problems.length - 4} more`] : problems
127  $.ui.status(shown.length > 0 ? `KB: ${shown.join(' · ')}` : 'KB ok')
128}
129
130export const register: Register = on => {
131  on('session.start', async ($, e, next) => {
132    const ran = await next(e)
133    // The snapshot job's plist marks the KB Mac; anywhere else the mod stays silent.
134    if (await $.fs.exists(SNAPSHOT_JOB)) {
135      state.timer?.cancel()
136      state.timer = $.clock.every(CHECK_MS, () => void check($))
137      void check($)
138    }
139
140    return ran
141  })
142}
143