tokengolf 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/cli.js +15 -2
- package/hooks/session-end.js +6 -12
- package/hooks/session-start.js +17 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/scripts/session-end.js +925 -17
- package/plugin/scripts/session-start.js +17 -0
- package/src/cli.js +16 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "tokengolf",
|
|
12
12
|
"source": "./plugin",
|
|
13
13
|
"description": "Gamify your Claude Code sessions — track token efficiency, earn achievements, level up your prompting.",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.5",
|
|
15
15
|
"homepage": "https://josheche.github.io/tokengolf/",
|
|
16
16
|
"license": "MIT"
|
|
17
17
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1415,6 +1415,7 @@ var init_install = __esm({
|
|
|
1415
1415
|
// src/cli.js
|
|
1416
1416
|
import { program } from "commander";
|
|
1417
1417
|
import { render as render2 } from "ink";
|
|
1418
|
+
import { Readable as Readable2 } from "stream";
|
|
1418
1419
|
import React5 from "react";
|
|
1419
1420
|
|
|
1420
1421
|
// src/lib/store.js
|
|
@@ -1464,6 +1465,18 @@ init_config();
|
|
|
1464
1465
|
init_score();
|
|
1465
1466
|
init_ScoreCard();
|
|
1466
1467
|
init_StatsView();
|
|
1468
|
+
function safeRender(element) {
|
|
1469
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1470
|
+
return render2(element);
|
|
1471
|
+
}
|
|
1472
|
+
const s = new Readable2({ read() {
|
|
1473
|
+
} });
|
|
1474
|
+
s.setRawMode = () => s;
|
|
1475
|
+
s.ref = () => s;
|
|
1476
|
+
s.unref = () => s;
|
|
1477
|
+
s.isTTY = true;
|
|
1478
|
+
return render2(element, { stdin: s });
|
|
1479
|
+
}
|
|
1467
1480
|
program.name("tokengolf").description("\u26F3 Gamify your Claude Code sessions").version("0.5.4");
|
|
1468
1481
|
program.command("scorecard").description("Show the last run scorecard").action(() => {
|
|
1469
1482
|
const run = getLastRun();
|
|
@@ -1471,10 +1484,10 @@ program.command("scorecard").description("Show the last run scorecard").action((
|
|
|
1471
1484
|
console.log("No runs yet. Open Claude Code \u2014 sessions are tracked automatically.");
|
|
1472
1485
|
process.exit(0);
|
|
1473
1486
|
}
|
|
1474
|
-
|
|
1487
|
+
safeRender(React5.createElement(ScoreCard, { run }));
|
|
1475
1488
|
});
|
|
1476
1489
|
program.command("stats").description("Show career stats dashboard").action(() => {
|
|
1477
|
-
|
|
1490
|
+
safeRender(React5.createElement(StatsView, { stats: getStats() }));
|
|
1478
1491
|
});
|
|
1479
1492
|
program.command("demo [component]").description("Show UI demos \u2014 all, hud, scorecard, stats").option("-i, --index <n>", "Show only the Nth variant (0-based)").action(async (component, opts) => {
|
|
1480
1493
|
const idx = opts.index != null ? parseInt(opts.index) : void 0;
|
package/hooks/session-end.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import os from 'os';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import { autoDetectCost } from '../src/lib/cost.js';
|
|
6
|
+
import { getCurrentRun, clearCurrentRun, setCurrentRun } from '../src/lib/state.js';
|
|
7
|
+
import { saveRun } from '../src/lib/store.js';
|
|
8
|
+
import { renderScorecard } from '../src/lib/ansi-scorecard.js';
|
|
9
|
+
import { getParBudget as gp } from '../src/lib/score.js';
|
|
10
|
+
import { getEffectiveParRates, getEffectiveParFloors } from '../src/lib/config.js';
|
|
12
11
|
|
|
13
12
|
function writeTTY(text) {
|
|
14
13
|
try {
|
|
@@ -84,10 +83,6 @@ try {
|
|
|
84
83
|
const fainted = !cleanExits.includes(reason) && reason !== 'other' ? false : reason === 'other';
|
|
85
84
|
|
|
86
85
|
// Par-based death: spent > par = BUST (with user overrides from config.json)
|
|
87
|
-
const { getParBudget: gp } = await import(path.join(__dir, '../src/lib/score.js'));
|
|
88
|
-
const { getEffectiveParRates, getEffectiveParFloors } = await import(
|
|
89
|
-
path.join(__dir, '../src/lib/config.js')
|
|
90
|
-
);
|
|
91
86
|
const par = gp(run.model, run.promptCount, getEffectiveParRates(), getEffectiveParFloors());
|
|
92
87
|
let status;
|
|
93
88
|
if (result.spent > par) status = 'died';
|
|
@@ -102,7 +97,6 @@ try {
|
|
|
102
97
|
|
|
103
98
|
// For resting runs: update state but don't clear — run continues next session
|
|
104
99
|
if (status === 'resting') {
|
|
105
|
-
const { setCurrentRun } = await import(path.join(__dir, '../src/lib/state.js'));
|
|
106
100
|
setCurrentRun({ ...run, spent: result.spent, fainted: true, ...thinkingFields });
|
|
107
101
|
const saved = {
|
|
108
102
|
...run,
|
package/hooks/session-start.js
CHANGED
|
@@ -56,6 +56,23 @@ try {
|
|
|
56
56
|
}
|
|
57
57
|
} catch {}
|
|
58
58
|
|
|
59
|
+
// Auto-install statusLine if missing or stale
|
|
60
|
+
try {
|
|
61
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
62
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
63
|
+
const scriptDir = path.dirname(fs.realpathSync(process.argv[1]));
|
|
64
|
+
const statuslinePath = path.join(scriptDir, 'statusline.sh');
|
|
65
|
+
if (fs.existsSync(statuslinePath)) {
|
|
66
|
+
const current = settings.statusLine?.command || '';
|
|
67
|
+
const needsInstall = !current || current.includes('tokengolf');
|
|
68
|
+
const needsUpdate = needsInstall && current !== statuslinePath;
|
|
69
|
+
if (needsUpdate) {
|
|
70
|
+
settings.statusLine = { type: 'command', command: statuslinePath, padding: 1 };
|
|
71
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
59
76
|
function detectEffort() {
|
|
60
77
|
const fromEnv = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
61
78
|
if (fromEnv) return fromEnv;
|
package/package.json
CHANGED
|
@@ -1,20 +1,931 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// hooks/session-end.js
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import path5 from "path";
|
|
5
|
+
import fs5 from "fs";
|
|
6
|
+
import os3 from "os";
|
|
7
|
+
|
|
8
|
+
// src/lib/cost.js
|
|
6
9
|
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
7
11
|
import os from "os";
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
var PRICING = {
|
|
13
|
+
"claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
|
|
14
|
+
"claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
|
|
15
|
+
"claude-haiku-4": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 }
|
|
16
|
+
};
|
|
17
|
+
function getPrice(model) {
|
|
18
|
+
const lower = (model || "").toLowerCase();
|
|
19
|
+
for (const [key, price] of Object.entries(PRICING)) {
|
|
20
|
+
if (lower.includes(key)) return price;
|
|
21
|
+
}
|
|
22
|
+
return PRICING["claude-sonnet-4"];
|
|
23
|
+
}
|
|
24
|
+
function getProjectDir(cwd) {
|
|
25
|
+
return path.join(os.homedir(), ".claude", "projects", cwd.replace(/\//g, "-"));
|
|
26
|
+
}
|
|
27
|
+
function parseCostFromTranscript(transcriptPath) {
|
|
28
|
+
try {
|
|
29
|
+
const lines = fs.readFileSync(transcriptPath, "utf8").trim().split("\n");
|
|
30
|
+
let total = 0;
|
|
31
|
+
const byModel = {};
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
try {
|
|
34
|
+
const entry = JSON.parse(line);
|
|
35
|
+
if (entry.type === "assistant" && entry.message?.usage && entry.message?.model) {
|
|
36
|
+
const model = entry.message.model;
|
|
37
|
+
const p = getPrice(model);
|
|
38
|
+
const u = entry.message.usage;
|
|
39
|
+
const cost = (u.input_tokens || 0) / 1e6 * p.input + (u.output_tokens || 0) / 1e6 * p.output + (u.cache_creation_input_tokens || 0) / 1e6 * p.cacheWrite + (u.cache_read_input_tokens || 0) / 1e6 * p.cacheRead;
|
|
40
|
+
total += cost;
|
|
41
|
+
byModel[model] = (byModel[model] || 0) + cost;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return total > 0 ? { total, byModel } : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function findTranscriptsSince(projectDir, sinceMs) {
|
|
52
|
+
try {
|
|
53
|
+
return fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({
|
|
54
|
+
p: path.join(projectDir, f),
|
|
55
|
+
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
|
|
56
|
+
})).filter(({ mtime }) => mtime >= sinceMs).map(({ p }) => p);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parseThinkingFromTranscripts(paths) {
|
|
62
|
+
let invocations = 0;
|
|
63
|
+
let tokens = 0;
|
|
64
|
+
for (const p of paths) {
|
|
65
|
+
try {
|
|
66
|
+
const lines = fs.readFileSync(p, "utf8").trim().split("\n");
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
try {
|
|
69
|
+
const entry = JSON.parse(line);
|
|
70
|
+
if (entry.type === "assistant" && Array.isArray(entry.message?.content)) {
|
|
71
|
+
const thinkBlocks = entry.message.content.filter((b) => b.type === "thinking");
|
|
72
|
+
if (thinkBlocks.length > 0) {
|
|
73
|
+
invocations++;
|
|
74
|
+
for (const block of thinkBlocks) {
|
|
75
|
+
tokens += Math.round((block.thinking?.length || 0) / 4);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return invocations > 0 ? { thinkingInvocations: invocations, thinkingTokens: tokens } : null;
|
|
86
|
+
}
|
|
87
|
+
function parseAllTranscripts(paths) {
|
|
88
|
+
let total = 0;
|
|
89
|
+
const byModel = {};
|
|
90
|
+
for (const p of paths) {
|
|
91
|
+
const result = parseCostFromTranscript(p);
|
|
92
|
+
if (!result) continue;
|
|
93
|
+
total += result.total;
|
|
94
|
+
for (const [model, cost] of Object.entries(result.byModel)) {
|
|
95
|
+
byModel[model] = (byModel[model] || 0) + cost;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return total > 0 ? { total, byModel } : null;
|
|
99
|
+
}
|
|
100
|
+
function modelFamily(model) {
|
|
101
|
+
const m = (model || "").toLowerCase();
|
|
102
|
+
if (m.includes("haiku")) return "haiku";
|
|
103
|
+
if (m.includes("sonnet")) return "sonnet";
|
|
104
|
+
if (m.includes("opus")) return "opus";
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
function parseModelSwitches(transcriptPath) {
|
|
108
|
+
try {
|
|
109
|
+
const lines = fs.readFileSync(transcriptPath, "utf8").trim().split("\n");
|
|
110
|
+
let lastFamily = null;
|
|
111
|
+
let switches = 0;
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
try {
|
|
114
|
+
const entry = JSON.parse(line);
|
|
115
|
+
if (entry.type === "assistant" && entry.message?.model) {
|
|
116
|
+
const family = modelFamily(entry.message.model);
|
|
117
|
+
if (lastFamily !== null && family !== lastFamily) switches++;
|
|
118
|
+
lastFamily = family;
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { switches };
|
|
124
|
+
} catch {
|
|
125
|
+
return { switches: 0 };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function findTranscript(sessionId, projectDir) {
|
|
129
|
+
if (sessionId) {
|
|
130
|
+
try {
|
|
131
|
+
const p = path.join(projectDir, `${sessionId}.jsonl`);
|
|
132
|
+
fs.accessSync(p);
|
|
133
|
+
return p;
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const files = fs.readdirSync(projectDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: fs.statSync(path.join(projectDir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
139
|
+
return files.length ? path.join(projectDir, files[0].f) : null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function autoDetectCost(run) {
|
|
145
|
+
const projectDir = getProjectDir(process.cwd());
|
|
146
|
+
const sinceMs = run.startedAt ? new Date(run.startedAt).getTime() : 0;
|
|
147
|
+
const paths = sinceMs > 0 ? findTranscriptsSince(projectDir, sinceMs) : [findTranscript(run.sessionId, projectDir)].filter(Boolean);
|
|
148
|
+
const parsed = paths.length > 0 ? parseAllTranscripts(paths) : null;
|
|
149
|
+
const spent = parsed?.total ?? (run.spent > 0 ? run.spent : null);
|
|
150
|
+
if (spent === null) return null;
|
|
151
|
+
const modelBreakdown = parsed?.byModel ?? run.modelBreakdown ?? null;
|
|
152
|
+
const thinking = parseThinkingFromTranscripts(paths);
|
|
153
|
+
const primaryPath = findTranscript(run.sessionId, projectDir);
|
|
154
|
+
const { switches: modelSwitches } = primaryPath ? parseModelSwitches(primaryPath) : { switches: 0 };
|
|
155
|
+
const families = new Set(
|
|
156
|
+
Object.keys(parsed?.byModel ?? {}).map(modelFamily).filter((f) => f !== "unknown")
|
|
157
|
+
);
|
|
158
|
+
const distinctModels = families.size;
|
|
159
|
+
return {
|
|
160
|
+
spent,
|
|
161
|
+
modelBreakdown,
|
|
162
|
+
thinkingInvocations: thinking?.thinkingInvocations ?? 0,
|
|
163
|
+
thinkingTokens: thinking?.thinkingTokens ?? 0,
|
|
164
|
+
modelSwitches,
|
|
165
|
+
distinctModels
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/lib/state.js
|
|
170
|
+
import fs2 from "fs";
|
|
171
|
+
import path2 from "path";
|
|
172
|
+
import os2 from "os";
|
|
173
|
+
var STATE_DIR = path2.join(os2.homedir(), ".tokengolf");
|
|
174
|
+
var STATE_FILE = path2.join(STATE_DIR, "current-run.json");
|
|
175
|
+
function ensureDir() {
|
|
176
|
+
if (!fs2.existsSync(STATE_DIR)) fs2.mkdirSync(STATE_DIR, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
function getCurrentRun() {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(fs2.readFileSync(STATE_FILE, "utf8"));
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function setCurrentRun(run) {
|
|
186
|
+
ensureDir();
|
|
187
|
+
fs2.writeFileSync(STATE_FILE, JSON.stringify(run, null, 2));
|
|
188
|
+
}
|
|
189
|
+
function clearCurrentRun() {
|
|
190
|
+
if (fs2.existsSync(STATE_FILE)) fs2.unlinkSync(STATE_FILE);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/lib/store.js
|
|
194
|
+
import fs4 from "fs";
|
|
195
|
+
import path4 from "path";
|
|
196
|
+
|
|
197
|
+
// src/lib/score.js
|
|
198
|
+
var SPEND_TIER_DEFS = [
|
|
199
|
+
{ label: "Mythic", emoji: "\u2728", key: "mythic", color: "magenta" },
|
|
200
|
+
{ label: "Diamond", emoji: "\u{1F48E}", key: "diamond", color: "cyan" },
|
|
201
|
+
{ label: "Gold", emoji: "\u{1F947}", key: "gold", color: "yellow" },
|
|
202
|
+
{ label: "Silver", emoji: "\u{1F948}", key: "silver", color: "white" },
|
|
203
|
+
{ label: "Bronze", emoji: "\u{1F949}", key: "bronze", color: "yellow" },
|
|
204
|
+
{ label: "Reckless", emoji: "\u{1F4B8}", key: "reckless", color: "red" }
|
|
205
|
+
];
|
|
206
|
+
var EFFORT_LEVELS = {
|
|
207
|
+
low: { label: "Low", emoji: "\u{1FAB6}", color: "green" },
|
|
208
|
+
medium: { label: "Medium", emoji: "\u2696\uFE0F", color: "white" },
|
|
209
|
+
high: { label: "High", emoji: "\u{1F525}", color: "yellow" },
|
|
210
|
+
max: { label: "Max", emoji: "\u{1F4A5}", color: "magenta", opusOnly: true }
|
|
211
|
+
};
|
|
212
|
+
function getEffortLevel(effort) {
|
|
213
|
+
return EFFORT_LEVELS[effort] || null;
|
|
214
|
+
}
|
|
215
|
+
var MODEL_BUDGET_TIERS = {
|
|
216
|
+
haiku: { mythic: 0.03, diamond: 0.15, gold: 0.4, silver: 1, bronze: 2.5 },
|
|
217
|
+
sonnet: { mythic: 0.1, diamond: 0.5, gold: 1.5, silver: 4, bronze: 10 },
|
|
218
|
+
opusplan: { mythic: 0.3, diamond: 1.5, gold: 4.5, silver: 12, bronze: 30 },
|
|
219
|
+
opus: { mythic: 0.5, diamond: 2.5, gold: 7.5, silver: 20, bronze: 50 }
|
|
220
|
+
};
|
|
221
|
+
function getModelBudgets(model) {
|
|
222
|
+
const m = (model || "").toLowerCase();
|
|
223
|
+
if (m.includes("opusplan")) return MODEL_BUDGET_TIERS.opusplan;
|
|
224
|
+
if (m.includes("haiku")) return MODEL_BUDGET_TIERS.haiku;
|
|
225
|
+
if (m.includes("opus")) return MODEL_BUDGET_TIERS.opus;
|
|
226
|
+
return MODEL_BUDGET_TIERS.sonnet;
|
|
227
|
+
}
|
|
228
|
+
var MODEL_CLASSES = {
|
|
229
|
+
haiku: {
|
|
230
|
+
name: "Haiku",
|
|
231
|
+
label: "Rogue",
|
|
232
|
+
emoji: "\u{1F3F9}",
|
|
233
|
+
difficulty: "Nightmare",
|
|
234
|
+
color: "red"
|
|
235
|
+
},
|
|
236
|
+
sonnet: {
|
|
237
|
+
name: "Sonnet",
|
|
238
|
+
label: "Fighter",
|
|
239
|
+
emoji: "\u2694\uFE0F",
|
|
240
|
+
difficulty: "Standard",
|
|
241
|
+
color: "cyan"
|
|
242
|
+
},
|
|
243
|
+
opusplan: {
|
|
244
|
+
name: "Paladin",
|
|
245
|
+
label: "Paladin",
|
|
246
|
+
emoji: "\u269C\uFE0F",
|
|
247
|
+
difficulty: "Tactical",
|
|
248
|
+
color: "yellow"
|
|
249
|
+
},
|
|
250
|
+
opus: {
|
|
251
|
+
name: "Opus",
|
|
252
|
+
label: "Warlock",
|
|
253
|
+
emoji: "\u{1F9D9}",
|
|
254
|
+
difficulty: "Casual",
|
|
255
|
+
color: "magenta"
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var MODEL_PAR_RATES = {
|
|
259
|
+
haiku: 0.15,
|
|
260
|
+
sonnet: 1.5,
|
|
261
|
+
opusplan: 4.5,
|
|
262
|
+
opus: 8
|
|
263
|
+
};
|
|
264
|
+
var MODEL_PAR_FLOORS = {
|
|
265
|
+
haiku: 0.1,
|
|
266
|
+
sonnet: 0.75,
|
|
267
|
+
opusplan: 2,
|
|
268
|
+
opus: 3
|
|
269
|
+
};
|
|
270
|
+
function getParBudget(model, promptCount, rateOverrides, floorOverrides) {
|
|
271
|
+
const m = (model || "").toLowerCase();
|
|
272
|
+
let key = "sonnet";
|
|
273
|
+
if (m.includes("opusplan")) key = "opusplan";
|
|
274
|
+
else if (m.includes("haiku")) key = "haiku";
|
|
275
|
+
else if (m.includes("opus")) key = "opus";
|
|
276
|
+
const rates = rateOverrides ? { ...MODEL_PAR_RATES, ...rateOverrides } : MODEL_PAR_RATES;
|
|
277
|
+
const floors = floorOverrides ? { ...MODEL_PAR_FLOORS, ...floorOverrides } : MODEL_PAR_FLOORS;
|
|
278
|
+
return Math.max((promptCount || 0) > 0 ? rates[key] * Math.sqrt(promptCount) : 0, floors[key]);
|
|
279
|
+
}
|
|
280
|
+
function getTier(spent, model) {
|
|
281
|
+
const budgets = model ? getModelBudgets(model) : MODEL_BUDGET_TIERS.sonnet;
|
|
282
|
+
for (const def of SPEND_TIER_DEFS) {
|
|
283
|
+
const max = budgets[def.key];
|
|
284
|
+
if (max !== void 0 && spent <= max) return def;
|
|
285
|
+
}
|
|
286
|
+
return SPEND_TIER_DEFS[SPEND_TIER_DEFS.length - 1];
|
|
287
|
+
}
|
|
288
|
+
function getModelClass(model = "") {
|
|
289
|
+
const m = model.toLowerCase();
|
|
290
|
+
if (m.includes("opusplan")) return MODEL_CLASSES.opusplan;
|
|
291
|
+
const key = Object.keys(MODEL_CLASSES).find((k) => m.includes(k));
|
|
292
|
+
return MODEL_CLASSES[key] || MODEL_CLASSES.sonnet;
|
|
293
|
+
}
|
|
294
|
+
function getEfficiencyRating(spent, budget) {
|
|
295
|
+
const pct = spent / budget;
|
|
296
|
+
if (pct <= 0.15) return { label: "LEGENDARY", emoji: "\u{1F31F}", color: "yellow" };
|
|
297
|
+
if (pct <= 0.3) return { label: "EPIC", emoji: "\u{1F525}", color: "magenta" };
|
|
298
|
+
if (pct <= 0.5) return { label: "PRO", emoji: "\u{1F4AA}", color: "cyan" };
|
|
299
|
+
if (pct <= 0.75) return { label: "SOLID", emoji: "\u2705", color: "green" };
|
|
300
|
+
if (pct <= 1) return { label: "CLOSE CALL", emoji: "\u26A0\uFE0F", color: "white" };
|
|
301
|
+
return { label: "BUST", emoji: "\u{1F4A5}", color: "red" };
|
|
302
|
+
}
|
|
303
|
+
function getBudgetPct(spent, budget) {
|
|
304
|
+
return Math.min(Math.round(spent / budget * 100), 999);
|
|
305
|
+
}
|
|
306
|
+
function formatCost(amount = 0) {
|
|
307
|
+
if (amount === 0) return "$0.00";
|
|
308
|
+
if (amount < 0.01) return `$${amount.toFixed(5)}`;
|
|
309
|
+
return `$${amount.toFixed(2)}`;
|
|
310
|
+
}
|
|
311
|
+
function getOpusPct(modelBreakdown, totalSpent) {
|
|
312
|
+
if (!modelBreakdown || !totalSpent) return null;
|
|
313
|
+
const opusCost = Object.entries(modelBreakdown).filter(([m]) => m.toLowerCase().includes("opus")).reduce((sum, [, c]) => sum + c, 0);
|
|
314
|
+
if (opusCost === 0) return null;
|
|
315
|
+
return Math.round(opusCost / totalSpent * 100);
|
|
316
|
+
}
|
|
317
|
+
function getHaikuPct(modelBreakdown, totalSpent) {
|
|
318
|
+
if (!modelBreakdown || !totalSpent) return null;
|
|
319
|
+
const haikuCost = Object.entries(modelBreakdown).filter(([m]) => m.toLowerCase().includes("haiku")).reduce((sum, [, c]) => sum + c, 0);
|
|
320
|
+
if (haikuCost === 0) return null;
|
|
321
|
+
return Math.round(haikuCost / totalSpent * 100);
|
|
322
|
+
}
|
|
323
|
+
function calculateAchievements(run, rateOverrides, floorOverrides) {
|
|
324
|
+
const achievements = [];
|
|
325
|
+
const won = run.status === "won";
|
|
326
|
+
const effBudget = getParBudget(run.model, run.promptCount, rateOverrides, floorOverrides);
|
|
327
|
+
const pct = run.spent / effBudget;
|
|
328
|
+
const mc = getModelClass(run.model);
|
|
329
|
+
const isPaladin = mc === MODEL_CLASSES.opusplan;
|
|
330
|
+
if ((run.modelSwitches ?? 0) >= 3)
|
|
331
|
+
achievements.push({
|
|
332
|
+
key: "indecisive",
|
|
333
|
+
label: `Indecisive \u2014 ${run.modelSwitches} model switches mid-session`,
|
|
334
|
+
emoji: "\u{1F3B2}"
|
|
335
|
+
});
|
|
336
|
+
if (run.thinkingInvocations > 0 && run.status === "died")
|
|
337
|
+
achievements.push({
|
|
338
|
+
key: "hubris",
|
|
339
|
+
label: "Hubris \u2014 Used ultrathink, busted anyway",
|
|
340
|
+
emoji: "\u{1F926}"
|
|
341
|
+
});
|
|
342
|
+
if (!won) {
|
|
343
|
+
if (pct >= 2)
|
|
344
|
+
achievements.push({ key: "blowout", label: "Blowout \u2014 Spent 2\xD7 par", emoji: "\u{1F4A5}" });
|
|
345
|
+
else if (pct > 1 && pct <= 1.1)
|
|
346
|
+
achievements.push({
|
|
347
|
+
key: "so_close",
|
|
348
|
+
label: "So Close \u2014 Died within 10% of par",
|
|
349
|
+
emoji: "\u{1F62D}"
|
|
350
|
+
});
|
|
351
|
+
if ((run.totalToolCalls || 0) >= 30)
|
|
352
|
+
achievements.push({
|
|
353
|
+
key: "tool_happy",
|
|
354
|
+
label: `Tool Happy \u2014 Died with ${run.totalToolCalls} tool calls`,
|
|
355
|
+
emoji: "\u{1F528}"
|
|
356
|
+
});
|
|
357
|
+
if ((run.promptCount || 0) <= 2)
|
|
358
|
+
achievements.push({
|
|
359
|
+
key: "silent_death",
|
|
360
|
+
label: "Silent Death \u2014 Died with \u22642 prompts",
|
|
361
|
+
emoji: "\u{1FAA6}"
|
|
362
|
+
});
|
|
363
|
+
if ((run.failedToolCalls ?? 0) >= 5)
|
|
364
|
+
achievements.push({
|
|
365
|
+
key: "fumble",
|
|
366
|
+
label: `Fumble \u2014 Died with ${run.failedToolCalls} failed tool calls`,
|
|
367
|
+
emoji: "\u{1F921}"
|
|
368
|
+
});
|
|
369
|
+
if (pct >= 0.5) {
|
|
370
|
+
if ((run.promptCount || 0) >= 3 && run.spent / (run.promptCount || 1) >= 0.5)
|
|
371
|
+
achievements.push({
|
|
372
|
+
key: "expensive_taste",
|
|
373
|
+
label: "Expensive Taste \u2014 Over $0.50 per prompt",
|
|
374
|
+
emoji: "\u{1F377}"
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return achievements;
|
|
378
|
+
}
|
|
379
|
+
if (mc === MODEL_CLASSES.haiku) {
|
|
380
|
+
achievements.push({
|
|
381
|
+
key: "gold_haiku",
|
|
382
|
+
label: "Gold \u2014 Completed with Haiku",
|
|
383
|
+
emoji: "\u{1F947}"
|
|
384
|
+
});
|
|
385
|
+
if (run.spent < 0.1)
|
|
386
|
+
achievements.push({
|
|
387
|
+
key: "diamond",
|
|
388
|
+
label: "Diamond \u2014 Haiku under $0.10",
|
|
389
|
+
emoji: "\u{1F48E}"
|
|
390
|
+
});
|
|
391
|
+
} else if (mc === MODEL_CLASSES.sonnet) {
|
|
392
|
+
achievements.push({
|
|
393
|
+
key: "silver_sonnet",
|
|
394
|
+
label: "Silver \u2014 Completed with Sonnet",
|
|
395
|
+
emoji: "\u{1F948}"
|
|
396
|
+
});
|
|
397
|
+
} else if (mc === MODEL_CLASSES.opusplan) {
|
|
398
|
+
achievements.push({
|
|
399
|
+
key: "paladin",
|
|
400
|
+
label: "Paladin \u2014 Completed a run as Paladin",
|
|
401
|
+
emoji: "\u269C\uFE0F"
|
|
402
|
+
});
|
|
403
|
+
if (pct <= 0.25)
|
|
404
|
+
achievements.push({
|
|
405
|
+
key: "grand_strategist",
|
|
406
|
+
label: "Grand Strategist \u2014 EPIC efficiency as Paladin",
|
|
407
|
+
emoji: "\u265F\uFE0F"
|
|
408
|
+
});
|
|
409
|
+
} else if (mc === MODEL_CLASSES.opus) {
|
|
410
|
+
achievements.push({
|
|
411
|
+
key: "bronze_opus",
|
|
412
|
+
label: "Bronze \u2014 Completed with Opus",
|
|
413
|
+
emoji: "\u{1F949}"
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (pct <= 0.25)
|
|
417
|
+
achievements.push({
|
|
418
|
+
key: "sniper",
|
|
419
|
+
label: "Sniper \u2014 Under 25% of par",
|
|
420
|
+
emoji: "\u{1F3AF}"
|
|
421
|
+
});
|
|
422
|
+
if (pct <= 0.5)
|
|
423
|
+
achievements.push({
|
|
424
|
+
key: "efficient",
|
|
425
|
+
label: "Efficient \u2014 Under 50% of par",
|
|
426
|
+
emoji: "\u26A1"
|
|
427
|
+
});
|
|
428
|
+
if (run.spent < 0.1)
|
|
429
|
+
achievements.push({
|
|
430
|
+
key: "penny",
|
|
431
|
+
label: "Penny Pincher \u2014 Under $0.10",
|
|
432
|
+
emoji: "\u{1FA99}"
|
|
433
|
+
});
|
|
434
|
+
if (run.effort) {
|
|
435
|
+
if (run.effort === "low" && pct < 1)
|
|
436
|
+
achievements.push({
|
|
437
|
+
key: "speedrunner",
|
|
438
|
+
label: "Speedrunner \u2014 Low effort, completed under par",
|
|
439
|
+
emoji: "\u{1F3CE}\uFE0F"
|
|
440
|
+
});
|
|
441
|
+
if ((run.effort === "high" || run.effort === "max") && pct <= 0.25)
|
|
442
|
+
achievements.push({
|
|
443
|
+
key: "tryhard",
|
|
444
|
+
label: "Tryhard \u2014 High effort, EPIC efficiency",
|
|
445
|
+
emoji: "\u{1F3CB}\uFE0F"
|
|
446
|
+
});
|
|
447
|
+
if (run.effort === "max" && mc === MODEL_CLASSES.opus)
|
|
448
|
+
achievements.push({
|
|
449
|
+
key: "archmagus",
|
|
450
|
+
label: "Archmagus \u2014 Opus at max effort, completed",
|
|
451
|
+
emoji: "\u{1F451}"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (run.fastMode && mc === MODEL_CLASSES.opus) {
|
|
455
|
+
if (pct < 1)
|
|
456
|
+
achievements.push({
|
|
457
|
+
key: "lightning",
|
|
458
|
+
label: "Lightning Run \u2014 Opus fast mode, completed under par",
|
|
459
|
+
emoji: "\u26C8\uFE0F"
|
|
460
|
+
});
|
|
461
|
+
if (pct <= 0.25)
|
|
462
|
+
achievements.push({
|
|
463
|
+
key: "daredevil",
|
|
464
|
+
label: "Daredevil \u2014 Opus fast mode, EPIC efficiency",
|
|
465
|
+
emoji: "\u{1F3B0}"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const sessions = run.sessionCount || 1;
|
|
469
|
+
if (sessions >= 2)
|
|
470
|
+
achievements.push({
|
|
471
|
+
key: "made_camp",
|
|
472
|
+
label: `Made Camp \u2014 Completed across ${sessions} sessions`,
|
|
473
|
+
emoji: "\u{1F3D5}\uFE0F"
|
|
474
|
+
});
|
|
475
|
+
if (sessions === 1)
|
|
476
|
+
achievements.push({
|
|
477
|
+
key: "no_rest",
|
|
478
|
+
label: "No Rest for the Wicked \u2014 Completed in one session",
|
|
479
|
+
emoji: "\u{1F525}"
|
|
480
|
+
});
|
|
481
|
+
if (run.fainted)
|
|
482
|
+
achievements.push({
|
|
483
|
+
key: "came_back",
|
|
484
|
+
label: "Came Back \u2014 Fainted and finished anyway",
|
|
485
|
+
emoji: "\u{1F9DF}"
|
|
486
|
+
});
|
|
487
|
+
const compactionEvents = run.compactionEvents || [];
|
|
488
|
+
const manualCompactions = compactionEvents.filter((e) => e.trigger === "manual");
|
|
489
|
+
const autoCompactions = compactionEvents.filter((e) => e.trigger === "auto");
|
|
490
|
+
if (autoCompactions.length > 0)
|
|
491
|
+
achievements.push({
|
|
492
|
+
key: "overencumbered",
|
|
493
|
+
label: "Overencumbered \u2014 Context auto-compacted during run",
|
|
494
|
+
emoji: "\u{1F4E6}"
|
|
495
|
+
});
|
|
496
|
+
if (manualCompactions.length > 0) {
|
|
497
|
+
const minPct = Math.min(...manualCompactions.map((e) => e.contextPct ?? 100));
|
|
498
|
+
if (minPct <= 30)
|
|
499
|
+
achievements.push({
|
|
500
|
+
key: "ghost_run",
|
|
501
|
+
label: `Ghost Run \u2014 Manual compact at ${minPct}% context`,
|
|
502
|
+
emoji: "\u{1F977}"
|
|
503
|
+
});
|
|
504
|
+
else if (minPct <= 40)
|
|
505
|
+
achievements.push({
|
|
506
|
+
key: "ultralight",
|
|
507
|
+
label: `Ultralight \u2014 Manual compact at ${minPct}% context`,
|
|
508
|
+
emoji: "\u{1FAB6}"
|
|
509
|
+
});
|
|
510
|
+
else if (minPct <= 50)
|
|
511
|
+
achievements.push({
|
|
512
|
+
key: "traveling_light",
|
|
513
|
+
label: `Traveling Light \u2014 Manual compact at ${minPct}% context`,
|
|
514
|
+
emoji: "\u{1F392}"
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const ti = run.thinkingInvocations;
|
|
518
|
+
if (ti > 0) {
|
|
519
|
+
achievements.push({
|
|
520
|
+
key: "spell_cast",
|
|
521
|
+
label: `Spell Cast \u2014 Used extended thinking (${ti}\xD7)`,
|
|
522
|
+
emoji: "\u{1F52E}"
|
|
523
|
+
});
|
|
524
|
+
if (pct <= 0.25)
|
|
525
|
+
achievements.push({
|
|
526
|
+
key: "calculated_risk",
|
|
527
|
+
label: "Calculated Risk \u2014 Ultrathink + EPIC efficiency",
|
|
528
|
+
emoji: "\u{1F9EE}"
|
|
529
|
+
});
|
|
530
|
+
if (ti >= 3)
|
|
531
|
+
achievements.push({
|
|
532
|
+
key: "deep_thinker",
|
|
533
|
+
label: `Deep Thinker \u2014 ${ti} ultrathink invocations, completed`,
|
|
534
|
+
emoji: "\u{1F300}"
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
if (run.thinkingInvocations === 0 && pct <= 0.75)
|
|
538
|
+
achievements.push({
|
|
539
|
+
key: "silent_run",
|
|
540
|
+
label: "Silent Run \u2014 No extended thinking, completed under par",
|
|
541
|
+
emoji: "\u{1F92B}"
|
|
542
|
+
});
|
|
543
|
+
if (mc === MODEL_CLASSES.opusplan) {
|
|
544
|
+
const opusPct = getOpusPct(run.modelBreakdown, run.spent);
|
|
545
|
+
if (opusPct !== null) {
|
|
546
|
+
if (opusPct > 60)
|
|
547
|
+
achievements.push({
|
|
548
|
+
key: "architect",
|
|
549
|
+
label: `Architect \u2014 Opus handled ${opusPct}% of cost (heavy planner)`,
|
|
550
|
+
emoji: "\u{1F3DB}\uFE0F"
|
|
551
|
+
});
|
|
552
|
+
if (opusPct < 25)
|
|
553
|
+
achievements.push({
|
|
554
|
+
key: "blitz",
|
|
555
|
+
label: `Blitz \u2014 Opus handled only ${opusPct}% of cost (light plan, fast execution)`,
|
|
556
|
+
emoji: "\u{1F4A8}"
|
|
557
|
+
});
|
|
558
|
+
if (opusPct >= 40 && opusPct <= 60)
|
|
559
|
+
achievements.push({
|
|
560
|
+
key: "equilibrium",
|
|
561
|
+
label: `Equilibrium \u2014 Opus and Sonnet balanced at ${opusPct}% / ${100 - opusPct}%`,
|
|
562
|
+
emoji: "\u2696\uFE0F"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const switches = run.modelSwitches ?? 0;
|
|
567
|
+
const distinct = run.distinctModels ?? 0;
|
|
568
|
+
if (!isPaladin) {
|
|
569
|
+
if (distinct === 1)
|
|
570
|
+
achievements.push({
|
|
571
|
+
key: "purist",
|
|
572
|
+
label: "Purist \u2014 Single model family throughout",
|
|
573
|
+
emoji: "\u{1F537}"
|
|
574
|
+
});
|
|
575
|
+
if (distinct >= 2 && pct < 1)
|
|
576
|
+
achievements.push({
|
|
577
|
+
key: "chameleon",
|
|
578
|
+
label: `Chameleon \u2014 ${distinct} model families used, completed under par`,
|
|
579
|
+
emoji: "\u{1F98E}"
|
|
580
|
+
});
|
|
581
|
+
if (switches === 1 && pct < 1)
|
|
582
|
+
achievements.push({
|
|
583
|
+
key: "tactical_switch",
|
|
584
|
+
label: "Tactical Switch \u2014 Exactly 1 model switch, completed under par",
|
|
585
|
+
emoji: "\u{1F500}"
|
|
586
|
+
});
|
|
587
|
+
if (switches === 0 && distinct <= 1)
|
|
588
|
+
achievements.push({
|
|
589
|
+
key: "committed",
|
|
590
|
+
label: "Committed \u2014 No model switches, one model family",
|
|
591
|
+
emoji: "\u{1F512}"
|
|
592
|
+
});
|
|
593
|
+
if (run.modelBreakdown && run.spent > 0) {
|
|
594
|
+
const declared = (run.model || "").toLowerCase();
|
|
595
|
+
const isHaikuRun = declared.includes("haiku");
|
|
596
|
+
const isSonnetRun = declared.includes("sonnet") && !declared.includes("opus");
|
|
597
|
+
const opusPct2 = getOpusPct(run.modelBreakdown, run.spent) ?? 0;
|
|
598
|
+
const haikuPct2 = getHaikuPct(run.modelBreakdown, run.spent) ?? 0;
|
|
599
|
+
const nonHaikuPct = 100 - haikuPct2;
|
|
600
|
+
if (isHaikuRun && nonHaikuPct > 50)
|
|
601
|
+
achievements.push({
|
|
602
|
+
key: "class_defection",
|
|
603
|
+
label: `Class Defection \u2014 Declared Haiku but ${nonHaikuPct}% cost on heavier models`,
|
|
604
|
+
emoji: "\u26A0\uFE0F"
|
|
605
|
+
});
|
|
606
|
+
else if (isSonnetRun && opusPct2 > 40)
|
|
607
|
+
achievements.push({
|
|
608
|
+
key: "class_defection",
|
|
609
|
+
label: `Class Defection \u2014 Declared Sonnet but ${opusPct2}% cost on Opus`,
|
|
610
|
+
emoji: "\u26A0\uFE0F"
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
|
|
615
|
+
if (haikuPct !== null) {
|
|
616
|
+
if (haikuPct >= 50)
|
|
617
|
+
achievements.push({
|
|
618
|
+
key: "frugal",
|
|
619
|
+
label: `Frugal \u2014 Haiku handled ${haikuPct}% of session cost`,
|
|
620
|
+
emoji: "\u{1F3F9}"
|
|
621
|
+
});
|
|
622
|
+
if (haikuPct >= 75)
|
|
623
|
+
achievements.push({
|
|
624
|
+
key: "rogue_run",
|
|
625
|
+
label: `Rogue Run \u2014 Haiku handled ${haikuPct}% of session cost`,
|
|
626
|
+
emoji: "\u{1F3B2}"
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const promptCount = run.promptCount || 0;
|
|
630
|
+
const totalToolCalls = run.totalToolCalls || 0;
|
|
631
|
+
if (promptCount === 1)
|
|
632
|
+
achievements.push({
|
|
633
|
+
key: "one_shot",
|
|
634
|
+
label: "One Shot \u2014 Completed in a single prompt",
|
|
635
|
+
emoji: "\u{1F94A}"
|
|
636
|
+
});
|
|
637
|
+
if (promptCount >= 20)
|
|
638
|
+
achievements.push({
|
|
639
|
+
key: "conversationalist",
|
|
640
|
+
label: `Conversationalist \u2014 ${promptCount} prompts`,
|
|
641
|
+
emoji: "\u{1F4AC}"
|
|
642
|
+
});
|
|
643
|
+
if (promptCount <= 3 && totalToolCalls >= 10)
|
|
644
|
+
achievements.push({
|
|
645
|
+
key: "terse",
|
|
646
|
+
label: `Terse \u2014 ${promptCount} prompts, ${totalToolCalls} tool calls`,
|
|
647
|
+
emoji: "\u{1F910}"
|
|
648
|
+
});
|
|
649
|
+
if (promptCount >= 15 && totalToolCalls / promptCount < 1)
|
|
650
|
+
achievements.push({
|
|
651
|
+
key: "backseat_driver",
|
|
652
|
+
label: "Backseat Driver \u2014 Many prompts, few tool calls",
|
|
653
|
+
emoji: "\u{1FA91}"
|
|
654
|
+
});
|
|
655
|
+
if (promptCount >= 2 && totalToolCalls / promptCount >= 5)
|
|
656
|
+
achievements.push({
|
|
657
|
+
key: "high_leverage",
|
|
658
|
+
label: `High Leverage \u2014 ${(totalToolCalls / promptCount).toFixed(1)}\xD7 tools per prompt`,
|
|
659
|
+
emoji: "\u{1F3D7}\uFE0F"
|
|
660
|
+
});
|
|
661
|
+
const toolCalls = run.toolCalls || {};
|
|
662
|
+
const editCount = toolCalls["Edit"] || 0;
|
|
663
|
+
const writeCount = toolCalls["Write"] || 0;
|
|
664
|
+
const readCount = toolCalls["Read"] || 0;
|
|
665
|
+
const bashCount = toolCalls["Bash"] || 0;
|
|
666
|
+
const distinctTools = Object.keys(toolCalls).filter((k) => toolCalls[k] > 0).length;
|
|
667
|
+
if (editCount === 0 && writeCount === 0 && readCount >= 1)
|
|
668
|
+
achievements.push({ key: "read_only", label: "Read Only \u2014 No edits or writes", emoji: "\u{1F441}\uFE0F" });
|
|
669
|
+
if (editCount >= 10)
|
|
670
|
+
achievements.push({ key: "editor", label: `Editor \u2014 ${editCount} Edit calls`, emoji: "\u270F\uFE0F" });
|
|
671
|
+
if (bashCount >= 10 && totalToolCalls >= 1 && bashCount / totalToolCalls >= 0.5)
|
|
672
|
+
achievements.push({
|
|
673
|
+
key: "bash_warrior",
|
|
674
|
+
label: `Bash Warrior \u2014 ${bashCount} Bash calls (${Math.round(bashCount / totalToolCalls * 100)}% of tools)`,
|
|
675
|
+
emoji: "\u{1F41A}"
|
|
676
|
+
});
|
|
677
|
+
if (totalToolCalls >= 5 && readCount / totalToolCalls >= 0.6)
|
|
678
|
+
achievements.push({
|
|
679
|
+
key: "scout",
|
|
680
|
+
label: `Scout \u2014 ${Math.round(readCount / totalToolCalls * 100)}% Read calls`,
|
|
681
|
+
emoji: "\u{1F50D}"
|
|
682
|
+
});
|
|
683
|
+
if (editCount >= 1 && editCount <= 3 && pct < 1)
|
|
684
|
+
achievements.push({
|
|
685
|
+
key: "surgeon",
|
|
686
|
+
label: `Surgeon \u2014 Only ${editCount} Edit call${editCount > 1 ? "s" : ""}, under par`,
|
|
687
|
+
emoji: "\u{1F52A}"
|
|
688
|
+
});
|
|
689
|
+
if (distinctTools >= 5)
|
|
690
|
+
achievements.push({
|
|
691
|
+
key: "toolbox",
|
|
692
|
+
label: `Toolbox \u2014 ${distinctTools} distinct tools used`,
|
|
693
|
+
emoji: "\u{1F9F0}"
|
|
694
|
+
});
|
|
695
|
+
if (promptCount >= 3) {
|
|
696
|
+
const costPerPrompt = run.spent / promptCount;
|
|
697
|
+
if (costPerPrompt < 0.01)
|
|
698
|
+
achievements.push({
|
|
699
|
+
key: "cheap_shots",
|
|
700
|
+
label: `Cheap Shots \u2014 $${costPerPrompt.toFixed(4)} per prompt`,
|
|
701
|
+
emoji: "\u{1F4B2}"
|
|
702
|
+
});
|
|
703
|
+
if (costPerPrompt >= 0.5)
|
|
704
|
+
achievements.push({
|
|
705
|
+
key: "expensive_taste",
|
|
706
|
+
label: `Expensive Taste \u2014 $${costPerPrompt.toFixed(2)} per prompt`,
|
|
707
|
+
emoji: "\u{1F377}"
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (run.startedAt && run.endedAt) {
|
|
711
|
+
const elapsedMs = new Date(run.endedAt).getTime() - new Date(run.startedAt).getTime();
|
|
712
|
+
const elapsedMin = elapsedMs / 6e4;
|
|
713
|
+
if (elapsedMin < 5)
|
|
714
|
+
achievements.push({
|
|
715
|
+
key: "speedrun",
|
|
716
|
+
label: `Speedrun \u2014 Completed in ${Math.round(elapsedMin * 60)}s`,
|
|
717
|
+
emoji: "\u23F1\uFE0F"
|
|
718
|
+
});
|
|
719
|
+
if (elapsedMin > 60 && elapsedMin <= 180)
|
|
720
|
+
achievements.push({
|
|
721
|
+
key: "marathon",
|
|
722
|
+
label: `Marathon \u2014 ${Math.round(elapsedMin)}m session`,
|
|
723
|
+
emoji: "\u{1F3C3}"
|
|
724
|
+
});
|
|
725
|
+
if (elapsedMin > 180)
|
|
726
|
+
achievements.push({
|
|
727
|
+
key: "endurance",
|
|
728
|
+
label: `Endurance \u2014 ${Math.round(elapsedMin / 60)}h session`,
|
|
729
|
+
emoji: "\u{1FAE0}"
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const failedToolCalls = run.failedToolCalls ?? 0;
|
|
733
|
+
const subagentSpawns = run.subagentSpawns ?? 0;
|
|
734
|
+
const turnCount = run.turnCount ?? 0;
|
|
735
|
+
if (failedToolCalls === 0 && totalToolCalls >= 5)
|
|
736
|
+
achievements.push({ key: "clean_run", label: "Clean Run \u2014 No tool failures", emoji: "\u2705" });
|
|
737
|
+
if (failedToolCalls >= 10)
|
|
738
|
+
achievements.push({
|
|
739
|
+
key: "stubborn",
|
|
740
|
+
label: `Stubborn \u2014 ${failedToolCalls} failed tool calls, still won`,
|
|
741
|
+
emoji: "\u{1F402}"
|
|
742
|
+
});
|
|
743
|
+
if (subagentSpawns === 0)
|
|
744
|
+
achievements.push({ key: "lone_wolf", label: "Lone Wolf \u2014 No subagents spawned", emoji: "\u{1F43A}" });
|
|
745
|
+
if (subagentSpawns >= 5)
|
|
746
|
+
achievements.push({
|
|
747
|
+
key: "summoner",
|
|
748
|
+
label: `Summoner \u2014 ${subagentSpawns} subagents spawned`,
|
|
749
|
+
emoji: "\u{1F4E1}"
|
|
750
|
+
});
|
|
751
|
+
if (subagentSpawns >= 10 && pct < 0.5)
|
|
752
|
+
achievements.push({
|
|
753
|
+
key: "army",
|
|
754
|
+
label: `Army of One \u2014 ${subagentSpawns} subagents, under 50% par`,
|
|
755
|
+
emoji: "\u{1FA96}"
|
|
756
|
+
});
|
|
757
|
+
if (promptCount >= 2 && turnCount >= 1 && turnCount / promptCount >= 3)
|
|
758
|
+
achievements.push({
|
|
759
|
+
key: "agentic",
|
|
760
|
+
label: `Agentic \u2014 ${(turnCount / promptCount).toFixed(1)} turns per prompt`,
|
|
761
|
+
emoji: "\u{1F916}"
|
|
762
|
+
});
|
|
763
|
+
if (promptCount >= 3 && turnCount === promptCount)
|
|
764
|
+
achievements.push({ key: "obedient", label: "Obedient \u2014 One turn per prompt", emoji: "\u{1F415}" });
|
|
765
|
+
return achievements;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/lib/config.js
|
|
769
|
+
import fs3 from "fs";
|
|
770
|
+
import path3 from "path";
|
|
771
|
+
var CONFIG_FILE = path3.join(STATE_DIR, "config.json");
|
|
772
|
+
function getConfig() {
|
|
773
|
+
try {
|
|
774
|
+
return JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf8"));
|
|
775
|
+
} catch {
|
|
776
|
+
return { emotionMode: "emoji" };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function getEffectiveParRates() {
|
|
780
|
+
const config = getConfig();
|
|
781
|
+
return { ...MODEL_PAR_RATES, ...config.parRates || {} };
|
|
782
|
+
}
|
|
783
|
+
function getEffectiveParFloors() {
|
|
784
|
+
const config = getConfig();
|
|
785
|
+
return { ...MODEL_PAR_FLOORS, ...config.parFloors || {} };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/lib/store.js
|
|
789
|
+
var RUNS_FILE = path4.join(STATE_DIR, "runs.json");
|
|
790
|
+
function ensureDir2() {
|
|
791
|
+
if (!fs4.existsSync(STATE_DIR)) fs4.mkdirSync(STATE_DIR, { recursive: true });
|
|
792
|
+
}
|
|
793
|
+
function readRuns() {
|
|
794
|
+
try {
|
|
795
|
+
return JSON.parse(fs4.readFileSync(RUNS_FILE, "utf8"));
|
|
796
|
+
} catch {
|
|
797
|
+
return [];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function writeRuns(runs) {
|
|
801
|
+
ensureDir2();
|
|
802
|
+
fs4.writeFileSync(RUNS_FILE, JSON.stringify(runs, null, 2));
|
|
803
|
+
}
|
|
804
|
+
function saveRun(run) {
|
|
805
|
+
const runs = readRuns();
|
|
806
|
+
const achievements = calculateAchievements(run, getEffectiveParRates(), getEffectiveParFloors());
|
|
807
|
+
const record = {
|
|
808
|
+
id: `run_${Date.now()}`,
|
|
809
|
+
...run,
|
|
810
|
+
achievements,
|
|
811
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
812
|
+
};
|
|
813
|
+
runs.push(record);
|
|
814
|
+
writeRuns(runs);
|
|
815
|
+
return record;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/lib/ansi-scorecard.js
|
|
819
|
+
var R = "\x1B[31m";
|
|
820
|
+
var G = "\x1B[32m";
|
|
821
|
+
var Y = "\x1B[33m";
|
|
822
|
+
var C = "\x1B[36m";
|
|
823
|
+
var M = "\x1B[35m";
|
|
824
|
+
var WH = "\x1B[37m";
|
|
825
|
+
var DIM = "\x1B[2m";
|
|
826
|
+
var RESET = "\x1B[0m";
|
|
827
|
+
var BOLD = "\x1B[1m";
|
|
828
|
+
function termWidth(str) {
|
|
829
|
+
const plain = str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
830
|
+
const cps = [...plain].map((c) => c.codePointAt(0));
|
|
831
|
+
let width = 0;
|
|
832
|
+
for (let i = 0; i < cps.length; i++) {
|
|
833
|
+
const cp = cps[i];
|
|
834
|
+
if (cp === 65039) {
|
|
835
|
+
if (i > 0 && cps[i - 1] <= 65535 && cps[i - 1] !== 8205) width += 1;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (cp === 65038 || cp === 8205 || cp >= 8203 && cp <= 8207) continue;
|
|
839
|
+
if (cp > 65535) {
|
|
840
|
+
width += 2;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
width += 1;
|
|
844
|
+
}
|
|
845
|
+
return width;
|
|
846
|
+
}
|
|
847
|
+
function renderScorecard(run) {
|
|
848
|
+
const W = Math.min(Math.max((process.stdout.columns || 88) - 8, 40), 80);
|
|
849
|
+
const won = run.status === "won";
|
|
850
|
+
const effBudget = getParBudget(
|
|
851
|
+
run.model,
|
|
852
|
+
run.promptCount,
|
|
853
|
+
getEffectiveParRates(),
|
|
854
|
+
getEffectiveParFloors()
|
|
855
|
+
);
|
|
856
|
+
const bc = won ? Y : R;
|
|
857
|
+
const BLK = "\u2588\u2588";
|
|
858
|
+
function row(content) {
|
|
859
|
+
return bc + BLK + RESET + " " + content;
|
|
860
|
+
}
|
|
861
|
+
function bar() {
|
|
862
|
+
return bc + BLK + RESET + " " + DIM + "\u2500".repeat(W) + RESET;
|
|
863
|
+
}
|
|
864
|
+
const mc = getModelClass(run.model);
|
|
865
|
+
const tier = getTier(run.spent, run.model);
|
|
866
|
+
const fainted = run.fainted;
|
|
867
|
+
const sessions = run.sessionCount || 1;
|
|
868
|
+
const header = won ? `${BOLD}${Y}\u{1F3C6} SESSION COMPLETE${RESET}` : fainted ? `${BOLD}${Y}\u{1F4A4} FAINTED \u2014 Run Continues${RESET}` : `${BOLD}${R}\u{1F480} PAR BUST${RESET}`;
|
|
869
|
+
const questStr = `${DIM}${run.promptCount || 0} prompts \xB7 par $${effBudget.toFixed(2)}${RESET}`;
|
|
870
|
+
const spentBefore = run.spentBeforeThisSession || 0;
|
|
871
|
+
const spentThisSession = run.spent - spentBefore;
|
|
872
|
+
const multiSession = sessions > 1 && spentBefore > 0;
|
|
873
|
+
const spentStr = `${won ? G : R}${formatCost(run.spent)}${RESET}` + (multiSession ? ` ${DIM}(+${formatCost(spentThisSession)} this session)${RESET}` : "");
|
|
874
|
+
let midRow = spentStr;
|
|
875
|
+
{
|
|
876
|
+
const pct = getBudgetPct(run.spent, effBudget);
|
|
877
|
+
const eff = getEfficiencyRating(run.spent, effBudget);
|
|
878
|
+
const effC = eff.color === "yellow" ? Y : eff.color === "magenta" ? M : eff.color === "cyan" ? C : eff.color === "green" ? G : eff.color === "white" ? WH : R;
|
|
879
|
+
midRow += ` ${DIM}/${RESET}$${effBudget.toFixed(2)} ${pct}% ${effC}${eff.emoji} ${eff.label}${RESET}`;
|
|
880
|
+
}
|
|
881
|
+
const effortInfo = run.effort ? getEffortLevel(run.effort) : null;
|
|
882
|
+
const modelSuffix = [
|
|
883
|
+
run.effort && effortInfo ? effortInfo.label : null,
|
|
884
|
+
run.fastMode ? "\u26A1Fast" : null
|
|
885
|
+
].filter(Boolean).join("\xB7");
|
|
886
|
+
midRow += ` ${C}${mc.emoji} ${mc.name}${modelSuffix ? "\xB7" + modelSuffix : ""}${RESET}`;
|
|
887
|
+
midRow += ` ${tier.emoji} ${tier.label}`;
|
|
888
|
+
if (multiSession) midRow += ` ${DIM}${sessions} sessions${RESET}`;
|
|
889
|
+
const achievements = run.achievements || [];
|
|
890
|
+
const achTokens = achievements.map((a) => `${a.emoji} ${a.label || a.key}`);
|
|
891
|
+
const achLines = [];
|
|
892
|
+
let currentLine = "";
|
|
893
|
+
for (const token of achTokens) {
|
|
894
|
+
const sep = currentLine ? " " : "";
|
|
895
|
+
const testLen = termWidth(currentLine + sep + token);
|
|
896
|
+
if (currentLine && testLen > W) {
|
|
897
|
+
achLines.push(currentLine);
|
|
898
|
+
currentLine = token;
|
|
899
|
+
} else {
|
|
900
|
+
currentLine += sep + token;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (currentLine) achLines.push(currentLine);
|
|
904
|
+
const ti = run.thinkingInvocations || 0;
|
|
905
|
+
const thinkRow = ti > 0 ? `${M}\u{1F52E} ${ti} ultrathink${ti > 1 ? " invocations" : " invocation"}${RESET}` : null;
|
|
906
|
+
const lines = ["", row(header), row(questStr), bar(), row(midRow)];
|
|
907
|
+
if (thinkRow) {
|
|
908
|
+
lines.push(bar());
|
|
909
|
+
lines.push(row(thinkRow));
|
|
910
|
+
}
|
|
911
|
+
if (achLines.length > 0) {
|
|
912
|
+
lines.push(bar());
|
|
913
|
+
for (const line of achLines) {
|
|
914
|
+
lines.push(row(line));
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
lines.push(bar());
|
|
918
|
+
lines.push(row(`${DIM}tokengolf scorecard${RESET} \xB7 ${DIM}tokengolf stats${RESET}`));
|
|
919
|
+
lines.push("");
|
|
920
|
+
return lines.join("\n");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// hooks/session-end.js
|
|
13
924
|
function writeTTY(text) {
|
|
14
925
|
try {
|
|
15
|
-
const ttyFd =
|
|
16
|
-
|
|
17
|
-
|
|
926
|
+
const ttyFd = fs5.openSync("/dev/tty", "w");
|
|
927
|
+
fs5.writeSync(ttyFd, text);
|
|
928
|
+
fs5.closeSync(ttyFd);
|
|
18
929
|
} catch {
|
|
19
930
|
process.stdout.write(text);
|
|
20
931
|
}
|
|
@@ -22,7 +933,7 @@ function writeTTY(text) {
|
|
|
22
933
|
try {
|
|
23
934
|
let stdin = "";
|
|
24
935
|
try {
|
|
25
|
-
stdin =
|
|
936
|
+
stdin = fs5.readFileSync("/dev/stdin", "utf8");
|
|
26
937
|
} catch {
|
|
27
938
|
}
|
|
28
939
|
let event = {};
|
|
@@ -33,7 +944,7 @@ try {
|
|
|
33
944
|
const reason = event.reason || "other";
|
|
34
945
|
let liveCost = null;
|
|
35
946
|
try {
|
|
36
|
-
const raw =
|
|
947
|
+
const raw = fs5.readFileSync(path5.join(os3.homedir(), ".tokengolf", "session-cost"), "utf8").trim();
|
|
37
948
|
const parsed = parseFloat(raw);
|
|
38
949
|
if (!isNaN(parsed) && parsed > 0) liveCost = parsed;
|
|
39
950
|
} catch {
|
|
@@ -68,9 +979,7 @@ try {
|
|
|
68
979
|
}
|
|
69
980
|
const cleanExits = ["clear", "logout", "prompt_input_exit", "bypass_permissions_disabled"];
|
|
70
981
|
const fainted = !cleanExits.includes(reason) && reason !== "other" ? false : reason === "other";
|
|
71
|
-
const
|
|
72
|
-
const { getEffectiveParRates, getEffectiveParFloors } = await import(path.join(__dir, "../src/lib/config.js"));
|
|
73
|
-
const par = gp(run.model, run.promptCount, getEffectiveParRates(), getEffectiveParFloors());
|
|
982
|
+
const par = getParBudget(run.model, run.promptCount, getEffectiveParRates(), getEffectiveParFloors());
|
|
74
983
|
let status;
|
|
75
984
|
if (result.spent > par) status = "died";
|
|
76
985
|
else if (fainted)
|
|
@@ -81,7 +990,6 @@ try {
|
|
|
81
990
|
thinkingTokens: result.thinkingTokens ?? 0
|
|
82
991
|
};
|
|
83
992
|
if (status === "resting") {
|
|
84
|
-
const { setCurrentRun } = await import(path.join(__dir, "../src/lib/state.js"));
|
|
85
993
|
setCurrentRun({ ...run, spent: result.spent, fainted: true, ...thinkingFields });
|
|
86
994
|
const saved2 = {
|
|
87
995
|
...run,
|
|
@@ -104,7 +1012,7 @@ try {
|
|
|
104
1012
|
});
|
|
105
1013
|
clearCurrentRun();
|
|
106
1014
|
try {
|
|
107
|
-
|
|
1015
|
+
fs5.unlinkSync(path5.join(os3.homedir(), ".tokengolf", "session-cost"));
|
|
108
1016
|
} catch {
|
|
109
1017
|
}
|
|
110
1018
|
writeTTY("\n" + renderScorecard(saved) + "\n\n");
|
|
@@ -56,6 +56,23 @@ try {
|
|
|
56
56
|
}
|
|
57
57
|
} catch {}
|
|
58
58
|
|
|
59
|
+
// Auto-install statusLine if missing or stale
|
|
60
|
+
try {
|
|
61
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
62
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
63
|
+
const scriptDir = path.dirname(fs.realpathSync(process.argv[1]));
|
|
64
|
+
const statuslinePath = path.join(scriptDir, 'statusline.sh');
|
|
65
|
+
if (fs.existsSync(statuslinePath)) {
|
|
66
|
+
const current = settings.statusLine?.command || '';
|
|
67
|
+
const needsInstall = !current || current.includes('tokengolf');
|
|
68
|
+
const needsUpdate = needsInstall && current !== statuslinePath;
|
|
69
|
+
if (needsUpdate) {
|
|
70
|
+
settings.statusLine = { type: 'command', command: statuslinePath, padding: 1 };
|
|
71
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
|
|
59
76
|
function detectEffort() {
|
|
60
77
|
const fromEnv = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
|
61
78
|
if (fromEnv) return fromEnv;
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
+
import { Readable } from 'stream';
|
|
4
5
|
import React from 'react';
|
|
5
6
|
|
|
6
7
|
import { getLastRun, getStats } from './lib/store.js';
|
|
@@ -17,6 +18,19 @@ import { MODEL_PAR_RATES, MODEL_PAR_FLOORS } from './lib/score.js';
|
|
|
17
18
|
import { ScoreCard } from './components/ScoreCard.js';
|
|
18
19
|
import { StatsView } from './components/StatsView.js';
|
|
19
20
|
|
|
21
|
+
// Use a fake stdin when raw mode is not supported (e.g., running inside Claude Code)
|
|
22
|
+
function safeRender(element) {
|
|
23
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
24
|
+
return render(element);
|
|
25
|
+
}
|
|
26
|
+
const s = new Readable({ read() {} });
|
|
27
|
+
s.setRawMode = () => s;
|
|
28
|
+
s.ref = () => s;
|
|
29
|
+
s.unref = () => s;
|
|
30
|
+
s.isTTY = true;
|
|
31
|
+
return render(element, { stdin: s });
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
program.name('tokengolf').description('⛳ Gamify your Claude Code sessions').version('0.5.4');
|
|
21
35
|
|
|
22
36
|
program
|
|
@@ -28,14 +42,14 @@ program
|
|
|
28
42
|
console.log('No runs yet. Open Claude Code — sessions are tracked automatically.');
|
|
29
43
|
process.exit(0);
|
|
30
44
|
}
|
|
31
|
-
|
|
45
|
+
safeRender(React.createElement(ScoreCard, { run }));
|
|
32
46
|
});
|
|
33
47
|
|
|
34
48
|
program
|
|
35
49
|
.command('stats')
|
|
36
50
|
.description('Show career stats dashboard')
|
|
37
51
|
.action(() => {
|
|
38
|
-
|
|
52
|
+
safeRender(React.createElement(StatsView, { stats: getStats() }));
|
|
39
53
|
});
|
|
40
54
|
|
|
41
55
|
program
|