SLOPSHOPPER

rtk-gain

A status line entry with the tokens RTK saved, refreshed at session start and at most every 5 minutes

newcommandstatusprocess
v0.1.0MITupdated 2026-10-05vincentlauriat/ModsTools/mods/rtk-gain
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · rtk-gain
› 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 › /rtk-gain ⎿ rtk-gain: rtk gain is unavailable (rtk missing or failed). ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

rtk-gain

Shows the tokens saved by RTK (Rust Token Killer) in the status line.

What it does

  • Runs rtk gain --format json at session start and shows rtk −19.3M tok (50%) (total tokens saved, average savings percent when reported).
  • Refreshes after a main-conversation turn when at least 5 minutes have passed since the last refresh.
  • Clears the status line when rtk is missing, times out (15 s) or returns unparseable output.

Commands

CommandEffect
/rtk-gainRefreshes the status line and prints the full rtk gain report

Install

claude --plugin-dir /path/to/ModsTools/mods/rtk-gain

Limits

  • Requires the rtk binary on PATH; otherwise the status line stays empty and /rtk-gain says it is unavailable.
  • Figures are RTK's global totals, not per session.
  • Relies on the JSON shape { summary: { total_saved, avg_savings_pct } }.

Develop

claude plugin validate mods/rtk-gain
claude plugin test mods/rtk-gain   # 11 tests
Source 3 files
hooks/register.ts 56 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { parseGain, statusLine } from './gain'
4
5const REFRESH_MS = 5 * 60_000
6const TIMEOUT_MS = 15_000
7
8// stdout of `rtk gain <args>`; null when rtk is missing, times out or fails.
9async function rtk($: EngineInterface, args: string[]): Promise<string | null> {
10  try {
11    const ran = await $.process.run(['rtk', 'gain', ...args], { timeoutMs: TIMEOUT_MS })
12
13    return ran.exitCode === 0 ? ran.stdout : null
14  } catch {
15    return null
16  }
17}
18
19async function refresh($: EngineInterface) {
20  const stdout = await rtk($, ['--format', 'json'])
21  const gain = stdout === null ? null : parseGain(stdout)
22  $.ui.status(gain === null ? undefined : statusLine(gain))
23}
24
25export const register: Register = on => {
26  let lastAt: number | null = null
27
28  on('session.start', async ($, e, next) => {
29    await $.command.register({ name: 'rtk-gain', description: 'Show the RTK token savings summary' })
30    lastAt = await $.clock.now()
31    await refresh($)
32
33    return next(e)
34  })
35
36  on('command.run', { command: 'rtk-gain' }, async $ => {
37    lastAt = await $.clock.now()
38    await refresh($)
39    const text = await rtk($, [])
40
41    return { text: text === null || text.trim() === '' ? 'rtk gain is unavailable (rtk missing or failed).' : text.trim() }
42  })
43
44  on('turn.complete', async ($, e, next) => {
45    if (e.agentId === undefined) {
46      const now = await $.clock.now()
47      if (lastAt === null || now - lastAt >= REFRESH_MS) {
48        lastAt = now
49        await refresh($)
50      }
51    }
52
53    return next(e)
54  })
55}
56
hooks/gain.ts 31 lines
1import type { Gain } from '../types'
2
3const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
4
5// `rtk gain --format json` prints { summary: { total_saved, avg_savings_pct, ... } }.
6export function parseGain(stdout: string): Gain | null {
7  let data: unknown
8  try {
9    data = JSON.parse(stdout)
10  } catch {
11    return null
12  }
13  const summary = (data as { summary?: Record<string, unknown> } | null)?.summary
14  if (summary === undefined || summary === null || typeof summary !== 'object') return null
15  const saved = summary['total_saved']
16  if (!finite(saved) || saved < 0) return null
17  const percent = summary['avg_savings_pct']
18
19  return { saved, percent: finite(percent) ? Math.round(percent) : null }
20}
21
22export function formatTokens(count: number): string {
23  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`
24  if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`
25
26  return String(Math.round(count))
27}
28
29export const statusLine = (gain: Gain) =>
30  `rtk −${formatTokens(gain.saved)} tok${gain.percent === null ? '' : ` (${gain.percent}%)`}`
31
types/index.d.ts 2 lines
1export type Gain = { saved: number; percent: number | null }
2