tokens-metric 0.3.3 → 0.3.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.
@@ -0,0 +1,65 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ const CACHE_PATH = () => join(homedir(), '.tokens-metric', 'update-check.json');
5
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
6
+ const REGISTRY_URL = 'https://registry.npmjs.org/tokens-metric/latest';
7
+ function readCache() {
8
+ try {
9
+ const raw = readFileSync(CACHE_PATH(), 'utf8');
10
+ const c = JSON.parse(raw);
11
+ if (typeof c.checkedAt !== 'number' || typeof c.latestVersion !== 'string')
12
+ return null;
13
+ return c;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writeCache(latestVersion) {
20
+ try {
21
+ const dir = join(homedir(), '.tokens-metric');
22
+ mkdirSync(dir, { recursive: true });
23
+ writeFileSync(CACHE_PATH(), JSON.stringify({ checkedAt: Date.now(), latestVersion }), 'utf8');
24
+ }
25
+ catch {
26
+ // ignore
27
+ }
28
+ }
29
+ function isNewer(latest, current) {
30
+ const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
31
+ const [lMaj, lMin, lPat] = parse(latest);
32
+ const [cMaj, cMin, cPat] = parse(current);
33
+ if (lMaj !== cMaj)
34
+ return (lMaj ?? 0) > (cMaj ?? 0);
35
+ if (lMin !== cMin)
36
+ return (lMin ?? 0) > (cMin ?? 0);
37
+ return (lPat ?? 0) > (cPat ?? 0);
38
+ }
39
+ /**
40
+ * Checks npm registry for a newer version. Non-blocking — uses a 24h cache
41
+ * so most startups skip the network call entirely.
42
+ *
43
+ * Returns the latest version string if an update is available, null otherwise.
44
+ */
45
+ export async function checkForUpdate(currentVersion) {
46
+ try {
47
+ const cached = readCache();
48
+ let latestVersion;
49
+ if (cached && Date.now() - cached.checkedAt < CACHE_TTL_MS) {
50
+ latestVersion = cached.latestVersion;
51
+ }
52
+ else {
53
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5_000) });
54
+ if (!res.ok)
55
+ return null;
56
+ const data = await res.json();
57
+ latestVersion = data.version;
58
+ writeCache(latestVersion);
59
+ }
60
+ return isNewer(latestVersion, currentVersion) ? latestVersion : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
File without changes
package/dist/tui/index.js CHANGED
@@ -11,6 +11,7 @@ import { totalTokens } from '../core/types.js';
11
11
  import { anonymizePath } from '../core/privacy.js';
12
12
  import { createRequire } from 'node:module';
13
13
  import { HELP_TEXT, parseArgs } from '../core/args.js';
14
+ import { checkForUpdate } from '../core/updater.js';
14
15
  const require = createRequire(import.meta.url);
15
16
  const pkg = require('../../package.json');
16
17
  import { bar, sparklineCells } from './bars.js';
@@ -32,6 +33,7 @@ function App() {
32
33
  const [now, setNow] = useState(Date.now());
33
34
  const [lastTailAt, setLastTailAt] = useState(null);
34
35
  const [history, setHistory] = useState(null);
36
+ const [updateAvailable, setUpdateAvailable] = useState(null);
35
37
  const startedAtRef = useRef(Date.now());
36
38
  const lastTotalRef = useRef(0);
37
39
  // useInput requires raw mode (interactive TTY). Skip it when stdin is piped
@@ -45,6 +47,13 @@ function App() {
45
47
  exit();
46
48
  });
47
49
  }
50
+ // Check for newer version on npm — non-blocking, result cached 24h.
51
+ useEffect(() => {
52
+ checkForUpdate(pkg.version)
53
+ .then((v) => { if (v)
54
+ setUpdateAvailable(v); })
55
+ .catch(() => undefined);
56
+ }, []);
48
57
  // Historical aggregate over all transcripts. Refreshed on an interval —
49
58
  // the live session is already shown in SessionPanel, so minute-grain
50
59
  // accuracy on today's bucket is enough and keeps full parses out of the
@@ -125,15 +134,15 @@ function App() {
125
134
  const today = countToday(allTranscripts, now);
126
135
  const ratePerSec = series.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
127
136
  const todaySessions = getTodaySessions(now, transcriptPath);
128
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now }), _jsxs(Box, { marginTop: 1, flexDirection: "row", gap: 1, children: [_jsx(SessionPanel, { stats: stats, ratePerSec: ratePerSec, now: now, series: series }), _jsx(BreakdownPanel, { stats: stats })] }), _jsx(Box, { marginTop: 1, children: _jsx(HistoryPanel, { history: history }) }), todaySessions.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }) })), _jsx(Box, { marginTop: 1, children: _jsx(TranscriptsPanel, { transcripts: transcripts, activePath: transcriptPath, 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: "live" }), " tail of ~/.claude/projects \u00B7 pricing is", ' ', _jsx(Text, { italic: true, children: "API-equivalent" }), ", not your real bill on a subscription"] }) })] }));
137
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsxs(Box, { marginTop: 1, flexDirection: "row", gap: 1, children: [_jsx(SessionPanel, { stats: stats, ratePerSec: ratePerSec, now: now, series: series }), _jsx(BreakdownPanel, { stats: stats })] }), _jsx(Box, { marginTop: 1, children: _jsx(HistoryPanel, { history: history }) }), todaySessions.length > 0 && (_jsx(Box, { marginTop: 1, children: _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }) })), _jsx(Box, { marginTop: 1, children: _jsx(TranscriptsPanel, { transcripts: transcripts, activePath: transcriptPath, 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: "live" }), " tail of ~/.claude/projects \u00B7 pricing is", ' ', _jsx(Text, { italic: true, children: "API-equivalent" }), ", not your real bill on a subscription"] }) })] }));
129
138
  }
130
139
  // ── Header ───────────────────────────────────────────────────────────────────
131
- function Header({ auth, sessionsToday, projectsToday, lastTailAt, startedAt, now, }) {
140
+ function Header({ auth, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
132
141
  const ok = auth.installed && auth.loggedIn;
133
142
  const dot = ok ? 'green' : auth.installed ? 'yellow' : 'red';
134
143
  const tailLabel = lastTailAt ? `updated ${timeAgo(now - lastTailAt)} ago` : 'waiting…';
135
144
  const tailColor = !lastTailAt ? 'gray' : now - lastTailAt < 10_000 ? 'green' : 'yellow';
136
- 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 " }), _jsx(Text, { color: tailColor, children: tailLabel }), _jsx(Text, { dimColor: true, children: ' · uptime ' }), _jsx(Text, { children: timeAgo(now - startedAt) })] })] }));
145
+ 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 " }), _jsx(Text, { color: tailColor, children: tailLabel }), _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" })] }))] }));
137
146
  }
138
147
  function countToday(transcripts, now) {
139
148
  const startOfDay = new Date(now);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {