tokengolf 0.3.0 → 0.4.0

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.
@@ -6,30 +6,36 @@ import { getModelClass, getEffortLevel, getModelBudgets, FLOORS } from '../lib/s
6
6
 
7
7
  const MODEL_OPTIONS = [
8
8
  { label: '⚔️ Sonnet — Balanced. The default run. [Normal]', value: 'claude-sonnet-4-6' },
9
- { label: '🏹 Haiku — Glass cannon. Hard mode. [Hard]', value: 'claude-haiku-4-5-20251001' },
10
- { label: '🧙 Opus Powerful but expensive. [Easy]', value: 'claude-opus-4-6' },
9
+ { label: '🏹 Haiku — Glass cannon. Hard mode. [Hard]', value: 'claude-haiku-4-5-20251001' },
10
+ { label: '⚜️ PaladinOpus plans, Sonnet executes. [Calculated]', value: 'opusplan' },
11
+ { label: '🧙 Opus — Powerful but expensive. [Easy]', value: 'claude-opus-4-6' },
11
12
  ];
12
13
 
13
14
  const EFFORT_OPTIONS_BASE = [
14
15
  { label: '⚖️ Medium — Balanced (Anthropic recommended for Sonnet)', value: 'medium' },
15
- { label: '🪶 Low — Fewest tokens, fastest, cheapest', value: 'low' },
16
- { label: '🔥 High — Most thorough, costs more', value: 'high' },
16
+ { label: '🪶 Low — Fewest tokens, fastest, cheapest', value: 'low' },
17
+ { label: '🔥 High — Most thorough, costs more', value: 'high' },
17
18
  ];
18
19
  const EFFORT_OPTIONS_OPUS = [
19
20
  ...EFFORT_OPTIONS_BASE,
20
- { label: '💥 Max — Absolute max, no token constraints (Opus only)', value: 'max' },
21
+ { label: '💥 Max — Absolute max, no token constraints (Opus only)', value: 'max' },
21
22
  ];
22
23
  const getEffortOptions = (model) =>
23
- model.toLowerCase().includes('opus') ? EFFORT_OPTIONS_OPUS : EFFORT_OPTIONS_BASE;
24
+ model.toLowerCase().includes('opus') && !model.toLowerCase().includes('opusplan')
25
+ ? EFFORT_OPTIONS_OPUS
26
+ : EFFORT_OPTIONS_BASE;
24
27
 
25
28
  function getBudgetOptions(model) {
26
29
  const b = getModelBudgets(model);
27
30
  return [
28
- { label: `💎 Diamond — $${b.diamond.toFixed(2)} surgical micro-task`, value: String(b.diamond) },
29
- { label: `🥇 Gold — $${b.gold.toFixed(2)} focused small task`, value: String(b.gold) },
30
- { label: `🥈 Silver — $${b.silver.toFixed(2)} medium task`, value: String(b.silver) },
31
- { label: `🥉 Bronze — $${b.bronze.toFixed(2)} heavy / complex`, value: String(b.bronze) },
32
- { label: `✏️ Custom set your own`, value: 'custom' },
31
+ {
32
+ label: `💎 Diamond — $${b.diamond.toFixed(2)} surgical micro-task`,
33
+ value: String(b.diamond),
34
+ },
35
+ { label: `🥇 Gold $${b.gold.toFixed(2)} focused small task`, value: String(b.gold) },
36
+ { label: `🥈 Silver — $${b.silver.toFixed(2)} medium task`, value: String(b.silver) },
37
+ { label: `🥉 Bronze — $${b.bronze.toFixed(2)} heavy / complex`, value: String(b.bronze) },
38
+ { label: `✏️ Custom — set your own`, value: 'custom' },
33
39
  ];
34
40
  }
35
41
 
