@tokz/cli 0.2.3 → 0.2.4
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 +52 -3
- package/dist/{Root-6SZ5FQDN.js → Root-WEIYEE6M.js} +239 -41
- package/dist/chunk-65BQE6TI.js +39 -0
- package/dist/chunk-6Y6MKFHZ.js +125 -0
- package/dist/{chunk-JAZOB7NC.js → chunk-Z5W5QH2Z.js} +114 -75
- package/dist/cli.js +166 -19
- package/dist/statusline-A36XPBG6.js +145 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,14 @@ Audit where your coding agents' context windows — and API dollars — actually
|
|
|
4
4
|
|
|
5
5
|
Supports multiple agentic coders: **Claude Code** (`~/.claude/projects`),
|
|
6
6
|
**OpenAI Codex CLI** (`~/.codex/sessions` rollout files, cumulative
|
|
7
|
-
token-count deltas),
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
token-count deltas), **OpenCode** (`~/.local/share/opencode/storage`), and
|
|
8
|
+
**Antigravity CLI** (`~/.gemini/antigravity-cli`). Antigravity stores no
|
|
9
|
+
token counts on disk, so its numbers are *estimates*: per-model turn counts
|
|
10
|
+
are read from the conversation databases' generation-metadata records,
|
|
11
|
+
projects and dates from `history.jsonl`, and tokens are derived from
|
|
12
|
+
conversation content size (~4 chars/token) — the UI labels it "estimated"
|
|
13
|
+
everywhere. Cursor CLI is detected and listed with the reason it can't be
|
|
14
|
+
parsed yet. The TUI opens with an agent picker — choose which agent's
|
|
10
15
|
analytics to explore; everything downstream (projects, dashboards,
|
|
11
16
|
timeframes) is scoped to it.
|
|
12
17
|
|
|
@@ -14,11 +19,55 @@ timeframes) is scoped to it.
|
|
|
14
19
|
npx @tokz/cli # interactive TUI: browse projects, drill into charts
|
|
15
20
|
npx @tokz/cli audit # static report for the current project
|
|
16
21
|
npx @tokz/cli audit --all # static report across every project on this machine
|
|
22
|
+
npx @tokz/cli blocks # Claude usage by rolling 5-hour billing window
|
|
17
23
|
|
|
18
24
|
npm i -g @tokz/cli # installs the `tokz` command
|
|
19
25
|
tokz # then just: tokz
|
|
20
26
|
```
|
|
21
27
|
|
|
28
|
+
## Blocks (5-hour billing windows)
|
|
29
|
+
|
|
30
|
+
`tokz blocks` groups Claude usage into the rolling 5-hour windows Claude's
|
|
31
|
+
usage limits operate on: one row per block with models, total tokens, and
|
|
32
|
+
cost, plus burn rate (tok/min and $/hr), projected tokens/cost by block end,
|
|
33
|
+
and time remaining for the active block. `--active` shows only the current
|
|
34
|
+
block, `--recent` the last 3 days, `--token-limit N` (or `max` — your
|
|
35
|
+
biggest past block) adds ⚠️/🚨 warnings, `--session-length H` changes the
|
|
36
|
+
window, `--json` for raw output.
|
|
37
|
+
|
|
38
|
+
## Statusline for Claude Code
|
|
39
|
+
|
|
40
|
+
`tokz statusline` renders one compact line for Claude Code's status bar —
|
|
41
|
+
model, session cost, today's total, active block cost with time left, burn
|
|
42
|
+
rate, and context usage with percentage. One command wires it up:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tokz statusline enable # writes the hook into ~/.claude/settings.json
|
|
46
|
+
tokz statusline disable # removes it (only if it's tokz's)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`enable` preserves everything else in your settings file (and refuses to
|
|
50
|
+
touch one it can't parse); it's equivalent to:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"statusLine": { "type": "command", "command": "npx -y @tokz/cli statusline" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
It reads only transcripts touched in the last few hours and never fetches
|
|
59
|
+
pricing from the network on this path, so it stays fast.
|
|
60
|
+
|
|
61
|
+
## Dates, ranges, and timezones
|
|
62
|
+
|
|
63
|
+
- `tokz audit --since 2026-07-01 --until 2026-07-15` — any inclusive date
|
|
64
|
+
range (also accepts `YYYYMMDD`); `--days N` still works.
|
|
65
|
+
- `tokz audit --weekly` / `--monthly` — appends an activity table rolled up
|
|
66
|
+
by ISO week (keyed by its Monday) or calendar month. In the TUI the
|
|
67
|
+
Activity tab cycles day · week · month with `g`.
|
|
68
|
+
- `tokz --timezone local audit …` — group days in your system timezone, or
|
|
69
|
+
any IANA zone (`--timezone Asia/Amman`); default is UTC.
|
|
70
|
+
|
|
22
71
|
## Interactive TUI
|
|
23
72
|
|
|
24
73
|
Bare `tokz` launches a full-screen terminal UI (Ink) with a live parse
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findMcpServers
|
|
4
|
+
} from "./chunk-65BQE6TI.js";
|
|
2
5
|
import {
|
|
3
6
|
addUsage,
|
|
4
7
|
buildReport,
|
|
@@ -6,9 +9,10 @@ import {
|
|
|
6
9
|
cacheSavings,
|
|
7
10
|
compact,
|
|
8
11
|
costUsd,
|
|
12
|
+
dayKey,
|
|
9
13
|
duration,
|
|
10
14
|
emptyUsage,
|
|
11
|
-
|
|
15
|
+
groupDaily,
|
|
12
16
|
parseTranscript,
|
|
13
17
|
pct,
|
|
14
18
|
pct1,
|
|
@@ -16,19 +20,19 @@ import {
|
|
|
16
20
|
sanitizeProjectPath,
|
|
17
21
|
shortModel,
|
|
18
22
|
usd
|
|
19
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-Z5W5QH2Z.js";
|
|
20
24
|
|
|
21
25
|
// src/ui/Root.tsx
|
|
22
26
|
import { useEffect as useEffect2, useState as useState6 } from "react";
|
|
23
27
|
import { Box as Box10, Text as Text11, useApp as useApp2 } from "ink";
|
|
24
28
|
|
|
25
29
|
// src/agents/index.ts
|
|
26
|
-
import { access as
|
|
27
|
-
import { homedir as
|
|
28
|
-
import { join as
|
|
30
|
+
import { access as access5 } from "fs/promises";
|
|
31
|
+
import { homedir as homedir6 } from "os";
|
|
32
|
+
import { join as join6 } from "path";
|
|
29
33
|
|
|
30
|
-
// src/agents/
|
|
31
|
-
import { access } from "fs/promises";
|
|
34
|
+
// src/agents/antigravity.ts
|
|
35
|
+
import { access, readFile as readFile2, readdir } from "fs/promises";
|
|
32
36
|
import { homedir as homedir2 } from "os";
|
|
33
37
|
import { join as join2 } from "path";
|
|
34
38
|
|
|
@@ -175,14 +179,187 @@ async function loadProjects(home = homedir(), onProgress) {
|
|
|
175
179
|
return out;
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// src/agents/antigravity.ts
|
|
183
|
+
var ESTIMATED_CHARS_PER_TOKEN = 4;
|
|
184
|
+
var INPUT_SHARE = 0.85;
|
|
185
|
+
function antigravityRoot(home) {
|
|
186
|
+
return join2(home ?? homedir2(), ".gemini", "antigravity-cli");
|
|
187
|
+
}
|
|
188
|
+
function modelIdFromDisplayName(name) {
|
|
189
|
+
const bare = name.replace(/\s*\(.*\)\s*$/, "").trim();
|
|
190
|
+
const slug = bare.toLowerCase().replace(/\s+/g, "-");
|
|
191
|
+
return slug.startsWith("claude") ? slug.replace(/\./g, "-") : slug;
|
|
192
|
+
}
|
|
193
|
+
var MODEL_NAME_RX = /(Gemini|Claude|GPT)[- ][0-9][0-9A-Za-z. ]{0,25}(\((Low|Medium|High|Max|Thinking)\))?/;
|
|
194
|
+
function scanConversation(buf) {
|
|
195
|
+
const text = buf.toString("latin1");
|
|
196
|
+
const turnsByModel = {};
|
|
197
|
+
const anchor = /used_claude_conservative/g;
|
|
198
|
+
let m;
|
|
199
|
+
while (m = anchor.exec(text)) {
|
|
200
|
+
const window = text.slice(m.index + 24, m.index + 250);
|
|
201
|
+
const name = window.match(MODEL_NAME_RX);
|
|
202
|
+
const key = name ? modelIdFromDisplayName(name[0]) : "";
|
|
203
|
+
turnsByModel[key] = (turnsByModel[key] ?? 0) + 1;
|
|
204
|
+
}
|
|
205
|
+
let textChars = 0;
|
|
206
|
+
let start = -1;
|
|
207
|
+
for (let i = 0; i <= buf.length; i++) {
|
|
208
|
+
const c = i < buf.length ? buf[i] : 0;
|
|
209
|
+
const printable = c >= 32 && c < 127;
|
|
210
|
+
if (printable && start < 0) start = i;
|
|
211
|
+
if (!printable && start >= 0) {
|
|
212
|
+
if (i - start >= 20) textChars += i - start;
|
|
213
|
+
start = -1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { turnsByModel, textChars };
|
|
217
|
+
}
|
|
218
|
+
async function readHistory(root) {
|
|
219
|
+
try {
|
|
220
|
+
const raw = await readFile2(join2(root, "history.jsonl"), "utf8");
|
|
221
|
+
const out = [];
|
|
222
|
+
for (const line of raw.split("\n")) {
|
|
223
|
+
if (!line.trim()) continue;
|
|
224
|
+
try {
|
|
225
|
+
out.push(JSON.parse(line));
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function loadAntigravityProjects(home, onProgress) {
|
|
235
|
+
const root = antigravityRoot(home);
|
|
236
|
+
const convDir = join2(root, "conversations");
|
|
237
|
+
let dbFiles;
|
|
238
|
+
try {
|
|
239
|
+
dbFiles = (await readdir(convDir)).filter((f) => f.endsWith(".db"));
|
|
240
|
+
} catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
if (dbFiles.length === 0) return [];
|
|
244
|
+
const history = await readHistory(root);
|
|
245
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const e of history) {
|
|
247
|
+
if (!e.conversationId || !e.timestamp) continue;
|
|
248
|
+
const c = byConversation.get(e.conversationId) ?? {};
|
|
249
|
+
c.workspace ??= e.workspace;
|
|
250
|
+
if (!c.firstTs || e.timestamp < c.firstTs) c.firstTs = e.timestamp;
|
|
251
|
+
if (!c.lastTs || e.timestamp > c.lastTs) c.lastTs = e.timestamp;
|
|
252
|
+
byConversation.set(e.conversationId, c);
|
|
253
|
+
}
|
|
254
|
+
const sessions = [];
|
|
255
|
+
let parsed = 0;
|
|
256
|
+
for (const file of dbFiles) {
|
|
257
|
+
onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
|
|
258
|
+
let scan;
|
|
259
|
+
try {
|
|
260
|
+
scan = scanConversation(await readFile2(join2(convDir, file)));
|
|
261
|
+
} catch {
|
|
262
|
+
parsed += 1;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
parsed += 1;
|
|
266
|
+
onProgress?.({ parsed, total: dbFiles.length, currentProject: "Antigravity conversations" });
|
|
267
|
+
const named = Object.entries(scan.turnsByModel).filter(([k]) => k !== "");
|
|
268
|
+
named.sort((a, b) => b[1] - a[1]);
|
|
269
|
+
const fallback = named[0]?.[0] ?? "antigravity-unknown";
|
|
270
|
+
const turnsByModel = {};
|
|
271
|
+
for (const [model, turns] of Object.entries(scan.turnsByModel)) {
|
|
272
|
+
const key = model === "" ? fallback : model;
|
|
273
|
+
turnsByModel[key] = (turnsByModel[key] ?? 0) + turns;
|
|
274
|
+
}
|
|
275
|
+
const totalTurns = Object.values(turnsByModel).reduce((s, n) => s + n, 0);
|
|
276
|
+
if (totalTurns === 0) continue;
|
|
277
|
+
const conversationId = file.replace(/\.db$/, "");
|
|
278
|
+
const meta = byConversation.get(conversationId);
|
|
279
|
+
const firstTs = meta?.firstTs ? new Date(meta.firstTs).toISOString() : void 0;
|
|
280
|
+
const lastTs = meta?.lastTs ? new Date(meta.lastTs).toISOString() : void 0;
|
|
281
|
+
const day = (lastTs ?? firstTs)?.slice(0, 10);
|
|
282
|
+
const estTokens = Math.round(scan.textChars / ESTIMATED_CHARS_PER_TOKEN);
|
|
283
|
+
const stats = {
|
|
284
|
+
file: conversationId,
|
|
285
|
+
cwd: meta?.workspace,
|
|
286
|
+
firstTs,
|
|
287
|
+
lastTs,
|
|
288
|
+
usageByModel: {},
|
|
289
|
+
toolCalls: {},
|
|
290
|
+
toolCostUsd: {},
|
|
291
|
+
dailyUsage: {}
|
|
292
|
+
};
|
|
293
|
+
for (const [model, turns] of Object.entries(turnsByModel)) {
|
|
294
|
+
const share = turns / totalTurns;
|
|
295
|
+
const u = stats.usageByModel[model] ??= emptyUsage();
|
|
296
|
+
u.inputTokens += Math.round(estTokens * share * INPUT_SHARE);
|
|
297
|
+
u.outputTokens += Math.round(estTokens * share * (1 - INPUT_SHARE));
|
|
298
|
+
u.turns += turns;
|
|
299
|
+
if (day) {
|
|
300
|
+
const d = (stats.dailyUsage[day] ??= {})[model] ??= emptyUsage();
|
|
301
|
+
d.inputTokens += u.inputTokens;
|
|
302
|
+
d.outputTokens += u.outputTokens;
|
|
303
|
+
d.turns += turns;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
sessions.push(stats);
|
|
307
|
+
}
|
|
308
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
309
|
+
for (const s of sessions) {
|
|
310
|
+
const key = s.cwd ?? "(unknown project)";
|
|
311
|
+
const list = byDir.get(key) ?? [];
|
|
312
|
+
list.push(s);
|
|
313
|
+
byDir.set(key, list);
|
|
314
|
+
}
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const [dir, list] of byDir) {
|
|
317
|
+
out.push({
|
|
318
|
+
id: `antigravity:${dir}`,
|
|
319
|
+
name: dir,
|
|
320
|
+
label: dir === "(unknown project)" ? dir : baseName(dir),
|
|
321
|
+
realPath: dir,
|
|
322
|
+
report: buildReport(list, []),
|
|
323
|
+
sessions: list,
|
|
324
|
+
serverList: []
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
out.sort((a, b) => b.report.totalCostUsd - a.report.totalCostUsd);
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
var antigravityAdapter = {
|
|
331
|
+
id: "antigravity",
|
|
332
|
+
name: "Antigravity",
|
|
333
|
+
supported: true,
|
|
334
|
+
estimated: true,
|
|
335
|
+
estimateNote: "no token counts on disk \u2014 tokens/costs estimated from conversation size",
|
|
336
|
+
async detect(home) {
|
|
337
|
+
try {
|
|
338
|
+
await access(join2(antigravityRoot(home), "conversations"));
|
|
339
|
+
return true;
|
|
340
|
+
} catch {
|
|
341
|
+
try {
|
|
342
|
+
await access(join2(home ?? homedir2(), ".gemini", "antigravity", "conversations"));
|
|
343
|
+
return true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
loadProjects: loadAntigravityProjects
|
|
350
|
+
};
|
|
351
|
+
|
|
178
352
|
// src/agents/claude.ts
|
|
353
|
+
import { access as access2 } from "fs/promises";
|
|
354
|
+
import { homedir as homedir3 } from "os";
|
|
355
|
+
import { join as join3 } from "path";
|
|
179
356
|
var claudeAdapter = {
|
|
180
357
|
id: "claude",
|
|
181
358
|
name: "Claude Code",
|
|
182
359
|
supported: true,
|
|
183
|
-
async detect(home =
|
|
360
|
+
async detect(home = homedir3()) {
|
|
184
361
|
try {
|
|
185
|
-
await
|
|
362
|
+
await access2(join3(home, ".claude", "projects"));
|
|
186
363
|
return true;
|
|
187
364
|
} catch {
|
|
188
365
|
return false;
|
|
@@ -193,10 +370,10 @@ var claudeAdapter = {
|
|
|
193
370
|
|
|
194
371
|
// src/agents/codex.ts
|
|
195
372
|
import { createReadStream } from "fs";
|
|
196
|
-
import { access as
|
|
373
|
+
import { access as access3 } from "fs/promises";
|
|
197
374
|
import { createInterface } from "readline";
|
|
198
|
-
import { homedir as
|
|
199
|
-
import { join as
|
|
375
|
+
import { homedir as homedir4 } from "os";
|
|
376
|
+
import { join as join4 } from "path";
|
|
200
377
|
import { glob as glob2 } from "tinyglobby";
|
|
201
378
|
import { z } from "zod";
|
|
202
379
|
var TokenTotals = z.object({
|
|
@@ -220,8 +397,8 @@ var Line = z.object({
|
|
|
220
397
|
}).passthrough().optional()
|
|
221
398
|
});
|
|
222
399
|
function codexHome(home) {
|
|
223
|
-
if (home) return
|
|
224
|
-
return process.env.CODEX_HOME ??
|
|
400
|
+
if (home) return join4(home, ".codex");
|
|
401
|
+
return process.env.CODEX_HOME ?? join4(homedir4(), ".codex");
|
|
225
402
|
}
|
|
226
403
|
var DEFAULT_MODEL = "gpt-5";
|
|
227
404
|
async function parseCodexRollout(file) {
|
|
@@ -337,7 +514,7 @@ var codexAdapter = {
|
|
|
337
514
|
supported: true,
|
|
338
515
|
async detect(home) {
|
|
339
516
|
try {
|
|
340
|
-
await
|
|
517
|
+
await access3(join4(codexHome(home), "sessions"));
|
|
341
518
|
return true;
|
|
342
519
|
} catch {
|
|
343
520
|
return false;
|
|
@@ -347,9 +524,9 @@ var codexAdapter = {
|
|
|
347
524
|
};
|
|
348
525
|
|
|
349
526
|
// src/agents/opencode.ts
|
|
350
|
-
import { access as
|
|
351
|
-
import { homedir as
|
|
352
|
-
import { join as
|
|
527
|
+
import { access as access4, readFile as readFile3 } from "fs/promises";
|
|
528
|
+
import { homedir as homedir5 } from "os";
|
|
529
|
+
import { join as join5 } from "path";
|
|
353
530
|
import { glob as glob3 } from "tinyglobby";
|
|
354
531
|
import { z as z2 } from "zod";
|
|
355
532
|
var Message = z2.object({
|
|
@@ -373,19 +550,19 @@ var Session = z2.object({
|
|
|
373
550
|
});
|
|
374
551
|
var Project = z2.object({ id: z2.string(), worktree: z2.string().optional() });
|
|
375
552
|
function opencodeRoot(home) {
|
|
376
|
-
if (home) return
|
|
377
|
-
return process.env.OPENCODE_DATA_DIR ??
|
|
553
|
+
if (home) return join5(home, ".local", "share", "opencode");
|
|
554
|
+
return process.env.OPENCODE_DATA_DIR ?? join5(homedir5(), ".local", "share", "opencode");
|
|
378
555
|
}
|
|
379
556
|
async function readJson(file) {
|
|
380
557
|
try {
|
|
381
|
-
return JSON.parse(await
|
|
558
|
+
return JSON.parse(await readFile3(file, "utf8"));
|
|
382
559
|
} catch {
|
|
383
560
|
return void 0;
|
|
384
561
|
}
|
|
385
562
|
}
|
|
386
563
|
async function loadOpencodeProjects(home, onProgress) {
|
|
387
564
|
const root = opencodeRoot(home);
|
|
388
|
-
const storage =
|
|
565
|
+
const storage = join5(root, "storage");
|
|
389
566
|
const messageFiles = await glob3(["message/*/*.json"], { cwd: storage, absolute: true }).catch(
|
|
390
567
|
() => []
|
|
391
568
|
);
|
|
@@ -471,7 +648,7 @@ var opencodeAdapter = {
|
|
|
471
648
|
supported: true,
|
|
472
649
|
async detect(home) {
|
|
473
650
|
try {
|
|
474
|
-
await
|
|
651
|
+
await access4(join5(opencodeRoot(home), "storage", "message"));
|
|
475
652
|
return true;
|
|
476
653
|
} catch {
|
|
477
654
|
return false;
|
|
@@ -481,14 +658,15 @@ var opencodeAdapter = {
|
|
|
481
658
|
};
|
|
482
659
|
|
|
483
660
|
// src/agents/index.ts
|
|
484
|
-
function detectOnly(id, name, ...pathParts) {
|
|
661
|
+
function detectOnly(id, name, reason, ...pathParts) {
|
|
485
662
|
return {
|
|
486
663
|
id,
|
|
487
664
|
name,
|
|
488
665
|
supported: false,
|
|
489
|
-
|
|
666
|
+
unsupportedReason: reason,
|
|
667
|
+
async detect(home = homedir6()) {
|
|
490
668
|
try {
|
|
491
|
-
await
|
|
669
|
+
await access5(join6(home, ...pathParts));
|
|
492
670
|
return true;
|
|
493
671
|
} catch {
|
|
494
672
|
return false;
|
|
@@ -501,8 +679,8 @@ var ADAPTERS = [
|
|
|
501
679
|
claudeAdapter,
|
|
502
680
|
codexAdapter,
|
|
503
681
|
opencodeAdapter,
|
|
504
|
-
|
|
505
|
-
detectOnly("cursor", "Cursor CLI", ".cursor", "chats")
|
|
682
|
+
antigravityAdapter,
|
|
683
|
+
detectOnly("cursor", "Cursor CLI", "sessions live in SQLite; parsing not supported yet", ".cursor", "chats")
|
|
506
684
|
];
|
|
507
685
|
async function loadAllAgents(home, onProgress) {
|
|
508
686
|
const out = [];
|
|
@@ -529,7 +707,7 @@ var TIMEFRAMES = [
|
|
|
529
707
|
{ id: "7d", label: "Last 7 days" },
|
|
530
708
|
{ id: "30d", label: "Last 30 days" }
|
|
531
709
|
];
|
|
532
|
-
var isoDay = (offsetDays, now) =>
|
|
710
|
+
var isoDay = (offsetDays, now) => dayKey(now - offsetDays * 864e5);
|
|
533
711
|
function timeframeLabel(id) {
|
|
534
712
|
return TIMEFRAMES.find((t) => t.id === id)?.label ?? id;
|
|
535
713
|
}
|
|
@@ -625,9 +803,15 @@ function status(a) {
|
|
|
625
803
|
if (a.projects.length > 0) {
|
|
626
804
|
const cost = a.projects.reduce((s, p) => s + p.report.totalCostUsd, 0);
|
|
627
805
|
const sessions = a.projects.reduce((s, p) => s + p.report.sessionCount, 0);
|
|
628
|
-
|
|
806
|
+
const est = a.adapter.estimated ? `~` : "";
|
|
807
|
+
const tail = a.adapter.estimated ? " \xB7 estimated" : "";
|
|
808
|
+
return { text: `${a.projects.length} projects \xB7 ${sessions} sessions \xB7 ${est}${usd(cost)}${tail}` };
|
|
629
809
|
}
|
|
630
|
-
if (a.detected && !a.adapter.supported)
|
|
810
|
+
if (a.detected && !a.adapter.supported)
|
|
811
|
+
return {
|
|
812
|
+
text: `detected \xB7 ${a.adapter.unsupportedReason ?? "parsing not supported yet"}`,
|
|
813
|
+
color: theme.warn
|
|
814
|
+
};
|
|
631
815
|
if (a.detected) return { text: "detected \xB7 no usage data", dim: true };
|
|
632
816
|
return { text: "not detected", dim: true };
|
|
633
817
|
}
|
|
@@ -1188,24 +1372,33 @@ function Sessions({ r, cols }) {
|
|
|
1188
1372
|
] }) : null
|
|
1189
1373
|
] });
|
|
1190
1374
|
}
|
|
1191
|
-
function Activity({
|
|
1192
|
-
|
|
1375
|
+
function Activity({
|
|
1376
|
+
r,
|
|
1377
|
+
chartW,
|
|
1378
|
+
cols,
|
|
1379
|
+
grouping
|
|
1380
|
+
}) {
|
|
1381
|
+
if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
|
|
1193
1382
|
const wide = cols >= 66;
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1383
|
+
const source = grouping === "day" ? lastDays(r, 21).filter((d, i, all) => d.costUsd > 0 || i >= all.length - 14) : groupDaily(r.daily, grouping);
|
|
1384
|
+
const rows = source.slice(-16).map((d) => ({
|
|
1385
|
+
label: grouping === "day" ? `${d.date} ${relativeDate(d.date) === "today" ? "\u25C2" : " "}` : grouping === "week" ? `wk ${d.date}` : d.date,
|
|
1196
1386
|
value: d.costUsd,
|
|
1197
1387
|
display: d.costUsd > 0 ? wide ? `${usd(d.costUsd)} \xB7 ${compact(d.turns)} turns` : usd(d.costUsd) : "\u2014"
|
|
1198
1388
|
}));
|
|
1199
|
-
if (r.daily.length === 0) return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "no dated activity" });
|
|
1200
1389
|
const avg = r.totalCostUsd / Math.max(1, r.daily.length);
|
|
1201
1390
|
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
|
|
1202
1391
|
/* @__PURE__ */ jsx8(BarChart, { rows, width: chartW }),
|
|
1203
1392
|
/* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
|
|
1393
|
+
/* @__PURE__ */ jsx8(Text8, { color: theme.accent, children: "g" }),
|
|
1394
|
+
" group: ",
|
|
1395
|
+
grouping,
|
|
1396
|
+
" \xB7 ",
|
|
1204
1397
|
r.daily.length,
|
|
1205
|
-
" active days \xB7 avg
|
|
1206
|
-
usd(avg),
|
|
1207
|
-
"/active day \xB7 projected",
|
|
1398
|
+
" active days \xB7 avg",
|
|
1208
1399
|
" ",
|
|
1400
|
+
usd(avg),
|
|
1401
|
+
"/active day \xB7 projected ",
|
|
1209
1402
|
usd(r.monthlyProjectionUsd),
|
|
1210
1403
|
"/mo"
|
|
1211
1404
|
] }) })
|
|
@@ -1217,12 +1410,15 @@ function Dashboard({
|
|
|
1217
1410
|
timeframe
|
|
1218
1411
|
}) {
|
|
1219
1412
|
const [tab, setTab] = useState4(initialTab);
|
|
1413
|
+
const [grouping, setGrouping] = useState4("day");
|
|
1220
1414
|
const { cols } = useTerminalSize();
|
|
1221
1415
|
useInput3((input, key) => {
|
|
1222
1416
|
const n = Number.parseInt(input, 10);
|
|
1223
1417
|
if (n >= 1 && n <= TABS.length) setTab(n - 1);
|
|
1224
1418
|
if (key.rightArrow) setTab((t) => (t + 1) % TABS.length);
|
|
1225
1419
|
if (key.leftArrow) setTab((t) => (t + TABS.length - 1) % TABS.length);
|
|
1420
|
+
if (input === "g")
|
|
1421
|
+
setGrouping((g) => g === "day" ? "week" : g === "week" ? "month" : "day");
|
|
1226
1422
|
});
|
|
1227
1423
|
const r = project.report;
|
|
1228
1424
|
const span = r.spanStart && r.spanEnd ? `${r.spanStart} \u2192 ${r.spanEnd}` : `${r.spanDays}d`;
|
|
@@ -1255,7 +1451,7 @@ function Dashboard({
|
|
|
1255
1451
|
tab === 2 ? /* @__PURE__ */ jsx8(Tools, { r, chartW, cols }) : null,
|
|
1256
1452
|
tab === 3 ? /* @__PURE__ */ jsx8(Servers, { r, cols }) : null,
|
|
1257
1453
|
tab === 4 ? /* @__PURE__ */ jsx8(Sessions, { r, cols }) : null,
|
|
1258
|
-
tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols }) : null
|
|
1454
|
+
tab === 5 ? /* @__PURE__ */ jsx8(Activity, { r, chartW, cols, grouping }) : null
|
|
1259
1455
|
] })
|
|
1260
1456
|
] });
|
|
1261
1457
|
}
|
|
@@ -1270,6 +1466,7 @@ var KEYS = [
|
|
|
1270
1466
|
["/", "filter project list (esc clears)"],
|
|
1271
1467
|
["s", "cycle project sort: cost \xB7 recent \xB7 name"],
|
|
1272
1468
|
["t", "cycle timeframe: all \xB7 today \xB7 yesterday \xB7 7d \xB7 30d"],
|
|
1469
|
+
["g", "Activity tab: group by day \xB7 week \xB7 month"],
|
|
1273
1470
|
["esc", "go back"],
|
|
1274
1471
|
["?", "toggle this help"],
|
|
1275
1472
|
["q", "quit"]
|
|
@@ -1339,7 +1536,8 @@ function App({
|
|
|
1339
1536
|
[activeProjects, timeframe]
|
|
1340
1537
|
);
|
|
1341
1538
|
const totals = useMemo2(() => aggregate(scoped), [scoped]);
|
|
1342
|
-
const
|
|
1539
|
+
const activeAdapter = agentList[agentIdx]?.adapter;
|
|
1540
|
+
const agentName = activeAdapter ? activeAdapter.name + (activeAdapter.estimated ? " (estimated)" : "") : "";
|
|
1343
1541
|
useInput4((input, key) => {
|
|
1344
1542
|
if (help) {
|
|
1345
1543
|
setHelp(false);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp.ts
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
async function readJson(file) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function serverNames(obj) {
|
|
15
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) return Object.keys(obj);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
async function findMcpServers(projectPath, home = homedir()) {
|
|
19
|
+
const out = /* @__PURE__ */ new Map();
|
|
20
|
+
const add = (names, source) => {
|
|
21
|
+
for (const name of names) if (!out.has(name)) out.set(name, { name, source });
|
|
22
|
+
};
|
|
23
|
+
const projectFile = join(projectPath, ".mcp.json");
|
|
24
|
+
const projectCfg = await readJson(projectFile);
|
|
25
|
+
add(serverNames(projectCfg?.mcpServers), projectFile);
|
|
26
|
+
const globalFile = join(home, ".claude.json");
|
|
27
|
+
const globalCfg = await readJson(globalFile);
|
|
28
|
+
add(serverNames(globalCfg?.mcpServers), globalFile);
|
|
29
|
+
const projects = globalCfg?.projects;
|
|
30
|
+
if (projects && typeof projects === "object") {
|
|
31
|
+
const entry = projects[projectPath];
|
|
32
|
+
add(serverNames(entry?.mcpServers), `${globalFile} (project entry)`);
|
|
33
|
+
}
|
|
34
|
+
return [...out.values()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
findMcpServers
|
|
39
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addUsage,
|
|
4
|
+
compact,
|
|
5
|
+
costUsd,
|
|
6
|
+
emptyUsage,
|
|
7
|
+
shortModel,
|
|
8
|
+
usd
|
|
9
|
+
} from "./chunk-Z5W5QH2Z.js";
|
|
10
|
+
|
|
11
|
+
// src/blocks.ts
|
|
12
|
+
var BLOCK_LENGTH_MS = 5 * 60 * 60 * 1e3;
|
|
13
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
14
|
+
function finalize(b) {
|
|
15
|
+
for (const [model, u] of Object.entries(b.usageByModel)) {
|
|
16
|
+
b.totalTokens += u.inputTokens + u.cacheReadTokens + u.cacheCreationTokens + u.outputTokens;
|
|
17
|
+
b.costUsd += costUsd(u, model).total;
|
|
18
|
+
}
|
|
19
|
+
return b;
|
|
20
|
+
}
|
|
21
|
+
function buildBlocks(events, opts) {
|
|
22
|
+
const len = opts?.sessionLengthMs ?? BLOCK_LENGTH_MS;
|
|
23
|
+
const now = opts?.now ?? Date.now();
|
|
24
|
+
const sorted = [...events].sort((a, b) => a.ts - b.ts);
|
|
25
|
+
const blocks = [];
|
|
26
|
+
let cur;
|
|
27
|
+
for (const e of sorted) {
|
|
28
|
+
if (!cur || e.ts >= cur.end) {
|
|
29
|
+
if (cur) blocks.push(finalize(cur));
|
|
30
|
+
const start = Math.floor(e.ts / HOUR_MS) * HOUR_MS;
|
|
31
|
+
cur = {
|
|
32
|
+
start,
|
|
33
|
+
end: start + len,
|
|
34
|
+
firstTs: e.ts,
|
|
35
|
+
lastTs: e.ts,
|
|
36
|
+
usageByModel: {},
|
|
37
|
+
totalTokens: 0,
|
|
38
|
+
costUsd: 0,
|
|
39
|
+
active: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
cur.lastTs = Math.max(cur.lastTs, e.ts);
|
|
43
|
+
addUsage(cur.usageByModel[e.model] ??= emptyUsage(), e.usage);
|
|
44
|
+
}
|
|
45
|
+
if (cur) {
|
|
46
|
+
cur.active = now < cur.end;
|
|
47
|
+
blocks.push(finalize(cur));
|
|
48
|
+
}
|
|
49
|
+
return blocks;
|
|
50
|
+
}
|
|
51
|
+
function burnRate(b, now = Date.now()) {
|
|
52
|
+
if (!b.active) return void 0;
|
|
53
|
+
const elapsed = Math.max(6e4, now - b.firstTs);
|
|
54
|
+
const remainingMs = Math.max(0, b.end - now);
|
|
55
|
+
const tokensPerMinute = b.totalTokens / (elapsed / 6e4);
|
|
56
|
+
const costPerHour = b.costUsd / (elapsed / HOUR_MS);
|
|
57
|
+
return {
|
|
58
|
+
tokensPerMinute,
|
|
59
|
+
costPerHour,
|
|
60
|
+
projectedTokens: Math.round(b.totalTokens + tokensPerMinute * (remainingMs / 6e4)),
|
|
61
|
+
projectedCostUsd: b.costUsd + costPerHour * (remainingMs / HOUR_MS),
|
|
62
|
+
remainingMs
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function maxBlockTokens(blocks) {
|
|
66
|
+
return blocks.filter((b) => !b.active).reduce((m, b) => Math.max(m, b.totalTokens), 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/blocksReport.ts
|
|
70
|
+
import Table from "cli-table3";
|
|
71
|
+
import pc from "picocolors";
|
|
72
|
+
function fmtMs(ms) {
|
|
73
|
+
const mins = Math.round(ms / 6e4);
|
|
74
|
+
if (mins < 1) return "<1m";
|
|
75
|
+
if (mins < 60) return `${mins}m`;
|
|
76
|
+
const h = Math.floor(mins / 60);
|
|
77
|
+
const m = mins % 60;
|
|
78
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
79
|
+
}
|
|
80
|
+
function fmtStart(ms) {
|
|
81
|
+
const d = new Date(ms);
|
|
82
|
+
return `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 16)} UTC`;
|
|
83
|
+
}
|
|
84
|
+
function renderBlocksReport(blocks, opts) {
|
|
85
|
+
if (blocks.length === 0) return "No Claude Code activity found.";
|
|
86
|
+
const now = opts?.now ?? Date.now();
|
|
87
|
+
const limit = opts?.tokenLimit === "max" ? maxBlockTokens(blocks) : opts?.tokenLimit;
|
|
88
|
+
const parts = [];
|
|
89
|
+
parts.push(
|
|
90
|
+
pc.bold(`tokz blocks \u2014 ${blocks.length} \xD7 5-hour billing windows`) + pc.dim(" (Claude usage limits reset per rolling 5h block)")
|
|
91
|
+
);
|
|
92
|
+
const table = new Table({ head: ["Block start", "Status", "Models", "Tokens", "Cost"] });
|
|
93
|
+
for (const b of blocks) {
|
|
94
|
+
const models = [...new Set(Object.keys(b.usageByModel).map(shortModel))].join(", ");
|
|
95
|
+
const status = b.active ? pc.green(`ACTIVE \xB7 ${fmtMs(b.end - now)} left`) : pc.dim(`done \xB7 ${fmtMs(b.lastTs - b.firstTs)}`);
|
|
96
|
+
const tokens = limit && b.totalTokens > limit ? pc.red(`${compact(b.totalTokens)} \u{1F6A8}`) : limit && b.totalTokens > limit * 0.8 ? pc.yellow(`${compact(b.totalTokens)} \u26A0\uFE0F`) : compact(b.totalTokens);
|
|
97
|
+
table.push([fmtStart(b.start), status, models, tokens, usd(b.costUsd)]);
|
|
98
|
+
}
|
|
99
|
+
parts.push(table.toString());
|
|
100
|
+
const active = blocks.find((b) => b.active);
|
|
101
|
+
const rate = active ? burnRate(active, now) : void 0;
|
|
102
|
+
if (active && rate) {
|
|
103
|
+
const lines = [
|
|
104
|
+
pc.bold("Active block") + ` \u2014 started ${fmtStart(active.start)}, ${pc.green(fmtMs(rate.remainingMs))} remaining`,
|
|
105
|
+
` burn rate ${compact(Math.round(rate.tokensPerMinute))} tok/min \xB7 ${usd(rate.costPerHour)}/hr`,
|
|
106
|
+
` projected ${compact(rate.projectedTokens)} tokens \xB7 ${usd(rate.projectedCostUsd)} by block end`
|
|
107
|
+
];
|
|
108
|
+
if (limit) {
|
|
109
|
+
const over = rate.projectedTokens > limit;
|
|
110
|
+
const nearing = !over && active.totalTokens > limit * 0.8;
|
|
111
|
+
lines.push(
|
|
112
|
+
` limit ${compact(limit)} tokens \u2014 ` + (over ? pc.red(`\u{1F6A8} projected to exceed (${compact(rate.projectedTokens)})`) : nearing ? pc.yellow(`\u26A0\uFE0F ${Math.round(active.totalTokens / limit * 100)}% used`) : pc.green(`${Math.round(active.totalTokens / limit * 100)}% used`))
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
parts.push(lines.join("\n"));
|
|
116
|
+
}
|
|
117
|
+
return parts.join("\n\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
buildBlocks,
|
|
122
|
+
burnRate,
|
|
123
|
+
fmtMs,
|
|
124
|
+
renderBlocksReport
|
|
125
|
+
};
|
|
@@ -12,6 +12,11 @@ var PRICES = {
|
|
|
12
12
|
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
13
13
|
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
|
|
14
14
|
// OpenAI (no cache-write charge; cached input 0.1x)
|
|
15
|
+
"gpt-5.6-sol": { inputPerMTok: 5, outputPerMTok: 30, cacheWriteMult: 0 },
|
|
16
|
+
"gpt-5.6-terra": { inputPerMTok: 2.5, outputPerMTok: 15, cacheWriteMult: 0 },
|
|
17
|
+
"gpt-5.6-luna": { inputPerMTok: 1, outputPerMTok: 6, cacheWriteMult: 0 },
|
|
18
|
+
"gpt-5.5-pro": { inputPerMTok: 30, outputPerMTok: 180, cacheWriteMult: 0, cacheReadMult: 1 },
|
|
19
|
+
"gpt-5.5": { inputPerMTok: 5, outputPerMTok: 30, cacheWriteMult: 0 },
|
|
15
20
|
"gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
|
|
16
21
|
"gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2, cacheWriteMult: 0 },
|
|
17
22
|
"gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4, cacheWriteMult: 0 },
|
|
@@ -22,6 +27,8 @@ var PRICES = {
|
|
|
22
27
|
"o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
23
28
|
o3: { inputPerMTok: 2, outputPerMTok: 8, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
24
29
|
// Google
|
|
30
|
+
"gemini-3.1-pro": { inputPerMTok: 2, outputPerMTok: 12, cacheWriteMult: 0 },
|
|
31
|
+
"gemini-3.5-flash": { inputPerMTok: 1.5, outputPerMTok: 9, cacheWriteMult: 0 },
|
|
25
32
|
"gemini-2.5-pro": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
|
|
26
33
|
"gemini-2.5-flash": { inputPerMTok: 0.3, outputPerMTok: 2.5, cacheWriteMult: 0 }
|
|
27
34
|
};
|
|
@@ -29,17 +36,29 @@ var DEFAULT_CACHE_READ_MULT = 0.1;
|
|
|
29
36
|
var DEFAULT_CACHE_WRITE_MULT = 1.25;
|
|
30
37
|
var CLAUDE_FALLBACK = PRICES["claude-opus-4-8"];
|
|
31
38
|
var UNKNOWN = { inputPerMTok: 0, outputPerMTok: 0 };
|
|
32
|
-
|
|
39
|
+
var livePrices = {};
|
|
40
|
+
var resolveCache = /* @__PURE__ */ new Map();
|
|
41
|
+
function setLivePrices(prices) {
|
|
42
|
+
livePrices = prices;
|
|
43
|
+
resolveCache.clear();
|
|
44
|
+
}
|
|
45
|
+
function longestPrefix(modelId, table) {
|
|
33
46
|
let best;
|
|
34
47
|
let bestLen = -1;
|
|
35
|
-
for (const [prefix, price] of Object.entries(
|
|
48
|
+
for (const [prefix, price] of Object.entries(table)) {
|
|
36
49
|
if (modelId.startsWith(prefix) && prefix.length > bestLen) {
|
|
37
50
|
best = price;
|
|
38
51
|
bestLen = prefix.length;
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
return best;
|
|
55
|
+
}
|
|
56
|
+
function resolvePrice(modelId) {
|
|
57
|
+
const hit = resolveCache.get(modelId);
|
|
58
|
+
if (hit) return hit;
|
|
59
|
+
const price = livePrices[modelId] ?? longestPrefix(modelId, PRICES) ?? longestPrefix(modelId, livePrices) ?? (modelId.startsWith("claude") ? CLAUDE_FALLBACK : UNKNOWN);
|
|
60
|
+
resolveCache.set(modelId, price);
|
|
61
|
+
return price;
|
|
43
62
|
}
|
|
44
63
|
function emptyUsage() {
|
|
45
64
|
return { inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, outputTokens: 0, turns: 0 };
|
|
@@ -53,7 +72,8 @@ function costUsd(usage, modelId) {
|
|
|
53
72
|
const cacheRead = usage.cacheReadTokens / 1e6 * p.inputPerMTok * readMult;
|
|
54
73
|
const write1h = Math.min(usage.cacheCreation1hTokens ?? 0, usage.cacheCreationTokens);
|
|
55
74
|
const write5m = usage.cacheCreationTokens - write1h;
|
|
56
|
-
const
|
|
75
|
+
const write1hMult = p.cacheWrite1hMult ?? (p.cacheWriteMult === 0 ? 0 : CACHE_WRITE_1H_MULT);
|
|
76
|
+
const cacheWrite = write5m / 1e6 * p.inputPerMTok * writeMult + write1h / 1e6 * p.inputPerMTok * write1hMult;
|
|
57
77
|
const output = usage.outputTokens / 1e6 * p.outputPerMTok;
|
|
58
78
|
return { input, cacheRead, cacheWrite, output, total: input + cacheRead + cacheWrite + output };
|
|
59
79
|
}
|
|
@@ -220,6 +240,85 @@ function buildReport(sessions, servers, range) {
|
|
|
220
240
|
};
|
|
221
241
|
}
|
|
222
242
|
|
|
243
|
+
// src/format.ts
|
|
244
|
+
var usd = (n) => `$${n.toFixed(2)}`;
|
|
245
|
+
var tok = (n) => n.toLocaleString("en-US");
|
|
246
|
+
function compact(n) {
|
|
247
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
248
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
249
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
250
|
+
return String(n);
|
|
251
|
+
}
|
|
252
|
+
var pct = (fraction) => `${Math.round(fraction * 100)}%`;
|
|
253
|
+
var pct1 = (fraction) => `${(fraction * 100).toFixed(1)}%`;
|
|
254
|
+
var shortModel = (id) => id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
|
|
255
|
+
function duration(startIso, endIso) {
|
|
256
|
+
if (!startIso || !endIso) return "\u2014";
|
|
257
|
+
const mins = Math.round((Date.parse(endIso) - Date.parse(startIso)) / 6e4);
|
|
258
|
+
if (mins < 1) return "<1m";
|
|
259
|
+
if (mins < 60) return `${mins}m`;
|
|
260
|
+
const h = Math.floor(mins / 60);
|
|
261
|
+
const m = mins % 60;
|
|
262
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
263
|
+
}
|
|
264
|
+
function relativeDate(iso, now = Date.now()) {
|
|
265
|
+
if (!iso) return "\u2014";
|
|
266
|
+
const date = iso.slice(0, 10);
|
|
267
|
+
const days = Math.floor((now - Date.parse(date)) / 864e5);
|
|
268
|
+
if (days <= 0) return "today";
|
|
269
|
+
if (days === 1) return "yesterday";
|
|
270
|
+
if (days < 30) return `${days}d ago`;
|
|
271
|
+
return date;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/dates.ts
|
|
275
|
+
var zone;
|
|
276
|
+
var formatter;
|
|
277
|
+
function setTimezone(tz) {
|
|
278
|
+
zone = tz === "utc" || tz === void 0 ? void 0 : tz;
|
|
279
|
+
formatter = zone ? new Intl.DateTimeFormat("en-CA", {
|
|
280
|
+
timeZone: zone === "local" ? void 0 : zone,
|
|
281
|
+
year: "numeric",
|
|
282
|
+
month: "2-digit",
|
|
283
|
+
day: "2-digit"
|
|
284
|
+
}) : void 0;
|
|
285
|
+
}
|
|
286
|
+
function dayKey(ts) {
|
|
287
|
+
if (!formatter) return typeof ts === "string" ? ts.slice(0, 10) : new Date(ts).toISOString().slice(0, 10);
|
|
288
|
+
return formatter.format(typeof ts === "string" ? new Date(ts) : ts);
|
|
289
|
+
}
|
|
290
|
+
function weekKey(day) {
|
|
291
|
+
const d = /* @__PURE__ */ new Date(`${day}T00:00:00Z`);
|
|
292
|
+
const dow = (d.getUTCDay() + 6) % 7;
|
|
293
|
+
d.setUTCDate(d.getUTCDate() - dow);
|
|
294
|
+
return d.toISOString().slice(0, 10);
|
|
295
|
+
}
|
|
296
|
+
function monthKey(day) {
|
|
297
|
+
return day.slice(0, 7);
|
|
298
|
+
}
|
|
299
|
+
function groupDaily(daily, unit) {
|
|
300
|
+
if (unit === "day") return daily;
|
|
301
|
+
const keyOf = unit === "week" ? weekKey : monthKey;
|
|
302
|
+
const out = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const d of daily) {
|
|
304
|
+
const key = keyOf(d.date);
|
|
305
|
+
const acc = out.get(key) ?? { date: key, costUsd: 0, inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, outputTokens: 0, turns: 0 };
|
|
306
|
+
acc.costUsd += d.costUsd;
|
|
307
|
+
acc.inputTokens += d.inputTokens;
|
|
308
|
+
acc.cacheReadTokens += d.cacheReadTokens;
|
|
309
|
+
acc.cacheCreationTokens += d.cacheCreationTokens;
|
|
310
|
+
acc.outputTokens += d.outputTokens;
|
|
311
|
+
acc.turns += d.turns;
|
|
312
|
+
out.set(key, acc);
|
|
313
|
+
}
|
|
314
|
+
return [...out.values()].sort((a, b) => a.date < b.date ? -1 : 1);
|
|
315
|
+
}
|
|
316
|
+
function parseDateArg(raw) {
|
|
317
|
+
if (!raw) return void 0;
|
|
318
|
+
const m = /^(\d{4})-?(\d{2})-?(\d{2})$/.exec(raw.trim());
|
|
319
|
+
return m ? `${m[1]}-${m[2]}-${m[3]}` : void 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
223
322
|
// src/discover.ts
|
|
224
323
|
import { homedir } from "os";
|
|
225
324
|
import { join } from "path";
|
|
@@ -235,40 +334,6 @@ async function findTranscripts(projectPath, home = homedir()) {
|
|
|
235
334
|
return glob(["**/*.jsonl"], { cwd, absolute: true }).catch(() => []);
|
|
236
335
|
}
|
|
237
336
|
|
|
238
|
-
// src/mcp.ts
|
|
239
|
-
import { readFile } from "fs/promises";
|
|
240
|
-
import { homedir as homedir2 } from "os";
|
|
241
|
-
import { join as join2 } from "path";
|
|
242
|
-
async function readJson(file) {
|
|
243
|
-
try {
|
|
244
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
245
|
-
} catch {
|
|
246
|
-
return void 0;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
function serverNames(obj) {
|
|
250
|
-
if (obj && typeof obj === "object" && !Array.isArray(obj)) return Object.keys(obj);
|
|
251
|
-
return [];
|
|
252
|
-
}
|
|
253
|
-
async function findMcpServers(projectPath, home = homedir2()) {
|
|
254
|
-
const out = /* @__PURE__ */ new Map();
|
|
255
|
-
const add = (names, source) => {
|
|
256
|
-
for (const name of names) if (!out.has(name)) out.set(name, { name, source });
|
|
257
|
-
};
|
|
258
|
-
const projectFile = join2(projectPath, ".mcp.json");
|
|
259
|
-
const projectCfg = await readJson(projectFile);
|
|
260
|
-
add(serverNames(projectCfg?.mcpServers), projectFile);
|
|
261
|
-
const globalFile = join2(home, ".claude.json");
|
|
262
|
-
const globalCfg = await readJson(globalFile);
|
|
263
|
-
add(serverNames(globalCfg?.mcpServers), globalFile);
|
|
264
|
-
const projects = globalCfg?.projects;
|
|
265
|
-
if (projects && typeof projects === "object") {
|
|
266
|
-
const entry = projects[projectPath];
|
|
267
|
-
add(serverNames(entry?.mcpServers), `${globalFile} (project entry)`);
|
|
268
|
-
}
|
|
269
|
-
return [...out.values()];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
337
|
// src/transcript.ts
|
|
273
338
|
import { createReadStream } from "fs";
|
|
274
339
|
import { createInterface } from "readline";
|
|
@@ -293,7 +358,7 @@ var AssistantLine = z.object({
|
|
|
293
358
|
content: z.array(z.object({ type: z.string(), id: z.string().optional(), name: z.string().optional() }).passthrough()).optional()
|
|
294
359
|
})
|
|
295
360
|
});
|
|
296
|
-
async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set()) {
|
|
361
|
+
async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), seenToolIds = /* @__PURE__ */ new Set(), events) {
|
|
297
362
|
const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
|
|
298
363
|
const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
299
364
|
for await (const line of rl) {
|
|
@@ -345,8 +410,9 @@ async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), s
|
|
|
345
410
|
if (hasDelta) {
|
|
346
411
|
const accs = [stats.usageByModel[model] ??= emptyUsage()];
|
|
347
412
|
if (timestamp) {
|
|
348
|
-
const day = stats.dailyUsage[timestamp
|
|
413
|
+
const day = stats.dailyUsage[dayKey(timestamp)] ??= {};
|
|
349
414
|
accs.push(day[model] ??= emptyUsage());
|
|
415
|
+
events?.push({ ts: Date.parse(timestamp), model, usage: delta });
|
|
350
416
|
}
|
|
351
417
|
for (const acc of accs) {
|
|
352
418
|
acc.inputTokens += delta.inputTokens;
|
|
@@ -379,47 +445,14 @@ async function parseTranscript(file, seenMessages = /* @__PURE__ */ new Map(), s
|
|
|
379
445
|
return stats;
|
|
380
446
|
}
|
|
381
447
|
|
|
382
|
-
// src/format.ts
|
|
383
|
-
var usd = (n) => `$${n.toFixed(2)}`;
|
|
384
|
-
var tok = (n) => n.toLocaleString("en-US");
|
|
385
|
-
function compact(n) {
|
|
386
|
-
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
387
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
388
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
389
|
-
return String(n);
|
|
390
|
-
}
|
|
391
|
-
var pct = (fraction) => `${Math.round(fraction * 100)}%`;
|
|
392
|
-
var pct1 = (fraction) => `${(fraction * 100).toFixed(1)}%`;
|
|
393
|
-
var shortModel = (id) => id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
|
|
394
|
-
function duration(startIso, endIso) {
|
|
395
|
-
if (!startIso || !endIso) return "\u2014";
|
|
396
|
-
const mins = Math.round((Date.parse(endIso) - Date.parse(startIso)) / 6e4);
|
|
397
|
-
if (mins < 1) return "<1m";
|
|
398
|
-
if (mins < 60) return `${mins}m`;
|
|
399
|
-
const h = Math.floor(mins / 60);
|
|
400
|
-
const m = mins % 60;
|
|
401
|
-
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
402
|
-
}
|
|
403
|
-
function relativeDate(iso, now = Date.now()) {
|
|
404
|
-
if (!iso) return "\u2014";
|
|
405
|
-
const date = iso.slice(0, 10);
|
|
406
|
-
const days = Math.floor((now - Date.parse(date)) / 864e5);
|
|
407
|
-
if (days <= 0) return "today";
|
|
408
|
-
if (days === 1) return "yesterday";
|
|
409
|
-
if (days < 30) return `${days}d ago`;
|
|
410
|
-
return date;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
448
|
export {
|
|
449
|
+
setLivePrices,
|
|
414
450
|
emptyUsage,
|
|
415
451
|
costUsd,
|
|
416
452
|
addUsage,
|
|
417
453
|
cacheSavings,
|
|
418
454
|
cacheHitRate,
|
|
419
455
|
buildReport,
|
|
420
|
-
sanitizeProjectPath,
|
|
421
|
-
findTranscripts,
|
|
422
|
-
findMcpServers,
|
|
423
456
|
usd,
|
|
424
457
|
tok,
|
|
425
458
|
compact,
|
|
@@ -428,5 +461,11 @@ export {
|
|
|
428
461
|
shortModel,
|
|
429
462
|
duration,
|
|
430
463
|
relativeDate,
|
|
464
|
+
setTimezone,
|
|
465
|
+
dayKey,
|
|
466
|
+
groupDaily,
|
|
467
|
+
parseDateArg,
|
|
468
|
+
sanitizeProjectPath,
|
|
469
|
+
findTranscripts,
|
|
431
470
|
parseTranscript
|
|
432
471
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,107 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildBlocks,
|
|
4
|
+
renderBlocksReport
|
|
5
|
+
} from "./chunk-6Y6MKFHZ.js";
|
|
6
|
+
import {
|
|
7
|
+
findMcpServers
|
|
8
|
+
} from "./chunk-65BQE6TI.js";
|
|
2
9
|
import {
|
|
3
10
|
buildReport,
|
|
4
|
-
findMcpServers,
|
|
5
11
|
findTranscripts,
|
|
12
|
+
groupDaily,
|
|
13
|
+
parseDateArg,
|
|
6
14
|
parseTranscript,
|
|
7
15
|
pct1,
|
|
16
|
+
setLivePrices,
|
|
17
|
+
setTimezone,
|
|
8
18
|
tok,
|
|
9
19
|
usd
|
|
10
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-Z5W5QH2Z.js";
|
|
11
21
|
|
|
12
22
|
// src/cli.ts
|
|
13
23
|
import { createRequire } from "module";
|
|
14
24
|
import { Command } from "commander";
|
|
15
25
|
|
|
26
|
+
// src/livePricing.ts
|
|
27
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
31
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32
|
+
var FETCH_TIMEOUT_MS = 3500;
|
|
33
|
+
function mapLitellmPrices(raw) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
36
|
+
if (!value || typeof value !== "object") continue;
|
|
37
|
+
const e = value;
|
|
38
|
+
const inCost = e.input_cost_per_token;
|
|
39
|
+
const outCost = e.output_cost_per_token;
|
|
40
|
+
if (typeof inCost !== "number" || typeof outCost !== "number" || inCost <= 0) continue;
|
|
41
|
+
const name = key.includes("/") ? key.slice(key.lastIndexOf("/") + 1) : key;
|
|
42
|
+
if (key.includes("/") && out[name]) continue;
|
|
43
|
+
const p = { inputPerMTok: inCost * 1e6, outputPerMTok: outCost * 1e6 };
|
|
44
|
+
if (typeof e.cache_read_input_token_cost === "number")
|
|
45
|
+
p.cacheReadMult = e.cache_read_input_token_cost / inCost;
|
|
46
|
+
if (typeof e.cache_creation_input_token_cost === "number")
|
|
47
|
+
p.cacheWriteMult = e.cache_creation_input_token_cost / inCost;
|
|
48
|
+
else if (!name.startsWith("claude"))
|
|
49
|
+
p.cacheWriteMult = 0;
|
|
50
|
+
if (typeof e.cache_creation_input_token_cost_above_1hr === "number")
|
|
51
|
+
p.cacheWrite1hMult = e.cache_creation_input_token_cost_above_1hr / inCost;
|
|
52
|
+
out[name] = p;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
async function initPricing(opts) {
|
|
57
|
+
const dir = opts?.cacheDir ?? join(homedir(), ".tokz");
|
|
58
|
+
const file = join(dir, "litellm-prices.json");
|
|
59
|
+
let cached;
|
|
60
|
+
try {
|
|
61
|
+
cached = JSON.parse(await readFile(file, "utf8"));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
const fresh = cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS;
|
|
65
|
+
if (cached && (fresh || opts?.offline)) {
|
|
66
|
+
setLivePrices(cached.prices);
|
|
67
|
+
return "cached";
|
|
68
|
+
}
|
|
69
|
+
if (opts?.offline) return "static";
|
|
70
|
+
try {
|
|
71
|
+
const ctrl = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
73
|
+
const res = await fetch(LITELLM_URL, { signal: ctrl.signal });
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
76
|
+
const prices = mapLitellmPrices(await res.json());
|
|
77
|
+
setLivePrices(prices);
|
|
78
|
+
try {
|
|
79
|
+
await mkdir(dir, { recursive: true });
|
|
80
|
+
await writeFile(file, JSON.stringify({ fetchedAt: Date.now(), prices }));
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
return "live";
|
|
84
|
+
} catch {
|
|
85
|
+
if (cached) {
|
|
86
|
+
setLivePrices(cached.prices);
|
|
87
|
+
return "cached";
|
|
88
|
+
}
|
|
89
|
+
return "static";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
16
93
|
// src/report.ts
|
|
17
94
|
import Table from "cli-table3";
|
|
18
95
|
import pc from "picocolors";
|
|
96
|
+
function renderActivity(rows, unit) {
|
|
97
|
+
const head = unit === "week" ? "Week of" : "Month";
|
|
98
|
+
const table = new Table({ head: [head, "Cost", "Input", "Cache read", "Cache write", "Output", "Turns"] });
|
|
99
|
+
for (const d of rows) {
|
|
100
|
+
table.push([d.date, usd(d.costUsd), tok(d.inputTokens), tok(d.cacheReadTokens), tok(d.cacheCreationTokens), tok(d.outputTokens), String(d.turns)]);
|
|
101
|
+
}
|
|
102
|
+
return `${pc.bold(`Activity by ${unit}`)}
|
|
103
|
+
${table.toString()}`;
|
|
104
|
+
}
|
|
19
105
|
function renderReport(report) {
|
|
20
106
|
const parts = [];
|
|
21
107
|
const span = report.spanStart && report.spanEnd ? `${report.spanStart} \u2192 ${report.spanEnd}` : `${report.spanDays} days`;
|
|
@@ -62,47 +148,108 @@ function renderReport(report) {
|
|
|
62
148
|
// src/cli.ts
|
|
63
149
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
64
150
|
var program = new Command();
|
|
65
|
-
program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").version(version);
|
|
66
|
-
|
|
151
|
+
program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").option("--offline", "don't fetch live pricing; use cached/built-in rates").option("--timezone <zone>", 'group days in this timezone: "utc" (default), "local", or an IANA name').version(version);
|
|
152
|
+
function applyGlobals() {
|
|
153
|
+
const opts = program.opts();
|
|
154
|
+
setTimezone(opts.timezone);
|
|
155
|
+
return { offline: opts.offline };
|
|
156
|
+
}
|
|
157
|
+
async function parseAll(projectPath, events) {
|
|
158
|
+
const transcripts = await findTranscripts(projectPath);
|
|
159
|
+
const seenMessageIds = /* @__PURE__ */ new Map();
|
|
160
|
+
const seenToolIds = /* @__PURE__ */ new Set();
|
|
161
|
+
const sessions = await Promise.all(
|
|
162
|
+
transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds, events))
|
|
163
|
+
);
|
|
164
|
+
return { transcripts, sessions };
|
|
165
|
+
}
|
|
166
|
+
program.command("audit").argument("[project]", "project path (default: current directory)").option("--all", "scan all projects under ~/.claude/projects").option("--json", "output raw JSON report").option("--days <n>", "only include the last N days of activity").option("--since <date>", "start date, YYYY-MM-DD or YYYYMMDD (inclusive)").option("--until <date>", "end date, YYYY-MM-DD or YYYYMMDD (inclusive)").option("--weekly", "append activity grouped by week").option("--monthly", "append activity grouped by month").action(async (project, opts) => {
|
|
167
|
+
const globals = applyGlobals();
|
|
67
168
|
const projectPath = project ?? process.cwd();
|
|
68
|
-
const transcripts = await
|
|
169
|
+
const [{ transcripts, sessions }] = await Promise.all([
|
|
170
|
+
parseAll(opts.all ? void 0 : projectPath),
|
|
171
|
+
initPricing(globals)
|
|
172
|
+
]);
|
|
69
173
|
if (transcripts.length === 0) {
|
|
70
174
|
console.error(`No Claude Code transcripts found for ${opts.all ? "any project" : projectPath}.`);
|
|
71
175
|
process.exitCode = 1;
|
|
72
176
|
return;
|
|
73
177
|
}
|
|
74
|
-
const seenMessageIds = /* @__PURE__ */ new Map();
|
|
75
|
-
const seenToolIds = /* @__PURE__ */ new Set();
|
|
76
|
-
const sessions = await Promise.all(
|
|
77
|
-
transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
|
|
78
|
-
);
|
|
79
178
|
const servers = opts.all ? [] : await findMcpServers(projectPath);
|
|
80
179
|
const days = opts.days ? Number.parseInt(opts.days, 10) : void 0;
|
|
81
180
|
const isoDay = (offset) => new Date(Date.now() - offset * 864e5).toISOString().slice(0, 10);
|
|
82
|
-
const
|
|
181
|
+
const since = parseDateArg(opts.since);
|
|
182
|
+
const until = parseDateArg(opts.until);
|
|
183
|
+
let range;
|
|
184
|
+
if (since || until) range = { from: since ?? "0000-01-01", to: until ?? "9999-12-31" };
|
|
185
|
+
else if (days && days > 0) range = { from: isoDay(days - 1), to: isoDay(0) };
|
|
83
186
|
const report = buildReport(sessions, servers, range);
|
|
84
|
-
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify(report, null, 2));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const parts = [renderReport(report)];
|
|
192
|
+
const grouping = opts.monthly ? "month" : opts.weekly ? "week" : void 0;
|
|
193
|
+
if (grouping) parts.push(renderActivity(groupDaily(report.daily, grouping), grouping));
|
|
194
|
+
console.log(parts.join("\n\n"));
|
|
195
|
+
});
|
|
196
|
+
program.command("blocks").description("Claude usage grouped into rolling 5-hour billing windows").option("--json", "output raw JSON").option("--active", "show only the currently active block").option("--recent", "only blocks from the last 3 days").option("--token-limit <n>", 'warn near this many tokens per block ("max" = highest past block)').option("--session-length <hours>", "block length in hours (default 5)").action(async (opts) => {
|
|
197
|
+
const globals = applyGlobals();
|
|
198
|
+
const events = [];
|
|
199
|
+
await Promise.all([parseAll(void 0, events), initPricing(globals)]);
|
|
200
|
+
const hours = opts.sessionLength ? Number.parseFloat(opts.sessionLength) : 5;
|
|
201
|
+
let blocks = buildBlocks(events, { sessionLengthMs: hours * 36e5 });
|
|
202
|
+
if (opts.recent) blocks = blocks.filter((b) => b.lastTs >= Date.now() - 3 * 864e5);
|
|
203
|
+
if (opts.active) blocks = blocks.filter((b) => b.active);
|
|
204
|
+
const tokenLimit = opts.tokenLimit === "max" ? "max" : opts.tokenLimit ? Number.parseInt(opts.tokenLimit, 10) : void 0;
|
|
205
|
+
if (opts.json) {
|
|
206
|
+
console.log(JSON.stringify(blocks, null, 2));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(renderBlocksReport(blocks, { tokenLimit }));
|
|
210
|
+
});
|
|
211
|
+
program.command("statusline").description("compact usage line for Claude Code's statusLine hook (reads hook JSON on stdin)").argument("[action]", '"enable" writes the hook into ~/.claude/settings.json, "disable" removes it').action(async (action) => {
|
|
212
|
+
const globals = applyGlobals();
|
|
213
|
+
const sl = await import("./statusline-A36XPBG6.js");
|
|
214
|
+
if (action === "enable" || action === "disable") {
|
|
215
|
+
try {
|
|
216
|
+
console.log(action === "enable" ? await sl.enableStatusline() : await sl.disableStatusline());
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(`Could not update settings: ${err instanceof Error ? err.message : err}`);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (action !== void 0) {
|
|
224
|
+
console.error(`Unknown action "${action}" \u2014 use "enable" or "disable".`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await initPricing({ ...globals, offline: true });
|
|
229
|
+
let input = {};
|
|
230
|
+
try {
|
|
231
|
+
input = JSON.parse(await sl.readStdin());
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
console.log(await sl.statusline(input));
|
|
85
235
|
});
|
|
86
236
|
program.action(async () => {
|
|
237
|
+
const globals = applyGlobals();
|
|
238
|
+
await initPricing(globals);
|
|
87
239
|
if (!process.stdout.isTTY) {
|
|
88
|
-
const transcripts = await
|
|
240
|
+
const { transcripts, sessions } = await parseAll(void 0);
|
|
89
241
|
if (transcripts.length === 0) {
|
|
90
242
|
console.error("No Claude Code transcripts found.");
|
|
91
243
|
process.exitCode = 1;
|
|
92
244
|
return;
|
|
93
245
|
}
|
|
94
|
-
const seenMessageIds = /* @__PURE__ */ new Map();
|
|
95
|
-
const seenToolIds = /* @__PURE__ */ new Set();
|
|
96
|
-
const sessions = await Promise.all(
|
|
97
|
-
transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
|
|
98
|
-
);
|
|
99
246
|
console.log(renderReport(buildReport(sessions, [])));
|
|
100
247
|
return;
|
|
101
248
|
}
|
|
102
249
|
const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
|
|
103
250
|
import("ink"),
|
|
104
251
|
import("react"),
|
|
105
|
-
import("./Root-
|
|
252
|
+
import("./Root-WEIYEE6M.js"),
|
|
106
253
|
import("./Fullscreen-GK2ZYXHV.js")
|
|
107
254
|
]);
|
|
108
255
|
render(React.createElement(Fullscreen, null, React.createElement(Root)));
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildBlocks,
|
|
4
|
+
burnRate,
|
|
5
|
+
fmtMs
|
|
6
|
+
} from "./chunk-6Y6MKFHZ.js";
|
|
7
|
+
import {
|
|
8
|
+
compact,
|
|
9
|
+
costUsd,
|
|
10
|
+
dayKey,
|
|
11
|
+
findTranscripts,
|
|
12
|
+
parseTranscript,
|
|
13
|
+
shortModel,
|
|
14
|
+
usd
|
|
15
|
+
} from "./chunk-Z5W5QH2Z.js";
|
|
16
|
+
|
|
17
|
+
// src/statusline.ts
|
|
18
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import pc from "picocolors";
|
|
22
|
+
var CONTEXT_WINDOW = 2e5;
|
|
23
|
+
var LOOKBACK_MS = 6 * 60 * 60 * 1e3;
|
|
24
|
+
async function lastContextTokens(transcriptPath) {
|
|
25
|
+
try {
|
|
26
|
+
const lines = (await readFile(transcriptPath, "utf8")).split("\n");
|
|
27
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
28
|
+
if (!lines[i].includes('"usage"')) continue;
|
|
29
|
+
try {
|
|
30
|
+
const j = JSON.parse(lines[i]);
|
|
31
|
+
const u = j?.message?.usage;
|
|
32
|
+
if (u && typeof u.input_tokens === "number") {
|
|
33
|
+
return u.input_tokens + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
async function statusline(input, now = Date.now(), home) {
|
|
43
|
+
const files = await findTranscripts(void 0, home);
|
|
44
|
+
const recent = [];
|
|
45
|
+
await Promise.all(
|
|
46
|
+
files.map(async (f) => {
|
|
47
|
+
try {
|
|
48
|
+
if ((await stat(f)).mtimeMs >= now - Math.max(LOOKBACK_MS, msSinceMidnight(now) + 6e4))
|
|
49
|
+
recent.push(f);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
const seen = /* @__PURE__ */ new Map();
|
|
55
|
+
const seenTools = /* @__PURE__ */ new Set();
|
|
56
|
+
const events = [];
|
|
57
|
+
const sessions = await Promise.all(recent.map((f) => parseTranscript(f, seen, seenTools, events)));
|
|
58
|
+
const today = dayKey(now);
|
|
59
|
+
let todayCost = 0;
|
|
60
|
+
let sessionCost = input.cost?.total_cost_usd;
|
|
61
|
+
for (const s of sessions) {
|
|
62
|
+
for (const [model2, u] of Object.entries(s.dailyUsage[today] ?? {})) todayCost += costUsd(u, model2).total;
|
|
63
|
+
if (sessionCost === void 0 && input.transcript_path && s.file === input.transcript_path) {
|
|
64
|
+
sessionCost = Object.entries(s.usageByModel).reduce((sum, [m, u]) => sum + costUsd(u, m).total, 0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const blocks = buildBlocks(events, { now });
|
|
68
|
+
const active = blocks.find((b) => b.active);
|
|
69
|
+
const rate = active ? burnRate(active, now) : void 0;
|
|
70
|
+
const model = input.model?.display_name ?? shortModel(input.model?.id ?? "?");
|
|
71
|
+
const parts = [`\u{1F916} ${model}`];
|
|
72
|
+
const costs = [
|
|
73
|
+
sessionCost !== void 0 ? `${usd(sessionCost)} session` : void 0,
|
|
74
|
+
`${usd(todayCost)} today`,
|
|
75
|
+
active ? `${usd(active.costUsd)} block (${fmtMs(active.end - now)} left)` : void 0
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
parts.push(`\u{1F4B0} ${costs.join(" / ")}`);
|
|
78
|
+
if (rate) parts.push(`\u{1F525} ${usd(rate.costPerHour)}/hr`);
|
|
79
|
+
const ctx = input.transcript_path ? await lastContextTokens(input.transcript_path) : void 0;
|
|
80
|
+
if (ctx !== void 0) {
|
|
81
|
+
const p = Math.round(ctx / CONTEXT_WINDOW * 100);
|
|
82
|
+
const colored = p >= 80 ? pc.red(`${p}%`) : p >= 50 ? pc.yellow(`${p}%`) : pc.green(`${p}%`);
|
|
83
|
+
parts.push(`\u{1F9E0} ${compact(ctx)} (${colored})`);
|
|
84
|
+
}
|
|
85
|
+
return parts.join(pc.dim(" | "));
|
|
86
|
+
}
|
|
87
|
+
function msSinceMidnight(now) {
|
|
88
|
+
const day = dayKey(now);
|
|
89
|
+
const midnight = Date.parse(`${day}T00:00:00Z`);
|
|
90
|
+
const diff = now - midnight;
|
|
91
|
+
return Math.min(Math.max(diff, 0), 36 * 60 * 60 * 1e3);
|
|
92
|
+
}
|
|
93
|
+
async function readStdin() {
|
|
94
|
+
let data = "";
|
|
95
|
+
for await (const chunk of process.stdin) data += chunk;
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
var STATUSLINE_COMMAND = "npx -y @tokz/cli statusline";
|
|
99
|
+
function settingsPath(home = homedir()) {
|
|
100
|
+
return join(home, ".claude", "settings.json");
|
|
101
|
+
}
|
|
102
|
+
async function readSettings(file) {
|
|
103
|
+
let raw;
|
|
104
|
+
try {
|
|
105
|
+
raw = await readFile(file, "utf8");
|
|
106
|
+
} catch {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
111
|
+
throw new Error(`${file} is not a JSON object`);
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
async function enableStatusline(home) {
|
|
115
|
+
const file = settingsPath(home);
|
|
116
|
+
const settings = await readSettings(file);
|
|
117
|
+
const prev = settings.statusLine;
|
|
118
|
+
if (prev?.command === STATUSLINE_COMMAND) return `Already enabled in ${file}.`;
|
|
119
|
+
settings.statusLine = { type: "command", command: STATUSLINE_COMMAND };
|
|
120
|
+
await mkdir(join(file, ".."), { recursive: true });
|
|
121
|
+
await writeFile(file, `${JSON.stringify(settings, null, 2)}
|
|
122
|
+
`);
|
|
123
|
+
const note = prev?.command ? ` (replaced previous statusLine: ${prev.command})` : "";
|
|
124
|
+
return `Statusline enabled \u2014 Claude Code will run "${STATUSLINE_COMMAND}".${note}
|
|
125
|
+
Restart Claude Code (or start a new session) to see it.`;
|
|
126
|
+
}
|
|
127
|
+
async function disableStatusline(home) {
|
|
128
|
+
const file = settingsPath(home);
|
|
129
|
+
const settings = await readSettings(file);
|
|
130
|
+
const prev = settings.statusLine;
|
|
131
|
+
if (!prev) return `No statusLine configured in ${file}; nothing to do.`;
|
|
132
|
+
if (prev.command !== void 0 && !prev.command.includes("tokz") && !prev.command.includes("@tokz/cli"))
|
|
133
|
+
return `statusLine in ${file} is "${prev.command}" \u2014 not tokz, leaving it alone.`;
|
|
134
|
+
delete settings.statusLine;
|
|
135
|
+
await writeFile(file, `${JSON.stringify(settings, null, 2)}
|
|
136
|
+
`);
|
|
137
|
+
return `Statusline disabled \u2014 removed from ${file}.`;
|
|
138
|
+
}
|
|
139
|
+
export {
|
|
140
|
+
STATUSLINE_COMMAND,
|
|
141
|
+
disableStatusline,
|
|
142
|
+
enableStatusline,
|
|
143
|
+
readStdin,
|
|
144
|
+
statusline
|
|
145
|
+
};
|