Meldet den Claude-Token-Verbrauch an das öffentliche mummentum-Leaderboard.

Ein Claude-Code-Mod, der deinen Token-Verbrauch an das öffentliche mummentum Token-Leaderboard meldet: https://mummentum-leaderboard.vercel.app
Je mehr Tokens Claude Code für dich verarbeitet (Input, Output und Cache zusammen), desto höher dein Platz.
In einer Claude-Code-Session im Terminal eingeben:
/plugin install mummentum-leaderboard --marketplace JUiCYK95/mummentum-leaderboard-mod
Die Frage „Add marketplace?“ mit y bestätigen, dann mit Enter den User-Scope wählen. Der Mod ist sofort aktiv, auch in allen künftigen Sessions und im Code-Tab der Claude-Desktop-App.
Voraussetzung ist eine aktuelle Claude-Code-Version (claude update).
git config user.name ein.🏆 #2 · 1,3 Mio. Tokens.Übertragen werden nur Token-Zahlen, der Modellname und dein Git-Name. Keine Prompts, keine Antworten, keine Dateien.
| Befehl | Wirkung |
|---|---|
/leaderboard | Zeigt deinen Platz, deine Tokens und den Link |
/leaderboard name Neuer Name | Ändert deinen Anzeigenamen |
/leaderboard austreten | Löscht deinen Eintrag; der Mod meldet danach nichts mehr |
/leaderboard beitreten | Trägt dich wieder ein |
claude plugin validate .
claude plugin test .hooks/register.ts 226 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Standing, TokenUsage } from '../types'
5
6export const LEADERBOARD_URL: string = 'https://mummentum-leaderboard.vercel.app'
7
8const FLUSH_EVERY_MS = 60_000
9const MIN_GAP_MS = 15_000
10const ZERO: TokenUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
11
12const pending = atom({ plugin: 'mummentum-leaderboard', key: 'pending' } as const, ZERO)
13const standing = atom({ plugin: 'mummentum-leaderboard', key: 'standing' } as const, null)
14
15type $ = EngineInterface
16type Credentials = { id: string; secret: string; name: string }
17
18const sum = (u: TokenUsage) => u.input + u.output + u.cacheRead + u.cacheWrite
19
20export function formatTokens(n: number): string {
21 if (n >= 1e9) return `${(n / 1e9).toFixed(1).replace('.', ',')} Mrd.`
22 if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace('.', ',')} Mio.`
23 if (n >= 1e4) return `${Math.round(n / 1e3)} Tsd.`
24 return String(n)
25}
26
27function isCredentials(value: unknown): value is Credentials {
28 const v = value as Partial<Credentials> | null
29 return typeof v?.id === 'string' && typeof v.secret === 'string' && typeof v.name === 'string'
30}
31
32async function post($: $, path: string, body: unknown) {
33 if (LEADERBOARD_URL === '') return { status: 0, ok: false, data: {} as Record<string, unknown> }
34 const res = await $.http.fetch(`${LEADERBOARD_URL}${path}`, {
35 method: 'POST',
36 headers: { 'content-type': 'application/json' },
37 body: JSON.stringify(body),
38 })
39 let data: Record<string, unknown> = {}
40 try {
41 data = JSON.parse(res.text) as Record<string, unknown>
42 } catch {}
43 return { status: res.status, ok: res.ok, data }
44}
45
46async function gitName($: $): Promise<string> {
47 for (const argv of [
48 ['git', 'config', '--global', '--get', 'user.name'],
49 ['git', 'config', '--get', 'user.name'],
50 ['whoami'],
51 ]) {
52 try {
53 const { exitCode, stdout } = await $.process.run(argv, { timeoutMs: 5000 })
54 const name = stdout.trim()
55 if (exitCode === 0 && name !== '') return name
56 } catch {}
57 }
58 return ''
59}
60
61// Was nur diese Ladung des Moduls braucht; ein Reload fängt hier neu an.
62const run: {
63 registering: Promise<Credentials | null> | undefined
64 flushing: Promise<void> | undefined
65 lastFlush: number
66 model: string | undefined
67} = { registering: undefined, flushing: undefined, lastFlush: 0, model: undefined }
68
69async function credentials($: $): Promise<Credentials | null> {
70 const stored = await $.store.get('credentials')
71 if (isCredentials(stored)) return stored
72 if ((await $.store.get('optedOut')) === true) return null
73 run.registering ??= (async () => {
74 const name = await gitName($)
75 const { ok, data } = await post($, '/api/register', { name })
76 if (!ok || typeof data.id !== 'string' || typeof data.secret !== 'string') return null
77 const creds: Credentials = { id: data.id, secret: data.secret, name: String(data.name ?? name) }
78 await $.store.set('credentials', creds)
79 $.ui.toast(`🏆 Im mummentum-Leaderboard eingetragen als „${creds.name}“`)
80 return creds
81 })().finally(() => {
82 run.registering = undefined
83 })
84 return run.registering
85}
86
87async function showStatus($: $): Promise<void> {
88 const s = await read($, standing)
89 const open = sum(await read($, pending))
90 if (s === null) return $.ui.status(open > 0 ? `🏆 ${formatTokens(open)} Tokens` : undefined)
91 $.ui.status(`🏆 #${s.rank} · ${formatTokens(s.tokens + open)} Tokens`)
92}
93
94/** Meldet den vorgemerkten Verbrauch; läuft schon eine Meldung, wird danach gemeldet. */
95async function flush($: $): Promise<void> {
96 while (run.flushing) await run.flushing
97 run.flushing = send($).finally(() => {
98 run.flushing = undefined
99 })
100 return run.flushing
101}
102
103async function send($: $): Promise<void> {
104 run.lastFlush = await $.clock.now()
105 try {
106 const creds = await credentials($)
107 if (creds === null) return
108 const sent = await read($, pending)
109 if (sum(sent) === 0) return
110 const { status, ok, data } = await post($, '/api/usage', {
111 id: creds.id,
112 secret: creds.secret,
113 usage: sent,
114 model: run.model,
115 })
116 if (status === 401) {
117 // Der Eintrag existiert auf dem Server nicht mehr: beim nächsten Mal neu anlegen.
118 await $.store.delete('credentials')
119 return
120 }
121 if (!ok) return
122 await update($, pending, cur => ({
123 input: Math.max(0, cur.input - sent.input),
124 output: Math.max(0, cur.output - sent.output),
125 cacheRead: Math.max(0, cur.cacheRead - sent.cacheRead),
126 cacheWrite: Math.max(0, cur.cacheWrite - sent.cacheWrite),
127 }))
128 if (typeof data.rank === 'number' && typeof data.tokens === 'number') {
129 const next: Standing = { rank: data.rank, tokens: data.tokens }
130 await update($, standing, () => next)
131 await $.store.set('standing', next)
132 }
133 } catch {
134 // Netzwerkfehler: der Verbrauch bleibt vorgemerkt und geht beim nächsten Mal mit.
135 } finally {
136 await showStatus($)
137 }
138}
139
140export const register: Register = on => {
141 on('session.start', async ($, e, next) => {
142 await $.command.register({
143 name: 'leaderboard',
144 description: 'mummentum-Leaderboard: Platz anzeigen (name <Name> | austreten | beitreten)',
145 })
146 const stored = await $.store.get('standing')
147 const s = stored as Partial<Standing> | undefined
148 if (typeof s?.rank === 'number' && typeof s.tokens === 'number') {
149 const known: Standing = { rank: s.rank, tokens: s.tokens }
150 await update($, standing, () => known)
151 }
152 await showStatus($)
153 $.clock.every(FLUSH_EVERY_MS, () => {
154 void (async () => {
155 if (sum(await read($, pending)) > 0) await flush($)
156 })().catch(() => undefined)
157 })
158 void flush($).catch(() => undefined)
159
160 return next(e)
161 })
162
163 on('turn.complete', async ($, e, next) => {
164 const done = await next(e)
165 const u = e.usage
166 if (u !== undefined && (await $.store.get('optedOut')) !== true) {
167 run.model = u.model
168 await update($, pending, cur => ({
169 input: cur.input + u.input_tokens,
170 output: cur.output + u.output_tokens,
171 cacheRead: cur.cacheRead + u.cache_read_input_tokens,
172 cacheWrite: cur.cacheWrite + u.cache_creation_input_tokens,
173 }))
174 if ((await $.clock.now()) - run.lastFlush >= MIN_GAP_MS) void flush($).catch(() => undefined)
175 else await showStatus($)
176 }
177
178 return done
179 })
180
181 on('session.end', async ($, e, next) => {
182 if (sum(await read($, pending)) > 0) await flush($)
183 return next(e)
184 })
185
186 on('command.run', { command: 'leaderboard' }, async ($, e) => {
187 const [sub, ...rest] = e.args.trim().split(/\s+/)
188 if (sub === 'austreten') {
189 const creds = await credentials($)
190 if (creds !== null) {
191 const { ok, status } = await post($, '/api/leave', { id: creds.id, secret: creds.secret })
192 if (!ok && status !== 401) return { text: 'Austreten fehlgeschlagen, das Leaderboard ist gerade nicht erreichbar.' }
193 }
194 await $.store.set('optedOut', true)
195 await $.store.delete('credentials')
196 await $.store.delete('standing')
197 await update($, standing, () => null)
198 await update($, pending, () => ZERO)
199 $.ui.status(undefined)
200 return { text: 'Du bist aus dem mummentum-Leaderboard ausgetreten. Mit /leaderboard beitreten kommst du zurück.' }
201 }
202 if (sub === 'beitreten') {
203 await $.store.delete('optedOut')
204 const creds = await credentials($)
205 return { text: creds ? `Willkommen zurück im Leaderboard als „${creds.name}“.` : 'Das Leaderboard ist gerade nicht erreichbar.' }
206 }
207 if (sub === 'name') {
208 const name = rest.join(' ').trim()
209 if (name === '') return { text: 'Bitte einen Namen angeben: /leaderboard name <Neuer Name>' }
210 const creds = await credentials($)
211 if (creds === null) return { text: 'Das Leaderboard ist gerade nicht erreichbar.' }
212 const { ok, data } = await post($, '/api/rename', { id: creds.id, secret: creds.secret, name })
213 if (!ok) return { text: `Umbenennen fehlgeschlagen: ${String(data.error ?? 'unbekannter Fehler')}` }
214 await $.store.set('credentials', { ...creds, name: String(data.name ?? name) })
215 return { text: `Du heißt im Leaderboard jetzt „${String(data.name ?? name)}“.` }
216 }
217
218 await flush($)
219 const creds = await credentials($)
220 const s = await read($, standing)
221 const who = creds ? `„${creds.name}“` : '(noch nicht eingetragen)'
222 const where = s ? `Platz ${s.rank} mit ${s.tokens.toLocaleString('de-DE')} Tokens` : 'noch kein Platz'
223 return { text: `🏆 mummentum-Leaderboard: ${who}, ${where}.\n${LEADERBOARD_URL}` }
224 })
225}
226types/index.d.ts 13 lines1export type TokenUsage = { input: number; output: number; cacheRead: number; cacheWrite: number }
2
3export type Standing = { rank: number; tokens: number }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'mummentum-leaderboard': {
8 pending: TokenUsage
9 standing: Standing | null
10 }
11 }
12}
13