@@ -47,66 +53,121 @@ export function StartRun() {
47
53
  return (
48
54
  <Box flexDirection="column" gap={1} paddingX={1} paddingY={1}>
49
55
  <Box gap={2}>
50
- <Text bold color="yellow">⛳ TokenGolf</Text>
56
+ <Text bold color="yellow">
57
+ ⛳ TokenGolf
58
+ </Text>
51
59
  <Text color="gray">New Run</Text>
52
60
  </Box>
53
61
 
54
- <Box flexDirection="column" gap={1} borderStyle="single" borderColor="gray" paddingX={1} paddingY={1}>
55
-
62
+ <Box
63
+ flexDirection="column"
64
+ gap={1}
65
+ borderStyle="single"
66
+ borderColor="gray"
67
+ paddingX={1}
68
+ paddingY={1}
69
+ >
56
70
  {/* Quest */}
57
71
  <Box gap={2} alignItems="flex-start">
58
- <Text color={step === 'quest' ? 'cyan' : 'gray'}>📋 Quest </Text>
59
- {step === 'quest'
60
- ? <TextInput placeholder="What are you shipping?" onSubmit={v => { if (v.trim()) { setQuest(v.trim()); setStep('model'); } }} />
61
- : <Text color="white">{quest}</Text>}
72
+ <Text color={step === 'quest' ? 'cyan' : 'gray'}>📋 Quest </Text>
73
+ {step === 'quest' ? (
74
+ <TextInput
75
+ placeholder="What are you shipping?"
76
+ onSubmit={(v) => {
77
+ if (v.trim()) {
78
+ setQuest(v.trim());
79
+ setStep('model');
80
+ }
81
+ }}
82
+ />
83
+ ) : (
84
+ <Text color="white">{quest}</Text>
85
+ )}
62
86
  </Box>
63
87
 
64
88
  {/* Model */}
65
89
  {step !== 'quest' && (
66
90
  <Box flexDirection="column" gap={0}>
67
91
  <Box gap={2}>
68
- <Text color={step === 'model' ? 'cyan' : 'gray'}>🎮 Class </Text>
69
- {step !== 'model' && <Text color="white">{mc?.emoji} {mc?.name} [{mc?.difficulty}]</Text>}
92
+ <Text color={step === 'model' ? 'cyan' : 'gray'}>🎮 Class </Text>
93
+ {step !== 'model' && (
94
+ <Text color="white">
95
+ {mc?.emoji} {mc?.name} [{mc?.difficulty}]
96
+ </Text>
97
+ )}
70
98
  </Box>
71
- {step === 'model' && <Select options={MODEL_OPTIONS} onChange={v => {
72
- setModel(v);
73
- if (v.toLowerCase().includes('haiku')) { setEffort(null); setStep('budget'); }
74
- else setStep('effort');
75
- }} />}
99
+ {step === 'model' && (
100
+ <Select
101
+ options={MODEL_OPTIONS}
102
+ onChange={(v) => {
103
+ setModel(v);
104
+ if (v.toLowerCase().includes('haiku')) {
105
+ setEffort(null);
106
+ setStep('budget');
107
+ } else setStep('effort');
108
+ }}
109
+ />
110
+ )}
76
111
  </Box>
77
112
  )}
78
113
 
79
114
  {/* Effort */}
80
- {(step === 'effort' || step === 'budget' || step === 'custom' || step === 'confirm') && effort !== null && (
81
- <Box flexDirection="column" gap={0}>
82
- <Box gap={2}>
83
- <Text color={step === 'effort' ? 'cyan' : 'gray'}>⚡ Effort </Text>
84
- {step !== 'effort' && effort && <Text color="white">{getEffortLevel(effort)?.emoji} {getEffortLevel(effort)?.label}</Text>}
115
+ {(step === 'effort' || step === 'budget' || step === 'custom' || step === 'confirm') &&
116
+ effort !== null && (
117
+ <Box flexDirection="column" gap={0}>
118
+ <Box gap={2}>
119
+ <Text color={step === 'effort' ? 'cyan' : 'gray'}>⚡ Effort </Text>
120
+ {step !== 'effort' && effort && (
121
+ <Text color="white">
122
+ {getEffortLevel(effort)?.emoji} {getEffortLevel(effort)?.label}
123
+ </Text>
124
+ )}
125
+ </Box>
126
+ {step === 'effort' && (
127
+ <Select
128
+ options={getEffortOptions(model)}
129
+ onChange={(v) => {
130
+ setEffort(v);
131
+ setStep('budget');
132
+ }}
133
+ />
134
+ )}
85
135
  </Box>
86
- {step === 'effort' && (
87
- <Select options={getEffortOptions(model)} onChange={v => { setEffort(v); setStep('budget'); }} />
88
- )}
89
- </Box>
90
- )}
136
+ )}
91
137
 
92
138
  {/* Budget */}
93
139
  {(step === 'budget' || step === 'custom' || step === 'confirm') && (
94
140
  <Box flexDirection="column" gap={0}>
95
141
  <Box gap={2}>
96
- <Text color={step === 'budget' || step === 'custom' ? 'cyan' : 'gray'}>💰 Budget </Text>
142
+ <Text color={step === 'budget' || step === 'custom' ? 'cyan' : 'gray'}>
143
+ 💰 Budget{' '}
144
+ </Text>
97
145
  {step === 'confirm' && <Text color="green">${budget.toFixed(2)}</Text>}
98
146
  </Box>
99
147
  {step === 'budget' && (
100
- <Select options={getBudgetOptions(model)} onChange={v => {
101
- if (v === 'custom') { setStep('custom'); }
102
- else { setBudgetVal(v); setStep('confirm'); }
103
- }} />
148
+ <Select
149
+ options={getBudgetOptions(model)}
150
+ onChange={(v) => {
151
+ if (v === 'custom') {
152
+ setStep('custom');
153
+ } else {
154
+ setBudgetVal(v);
155
+ setStep('confirm');
156
+ }
157
+ }}
158
+ />
104
159
  )}
105
160
  {step === 'custom' && (
106
- <TextInput placeholder="Enter amount e.g. 0.50" onSubmit={v => {
107
- const n = parseFloat(v);
108
- if (!isNaN(n) && n > 0) { setBudgetVal(String(n)); setStep('confirm'); }
109
- }} />
161
+ <TextInput
162
+ placeholder="Enter amount e.g. 0.50"
163
+ onSubmit={(v) => {
164
+ const n = parseFloat(v);
165
+ if (!isNaN(n) && n > 0) {
166
+ setBudgetVal(String(n));
167
+ setStep('confirm');
168
+ }
169
+ }}
170
+ />
110
171
  )}
111
172
  </Box>
112
173
  )}
@@ -114,26 +175,63 @@ export function StartRun() {
114
175
  {/* Confirm */}
115
176
  {step === 'confirm' && (
116
177
  <Box flexDirection="column" gap={1} marginTop={1}>
117
- <Box borderStyle="round" borderColor="yellow" paddingX={1} paddingY={1} flexDirection="column">
118
- <Text bold color="yellow">Ready?</Text>
119
- <Text color="gray">Quest <Text color="white">{quest}</Text></Text>
120
- <Text color="gray">Model <Text color="white">{mc?.emoji} {mc?.name} [{mc?.difficulty}]</Text></Text>
121
- {effort && <Text color="gray">Effort <Text color="white">{getEffortLevel(effort)?.emoji} {getEffortLevel(effort)?.label}</Text></Text>}
122
- <Text color="gray">Budget <Text color="green">${budget.toFixed(2)}</Text></Text>
178
+ <Box
179
+ borderStyle="round"
180
+ borderColor="yellow"
181
+ paddingX={1}
182
+ paddingY={1}
183
+ flexDirection="column"
184
+ >
185
+ <Text bold color="yellow">
186
+ Ready?
187
+ </Text>
188
+ <Text color="gray">
189
+ Quest <Text color="white">{quest}</Text>
190
+ </Text>
191
+ <Text color="gray">
192
+ Model{' '}
193
+ <Text color="white">
194
+ {mc?.emoji} {mc?.name} [{mc?.difficulty}]
195
+ </Text>
196
+ </Text>
197
+ {effort && (
198
+ <Text color="gray">
199
+ Effort{' '}
200
+ <Text color="white">
201
+ {getEffortLevel(effort)?.emoji} {getEffortLevel(effort)?.label}
202
+ </Text>
203
+ </Text>
204
+ )}
205
+ <Text color="gray">
206
+ Budget <Text color="green">${budget.toFixed(2)}</Text>
207
+ </Text>
123
208
  </Box>
124
209
  <Box gap={1}>
125
210
  <Text color="gray">Confirm? </Text>
126
211
  <ConfirmInput
127
212
  onConfirm={() => {
128
213
  setCurrentRun({
129
- quest, model, budget, effort,
214
+ id: `run_${Date.now()}`,
215
+ quest,
216
+ model,
217
+ budget,
218
+ effort,
130
219
  spent: 0,
131
220
  status: 'active',
221
+ mode: 'roguelike',
132
222
  floor: 1,
133
223
  totalFloors: FLOORS.length,
134
224
  promptCount: 0,
135
225
  totalToolCalls: 0,
136
226
  toolCalls: {},
227
+ failedToolCalls: 0,
228
+ subagentSpawns: 0,
229
+ turnCount: 0,
230
+ thinkingInvocations: 0,
231
+ thinkingTokens: 0,
232
+ fainted: false,
233
+ sessionCount: 1,
234
+ compactionEvents: [],
137
235
  startedAt: new Date().toISOString(),
138
236
  });
139
237
  exit();
@@ -146,10 +244,15 @@ export function StartRun() {
146
244
  </Box>
147
245
 
148
246
  {step !== 'confirm' && (
149
- <Text color="gray" dimColor>Use ↑↓ to navigate, Enter to select</Text>
247
+ <Text color="gray" dimColor>
248
+ Use ↑↓ to navigate, Enter to select
249
+ </Text>
150
250
  )}
151
251
  {step === 'confirm' && (
152
- <Text color="gray" dimColor>After confirming, work normally in Claude Code. Run `tokengolf win` or `tokengolf bust` when done.</Text>
252
+ <Text color="gray" dimColor>
253
+ After confirming, work normally in Claude Code. Run `tokengolf win` or `tokengolf bust`
254
+ when done.
255
+ </Text>
153
256
  )}
154
257
  </Box>
155
258
  );
@@ -5,14 +5,20 @@ import { getTier, getModelClass, getBudgetPct, formatCost } from '../lib/score.j
5
5
  export function StatsView({ stats }) {
6
6
  const { exit } = useApp();
7
7
 
8
- useInput((input) => { if (input === 'q') exit(); });
8
+ useInput((input) => {
9
+ if (input === 'q') exit();
10
+ });
9
11
 
10
12
  if (stats.total === 0) {
11
13
  return (
12
14
  <Box paddingX={1} paddingY={1} flexDirection="column" gap={1}>
13
- <Text bold color="yellow">⛳ TokenGolf Stats</Text>
15
+ <Text bold color="yellow">
16
+ ⛳ TokenGolf Stats
17
+ </Text>
14
18
  <Text color="gray">No completed runs yet.</Text>
15
- <Text color="gray">Start one: <Text color="cyan">tokengolf start</Text></Text>
19
+ <Text color="gray">
20
+ Start one: <Text color="cyan">tokengolf start</Text>
21
+ </Text>
16
22
  </Box>
17
23
  );
18
24
  }
@@ -20,73 +26,110 @@ export function StatsView({ stats }) {
20
26
  return (
21
27
  <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
22
28
  <Box gap={2}>
23
- <Text bold color="yellow">⛳ TokenGolf</Text>
29
+ <Text bold color="yellow">
30
+ ⛳ TokenGolf
31
+ </Text>
24
32
  <Text color="gray">Career Stats</Text>
25
33
  </Box>
26
34
 
27
35
  {/* Top line */}
28
36
  <Box borderStyle="single" borderColor="gray" paddingX={1} paddingY={1} gap={4}>
29
37
  <Box flexDirection="column">
30
- <Text color="gray" dimColor>RUNS</Text>
31
- <Text bold color="white">{stats.total}</Text>
38
+ <Text color="gray" dimColor>
39
+ RUNS
40
+ </Text>
41
+ <Text bold color="white">
42
+ {stats.total}
43
+ </Text>
32
44
  </Box>
33
45
  <Box flexDirection="column">
34
- <Text color="gray" dimColor>WINS</Text>
35
- <Text bold color="green">{stats.wins}</Text>
46
+ <Text color="gray" dimColor>
47
+ WINS
48
+ </Text>
49
+ <Text bold color="green">
50
+ {stats.wins}
51
+ </Text>
36
52
  </Box>
37
53
  <Box flexDirection="column">
38
- <Text color="gray" dimColor>DEATHS</Text>
39
- <Text bold color="red">{stats.deaths}</Text>
54
+ <Text color="gray" dimColor>
55
+ DEATHS
56
+ </Text>
57
+ <Text bold color="red">
58
+ {stats.deaths}
59
+ </Text>
40
60
  </Box>
41
61
  <Box flexDirection="column">
42
- <Text color="gray" dimColor>WIN RATE</Text>
62
+ <Text color="gray" dimColor>
63
+ WIN RATE
64
+ </Text>
43
65
  <Text bold color={stats.winRate >= 70 ? 'green' : stats.winRate >= 40 ? 'yellow' : 'red'}>
44
66
  {stats.winRate}%
45
67
  </Text>
46
68
  </Box>
47
69
  <Box flexDirection="column">
48
- <Text color="gray" dimColor>AVG SPEND</Text>
49
- <Text bold color="cyan">{formatCost(stats.avgSpend)}</Text>
70
+ <Text color="gray" dimColor>
71
+ AVG SPEND
72
+ </Text>
73
+ <Text bold color="cyan">
74
+ {formatCost(stats.avgSpend)}
75
+ </Text>
50
76
  </Box>
51
77
  </Box>
52
78
 
53
79
  {/* Personal best */}
54
- {stats.bestRun && (() => {
55
- const bestTier = getTier(stats.bestRun.spent);
56
- const bestMc = getModelClass(stats.bestRun.model);
57
- return (
58
- <Box flexDirection="column" gap={0}>
59
- <Text color="yellow">🏆 Personal Best</Text>
60
- <Box borderStyle="round" borderColor="yellow" paddingX={1} paddingY={1} flexDirection="column">
61
- <Text color="white">{stats.bestRun.quest}</Text>
62
- <Box gap={3} marginTop={1}>
63
- <Text color="green">{formatCost(stats.bestRun.spent)}</Text>
64
- <Text color="gray">of ${stats.bestRun.budget?.toFixed(2)}</Text>
65
- <Text>{bestMc.emoji}</Text>
66
- <Text color={bestTier.color}>{bestTier.emoji} {bestTier.label}</Text>
80
+ {stats.bestRun &&
81
+ (() => {
82
+ const bestTier = getTier(stats.bestRun.spent);
83
+ const bestMc = getModelClass(stats.bestRun.model);
84
+ return (
85
+ <Box flexDirection="column" gap={0}>
86
+ <Text color="yellow">🏆 Personal Best</Text>
87
+ <Box
88
+ borderStyle="round"
89
+ borderColor="yellow"
90
+ paddingX={1}
91
+ paddingY={1}
92
+ flexDirection="column"
93
+ >
94
+ <Text color="white">{stats.bestRun.quest}</Text>
95
+ <Box gap={3} marginTop={1}>
96
+ <Text color="green">{formatCost(stats.bestRun.spent)}</Text>
97
+ {stats.bestRun.budget ? (
98
+ <Text color="gray">of ${stats.bestRun.budget.toFixed(2)}</Text>
99
+ ) : null}
100
+ <Text>{bestMc.emoji}</Text>
101
+ <Text color={bestTier.color}>
102
+ {bestTier.emoji} {bestTier.label}
103
+ </Text>
104
+ </Box>
67
105
  </Box>
68
106
  </Box>
69
- </Box>
70
- );
71
- })()}
107
+ );
108
+ })()}
72
109
 
73
110
  {/* Recent runs */}
74
111
  <Box flexDirection="column" gap={0}>
75
- <Text color="gray" dimColor>Recent runs:</Text>
112
+ <Text color="gray" dimColor>
113
+ Recent runs:
114
+ </Text>
76
115
  {stats.recentRuns.slice(0, 8).map((run, i) => {
77
116
  const won = run.status === 'won';
78
117
  const tier = getTier(run.spent);
79
118
  const mc = getModelClass(run.model);
80
- const pct = getBudgetPct(run.spent, run.budget);
119
+ const pct = run.budget ? getBudgetPct(run.spent, run.budget) : null;
81
120
  return (
82
121
  <Box key={i} gap={2}>
83
122
  <Text color={won ? 'green' : 'red'}>{won ? '✓' : '✗'}</Text>
84
- <Text color="white">{(run.quest || '').slice(0, 34).padEnd(34)}</Text>
123
+ <Text color="white">{(run.quest || 'Flow').slice(0, 34).padEnd(34)}</Text>
85
124
  <Text color={won ? 'green' : 'red'}>{formatCost(run.spent)}</Text>
86
- <Text color="gray">/{formatCost(run.budget)}</Text>
125
+ {run.budget ? <Text color="gray">/{formatCost(run.budget)}</Text> : null}
87
126
  <Text>{mc.emoji}</Text>
88
127
  <Text color={tier.color}>{tier.emoji}</Text>
89
- <Text color="gray" dimColor>{pct}%</Text>
128
+ {pct !== null && (
129
+ <Text color="gray" dimColor>
130
+ {pct}%
131
+ </Text>
132
+ )}
90
133
  </Box>
91
134
  );
92
135
  })}
@@ -95,18 +138,27 @@ export function StatsView({ stats }) {
95
138
  {/* Achievements */}
96
139
  {stats.achievements.length > 0 && (
97
140
  <Box flexDirection="column" gap={0}>
98
- <Text color="gray" dimColor>Recent achievements:</Text>
141
+ <Text color="gray" dimColor>
142
+ Recent achievements:
143
+ </Text>
99
144
  <Box flexWrap="wrap" gap={1}>
100
145
  {stats.achievements.slice(0, 12).map((a, i) => (
101
146
  <Box key={i} borderStyle="single" borderColor="gray" paddingX={1}>
102
- <Text>{a.emoji} <Text color="gray" dimColor>{a.label}</Text></Text>
147
+ <Text>
148
+ {a.emoji}{' '}
149
+ <Text color="gray" dimColor>
150
+ {a.label}
151
+ </Text>
152
+ </Text>
103
153
  </Box>
104
154
  ))}
105
155
  </Box>
106
156
  </Box>
107
157
  )}
108
158
 
109
- <Text color="gray" dimColor>q to exit</Text>
159
+ <Text color="gray" dimColor>
160
+ q to exit
161
+ </Text>
110
162
  </Box>
111
163
  );
112
164
  }