SLOPSHOPPER

usage-forecast

Projects 5h usage at reset and shows it above the prompt. Optional: install only if you want it.

newband
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-forecast
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ⟨Claude Code's own drawing⟩ NaN% by Invalid Date - BURN SOME TOKENS! ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ NaN% by Invalid Date - BURN SOME TOKENS!
README

hardkoded-skills

A collection of skills by hardkoded packaged as plugins for Claude, Cursor, GitHub Copilot, and Codex.

Included skills

This is a growing collection — more skills will land here over time. Currently:

  • cronito — an interactive CLI for managing scheduled tasks on your machine, backed by a single system cron entry. This skill vendors a prebuilt, dependency-free bundle, so there's nothing to install to use it. Source lives at hardkoded/cronito. Run /cronito init once to set it up.
  • pr-memory — PR-keyed, on-disk session memory. save snapshots the current session as a handoff-style document (worktree, per-repo branches, every related PR), and load <pr> re-hydrates it later by any of those PR numbers. Works standalone in a single repo, or across a multi-repo workspace if you use one. Memories are pruned automatically once every PR they reference has merged.
  • user-email — an optional Claude Code mod that shows the signed-in account email above the prompt. Not installed with hardkoded; install it separately with /plugin install user-email@hardkoded-skills if you want it.
  • usage-forecast — an optional Claude Code mod that projects where the 5-hour usage window will land at reset, shown above the prompt. Not installed with hardkoded; install it separately with /plugin install usage-forecast@hardkoded-skills if you want it.

Installation

Claude Code

/plugin marketplace add hardkoded/hardkoded-skills
/plugin install hardkoded@hardkoded

Cursor

Import this repository as a team marketplace, then install hardkoded from the Cursor plugin UI.

GitHub Copilot (VS Code)

Use Chat: Install Plugin From Source and provide:

https://github.com/hardkoded/hardkoded-skills

Codex

codex plugin marketplace add hardkoded/hardkoded-skills
codex plugin add hardkoded@hardkoded

Repository layout

hardkoded-skills/
├── .claude-plugin/marketplace.json
├── .cursor-plugin/marketplace.json
├── plugin.json
└── plugins/
    └── hardkoded/
        ├── .claude-plugin/plugin.json
        ├── .cursor-plugin/plugin.json
        ├── .codex-plugin/plugin.json
        └── skills/
            ├── cronito/
            │   ├── SKILL.md
            │   ├── cronito.sh
            │   └── dist/cronito.cjs
            └── pr-memory/
                ├── SKILL.md
                └── scripts/

Every install path above (Claude, Cursor, Copilot, Codex) resolves skills through a plugin manifest's "skills" field, which points at plugins/hardkoded/skills/ — that's the single copy of each skill in this repo. Each skill only ships its built artifact here — source lives in its own repo (e.g. hardkoded/cronito).

License

MIT

Source 2 files
hooks/register.tsx 75 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Forecast } from '../types'
5
6const WINDOW_MS = 5 * 60 * 60 * 1000
7const PACE_MS = 45 * 60 * 1000
8const MIN_PACE_SPAN_MS = 10 * 60 * 1000
9const MIN_ELAPSED_MS = 60 * 1000
10const MAX_SHOWN_PERCENT = 300
11
12const forecast = atom({ plugin: 'usage-forecast', key: 'forecast' } as const, null as Forecast | null)
13
14let samples: { t: number; pct: number }[] = []
15
16async function recompute($: EngineInterface, limits: SessionRateLimit[]) {
17  const window = limits.find(l => l.kind === 'five_hour')
18  if (!window?.resetsAt) return update($, forecast, () => null)
19
20  const now = await $.clock.now()
21  const reset = Date.parse(window.resetsAt)
22
23  const last = samples[samples.length - 1]
24  if (last && window.percentUsed < last.pct) samples = []
25  samples = [...samples, { t: now, pct: window.percentUsed }].filter(s => now - s.t <= PACE_MS)
26
27  const first = samples[0]
28  const latest = samples[samples.length - 1]
29  const recent =
30    first && latest && latest.t - first.t >= MIN_PACE_SPAN_MS
31      ? (latest.pct - first.pct) / (latest.t - first.t)
32      : null
33  const average = window.percentUsed / Math.max(now - (reset - WINDOW_MS), MIN_ELAPSED_MS)
34  const rate = recent ?? average
35
36  const projected = window.percentUsed + rate * Math.max(reset - now, 0)
37  const at = new Date(reset).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
38
39  await update($, forecast, (): Forecast => ({ projected, at }))
40}
41
42export const register: Register = on => {
43  on('session.start', async ($, e, next) => {
44    const result = await next(e)
45    await recompute($, (await $.session.usage()).rateLimits)
46    return result
47  })
48
49  on('session.measure', async ($, e, next) => {
50    if (e.changed.includes('rateLimits')) await recompute($, e.rateLimits)
51    return next(e)
52  })
53
54  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
55    const beneath = await next(e)
56    const f = await read($, forecast)
57    if (e.props.hasSurvey || !f) return beneath
58
59    const { Box, Text } = $.ui.resolve(e)
60    const over = f.projected >= 100
61    const pct = f.projected > MAX_SHOWN_PERCENT ? `>${MAX_SHOWN_PERCENT}` : Math.round(f.projected)
62
63    return (
64      <Box justifyContent="flex-end">
65        {beneath}
66        <Box marginLeft={2}>
67          <Text color={over ? 'error' : 'success'}>
68            {pct}% by {f.at} - {over ? 'SLOW DOWN!' : 'BURN SOME TOKENS!'}
69          </Text>
70        </Box>
71      </Box>
72    )
73  })
74}
75
types/index.d.ts 8 lines
1export type Forecast = { projected: number; at: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-forecast': { forecast: Forecast | null }
6  }
7}
8