tokburn 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/statusline.js +113 -109
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokburn",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "See exactly how fast you're burning tokens and money across Claude Code sessions",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/statusline.js CHANGED
@@ -3,37 +3,16 @@
3
3
  * tokburn — Status line renderer for Claude Code
4
4
  * Reads session JSON from stdin, renders configured modules.
5
5
  * Configured via ~/.tokburn/config.json → statusline_modules
6
+ *
7
+ * IMPORTANT: Stdin reading and rendering only happens when run directly.
8
+ * When require()'d as a module, only MODULE_LIST, PRESETS are exported.
6
9
  */
7
10
 
8
11
  const fs = require('fs');
9
12
  const path = require('path');
10
13
  const { execFileSync } = require('child_process');
11
14
 
12
- // Read stdin synchronously
13
- let input = '';
14
- try {
15
- input = fs.readFileSync('/dev/stdin', 'utf8');
16
- } catch (_) {}
17
-
18
- let data = {};
19
- try {
20
- data = JSON.parse(input);
21
- } catch (_) {}
22
-
23
- // Load config
24
- const configPath = path.join(process.env.HOME || process.env.USERPROFILE, '.tokburn', 'config.json');
25
- let config = {};
26
- try {
27
- if (fs.existsSync(configPath)) {
28
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
29
- }
30
- } catch (_) {}
31
-
32
- const enabledModules = config.statusline_modules || [
33
- 'model_context', 'repo_branch', 'current_limit', 'weekly_limit', 'cost'
34
- ];
35
-
36
- // ── Module renderers ────────────────────────────────────────────────────────────
15
+ // ── Helpers ─────────────────────────────────────────────────────────────────────
37
16
 
