@tokz/cli 0.2.3 → 0.2.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/README.md +131 -56
- package/dist/{Root-6SZ5FQDN.js → Root-TUEV5MJB.js} +1180 -120
- 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 +168 -19
- package/dist/statusline-EXFMQYFF.js +166 -0
- package/package.json +1 -1
|
@@ -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,110 @@ 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').option("--cost-source <mode>", "session cost source: auto | cc | calc | both", "auto").action(async (action, opts) => {
|
|
212
|
+
const globals = applyGlobals();
|
|
213
|
+
const sl = await import("./statusline-EXFMQYFF.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
|
+
const valid = ["auto", "cc", "calc", "both"];
|
|
235
|
+
const costSource = valid.includes(opts.costSource ?? "auto") ? opts.costSource ?? "auto" : "auto";
|
|
236
|
+
console.log(await sl.statusline(input, Date.now(), void 0, { costSource }));
|
|
85
237
|
});
|
|
86
238
|
program.action(async () => {
|
|
239
|
+
const globals = applyGlobals();
|
|
240
|
+
await initPricing(globals);
|
|
87
241
|
if (!process.stdout.isTTY) {
|
|
88
|
-
const transcripts = await
|
|
242
|
+
const { transcripts, sessions } = await parseAll(void 0);
|
|
89
243
|
if (transcripts.length === 0) {
|
|
90
244
|
console.error("No Claude Code transcripts found.");
|
|
91
245
|
process.exitCode = 1;
|
|
92
246
|
return;
|
|
93
247
|
}
|
|
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
248
|
console.log(renderReport(buildReport(sessions, [])));
|
|
100
249
|
return;
|
|
101
250
|
}
|
|
102
251
|
const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
|
|
103
252
|
import("ink"),
|
|
104
253
|
import("react"),
|
|
105
|
-
import("./Root-
|
|
254
|
+
import("./Root-TUEV5MJB.js"),
|
|
106
255
|
import("./Fullscreen-GK2ZYXHV.js")
|
|
107
256
|
]);
|
|
108
257
|
render(React.createElement(Fullscreen, null, React.createElement(Root)));
|