SLOPSHOPPER

cache-clock

Status-line countdown to prompt-cache expiry; auto-compacts when 5 minutes remain.

newguardtoaststatusprompttimer
v0.1.0no licenseupdated 2026-10-14siinghd/skills/mods/cache-clock
A shopper browsing a rack in a slop shop
README

skills

Portable skills, slash commands, and agent definitions for AI coding assistants. Everything is plain Markdown with YAML frontmatter — no tool lock-in. The repo is the source of truth; config dirs on each machine just link to it.

Layout

DirectoryContentsClaude Code target
skills/One directory per skill (SKILL.md + optional refs)~/.claude/skills
commands/One Markdown file per slash command (subdirs = namespaces, e.g. git/commit.md → /git:commit)~/.claude/commands
agents/One Markdown file per subagent definition~/.claude/agents

The same files work with any tool that understands Markdown + frontmatter — point sync.sh --target at a different config dir to adapt.

Setup on a new machine

git clone git@github.com:siinghd/skills.git
cd skills
./sync.sh install          # symlinks into ~/.claude (edits stay in the repo)
# or: ./sync.sh install --copy   # plain copies instead of symlinks

Day-to-day sync

git pull                   # get latest (symlinked installs pick it up instantly)
./sync.sh status           # see what's linked / copied / missing
./sync.sh capture          # pull back edits made directly in ~/.claude (copy mode)
git add -A && git commit -m "update skills" && git push

Adding a new skill/command/agent

Drop the file in the matching directory, commit, push, and run ./sync.sh install on machines using copy mode (symlink mode needs a rerun only when a new top-level entry appears).

OpenRouter execution tier

skills/openrouter + agents/openrouter-worker.md (driven by /orchestrate ... use <model>) add a cheap delegation tier: workers on any OpenRouter-hosted model (DeepSeek, Qwen, GLM, Kimi, …) execute mechanical packets at ~2% of premium-model rates, dispatched either as one-shot "oracles", fully agentic headless Claude Code workers, native chaperone agents, or MCP tools in Claude Desktop.

One-command setup on a new machine (after ./sync.sh install):

~/.claude/skills/openrouter/setup.sh sk-or-v1-<your-openrouter-key>

That checks dependencies, stores the key via the platform's best backend (macOS Keychain, Linux Secret Service if available, else a 0600 key file), starts the local LiteLLM gateway, and registers the Claude Desktop MCP server with the per-OS config path. Re-run any time — it's idempotent. Details in skills/openrouter/SKILL.md. Works on macOS, Linux (including WSL), and Windows under Git Bash/MSYS2 — these are bash scripts, so native cmd.exe/PowerShell is not supported. On Windows the key file is not encrypted at rest; WSL is the recommended route for full parity.

Mods

mods/ holds Claude Code plugins (mods) that hook the engine itself.

  • mods/cache-clock — status-line countdown to prompt-cache expiry; when 5 minutes remain and the session is idle (context ≥ 30% full), it compacts so the post-expiry rebuild is the small summary. Assumes the 1 h cache TTL; edit the constants at the top of hooks/register.tsx otherwise.

Load one for a single session, or for every session via your shell profile:

claude --plugin-dir /path/to/skills/mods/cache-clock
export CLAUDE_CODE_PLUGIN_DIRS="/path/to/skills/mods/cache-clock"

.claude-plugin/types/ is generated by the engine on load and is not tracked.

Source 1 files
hooks/register.tsx 80 lines
1import type { Register } from 'claude-code'
2
3// The cache TTL is 5 min by default, 1 h on the extended tier. Set it to match
4// your session; the compact trigger only makes sense when TTL > COMPACT_AT.
5const TTL_MS = 60 * 60 * 1000
6const COMPACT_AT_MS = 5 * 60 * 1000
7// Compacting spends tokens (reads the history, writes a summary), so it only
8// pays off when rebuilding the cache after expiry would cost more. Below this
9// context fill, let the cache expire and skip the compact.
10const MIN_CONTEXT_PERCENT = 30
11
12const fmt = (ms: number) => {
13  const s = Math.max(0, Math.ceil(ms / 1000))
14  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
15}
16
17export const register: Register = on => {
18  // The API times the TTL from the START of a request that writes or reads the
19  // cache. We touch at prompt/tool-call time, which is at or before that start,
20  // so the countdown errs short (compacts early), never late.
21  let lastTouch: number | null = null
22  let isCompacting = false
23  let hasCompactedThisWindow = false
24
25  on('session.start', async ($, e, next) => {
26    lastTouch = await $.clock.now()
27    hasCompactedThisWindow = false
28
29    $.clock.every(1000, async () => {
30      if (lastTouch === null) {
31        $.ui.status('cache: cold')
32        return
33      }
34      const left = TTL_MS - ((await $.clock.now()) - lastTouch)
35      if (left <= 0) {
36        $.ui.status('cache: expired')
37        return
38      }
39      $.ui.status(`cache ${fmt(left)}`)
40
41      // Compact while idle so the rebuild after expiry is the small summary,
42      // not the full conversation. compact() rejects mid-turn; retry next tick.
43      if (left <= COMPACT_AT_MS && !hasCompactedThisWindow && !isCompacting) {
44        isCompacting = true
45        try {
46          const { context } = await $.session.usage()
47          if ((context.percent ?? 0) < MIN_CONTEXT_PERCENT) {
48            hasCompactedThisWindow = true // small context: not worth the tokens
49            return
50          }
51          await $.session.compact()
52          hasCompactedThisWindow = true
53          lastTouch = null // new prefix is not cached until the next request
54          $.ui.toast('cache-clock: auto-compacted before cache expiry')
55        } catch {
56          // a turn is running; try again on the next tick
57        } finally {
58          isCompacting = false
59        }
60      }
61    })
62
63    return next(e)
64  })
65
66  // On failure the hook counts as absent, so the countdown never blocks a
67  // prompt or tool call.
68  on('prompt.submit', async ($, e, next) => {
69    lastTouch = await $.clock.now()
70    hasCompactedThisWindow = false
71    return next(e)
72  }).catch(() => undefined)
73
74  on('tool.call', async ($, e, next) => {
75    lastTouch = await $.clock.now()
76    hasCompactedThisWindow = false
77    return next(e)
78  }).catch(() => undefined)
79}
80