38
17
  function dotBar(pct, count) {
39
18
  count = count || 10;
@@ -64,7 +43,6 @@ function formatResetTime(resetTimestamp) {
64
43
  const remainMins = mins % 60;
65
44
  if (hrs < 24) return hrs + 'hr ' + (remainMins > 0 ? remainMins + 'min' : '');
66
45
 
67
- // Show day + time for >24hr
68
46
  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
69
47
  const day = days[reset.getDay()];
70
48
  const h = reset.getHours();
@@ -74,86 +52,90 @@ function formatResetTime(resetTimestamp) {
74
52
  return day + ' ' + h12 + ':' + String(m).padStart(2, '0') + ampm;
75
53
  }
76
54
 
77
- const MODULES = {
78
- model_context: function () {
79
- const model = (data.model && data.model.display_name) || '?';
80
- const ctxPct = Math.round((data.context_window && data.context_window.used_percentage) || 0);
81
- return model + ' | ctx ' + ctxPct + '%';
82
- },
83
-
84
- repo_branch: function () {
85
- const cwd = (data.workspace && data.workspace.current_dir) || data.cwd || '';
86
- const repoName = path.basename(cwd);
87
- let branch = '';
88
- try {
89
- branch = execFileSync('git', ['-C', cwd, 'branch', '--show-current'], {
90
- encoding: 'utf8', timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
91
- }).trim();
92
- const status = execFileSync('git', ['-C', cwd, 'status', '--porcelain'], {
93
- encoding: 'utf8', timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
94
- }).trim();
95
- if (status) branch += '*';
96
- } catch (_) {}
97
-
98
- if (branch) return repoName + ' (' + branch + ')';
99
- return repoName;
100
- },
101
-
102
- current_limit: function () {
103
- const rl = data.rate_limits && data.rate_limits.five_hour;
104
- if (!rl) return 'current ' + dotBar(0) + ' 0%';
105
-
106
- const pct = Math.round(rl.used_percentage || 0);
107
- const reset = formatResetTime(rl.resets_at);
108
- return 'current ' + dotBar(pct) + ' ' + pct + '%' + (reset ? ' \u21BB ' + reset : '');
109
- },
110
-
111
- weekly_limit: function () {
112
- const rl = data.rate_limits && data.rate_limits.seven_day;
113
- if (!rl) return 'weekly ' + dotBar(0) + ' 0%';
114
-
115
- const pct = Math.round(rl.used_percentage || 0);
116
- const reset = formatResetTime(rl.resets_at);
117
- return 'weekly ' + dotBar(pct) + ' ' + pct + '%' + (reset ? ' \u21BB ' + reset : '');
118
- },
119
-
120
- token_count: function () {
121
- const input = (data.context_window && data.context_window.total_input_tokens) || 0;
122
- const output = (data.context_window && data.context_window.total_output_tokens) || 0;
123
- return abbreviate(input + output) + ' tok';
124
- },
125
-
126
- cost: function () {
127
- const cost = (data.cost && data.cost.total_cost_usd) || 0;
128
- return '$' + cost.toFixed(2);
129
- },
130
-
131
- burn_rate: function () {
132
- try {
133
- const usagePath = path.join(process.env.HOME || '', '.tokburn', 'usage.jsonl');
134
- if (!fs.existsSync(usagePath)) return '';
135
- const raw = fs.readFileSync(usagePath, 'utf8').trim();
136
- if (!raw) return '';
137
- const lines = raw.split('\n');
138
- const today = new Date().toISOString().split('T')[0];
139
- const todayEntries = [];
140
- for (const l of lines) {
141
- if (!l.startsWith('{"timestamp":"' + today)) continue;
142
- try { todayEntries.push(JSON.parse(l)); } catch (_) {}
55
+ // ── Module builders (take data as parameter) ────────────────────────────────────
56
+
57
+ function buildModules(data) {
58
+ return {
59
+ model_context: function () {
60
+ const model = (data.model && data.model.display_name) || '?';
61
+ const ctxPct = Math.round((data.context_window && data.context_window.used_percentage) || 0);
62
+ return model + ' | ctx ' + ctxPct + '%';
63
+ },
64
+
65
+ repo_branch: function () {
66
+ const cwd = (data.workspace && data.workspace.current_dir) || data.cwd || '';
67
+ const repoName = path.basename(cwd);
68
+ let branch = '';
69
+ try {
70
+ branch = execFileSync('git', ['-C', cwd, 'branch', '--show-current'], {
71
+ encoding: 'utf8', timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
72
+ }).trim();
73
+ const status = execFileSync('git', ['-C', cwd, 'status', '--porcelain'], {
74
+ encoding: 'utf8', timeout: 500, stdio: ['pipe', 'pipe', 'pipe'],
75
+ }).trim();
76
+ if (status) branch += '*';
77
+ } catch (_) {}
78
+
79
+ if (branch) return repoName + ' (' + branch + ')';
80
+ return repoName;
81
+ },
82
+
83
+ current_limit: function () {
84
+ const rl = data.rate_limits && data.rate_limits.five_hour;
85
+ if (!rl) return 'current ' + dotBar(0) + ' 0%';
86
+
87
+ const pct = Math.round(rl.used_percentage || 0);
88
+ const reset = formatResetTime(rl.resets_at);
89
+ return 'current ' + dotBar(pct) + ' ' + pct + '%' + (reset ? ' \u21BB ' + reset : '');
90
+ },
91
+
92
+ weekly_limit: function () {
93
+ const rl = data.rate_limits && data.rate_limits.seven_day;
94
+ if (!rl) return 'weekly ' + dotBar(0) + ' 0%';
95
+
96
+ const pct = Math.round(rl.used_percentage || 0);
97
+ const reset = formatResetTime(rl.resets_at);
98
+ return 'weekly ' + dotBar(pct) + ' ' + pct + '%' + (reset ? ' \u21BB ' + reset : '');
99
+ },
100
+
101
+ token_count: function () {
102
+ const inp = (data.context_window && data.context_window.total_input_tokens) || 0;
103
+ const out = (data.context_window && data.context_window.total_output_tokens) || 0;
104
+ return abbreviate(inp + out) + ' tok';
105
+ },
106
+
107
+ cost: function () {
108
+ const cost = (data.cost && data.cost.total_cost_usd) || 0;
109
+ return '$' + cost.toFixed(2);
110
+ },
111
+
112
+ burn_rate: function () {
113
+ try {
114
+ const usagePath = path.join(process.env.HOME || '', '.tokburn', 'usage.jsonl');
115
+ if (!fs.existsSync(usagePath)) return '';
116
+ const raw = fs.readFileSync(usagePath, 'utf8').trim();
117
+ if (!raw) return '';
118
+ const lines = raw.split('\n');
119
+ const today = new Date().toISOString().split('T')[0];
120
+ const todayEntries = [];
121
+ for (const l of lines) {
122
+ if (!l.startsWith('{"timestamp":"' + today)) continue;
123
+ try { todayEntries.push(JSON.parse(l)); } catch (_) {}
124
+ }
125
+ if (todayEntries.length < 2) return '';
126
+ const first = new Date(todayEntries[0].timestamp);
127
+ const last = new Date(todayEntries[todayEntries.length - 1].timestamp);
128
+ const elapsed = (last - first) / 60000;
129
+ if (elapsed <= 0) return '';
130
+ let total = 0;
131
+ for (const e of todayEntries) total += (e.input_tokens || 0) + (e.output_tokens || 0);
132
+ return '~' + abbreviate(Math.round(total / elapsed)) + '/min';
133
+ } catch (_) {
134
+ return '';
143
135
  }
144
- if (todayEntries.length < 2) return '';
145
- const first = new Date(todayEntries[0].timestamp);
146
- const last = new Date(todayEntries[todayEntries.length - 1].timestamp);
147
- const elapsed = (last - first) / 60000;
148
- if (elapsed <= 0) return '';
149
- let total = 0;
150
- for (const e of todayEntries) total += (e.input_tokens || 0) + (e.output_tokens || 0);
151
- return '~' + abbreviate(Math.round(total / elapsed)) + '/min';
152
- } catch (_) {
153
- return '';
154
- }
155
- },
156
- };
136
+ },
137
+ };
138
+ }
157
139
 
158
140
  // ── Available modules metadata (used by init wizard) ────────────────────────────
159
141
 
@@ -175,16 +157,38 @@ const PRESETS = {
175
157
  full: ['model_context', 'repo_branch', 'current_limit', 'weekly_limit', 'token_count', 'cost', 'burn_rate'],
176
158
  };
177
159
 
178
- // ── Render ───────────────────────────────────────────────────────────────────────
160
+ // ── Main: only runs when executed directly (not require'd) ──────────────────────
179
161
 
180
162
  if (require.main === module) {
163
+ let input = '';
164
+ try {
165
+ input = fs.readFileSync('/dev/stdin', 'utf8');
166
+ } catch (_) {}
167
+
168
+ let data = {};
169
+ try {
170
+ data = JSON.parse(input);
171
+ } catch (_) {}
172
+
173
+ // Load config
174
+ const configPath = path.join(process.env.HOME || process.env.USERPROFILE, '.tokburn', 'config.json');
175
+ let config = {};
176
+ try {
177
+ if (fs.existsSync(configPath)) {
178
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
179
+ }
180
+ } catch (_) {}
181
+
182
+ const enabledModules = config.statusline_modules || PRESETS.recommended;
183
+ const modules = buildModules(data);
184
+
181
185
  const outputLines = [];
182
186
  const lineOneModules = [];
183
187
  const extraLines = [];
184
188
 
185
189
  for (const mod of enabledModules) {
186
- if (!MODULES[mod]) continue;
187
- const val = MODULES[mod]();
190
+ if (!modules[mod]) continue;
191
+ const val = modules[mod]();
188
192
  if (!val) continue;
189
193
 
190
194
  if (mod === 'current_limit' || mod === 'weekly_limit') {
@@ -202,4 +206,4 @@ if (require.main === module) {
202
206
  process.stdout.write(outputLines.join('\n'));
203
207
  }
204
208
 
205
- module.exports = { MODULE_LIST, PRESETS, MODULES };
209
+ module.exports = { MODULE_LIST, PRESETS, buildModules };