claude-burn 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "claude-burn",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Monitor Claude Code token usage, burn rate, and session attribution. Zero dependencies.",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
5
8
  "bin": {
6
9
  "claude-burn": "./bin/claude-burn.js"
7
10
  },
package/public/api.js ADDED
@@ -0,0 +1,7 @@
1
+ export async function fetchSessions(hours) {
2
+ const response = await fetch(`/api/sessions?hours=${hours}`);
3
+ if (!response.ok) {
4
+ throw new Error(`sessions request failed: ${response.status}`);
5
+ }
6
+ return response.json();
7
+ }
package/public/app.js ADDED
@@ -0,0 +1,215 @@
1
+ import { fetchSessions } from './api.js';
2
+ import { state } from './state.js';
3
+ import { getFilteredSessions, renderSessions } from './renderSessions.js';
4
+ import { renderDetail, renderEmptyDetail } from './renderDetail.js';
5
+ import { renderShareBar, renderSummary } from './renderSummary.js';
6
+
7
+ function updateLastUpdate() {
8
+ document.getElementById('last-update').textContent = new Date().toLocaleTimeString([], {
9
+ hour: '2-digit',
10
+ minute: '2-digit',
11
+ second: '2-digit',
12
+ });
13
+ }
14
+
15
+ function updateAutoRefresh() {
16
+ const hours = parseInt(document.getElementById('hours-select').value, 10);
17
+ const statusEl = document.getElementById('autorefresh-status');
18
+
19
+ if (state.autoRefreshInterval) {
20
+ clearInterval(state.autoRefreshInterval);
21
+ }
22
+
23
+ if (hours <= 24) {
24
+ state.autoRefreshInterval = setInterval(fetchData, 5000);
25
+ statusEl.textContent = '';
26
+ } else {
27
+ state.autoRefreshInterval = null;
28
+ statusEl.textContent = 'auto-refresh off';
29
+ statusEl.style.color = 'var(--text2)';
30
+ }
31
+ }
32
+
33
+ function scrollSelectedCardIntoView() {
34
+ const card = document.querySelector('.session-card.selected');
35
+ if (card) {
36
+ card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
37
+ }
38
+ }
39
+
40
+ function selectSession(id, options = {}) {
41
+ state.selectedId = id;
42
+ renderSessions(state);
43
+ renderDetail(state);
44
+ if (options.scroll !== false) {
45
+ scrollSelectedCardIntoView();
46
+ }
47
+ }
48
+
49
+ async function copySessionId(titleEl) {
50
+ const sid = titleEl.dataset.sid;
51
+ const tooltip = titleEl.querySelector('.sid-tooltip');
52
+
53
+ try {
54
+ await navigator.clipboard.writeText(sid);
55
+ if (!tooltip) return;
56
+ const original = tooltip.textContent;
57
+ tooltip.textContent = 'Copied!';
58
+ tooltip.style.color = 'var(--accent)';
59
+ tooltip.classList.add('show');
60
+ setTimeout(() => {
61
+ tooltip.textContent = original;
62
+ tooltip.style.color = '';
63
+ tooltip.classList.remove('show');
64
+ }, 1200);
65
+ } catch {}
66
+ }
67
+
68
+ async function fetchData() {
69
+ const hours = document.getElementById('hours-select').value;
70
+ const btn = document.getElementById('refresh-btn');
71
+
72
+ btn.classList.add('loading');
73
+ try {
74
+ const data = await fetchSessions(hours);
75
+ state.sessions = data.sessions;
76
+
77
+ renderSummary(data.summary, state.sessions);
78
+ renderShareBar(state.sessions);
79
+ renderSessions(state);
80
+ updateLastUpdate();
81
+
82
+ if (state.selectedId) {
83
+ renderDetail(state);
84
+ } else {
85
+ renderEmptyDetail(state);
86
+ }
87
+ } catch (error) {
88
+ console.error('Fetch error:', error);
89
+ } finally {
90
+ btn.classList.remove('loading');
91
+ }
92
+ }
93
+
94
+ function setupTooltip() {
95
+ const tooltip = document.getElementById('tooltip');
96
+
97
+ document.addEventListener('mouseover', (event) => {
98
+ const tip = event.target.closest('[data-tip]');
99
+ if (!tip || !tip.dataset.tip) return;
100
+
101
+ tooltip.textContent = tip.dataset.tip;
102
+ tooltip.style.display = 'block';
103
+
104
+ const rect = tip.getBoundingClientRect();
105
+ let left = rect.left;
106
+ let top = rect.top - tooltip.offsetHeight - 8;
107
+
108
+ if (left + 240 > window.innerWidth) left = window.innerWidth - 250;
109
+ if (left < 8) left = 8;
110
+ if (top < 8) top = rect.bottom + 8;
111
+
112
+ tooltip.style.left = `${left}px`;
113
+ tooltip.style.top = `${top}px`;
114
+ });
115
+
116
+ document.addEventListener('mouseout', (event) => {
117
+ if (event.target.closest('[data-tip]')) {
118
+ tooltip.style.display = 'none';
119
+ }
120
+ });
121
+ }
122
+
123
+ function setupEventListeners() {
124
+ document.getElementById('refresh-btn').addEventListener('click', fetchData);
125
+
126
+ document.getElementById('hours-select').addEventListener('change', () => {
127
+ fetchData();
128
+ updateAutoRefresh();
129
+ });
130
+
131
+ document.getElementById('search-input').addEventListener('input', (event) => {
132
+ state.searchQuery = event.target.value;
133
+ renderSessions(state);
134
+ });
135
+
136
+ document.getElementById('sort-select').addEventListener('change', (event) => {
137
+ state.sortBy = event.target.value;
138
+ renderSessions(state);
139
+ });
140
+
141
+ document.getElementById('session-cards').addEventListener('click', (event) => {
142
+ const title = event.target.closest('.session-title');
143
+ if (title) {
144
+ event.stopPropagation();
145
+ copySessionId(title);
146
+ return;
147
+ }
148
+
149
+ const card = event.target.closest('.session-card[data-session-id]');
150
+ if (card) {
151
+ selectSession(card.dataset.sessionId);
152
+ }
153
+ });
154
+
155
+ document.getElementById('billing-progress').addEventListener('click', (event) => {
156
+ const fill = event.target.closest('.billing-progress-fill[data-session-id]');
157
+ if (fill) {
158
+ selectSession(fill.dataset.sessionId);
159
+ }
160
+ });
161
+
162
+ document.addEventListener('keydown', (event) => {
163
+ if (['INPUT', 'SELECT', 'TEXTAREA'].includes(event.target.tagName)) return;
164
+
165
+ const filtered = getFilteredSessions(state);
166
+ if (filtered.length === 0) return;
167
+
168
+ if (event.key === 'r' || event.key === 'R') {
169
+ event.preventDefault();
170
+ fetchData();
171
+ }
172
+
173
+ if (event.key === 'j' || event.key === 'ArrowDown') {
174
+ event.preventDefault();
175
+ const idx = filtered.findIndex((session) => session.id === state.selectedId);
176
+ const next = idx < filtered.length - 1 ? idx + 1 : 0;
177
+ selectSession(filtered[next].id);
178
+ }
179
+
180
+ if (event.key === 'k' || event.key === 'ArrowUp') {
181
+ event.preventDefault();
182
+ const idx = filtered.findIndex((session) => session.id === state.selectedId);
183
+ const prev = idx > 0 ? idx - 1 : filtered.length - 1;
184
+ selectSession(filtered[prev].id);
185
+ }
186
+
187
+ if (event.key === '/') {
188
+ event.preventDefault();
189
+ document.getElementById('search-input').focus();
190
+ }
191
+
192
+ if (event.key === 'Escape') {
193
+ const input = document.getElementById('search-input');
194
+ if (document.activeElement === input) {
195
+ state.searchQuery = '';
196
+ input.value = '';
197
+ input.blur();
198
+ renderSessions(state);
199
+ }
200
+ }
201
+
202
+ const timeMap = { 1: '1', 2: '6', 3: '12', 4: '24', 5: '72', 6: '168', 7: '720' };
203
+ if (timeMap[event.key]) {
204
+ document.getElementById('hours-select').value = timeMap[event.key];
205
+ fetchData();
206
+ updateAutoRefresh();
207
+ }
208
+ });
209
+ }
210
+
211
+ setupTooltip();
212
+ setupEventListeners();
213
+ renderEmptyDetail(state);
214
+ fetchData();
215
+ updateAutoRefresh();
@@ -0,0 +1,46 @@
1
+ export function fmt(value) {
2
+ if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`;
3
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
4
+ if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
5
+ return value.toString();
6
+ }
7
+
8
+ export function fmtDuration(sec) {
9
+ if (sec < 60) return `${sec}s`;
10
+ if (sec < 3600) return `${Math.floor(sec / 60)}m`;
11
+ const hours = Math.floor(sec / 3600);
12
+ const minutes = Math.floor((sec % 3600) / 60);
13
+ return `${hours}h ${minutes}m`;
14
+ }
15
+
16
+ export function fmtTime(ts) {
17
+ if (!ts) return '--';
18
+ try {
19
+ const date = new Date(ts);
20
+ const now = new Date();
21
+ const isToday = date.toDateString() === now.toDateString();
22
+ const time = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
23
+ return isToday ? time : `${date.toLocaleDateString([], { month: 'short', day: 'numeric' })} ${time}`;
24
+ } catch {
25
+ return '--';
26
+ }
27
+ }
28
+
29
+ export function fmtAgo(ts) {
30
+ if (!ts) return '';
31
+ try {
32
+ const sec = (Date.now() - new Date(ts).getTime()) / 1000;
33
+ if (sec < 60) return 'just now';
34
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
35
+ if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
36
+ return `${Math.floor(sec / 86400)}d ago`;
37
+ } catch {
38
+ return '';
39
+ }
40
+ }
41
+
42
+ export function esc(str) {
43
+ const div = document.createElement('div');
44
+ div.textContent = str;
45
+ return div.innerHTML;
46
+ }