tokens-metric 0.4.3 → 0.4.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/dist/core/detect.js +3 -0
- package/dist/tui/index.js +90 -27
- package/package.json +2 -2
package/dist/core/detect.js
CHANGED
|
@@ -14,6 +14,9 @@ export function codexHome() {
|
|
|
14
14
|
export function codexSessionsDir() {
|
|
15
15
|
return join(codexHome(), 'sessions');
|
|
16
16
|
}
|
|
17
|
+
export function isCodexInstalled() {
|
|
18
|
+
return existsSync(codexHome());
|
|
19
|
+
}
|
|
17
20
|
/**
|
|
18
21
|
* Detect whether Claude Code is installed, the user is logged in, and best-
|
|
19
22
|
* effort what plan they're on. All signals are LOCAL and BEST-EFFORT — we
|
package/dist/tui/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import {
|
|
7
|
+
import { findActiveTranscript, findActiveCodexTranscript, listTranscripts } from '../core/parser.js';
|
|
8
8
|
import { tailTranscript } from '../core/tailer.js';
|
|
9
|
-
import { detectAuth } from '../core/detect.js';
|
|
9
|
+
import { detectAuth, isCodexInstalled } from '../core/detect.js';
|
|
10
10
|
import { categoryCostUSD, contextWindowSize, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
|
|
11
11
|
import { buildHistory, bucketCostUSD, bucketTokens, bucketTopModel, getTodaySessions, } from '../core/history.js';
|
|
12
12
|
import { totalTokens } from '../core/types.js';
|
|
@@ -29,11 +29,20 @@ const BAR_WIDTH = 20;
|
|
|
29
29
|
function App() {
|
|
30
30
|
const { exit } = useApp();
|
|
31
31
|
const [auth] = useState(() => detectAuth());
|
|
32
|
-
const [
|
|
33
|
-
|
|
34
|
-
const [
|
|
32
|
+
const [codexDetected] = useState(() => isCodexInstalled());
|
|
33
|
+
// Claude source
|
|
34
|
+
const [claudeStats, setClaudeStats] = useState(null);
|
|
35
|
+
const [claudePath, setClaudePath] = useState(null);
|
|
36
|
+
const [claudeSeries, setClaudeSeries] = useState(() => Array(SPARK_WIDTH).fill(0));
|
|
37
|
+
const [claudeLastTailAt, setClaudeLastTailAt] = useState(null);
|
|
38
|
+
const claudeLastTotalRef = useRef(0);
|
|
39
|
+
// Codex source
|
|
40
|
+
const [codexStats, setCodexStats] = useState(null);
|
|
41
|
+
const [codexPath, setCodexPath] = useState(null);
|
|
42
|
+
const [codexSeries, setCodexSeries] = useState(() => Array(SPARK_WIDTH).fill(0));
|
|
43
|
+
const [codexLastTailAt, setCodexLastTailAt] = useState(null);
|
|
44
|
+
const codexLastTotalRef = useRef(0);
|
|
35
45
|
const [now, setNow] = useState(Date.now());
|
|
36
|
-
const [lastTailAt, setLastTailAt] = useState(null);
|
|
37
46
|
const [history, setHistory] = useState(null);
|
|
38
47
|
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
39
48
|
// focusedTab: where the cursor sits (arrow navigation)
|
|
@@ -41,7 +50,6 @@ function App() {
|
|
|
41
50
|
const [focusedTab, setFocusedTab] = useState(1);
|
|
42
51
|
const [openTab, setOpenTab] = useState(null);
|
|
43
52
|
const startedAtRef = useRef(Date.now());
|
|
44
|
-
const lastTotalRef = useRef(0);
|
|
45
53
|
// useInput requires raw mode (interactive TTY). Skip it when stdin is piped
|
|
46
54
|
// or otherwise non-interactive, so `node dist/tui/index.js | cat` still
|
|
47
55
|
// renders instead of crashing.
|
|
@@ -120,45 +128,88 @@ function App() {
|
|
|
120
128
|
useEffect(() => {
|
|
121
129
|
const t = setInterval(() => {
|
|
122
130
|
setNow(Date.now());
|
|
123
|
-
|
|
131
|
+
setClaudeSeries((s) => [...s.slice(1), 0]);
|
|
132
|
+
setCodexSeries((s) => [...s.slice(1), 0]);
|
|
124
133
|
}, 1000);
|
|
125
134
|
return () => clearInterval(t);
|
|
126
135
|
}, []);
|
|
136
|
+
// Claude tailing
|
|
127
137
|
useEffect(() => {
|
|
128
138
|
let handle = null;
|
|
129
139
|
let cancelled = false;
|
|
130
140
|
async function attach(path) {
|
|
131
141
|
handle?.stop().catch(() => undefined);
|
|
132
|
-
|
|
142
|
+
setClaudePath(path);
|
|
133
143
|
handle = await tailTranscript(path);
|
|
134
144
|
if (cancelled) {
|
|
135
145
|
handle.stop();
|
|
136
146
|
return;
|
|
137
147
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
lastTotalRef.current = totalTokens(handle.stats.totals);
|
|
142
|
-
setLastTailAt(Date.now());
|
|
143
|
-
setStats({ ...handle.stats });
|
|
148
|
+
claudeLastTotalRef.current = totalTokens(handle.stats.totals);
|
|
149
|
+
setClaudeLastTailAt(Date.now());
|
|
150
|
+
setClaudeStats({ ...handle.stats });
|
|
144
151
|
handle.onUpdate((s) => {
|
|
145
|
-
|
|
152
|
+
setClaudeLastTailAt(Date.now());
|
|
146
153
|
const tot = totalTokens(s.totals);
|
|
147
|
-
const delta = Math.max(0, tot -
|
|
154
|
+
const delta = Math.max(0, tot - claudeLastTotalRef.current);
|
|
148
155
|
if (delta > 0) {
|
|
149
|
-
|
|
156
|
+
setClaudeSeries((arr) => {
|
|
150
157
|
const next = arr.slice();
|
|
151
158
|
next[next.length - 1] = (next[next.length - 1] ?? 0) + delta;
|
|
152
159
|
return next;
|
|
153
160
|
});
|
|
154
161
|
}
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
claudeLastTotalRef.current = tot;
|
|
163
|
+
setClaudeStats({ ...s });
|
|
157
164
|
});
|
|
158
165
|
}
|
|
159
166
|
async function rescan() {
|
|
160
|
-
const active =
|
|
161
|
-
if (active && active.path !==
|
|
167
|
+
const active = findActiveTranscript();
|
|
168
|
+
if (active && active.path !== claudePath)
|
|
169
|
+
await attach(active.path);
|
|
170
|
+
}
|
|
171
|
+
rescan();
|
|
172
|
+
const interval = setInterval(rescan, RESCAN_MS);
|
|
173
|
+
return () => {
|
|
174
|
+
cancelled = true;
|
|
175
|
+
clearInterval(interval);
|
|
176
|
+
handle?.stop().catch(() => undefined);
|
|
177
|
+
};
|
|
178
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
179
|
+
}, []);
|
|
180
|
+
// Codex tailing
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
let handle = null;
|
|
183
|
+
let cancelled = false;
|
|
184
|
+
async function attach(path) {
|
|
185
|
+
handle?.stop().catch(() => undefined);
|
|
186
|
+
setCodexPath(path);
|
|
187
|
+
handle = await tailTranscript(path);
|
|
188
|
+
if (cancelled) {
|
|
189
|
+
handle.stop();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
codexLastTotalRef.current = totalTokens(handle.stats.totals);
|
|
193
|
+
setCodexLastTailAt(Date.now());
|
|
194
|
+
setCodexStats({ ...handle.stats });
|
|
195
|
+
handle.onUpdate((s) => {
|
|
196
|
+
setCodexLastTailAt(Date.now());
|
|
197
|
+
const tot = totalTokens(s.totals);
|
|
198
|
+
const delta = Math.max(0, tot - codexLastTotalRef.current);
|
|
199
|
+
if (delta > 0) {
|
|
200
|
+
setCodexSeries((arr) => {
|
|
201
|
+
const next = arr.slice();
|
|
202
|
+
next[next.length - 1] = (next[next.length - 1] ?? 0) + delta;
|
|
203
|
+
return next;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
codexLastTotalRef.current = tot;
|
|
207
|
+
setCodexStats({ ...s });
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function rescan() {
|
|
211
|
+
const active = findActiveCodexTranscript();
|
|
212
|
+
if (active && active.path !== codexPath)
|
|
162
213
|
await attach(active.path);
|
|
163
214
|
}
|
|
164
215
|
rescan();
|
|
@@ -173,19 +224,31 @@ function App() {
|
|
|
173
224
|
const allTranscripts = listTranscripts();
|
|
174
225
|
const transcripts = allTranscripts.slice(0, 5);
|
|
175
226
|
const today = countToday(allTranscripts, now);
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
227
|
+
const claudeRate = claudeSeries.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
|
|
228
|
+
const codexRate = codexSeries.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
|
|
229
|
+
const todaySessions = getTodaySessions(now, claudePath);
|
|
230
|
+
// BreakdownPanel follows the most recently active source
|
|
231
|
+
const claudeAt = claudeStats?.lastEventAt ?? 0;
|
|
232
|
+
const codexAt = codexStats?.lastEventAt ?? 0;
|
|
233
|
+
const primaryStats = codexAt > claudeAt ? codexStats : claudeStats;
|
|
234
|
+
const primarySeries = codexAt > claudeAt ? codexSeries : claudeSeries;
|
|
235
|
+
const primaryRate = codexAt > claudeAt ? codexRate : claudeRate;
|
|
236
|
+
const lastTailAt = claudeLastTailAt && codexLastTailAt
|
|
237
|
+
? Math.max(claudeLastTailAt, codexLastTailAt)
|
|
238
|
+
: claudeLastTailAt ?? codexLastTailAt;
|
|
239
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, codexDetected: codexDetected, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [claudeStats
|
|
240
|
+
? _jsx(SessionStatusBar, { stats: claudeStats, ratePerSec: claudeRate, now: now, series: claudeSeries })
|
|
241
|
+
: !codexStats && _jsx(SessionStatusBar, { stats: null, ratePerSec: 0, now: now, series: claudeSeries }), codexStats && (_jsx(Box, { marginTop: claudeStats ? 1 : 0, children: _jsx(SessionStatusBar, { stats: codexStats, ratePerSec: codexRate, now: now, series: codexSeries }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: primaryStats, series: primarySeries, ratePerSec: primaryRate })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: claudePath, now: now }))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "magenta", children: "q" }), " quit \u00B7", ' ', _jsx(Text, { color: "magenta", children: "\u2190\u2192" }), " move \u00B7", ' ', _jsx(Text, { color: "magenta", children: "enter" }), " open/close \u00B7", ' ', _jsx(Text, { color: "magenta", children: "1\u20134" }), " jump \u00B7 pricing is", ' ', _jsx(Text, { italic: true, children: "API-equivalent" }), ", not your real bill on a subscription"] }) })] }));
|
|
179
242
|
}
|
|
180
243
|
// ── Header ───────────────────────────────────────────────────────────────────
|
|
181
|
-
function Header({ auth, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
|
|
244
|
+
function Header({ auth, codexDetected, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
|
|
182
245
|
const ok = auth.installed && auth.loggedIn;
|
|
183
246
|
const dot = ok ? 'green' : auth.installed ? 'yellow' : 'red';
|
|
184
247
|
const tailAgo = lastTailAt ? `updated ${timeAgo(now - lastTailAt)} ago` : 'waiting…';
|
|
185
248
|
const tailIsLive = !!lastTailAt && now - lastTailAt < 10_000;
|
|
186
249
|
const tailColor = !lastTailAt ? 'gray' : tailIsLive ? 'green' : 'yellow';
|
|
187
250
|
const tailStatusText = !lastTailAt ? '' : tailIsLive ? '● live' : '⚠ stale';
|
|
188
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "\u258C tokens-metric " }), _jsxs(Text, { dimColor: true, children: ["v", pkg.version, " \u2014 real-time Claude Code usage"] })] }), _jsx(Box, { marginTop: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: dot, children: "\u25CF" }), ' ', auth.installed ? 'Claude Code detected' : 'Claude Code NOT detected', _jsx(Text, { dimColor: true, children: ' · ' }), _jsx(Text, { children: sessionsToday }), _jsx(Text, { dimColor: true, children: ` ${plural(sessionsToday, 'session', 'sessions')} · ` }), _jsx(Text, { children: projectsToday }), _jsx(Text, { dimColor: true, children: ` ${plural(projectsToday, 'project', 'projects')} today` })] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "watching ~/.claude/projects \u00B7 " }), tailStatusText ? _jsxs(Text, { color: tailColor, children: [tailStatusText, " "] }) : null, _jsx(Text, { dimColor: true, children: tailAgo }), _jsx(Text, { dimColor: true, children: ' · uptime ' }), _jsx(Text, { children: timeAgo(now - startedAt) })] }), updateAvailable && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u26A1 Update available: " }), _jsxs(Text, { dimColor: true, children: ["v", pkg.version] }), _jsx(Text, { color: "yellow", children: " \u2192 " }), _jsxs(Text, { color: "yellow", bold: true, children: ["v", updateAvailable] }), _jsx(Text, { dimColor: true, children: " npm install -g tokens-metric" })] }))] }));
|
|
251
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "\u258C tokens-metric " }), _jsxs(Text, { dimColor: true, children: ["v", pkg.version, " \u2014 real-time Claude Code usage"] })] }), _jsx(Box, { marginTop: 0, children: _jsxs(Text, { children: [_jsx(Text, { color: dot, children: "\u25CF" }), ' ', auth.installed ? 'Claude Code detected' : 'Claude Code NOT detected', _jsx(Text, { dimColor: true, children: ' · ' }), _jsx(Text, { color: codexDetected ? 'green' : 'red', children: "\u25CF" }), ' ', codexDetected ? 'Codex detected' : 'Codex NOT detected', _jsx(Text, { dimColor: true, children: ' · ' }), _jsx(Text, { children: sessionsToday }), _jsx(Text, { dimColor: true, children: ` ${plural(sessionsToday, 'session', 'sessions')} · ` }), _jsx(Text, { children: projectsToday }), _jsx(Text, { dimColor: true, children: ` ${plural(projectsToday, 'project', 'projects')} today` })] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "watching ~/.claude/projects \u00B7 " }), tailStatusText ? _jsxs(Text, { color: tailColor, children: [tailStatusText, " "] }) : null, _jsx(Text, { dimColor: true, children: tailAgo }), _jsx(Text, { dimColor: true, children: ' · uptime ' }), _jsx(Text, { children: timeAgo(now - startedAt) })] }), updateAvailable && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "\u26A1 Update available: " }), _jsxs(Text, { dimColor: true, children: ["v", pkg.version] }), _jsx(Text, { color: "yellow", children: " \u2192 " }), _jsxs(Text, { color: "yellow", bold: true, children: ["v", updateAvailable] }), _jsx(Text, { dimColor: true, children: " npm install -g tokens-metric" })] }))] }));
|
|
189
252
|
}
|
|
190
253
|
function countToday(transcripts, now) {
|
|
191
254
|
const startOfDay = new Date(now);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokens-metric",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Real-time token usage meter for Claude Code
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tokens-metric": "dist/tui/index.js",
|