tokens-metric 0.1.0 → 0.2.3
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/README.md +13 -0
- package/dist/core/args.js +22 -0
- package/dist/core/detect.js +5 -5
- package/dist/core/privacy.js +30 -0
- package/dist/statusline/index.js +6 -0
- package/dist/tui/index.js +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,19 @@ tokens-metric
|
|
|
35
35
|
|
|
36
36
|
Press `q` (or `Esc`, or `Ctrl-C`) to quit.
|
|
37
37
|
|
|
38
|
+
### Privacy defaults
|
|
39
|
+
|
|
40
|
+
Starting in v0.2.0, the TUI masks identifying information by default so screenshots can be shared safely:
|
|
41
|
+
|
|
42
|
+
- `cwd` paths are shown as `~/…` instead of `/Users/<you>/…`.
|
|
43
|
+
- The user ID is masked to `●●●●●●●●`.
|
|
44
|
+
|
|
45
|
+
Pass `--reveal` to show everything unmasked on your own machine:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
tokens-metric --reveal
|
|
49
|
+
```
|
|
50
|
+
|
|
38
51
|
### Statusline inside Claude Code
|
|
39
52
|
|
|
40
53
|
Add to `~/.claude/settings.json`:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function parseArgs(argv) {
|
|
2
|
+
const args = argv.slice(2);
|
|
3
|
+
return {
|
|
4
|
+
reveal: args.includes('--reveal'),
|
|
5
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export const HELP_TEXT = `tokens-metric — real-time Claude Code token meter
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
tokens-metric [--reveal] [--help]
|
|
12
|
+
tokens-metric-statusline [--reveal]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--reveal Show the full user ID and the unredacted cwd. By default these
|
|
16
|
+
are masked so screenshots don't expose identifying info.
|
|
17
|
+
-h, --help Show this help.
|
|
18
|
+
|
|
19
|
+
Privacy:
|
|
20
|
+
By default the UI shows ~/… instead of /Users/<you>/… and masks the user ID
|
|
21
|
+
to "●●●●●●●●". Pass --reveal to disable masking on your own machine.
|
|
22
|
+
`;
|
package/dist/core/detect.js
CHANGED
|
@@ -52,7 +52,7 @@ export function detectAuth() {
|
|
|
52
52
|
loggedIn: false,
|
|
53
53
|
authMethod: 'none',
|
|
54
54
|
planHint: 'unknown',
|
|
55
|
-
hint: 'Claude Code is installed but
|
|
55
|
+
hint: 'Claude Code is installed but you are not logged in. Run `claude` to log in.',
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
return {
|
|
@@ -113,15 +113,15 @@ function inferPlanHint(cfg) {
|
|
|
113
113
|
function planHintExplanation(plan, cfg) {
|
|
114
114
|
switch (plan) {
|
|
115
115
|
case 'team-or-enterprise':
|
|
116
|
-
return 'Org-managed
|
|
116
|
+
return 'Org-managed account — likely Team or Enterprise.';
|
|
117
117
|
case 'paid':
|
|
118
|
-
return 'Paid plan likely
|
|
118
|
+
return 'Paid plan likely — Pro or Max (not distinguishable locally).';
|
|
119
119
|
case 'free':
|
|
120
120
|
return cfg
|
|
121
|
-
? 'Logged in
|
|
121
|
+
? 'Logged in. No paid-plan signals found — likely Free.'
|
|
122
122
|
: 'Logged in.';
|
|
123
123
|
case 'api':
|
|
124
|
-
return 'API key
|
|
124
|
+
return 'Using API key — pay-per-token.';
|
|
125
125
|
case 'unknown':
|
|
126
126
|
default:
|
|
127
127
|
return 'Plan tier not determinable from local signals.';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
/**
|
|
3
|
+
* Replace the user's home directory with `~` so screenshots posted publicly
|
|
4
|
+
* don't reveal the macOS username. Returns the input untouched if it doesn't
|
|
5
|
+
* start with $HOME.
|
|
6
|
+
*/
|
|
7
|
+
export function anonymizePath(p) {
|
|
8
|
+
if (!p)
|
|
9
|
+
return '—';
|
|
10
|
+
const home = homedir();
|
|
11
|
+
if (p === home)
|
|
12
|
+
return '~';
|
|
13
|
+
if (p.startsWith(home + '/'))
|
|
14
|
+
return '~' + p.slice(home.length);
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Mask all but the first 2 characters of the user ID with dots, so a
|
|
19
|
+
* screenshot still hints at "I'm logged in" without exposing the full hash.
|
|
20
|
+
* Pass `reveal` to bypass.
|
|
21
|
+
*/
|
|
22
|
+
export function maskUserId(id, reveal) {
|
|
23
|
+
if (!id)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (reveal)
|
|
26
|
+
return id;
|
|
27
|
+
if (id.length <= 2)
|
|
28
|
+
return '●●●●●●●●';
|
|
29
|
+
return id.slice(0, 2) + '●●●●●●';
|
|
30
|
+
}
|
package/dist/statusline/index.js
CHANGED
|
@@ -3,6 +3,12 @@ import { aggregateTranscript, findActiveTranscript } from '../core/parser.js';
|
|
|
3
3
|
import { detectAuth } from '../core/detect.js';
|
|
4
4
|
import { estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
|
|
5
5
|
import { totalTokens } from '../core/types.js';
|
|
6
|
+
import { HELP_TEXT, parseArgs } from '../core/args.js';
|
|
7
|
+
const OPTS = parseArgs(process.argv);
|
|
8
|
+
if (OPTS.help) {
|
|
9
|
+
process.stdout.write(HELP_TEXT);
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
6
12
|
/**
|
|
7
13
|
* One-shot status line. Reads whatever JSONL is currently the most recent
|
|
8
14
|
* transcript, aggregates it, and prints a single line. Exits 0 always so
|
package/dist/tui/index.js
CHANGED
|
@@ -7,7 +7,14 @@ import { tailTranscript } from '../core/tailer.js';
|
|
|
7
7
|
import { detectAuth } from '../core/detect.js';
|
|
8
8
|
import { estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
|
|
9
9
|
import { totalTokens } from '../core/types.js';
|
|
10
|
+
import { anonymizePath, maskUserId } from '../core/privacy.js';
|
|
11
|
+
import { HELP_TEXT, parseArgs } from '../core/args.js';
|
|
10
12
|
import { bar, sparkline } from './bars.js';
|
|
13
|
+
const OPTS = parseArgs(process.argv);
|
|
14
|
+
if (OPTS.help) {
|
|
15
|
+
process.stdout.write(HELP_TEXT);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
11
18
|
const RESCAN_MS = 3_000;
|
|
12
19
|
const SPARK_WIDTH = 32;
|
|
13
20
|
const BAR_WIDTH = 20;
|
|
@@ -51,6 +58,12 @@ function App() {
|
|
|
51
58
|
handle.stop();
|
|
52
59
|
return;
|
|
53
60
|
}
|
|
61
|
+
// Seed initial state — tailTranscript already drained the file once
|
|
62
|
+
// before returning, but its first notify() fires before any listener
|
|
63
|
+
// is attached, so we'd otherwise wait for the next appended line.
|
|
64
|
+
lastTotalRef.current = totalTokens(handle.stats.totals);
|
|
65
|
+
lastSampleAtRef.current = Date.now();
|
|
66
|
+
setStats({ ...handle.stats });
|
|
54
67
|
handle.onUpdate((s) => {
|
|
55
68
|
const tot = totalTokens(s.totals);
|
|
56
69
|
const delta = Math.max(0, tot - lastTotalRef.current);
|
|
@@ -91,7 +104,7 @@ function Header({ auth }) {
|
|
|
91
104
|
const ok = auth.installed && auth.loggedIn;
|
|
92
105
|
const dot = ok ? 'green' : auth.installed ? 'yellow' : 'red';
|
|
93
106
|
const planChip = planChipFor(auth);
|
|
94
|
-
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 " }), _jsx(Text, { dimColor: true, children: "v0.1.0 \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, { ...planChip.style, children: planChip.label }), auth.userIdShort && _jsx(Text, { dimColor: true, children: ` user ${auth.userIdShort}` })] }) }), auth.hint && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", auth.hint] }) }))] }));
|
|
107
|
+
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 " }), _jsx(Text, { dimColor: true, children: "v0.1.0 \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, { ...planChip.style, children: planChip.label }), auth.userIdShort && (_jsx(Text, { dimColor: true, children: ` user ${maskUserId(auth.userIdShort, OPTS.reveal)}` }))] }) }), auth.hint && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", auth.hint] }) }))] }));
|
|
95
108
|
}
|
|
96
109
|
function planChipFor(auth) {
|
|
97
110
|
if (!auth.loggedIn)
|
|
@@ -111,7 +124,7 @@ function planChipFor(auth) {
|
|
|
111
124
|
}
|
|
112
125
|
// ── Session panel ────────────────────────────────────────────────────────────
|
|
113
126
|
function SessionPanel({ stats, rate, now, series, }) {
|
|
114
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", flexGrow: 1, minWidth: 42, children: [_jsx(Text, { bold: true, color: "green", children: "Active session" }), !stats ? (_jsx(Text, { dimColor: true, children: "Waiting for a Claude Code session\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(KV, { k: "Model", v: _jsx(Text, { color: "cyan", children: stats.lastModel ?? '—' }) }), _jsx(KV, { k: "Msgs ", v: _jsx(Text, { children: stats.messageCount }) }), _jsx(KV, { k: "Last ", v: stats.lastEventAt ? (_jsxs(Text, { children: [timeAgo(now - stats.lastEventAt), " ago"] })) : (_jsx(Text, { dimColor: true, children: "\u2014" })) }), _jsx(KV, { k: "cwd ", v: _jsx(Text, { dimColor: true, wrap: "truncate-middle", children: stats.cwd ?? '—' }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "\u03A3 " }), _jsx(Text, { color: "cyan", bold: true, children: fmtNumber(totalTokens(stats.totals)) }), _jsx(Text, { dimColor: true, children: " tokens" })] }), (() => {
|
|
127
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, flexDirection: "column", flexGrow: 1, minWidth: 42, children: [_jsx(Text, { bold: true, color: "green", children: "Active session" }), !stats ? (_jsx(Text, { dimColor: true, children: "Waiting for a Claude Code session\u2026" })) : (_jsxs(_Fragment, { children: [_jsx(KV, { k: "Model", v: _jsx(Text, { color: "cyan", children: stats.lastModel ?? '—' }) }), _jsx(KV, { k: "Msgs ", v: _jsx(Text, { children: stats.messageCount }) }), _jsx(KV, { k: "Last ", v: stats.lastEventAt ? (_jsxs(Text, { children: [timeAgo(now - stats.lastEventAt), " ago"] })) : (_jsx(Text, { dimColor: true, children: "\u2014" })) }), _jsx(KV, { k: "cwd ", v: _jsx(Text, { dimColor: true, wrap: "truncate-middle", children: OPTS.reveal ? (stats.cwd ?? '—') : anonymizePath(stats.cwd) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "\u03A3 " }), _jsx(Text, { color: "cyan", bold: true, children: fmtNumber(totalTokens(stats.totals)) }), _jsx(Text, { dimColor: true, children: " tokens" })] }), (() => {
|
|
115
128
|
const cost = estimateCostUSD(stats.lastModel ?? '', stats.totals);
|
|
116
129
|
return cost !== null ? (_jsxs(Text, { dimColor: true, children: ["~", fmtUSD(cost), " API-equivalent"] })) : null;
|
|
117
130
|
})(), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "~ " }), _jsx(Text, { color: "yellow", children: fmtNumber(Math.round(rate)) }), _jsx(Text, { dimColor: true, children: " tok/min" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["activity (last ", SPARK_WIDTH, "s)"] }), _jsx(Text, { color: "yellow", children: sparkline(series, SPARK_WIDTH) })] })] }))] }));
|
|
@@ -131,7 +144,7 @@ function BarRow({ label, value, max, color, }) {
|
|
|
131
144
|
function TranscriptsPanel({ transcripts, activePath, now, }) {
|
|
132
145
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsx(Text, { bold: true, children: "Recent transcripts" }), transcripts.length === 0 ? (_jsx(Text, { dimColor: true, children: "None found under ~/.claude/projects" })) : (transcripts.map((t) => {
|
|
133
146
|
const isActive = t.path === activePath;
|
|
134
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: isActive ? 'green' : 'gray', children: isActive ? '▶ ' : ' ' }), _jsx(Text, { dimColor: !isActive, wrap: "truncate-middle", children: t.cwd }), _jsx(Text, { dimColor: true, children: ` ${timeAgo(now - t.mtimeMs)}` })] }, t.path));
|
|
147
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: isActive ? 'green' : 'gray', children: isActive ? '▶ ' : ' ' }), _jsx(Text, { dimColor: !isActive, wrap: "truncate-middle", children: OPTS.reveal ? t.cwd : anonymizePath(t.cwd) }), _jsx(Text, { dimColor: true, children: ` ${timeAgo(now - t.mtimeMs)}` })] }, t.path));
|
|
135
148
|
}))] }));
|
|
136
149
|
}
|
|
137
150
|
// ── helpers ──────────────────────────────────────────────────────────────────
|