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

Shows the tokens saved by RTK (Rust Token Killer) in the status line.
rtk gain --format json at session start and shows rtk −19.3M tok (50%) (total tokens saved, average savings percent when reported).rtk is missing, times out (15 s) or returns unparseable output.| Command | Effect |
|---|---|
/rtk-gain | Refreshes the status line and prints the full rtk gain report |
claude --plugin-dir /path/to/ModsTools/mods/rtk-gain
rtk binary on PATH; otherwise the status line stays empty and /rtk-gain says it is unavailable.{ summary: { total_saved, avg_savings_pct } }.claude plugin validate mods/rtk-gain
claude plugin test mods/rtk-gain # 11 testshooks/register.ts 56 lines1import 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}
56hooks/gain.ts 31 lines1import 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}%)`}`
31types/index.d.ts 2 lines1export type Gain = { saved: number; percent: number | null }
2