tokengolf 0.4.2 → 0.5.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.
@@ -23,10 +23,10 @@ function writeTTY(text) {
23
23
  }
24
24
 
25
25
  function termWidth(str) {
26
- // Compute display width of a string, handling emoji variation selectors and surrogates.
26
+ // Compute display width of a string for achievement line wrapping.
27
+ // Handles emoji variation selectors and surrogates:
27
28
  // - Supplementary plane chars (> U+FFFF) → 2 cols
28
- // - U+FE0F (emoji variation selector after BMP char) → upgrades prev from 1→2, adds 0 itself
29
- // - U+FE0F after supplementary → 0 (already 2)
29
+ // - U+FE0F (emoji variation selector after BMP char) → upgrades prev from 1→2
30
30
  // - U+FE0E, ZWJ, zero-width chars → 0
31
31
  // - Everything else → 1
32
32
  /* eslint-disable no-control-regex */
@@ -52,7 +52,7 @@ function termWidth(str) {
52
52
  }
53
53
 
54
54
  function renderScorecard(run) {
55
- const W = Math.min(Math.max((process.stdout.columns || 88) - 4, 72), 120);
55
+ const W = Math.min(Math.max((process.stdout.columns || 88) - 8, 40), 80);
56
56
  const won = run.status === 'won';
57
57
  const flowMode = !run.budget;
58
58
 
@@ -65,28 +65,13 @@ function renderScorecard(run) {
65
65
  RESET = '\x1b[0m',
66
66
  BOLD = '\x1b[1m';
67
67
  const bc = won ? Y : R;
68
+ const BLK = '██';
68
69
 
69
- const tl = '╔',
70
- tr = '╗',
71
- bl = '╚',
72
- br = '╝';
73
- const h = '═',
74
- v = '║';
75
- const ml = '╠',
76
- mr = '╣';
77
-
78
- function bar() {
79
- return bc + ml + h.repeat(W) + mr + RESET;
80
- }
81
- function top() {
82
- return bc + tl + h.repeat(W) + tr + RESET;
83
- }
84
- function bot() {
85
- return bc + bl + h.repeat(W) + br + RESET;
86
- }
87
70
  function row(content) {
88
- const pad = Math.max(0, W - termWidth(content) - 2);
89
- return bc + v + RESET + ' ' + content + ' '.repeat(pad) + ' ' + bc + v + RESET;
71
+ return bc + BLK + RESET + ' ' + content;
72
+ }
73
+ function bar() {
74
+ return bc + BLK + RESET + ' ' + DIM + '─'.repeat(W) + RESET;
90
75
  }
91
76
 
92
77
  const mc = getModelClass(run.model);
@@ -147,7 +132,7 @@ function renderScorecard(run) {
147
132
  for (const token of achTokens) {
148
133
  const sep = currentLine ? ' ' : '';
149
134
  const testLen = termWidth(currentLine + sep + token);
150
- if (currentLine && testLen > W - 2) {
135
+ if (currentLine && testLen > W) {
151
136
  achLines.push(currentLine);
152
137
  currentLine = token;
153
138
  } else {
@@ -160,7 +145,7 @@ function renderScorecard(run) {
160
145
  const thinkRow =
161
146
  ti > 0 ? `${M}🔮 ${ti} ultrathink${ti > 1 ? ' invocations' : ' invocation'}${RESET}` : null;
162
147
 
163
- const lines = [top(), row(header), row(questStr), bar(), row(midRow)];
148
+ const lines = ['', row(header), row(questStr), bar(), row(midRow)];
164
149
 
165
150
  if (thinkRow) {
166
151
  lines.push(bar());
@@ -180,7 +165,7 @@ function renderScorecard(run) {
180
165
  `${DIM}tokengolf scorecard${RESET} · ${DIM}tokengolf start${RESET} · ${DIM}tokengolf stats${RESET}`
181
166
  )
182
167
  );
183
- lines.push(bot());
168
+ lines.push('');
184
169
 
185
170
  return lines.join('\n');
186
171
  }
package/install.sh ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # TokenGolf installer
3
+ # Usage: curl -fsSL https://raw.githubusercontent.com/josheche/tokengolf/main/install.sh | bash
4
+ set -euo pipefail
5
+
6
+ BOLD='\033[1m'
7
+ DIM='\033[2m'
8
+ GREEN='\033[32m'
9
+ YELLOW='\033[33m'
10
+ RED='\033[31m'
11
+ CYAN='\033[36m'
12
+ RESET='\033[0m'
13
+
14
+ info() { printf "${BOLD}${CYAN}▶${RESET} %s\n" "$1"; }
15
+ ok() { printf "${BOLD}${GREEN}✓${RESET} %s\n" "$1"; }
16
+ warn() { printf "${BOLD}${YELLOW}!${RESET} %s\n" "$1"; }
17
+ err() { printf "${BOLD}${RED}✗${RESET} %s\n" "$1" >&2; }
18
+
19
+ echo ""
20
+ printf "${YELLOW}██${RESET} ${BOLD}TokenGolf Installer${RESET}\n"
21
+ printf "${YELLOW}██${RESET} ${DIM}Every token matters.${RESET}\n"
22
+ echo ""
23
+
24
+ # Check Node.js
25
+ if ! command -v node &>/dev/null; then
26
+ err "Node.js is required but not installed."
27
+ echo ""
28
+ echo " Install Node.js (v18+) from:"
29
+ echo " https://nodejs.org"
30
+ echo " brew install node"
31
+ echo " curl -fsSL https://fnm.vercel.app/install | bash"
32
+ echo ""
33
+ exit 1
34
+ fi
35
+
36
+ NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
37
+ if [ "$NODE_VERSION" -lt 18 ]; then
38
+ err "Node.js v18+ required (found v$(node -v | sed 's/v//'))"
39
+ exit 1
40
+ fi
41
+
42
+ ok "Node.js $(node -v) detected"
43
+
44
+ # Check npm
45
+ if ! command -v npm &>/dev/null; then
46
+ err "npm is required but not installed."
47
+ exit 1
48
+ fi
49
+
50
+ # Install tokengolf globally
51
+ info "Installing tokengolf via npm..."
52
+ npm install -g tokengolf
53
+
54
+ ok "tokengolf installed ($(tokengolf --version 2>/dev/null || echo 'unknown version'))"
55
+
56
+ # Patch Claude Code hooks (only if Claude Code is installed)
57
+ if [ -d "$HOME/.claude" ]; then
58
+ info "Installing Claude Code hooks..."
59
+ tokengolf install
60
+ else
61
+ warn "Claude Code not detected — skipping hook setup."
62
+ printf " Run ${CYAN}tokengolf install${RESET} after installing Claude Code.\n"
63
+ fi
64
+
65
+ echo ""
66
+ printf "${YELLOW}██${RESET} ${BOLD}${GREEN}Ready to play!${RESET}\n"
67
+ printf "${YELLOW}██${RESET} ${DIM}Open Claude Code and /exit to see your first scorecard.${RESET}\n"
68
+ printf "${YELLOW}██${RESET} ${DIM}Or run: tokengolf start${RESET}\n"
69
+ echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokengolf",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Gamify your Claude Code sessions. Flow mode tracks you. Roguelike mode trains you.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,5 +36,6 @@
36
36
  },
37
37
  "engines": {
38
38
  "node": ">=18.0.0"
39
- }
39
+ },
40
+ "license": "MIT"
40
41
  }
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Updates the Homebrew formula after npm publish.
3
+ # Usage: ./scripts/update-homebrew.sh
4
+ set -euo pipefail
5
+
6
+ VERSION=$(node -p "require('./package.json').version")
7
+ TARBALL="https://registry.npmjs.org/tokengolf/-/tokengolf-${VERSION}.tgz"
8
+ TAP_DIR="${TAP_DIR:-../homebrew-tokengolf}"
9
+ FORMULA="${TAP_DIR}/Formula/tokengolf.rb"
10
+
11
+ echo "Updating Homebrew formula to v${VERSION}..."
12
+
13
+ # Fetch SHA256 of the published tarball
14
+ SHA=$(curl -sfL "$TARBALL" | shasum -a 256 | cut -d' ' -f1)
15
+ if [ -z "$SHA" ]; then
16
+ echo "Error: could not fetch tarball at ${TARBALL}" >&2
17
+ echo "Has the version been published to npm?" >&2
18
+ exit 1
19
+ fi
20
+
21
+ echo " tarball: ${TARBALL}"
22
+ echo " sha256: ${SHA}"
23
+
24
+ # Update formula (BSD sed on macOS; GNU sed on Linux omits the '')
25
+ if [[ "$OSTYPE" == "darwin"* ]]; then
26
+ sed -i '' "s|url \".*\"|url \"${TARBALL}\"|" "$FORMULA"
27
+ sed -i '' "s|sha256 \".*\"|sha256 \"${SHA}\"|" "$FORMULA"
28
+ else
29
+ sed -i "s|url \".*\"|url \"${TARBALL}\"|" "$FORMULA"
30
+ sed -i "s|sha256 \".*\"|sha256 \"${SHA}\"|" "$FORMULA"
31
+ fi
32
+
33
+ # Commit and push
34
+ cd "$TAP_DIR"
35
+ git add Formula/tokengolf.rb
36
+ git commit -m "tokengolf ${VERSION}"
37
+ git push
38
+
39
+ echo "Done — brew upgrade tokengolf will now pull v${VERSION}"
@@ -10,6 +10,7 @@ import {
10
10
  formatElapsed,
11
11
  FLOORS,
12
12
  } from '../lib/score.js';
13
+ import { ACCENT_BORDER, ACCENT_PADDING } from '../lib/ui.js';
13
14
 
14
15
  export function ActiveRun({ run: initialRun }) {
15
16
  const { exit } = useApp();
@@ -48,9 +49,12 @@ export function ActiveRun({ run: initialRun }) {
48
49
  </Box>
49
50
 
50
51
  <Box
51
- borderStyle="round"
52
+ borderStyle={ACCENT_BORDER}
52
53
  borderColor="yellow"
53
- paddingX={1}
54
+ borderRight={false}
55
+ borderTop={false}
56
+ borderBottom={false}
57
+ paddingLeft={ACCENT_PADDING}
54
58
  paddingY={1}
55
59
  flexDirection="column"
56
60
  gap={1}
@@ -121,7 +125,7 @@ export function ActiveRun({ run: initialRun }) {
121
125
  </Box>
122
126
 
123
127
  {pct >= 80 && pct < 100 && (
124
- <Box borderStyle="single" borderColor="red" paddingX={1}>
128
+ <Box paddingLeft={1}>
125
129
  <Text color="red" bold>
126
130
  ⚠️ BUDGET WARNING — {formatCost(run.budget - run.spent)} left
127
131
  </Text>
@@ -11,6 +11,7 @@ import {
11
11
  getOpusPct,
12
12
  MODEL_CLASSES,
13
13
  } from '../lib/score.js';
14
+ import { ACCENT_BORDER, ACCENT_PADDING } from '../lib/ui.js';
14
15
 
15
16
  export function ScoreCard({ run }) {
16
17
  const { exit } = useApp();
@@ -37,9 +38,12 @@ export function ScoreCard({ run }) {
37
38
  <Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
38
39
  {/* Big status header */}
39
40
  <Box
40
- borderStyle="double"
41
+ borderStyle={ACCENT_BORDER}
41
42
  borderColor={won ? 'yellow' : 'red'}
42
- paddingX={2}
43
+ borderRight={false}
44
+ borderTop={false}
45
+ borderBottom={false}
46
+ paddingLeft={ACCENT_PADDING}
43
47
  paddingY={1}
44
48
  flexDirection="column"
45
49
  gap={1}
@@ -213,13 +217,7 @@ export function ScoreCard({ run }) {
213
217
 
214
218
  {/* Death tip */}
215
219
  {!won && run.budget && (
216
- <Box
217
- borderStyle="single"
218
- borderColor="red"
219
- paddingX={1}
220
- marginTop={1}
221
- flexDirection="column"
222
- >
220
+ <Box marginTop={1} flexDirection="column" paddingLeft={1}>
223
221
  <Text color="red" bold>
224
222
  Cause of death: Budget exceeded by {formatCost(run.spent - run.budget)}
225
223
  </Text>
@@ -3,12 +3,16 @@ import { Box, Text, useApp } from 'ink';
3
3
  import { TextInput, Select, ConfirmInput } from '@inkjs/ui';
4
4
  import { setCurrentRun } from '../lib/state.js';
5
5
  import { getModelClass, getEffortLevel, getModelBudgets, FLOORS } from '../lib/score.js';
6
+ import { ACCENT_BORDER, ACCENT_PADDING } from '../lib/ui.js';
6
7
 
7
8
  const MODEL_OPTIONS = [
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: '⚜️ PaladinOpus plans, Sonnet executes. [Calculated]', value: 'opusplan' },
11
- { label: '🧙 Opus — Powerful but expensive. [Easy]', value: 'claude-opus-4-6' },
9
+ { label: '⚔️ Sonnet — Balanced. The default run. [Standard]', value: 'claude-sonnet-4-6' },
10
+ {
11
+ label: '🏹 Haiku Glass cannon. Hard mode. [Nightmare]',
12
+ value: 'claude-haiku-4-5-20251001',
13
+ },
14
+ { label: '⚜️ Paladin — Opus plans, Sonnet executes. [Tactical]', value: 'opusplan' },
15
+ { label: '🧙 Opus — Powerful but expensive. [Casual]', value: 'claude-opus-4-6' },
12
16
  ];
13
17
 
14
18
  const EFFORT_OPTIONS_BASE = [
@@ -62,9 +66,12 @@ export function StartRun() {
62
66
  <Box
63
67
  flexDirection="column"
64
68
  gap={1}
65
- borderStyle="single"
69
+ borderStyle={ACCENT_BORDER}
66
70
  borderColor="gray"
67
- paddingX={1}
71
+ borderRight={false}
72
+ borderTop={false}
73
+ borderBottom={false}
74
+ paddingLeft={ACCENT_PADDING}
68
75
  paddingY={1}
69
76
  >
70
77
  {/* Quest */}
@@ -176,9 +183,21 @@ export function StartRun() {
176
183
  {step === 'confirm' && (
177
184
  <Box flexDirection="column" gap={1} marginTop={1}>
178
185
  <Box
179
- borderStyle="round"
186
+ borderStyle={{
187
+ topLeft: ' ',
188
+ top: ' ',
189
+ topRight: ' ',
190
+ left: '██',
191
+ right: ' ',
192
+ bottomLeft: ' ',
193
+ bottom: ' ',
194
+ bottomRight: ' ',
195
+ }}
180
196
  borderColor="yellow"
181
- paddingX={1}
197
+ borderRight={false}
198
+ borderTop={false}
199
+ borderBottom={false}
200
+ paddingLeft={ACCENT_PADDING}
182
201
  paddingY={1}
183
202
  flexDirection="column"
184
203
  >
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { getTier, getModelClass, getBudgetPct, formatCost } from '../lib/score.js';
4
+ import { ACCENT_BORDER, ACCENT_PADDING } from '../lib/ui.js';
4
5
 
5
6
  export function StatsView({ stats }) {
6
7
  const { exit } = useApp();
@@ -33,7 +34,16 @@ export function StatsView({ stats }) {
33
34
  </Box>
34
35
 
35
36
  {/* Top line */}
36
- <Box borderStyle="single" borderColor="gray" paddingX={1} paddingY={1} gap={4}>
37
+ <Box
38
+ borderStyle={ACCENT_BORDER}
39
+ borderColor="gray"
40
+ borderRight={false}
41
+ borderTop={false}
42
+ borderBottom={false}
43
+ paddingLeft={ACCENT_PADDING}
44
+ paddingY={1}
45
+ gap={4}
46
+ >
37
47
  <Box flexDirection="column">
38
48
  <Text color="gray" dimColor>
39
49
  RUNS
@@ -85,9 +95,21 @@ export function StatsView({ stats }) {
85
95
  <Box flexDirection="column" gap={0}>
86
96
  <Text color="yellow">🏆 Personal Best</Text>
87
97
  <Box
88
- borderStyle="round"
98
+ borderStyle={{
99
+ topLeft: ' ',
100
+ top: ' ',
101
+ topRight: ' ',
102
+ left: '██',
103
+ right: ' ',
104
+ bottomLeft: ' ',
105
+ bottom: ' ',
106
+ bottomRight: ' ',
107
+ }}
89
108
  borderColor="yellow"
90
- paddingX={1}
109
+ borderRight={false}
110
+ borderTop={false}
111
+ borderBottom={false}
112
+ paddingLeft={ACCENT_PADDING}
91
113
  paddingY={1}
92
114
  flexDirection="column"
93
115
  >
@@ -141,16 +163,14 @@ export function StatsView({ stats }) {
141
163
  <Text color="gray" dimColor>
142
164
  Recent achievements:
143
165
  </Text>
144
- <Box flexWrap="wrap" gap={1}>
166
+ <Box flexWrap="wrap" columnGap={2}>
145
167
  {stats.achievements.slice(0, 12).map((a, i) => (
146
- <Box key={i} borderStyle="single" borderColor="gray" paddingX={1}>
147
- <Text>
148
- {a.emoji}{' '}
149
- <Text color="gray" dimColor>
150
- {a.label}
151
- </Text>
168
+ <Text key={i}>
169
+ {a.emoji}{' '}
170
+ <Text color="gray" dimColor>
171
+ {a.label}
152
172
  </Text>
153
- </Box>
173
+ </Text>
154
174
  ))}
155
175
  </Box>
156
176
  </Box>
package/src/lib/score.js CHANGED
@@ -37,28 +37,28 @@ export const MODEL_CLASSES = {
37
37
  name: 'Haiku',
38
38
  label: 'Rogue',
39
39
  emoji: '🏹',
40
- difficulty: 'Hard',
40
+ difficulty: 'Nightmare',
41
41
  color: 'red',
42
42
  },
43
43
  sonnet: {
44
44
  name: 'Sonnet',
45
45
  label: 'Fighter',
46
46
  emoji: '⚔️',
47
- difficulty: 'Normal',
47
+ difficulty: 'Standard',
48
48
  color: 'cyan',
49
49
  },
50
50
  opusplan: {
51
51
  name: 'Paladin',
52
52
  label: 'Paladin',
53
53
  emoji: '⚜️',
54
- difficulty: 'Calculated',
54
+ difficulty: 'Tactical',
55
55
  color: 'yellow',
56
56
  },
57
57
  opus: {
58
58
  name: 'Opus',
59
59
  label: 'Warlock',
60
60
  emoji: '🧙',
61
- difficulty: 'Easy',
61
+ difficulty: 'Casual',
62
62
  color: 'magenta',
63
63
  },
64
64
  };
package/src/lib/ui.js ADDED
@@ -0,0 +1,16 @@
1
+ // Shared UI constants for Ink components
2
+
3
+ // Design D: left-only ██ block accent bar.
4
+ // paddingLeft: 3 = 2 visible spaces after ██ (border eats 1)
5
+ export const ACCENT_BORDER = {
6
+ topLeft: ' ',
7
+ top: ' ',
8
+ topRight: ' ',
9
+ left: '██',
10
+ right: ' ',
11
+ bottomLeft: ' ',
12
+ bottom: ' ',
13
+ bottomRight: ' ',
14
+ };
15
+
16
+ export const ACCENT_PADDING = 3;
Binary file
Binary file
Binary file
Binary file