@tokz/cli 0.0.1 → 0.2.0
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 +78 -0
- package/dist/Fullscreen-GK2ZYXHV.js +26 -0
- package/dist/Root-T6AZELSZ.js +1499 -0
- package/dist/chunk-MO4Y7IF6.js +404 -0
- package/dist/cli.js +108 -0
- package/package.json +35 -8
- package/cli.js +0 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/pricing.ts
|
|
4
|
+
var PRICES = {
|
|
5
|
+
// Anthropic (cache write 1.25x input, cache read 0.1x)
|
|
6
|
+
"claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
|
|
7
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
8
|
+
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
9
|
+
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
10
|
+
"claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
11
|
+
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
12
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 },
|
|
13
|
+
// OpenAI (no cache-write charge; cached input 0.1x)
|
|
14
|
+
"gpt-5-codex": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
|
|
15
|
+
"gpt-5-mini": { inputPerMTok: 0.25, outputPerMTok: 2, cacheWriteMult: 0 },
|
|
16
|
+
"gpt-5-nano": { inputPerMTok: 0.05, outputPerMTok: 0.4, cacheWriteMult: 0 },
|
|
17
|
+
"gpt-5": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
|
|
18
|
+
"gpt-4.1-mini": { inputPerMTok: 0.4, outputPerMTok: 1.6, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
19
|
+
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
20
|
+
"codex-mini": { inputPerMTok: 1.5, outputPerMTok: 6, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
21
|
+
"o4-mini": { inputPerMTok: 1.1, outputPerMTok: 4.4, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
22
|
+
o3: { inputPerMTok: 2, outputPerMTok: 8, cacheWriteMult: 0, cacheReadMult: 0.25 },
|
|
23
|
+
// Google
|
|
24
|
+
"gemini-2.5-pro": { inputPerMTok: 1.25, outputPerMTok: 10, cacheWriteMult: 0 },
|
|
25
|
+
"gemini-2.5-flash": { inputPerMTok: 0.3, outputPerMTok: 2.5, cacheWriteMult: 0 }
|
|
26
|
+
};
|
|
27
|
+
var DEFAULT_CACHE_READ_MULT = 0.1;
|
|
28
|
+
var DEFAULT_CACHE_WRITE_MULT = 1.25;
|
|
29
|
+
var CLAUDE_FALLBACK = PRICES["claude-opus-4-8"];
|
|
30
|
+
var UNKNOWN = { inputPerMTok: 0, outputPerMTok: 0 };
|
|
31
|
+
function resolvePrice(modelId) {
|
|
32
|
+
let best;
|
|
33
|
+
let bestLen = -1;
|
|
34
|
+
for (const [prefix, price] of Object.entries(PRICES)) {
|
|
35
|
+
if (modelId.startsWith(prefix) && prefix.length > bestLen) {
|
|
36
|
+
best = price;
|
|
37
|
+
bestLen = prefix.length;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (best) return best;
|
|
41
|
+
return modelId.startsWith("claude") ? CLAUDE_FALLBACK : UNKNOWN;
|
|
42
|
+
}
|
|
43
|
+
function emptyUsage() {
|
|
44
|
+
return { inputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, outputTokens: 0, turns: 0 };
|
|
45
|
+
}
|
|
46
|
+
function costUsd(usage, modelId) {
|
|
47
|
+
const p = resolvePrice(modelId);
|
|
48
|
+
const readMult = p.cacheReadMult ?? DEFAULT_CACHE_READ_MULT;
|
|
49
|
+
const writeMult = p.cacheWriteMult ?? DEFAULT_CACHE_WRITE_MULT;
|
|
50
|
+
const input = usage.inputTokens / 1e6 * p.inputPerMTok;
|
|
51
|
+
const cacheRead = usage.cacheReadTokens / 1e6 * p.inputPerMTok * readMult;
|
|
52
|
+
const cacheWrite = usage.cacheCreationTokens / 1e6 * p.inputPerMTok * writeMult;
|
|
53
|
+
const output = usage.outputTokens / 1e6 * p.outputPerMTok;
|
|
54
|
+
return { input, cacheRead, cacheWrite, output, total: input + cacheRead + cacheWrite + output };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/attribute.ts
|
|
58
|
+
var DAY_MS = 864e5;
|
|
59
|
+
function addUsage(acc, u) {
|
|
60
|
+
acc.inputTokens += u.inputTokens;
|
|
61
|
+
acc.cacheReadTokens += u.cacheReadTokens;
|
|
62
|
+
acc.cacheCreationTokens += u.cacheCreationTokens;
|
|
63
|
+
acc.outputTokens += u.outputTokens;
|
|
64
|
+
acc.turns += u.turns;
|
|
65
|
+
}
|
|
66
|
+
function cacheSavings(usageByModel) {
|
|
67
|
+
let saved = 0;
|
|
68
|
+
for (const [model, u] of Object.entries(usageByModel)) {
|
|
69
|
+
const p = resolvePrice(model);
|
|
70
|
+
saved += u.cacheReadTokens / 1e6 * p.inputPerMTok * (1 - (p.cacheReadMult ?? 0.1));
|
|
71
|
+
}
|
|
72
|
+
return saved;
|
|
73
|
+
}
|
|
74
|
+
function cacheHitRate(usageByModel) {
|
|
75
|
+
let read = 0;
|
|
76
|
+
let input = 0;
|
|
77
|
+
for (const u of Object.values(usageByModel)) {
|
|
78
|
+
read += u.cacheReadTokens;
|
|
79
|
+
input += u.inputTokens;
|
|
80
|
+
}
|
|
81
|
+
const denom = read + input;
|
|
82
|
+
return denom > 0 ? read / denom : 0;
|
|
83
|
+
}
|
|
84
|
+
function buildDaily(dailyUsage) {
|
|
85
|
+
return Object.entries(dailyUsage).map(([date, byModel]) => {
|
|
86
|
+
const stat = {
|
|
87
|
+
date,
|
|
88
|
+
costUsd: 0,
|
|
89
|
+
inputTokens: 0,
|
|
90
|
+
cacheReadTokens: 0,
|
|
91
|
+
cacheCreationTokens: 0,
|
|
92
|
+
outputTokens: 0,
|
|
93
|
+
turns: 0
|
|
94
|
+
};
|
|
95
|
+
for (const [model, u] of Object.entries(byModel)) {
|
|
96
|
+
stat.costUsd += costUsd(u, model).total;
|
|
97
|
+
stat.inputTokens += u.inputTokens;
|
|
98
|
+
stat.cacheReadTokens += u.cacheReadTokens;
|
|
99
|
+
stat.cacheCreationTokens += u.cacheCreationTokens;
|
|
100
|
+
stat.outputTokens += u.outputTokens;
|
|
101
|
+
stat.turns += u.turns;
|
|
102
|
+
}
|
|
103
|
+
return stat;
|
|
104
|
+
}).sort((a, b) => a.date < b.date ? -1 : 1);
|
|
105
|
+
}
|
|
106
|
+
function summarizeSession(s) {
|
|
107
|
+
const byCost = Object.entries(s.usageByModel).map(([model, u]) => ({ model, cost: costUsd(u, model).total, turns: u.turns })).sort((a, b) => b.cost - a.cost);
|
|
108
|
+
return {
|
|
109
|
+
file: s.file,
|
|
110
|
+
start: s.firstTs,
|
|
111
|
+
end: s.lastTs,
|
|
112
|
+
costUsd: byCost.reduce((sum, m) => sum + m.cost, 0),
|
|
113
|
+
turns: byCost.reduce((sum, m) => sum + m.turns, 0),
|
|
114
|
+
toolCallCount: Object.values(s.toolCalls).reduce((sum, n) => sum + n, 0),
|
|
115
|
+
models: byCost.map((m) => m.model)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function clampSession(s, from, to) {
|
|
119
|
+
const days = Object.entries(s.dailyUsage ?? {}).filter(([d]) => d >= from && d <= to);
|
|
120
|
+
if (days.length === 0) return null;
|
|
121
|
+
const usageByModel = {};
|
|
122
|
+
for (const [, byModel] of days) {
|
|
123
|
+
for (const [model, u] of Object.entries(byModel)) {
|
|
124
|
+
addUsage(usageByModel[model] ??= emptyUsage(), u);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const lo = `${from}T00:00:00Z`;
|
|
128
|
+
const hi = `${to}T23:59:59.999Z`;
|
|
129
|
+
const firstTs = s.firstTs && s.firstTs > lo ? s.firstTs : lo;
|
|
130
|
+
const lastTs = s.lastTs && s.lastTs < hi ? s.lastTs : hi;
|
|
131
|
+
return { ...s, firstTs, lastTs, usageByModel, dailyUsage: Object.fromEntries(days) };
|
|
132
|
+
}
|
|
133
|
+
function buildReport(sessions, servers, range) {
|
|
134
|
+
if (range) {
|
|
135
|
+
sessions = sessions.map((s) => clampSession(s, range.from, range.to)).filter((s) => s !== null);
|
|
136
|
+
}
|
|
137
|
+
const usageByModel = {};
|
|
138
|
+
const toolCalls = {};
|
|
139
|
+
const toolCostUsd = {};
|
|
140
|
+
const dailyUsage = {};
|
|
141
|
+
let earliest = Infinity;
|
|
142
|
+
let latest = -Infinity;
|
|
143
|
+
for (const s of sessions) {
|
|
144
|
+
for (const [model, u] of Object.entries(s.usageByModel)) {
|
|
145
|
+
addUsage(usageByModel[model] ??= emptyUsage(), u);
|
|
146
|
+
}
|
|
147
|
+
for (const [name, n] of Object.entries(s.toolCalls)) {
|
|
148
|
+
toolCalls[name] = (toolCalls[name] ?? 0) + n;
|
|
149
|
+
}
|
|
150
|
+
for (const [name, c] of Object.entries(s.toolCostUsd ?? {})) {
|
|
151
|
+
toolCostUsd[name] = (toolCostUsd[name] ?? 0) + c;
|
|
152
|
+
}
|
|
153
|
+
for (const [date, byModel] of Object.entries(s.dailyUsage ?? {})) {
|
|
154
|
+
const day = dailyUsage[date] ??= {};
|
|
155
|
+
for (const [model, u] of Object.entries(byModel)) {
|
|
156
|
+
addUsage(day[model] ??= emptyUsage(), u);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (s.firstTs) earliest = Math.min(earliest, Date.parse(s.firstTs));
|
|
160
|
+
if (s.lastTs) latest = Math.max(latest, Date.parse(s.lastTs));
|
|
161
|
+
}
|
|
162
|
+
const costByModel = {};
|
|
163
|
+
let totalCostUsd = 0;
|
|
164
|
+
let totalTurns = 0;
|
|
165
|
+
for (const [model, u] of Object.entries(usageByModel)) {
|
|
166
|
+
costByModel[model] = costUsd(u, model);
|
|
167
|
+
totalCostUsd += costByModel[model].total;
|
|
168
|
+
totalTurns += u.turns;
|
|
169
|
+
}
|
|
170
|
+
const spanDays = Number.isFinite(earliest) && Number.isFinite(latest) ? Math.max(1, Math.round((latest - earliest) / DAY_MS)) : 1;
|
|
171
|
+
const mcpSum = (record, server) => Object.entries(record).filter(([name]) => name.startsWith(`mcp__${server}__`)).reduce((sum, [, n]) => sum + n, 0);
|
|
172
|
+
const serverAudits = servers.map((srv) => {
|
|
173
|
+
const callsObserved = mcpSum(toolCalls, srv.name);
|
|
174
|
+
return {
|
|
175
|
+
...srv,
|
|
176
|
+
callsObserved,
|
|
177
|
+
unused: callsObserved === 0,
|
|
178
|
+
estCostUsd: mcpSum(toolCostUsd, srv.name),
|
|
179
|
+
configured: true
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const configured = new Set(servers.map((s) => s.name));
|
|
183
|
+
const observed = new Set(
|
|
184
|
+
Object.keys(toolCalls).map((name) => /^mcp__(.+)__[^_]/.exec(name)?.[1]).filter((s) => !!s)
|
|
185
|
+
);
|
|
186
|
+
for (const name of [...observed].sort()) {
|
|
187
|
+
if ([...configured].some((c) => name === c || name.startsWith(`${c}__`))) continue;
|
|
188
|
+
serverAudits.push({
|
|
189
|
+
name,
|
|
190
|
+
source: "observed in transcripts (plugin or external config)",
|
|
191
|
+
callsObserved: mcpSum(toolCalls, name),
|
|
192
|
+
unused: false,
|
|
193
|
+
estCostUsd: mcpSum(toolCostUsd, name),
|
|
194
|
+
configured: false
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const isoDate = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
198
|
+
return {
|
|
199
|
+
sessionCount: sessions.length,
|
|
200
|
+
spanDays,
|
|
201
|
+
spanStart: Number.isFinite(earliest) ? isoDate(earliest) : void 0,
|
|
202
|
+
spanEnd: Number.isFinite(latest) ? isoDate(latest) : void 0,
|
|
203
|
+
usageByModel,
|
|
204
|
+
costByModel,
|
|
205
|
+
totalCostUsd,
|
|
206
|
+
monthlyProjectionUsd: totalCostUsd / spanDays * 30,
|
|
207
|
+
toolCalls,
|
|
208
|
+
toolCostUsd,
|
|
209
|
+
servers: serverAudits,
|
|
210
|
+
daily: buildDaily(dailyUsage),
|
|
211
|
+
sessions: sessions.map(summarizeSession).sort((a, b) => b.costUsd - a.costUsd),
|
|
212
|
+
cacheSavingsUsd: cacheSavings(usageByModel),
|
|
213
|
+
cacheHitRate: cacheHitRate(usageByModel),
|
|
214
|
+
totalTurns
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/discover.ts
|
|
219
|
+
import { homedir } from "os";
|
|
220
|
+
import { join } from "path";
|
|
221
|
+
import { glob } from "tinyglobby";
|
|
222
|
+
function sanitizeProjectPath(p) {
|
|
223
|
+
return p.replace(/[^a-zA-Z0-9]/g, "-");
|
|
224
|
+
}
|
|
225
|
+
function transcriptDir(projectPath, home = homedir()) {
|
|
226
|
+
return join(home, ".claude", "projects", sanitizeProjectPath(projectPath));
|
|
227
|
+
}
|
|
228
|
+
async function findTranscripts(projectPath, home = homedir()) {
|
|
229
|
+
const cwd = projectPath ? transcriptDir(projectPath, home) : join(home, ".claude", "projects");
|
|
230
|
+
return glob(["**/*.jsonl"], { cwd, absolute: true }).catch(() => []);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/mcp.ts
|
|
234
|
+
import { readFile } from "fs/promises";
|
|
235
|
+
import { homedir as homedir2 } from "os";
|
|
236
|
+
import { join as join2 } from "path";
|
|
237
|
+
async function readJson(file) {
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
240
|
+
} catch {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function serverNames(obj) {
|
|
245
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) return Object.keys(obj);
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
async function findMcpServers(projectPath, home = homedir2()) {
|
|
249
|
+
const out = /* @__PURE__ */ new Map();
|
|
250
|
+
const add = (names, source) => {
|
|
251
|
+
for (const name of names) if (!out.has(name)) out.set(name, { name, source });
|
|
252
|
+
};
|
|
253
|
+
const projectFile = join2(projectPath, ".mcp.json");
|
|
254
|
+
const projectCfg = await readJson(projectFile);
|
|
255
|
+
add(serverNames(projectCfg?.mcpServers), projectFile);
|
|
256
|
+
const globalFile = join2(home, ".claude.json");
|
|
257
|
+
const globalCfg = await readJson(globalFile);
|
|
258
|
+
add(serverNames(globalCfg?.mcpServers), globalFile);
|
|
259
|
+
const projects = globalCfg?.projects;
|
|
260
|
+
if (projects && typeof projects === "object") {
|
|
261
|
+
const entry = projects[projectPath];
|
|
262
|
+
add(serverNames(entry?.mcpServers), `${globalFile} (project entry)`);
|
|
263
|
+
}
|
|
264
|
+
return [...out.values()];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/transcript.ts
|
|
268
|
+
import { createReadStream } from "fs";
|
|
269
|
+
import { createInterface } from "readline";
|
|
270
|
+
import { z } from "zod";
|
|
271
|
+
var AssistantLine = z.object({
|
|
272
|
+
type: z.literal("assistant"),
|
|
273
|
+
cwd: z.string().optional(),
|
|
274
|
+
timestamp: z.string().optional(),
|
|
275
|
+
message: z.object({
|
|
276
|
+
id: z.string().optional(),
|
|
277
|
+
model: z.string().optional(),
|
|
278
|
+
usage: z.object({
|
|
279
|
+
input_tokens: z.number().catch(0).default(0),
|
|
280
|
+
cache_creation_input_tokens: z.number().catch(0).default(0),
|
|
281
|
+
cache_read_input_tokens: z.number().catch(0).default(0),
|
|
282
|
+
output_tokens: z.number().catch(0).default(0)
|
|
283
|
+
}).optional(),
|
|
284
|
+
content: z.array(z.object({ type: z.string(), id: z.string().optional(), name: z.string().optional() }).passthrough()).optional()
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
async function parseTranscript(file, seenMessageIds = /* @__PURE__ */ new Set(), seenToolIds = /* @__PURE__ */ new Set()) {
|
|
288
|
+
const stats = { file, usageByModel: {}, toolCalls: {}, toolCostUsd: {}, dailyUsage: {} };
|
|
289
|
+
const rl = createInterface({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
290
|
+
for await (const line of rl) {
|
|
291
|
+
if (!line.trim()) continue;
|
|
292
|
+
let raw;
|
|
293
|
+
try {
|
|
294
|
+
raw = JSON.parse(line);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const parsed = AssistantLine.safeParse(raw);
|
|
299
|
+
if (!parsed.success) continue;
|
|
300
|
+
const { message, timestamp, cwd } = parsed.data;
|
|
301
|
+
if (cwd && !stats.cwd) stats.cwd = cwd;
|
|
302
|
+
if (timestamp) {
|
|
303
|
+
if (!stats.firstTs) stats.firstTs = timestamp;
|
|
304
|
+
stats.lastTs = timestamp;
|
|
305
|
+
}
|
|
306
|
+
const model = message.model ?? "unknown";
|
|
307
|
+
const firstSeen = !message.id || !seenMessageIds.has(message.id);
|
|
308
|
+
if (message.id) seenMessageIds.add(message.id);
|
|
309
|
+
let turnCost = 0;
|
|
310
|
+
if (message.usage && firstSeen && model !== "<synthetic>") {
|
|
311
|
+
const accs = [stats.usageByModel[model] ??= emptyUsage()];
|
|
312
|
+
if (timestamp) {
|
|
313
|
+
const day = stats.dailyUsage[timestamp.slice(0, 10)] ??= {};
|
|
314
|
+
accs.push(day[model] ??= emptyUsage());
|
|
315
|
+
}
|
|
316
|
+
for (const u of accs) {
|
|
317
|
+
u.inputTokens += message.usage.input_tokens;
|
|
318
|
+
u.cacheCreationTokens += message.usage.cache_creation_input_tokens;
|
|
319
|
+
u.cacheReadTokens += message.usage.cache_read_input_tokens;
|
|
320
|
+
u.outputTokens += message.usage.output_tokens;
|
|
321
|
+
u.turns += 1;
|
|
322
|
+
}
|
|
323
|
+
turnCost = costUsd(
|
|
324
|
+
{
|
|
325
|
+
inputTokens: message.usage.input_tokens,
|
|
326
|
+
cacheCreationTokens: message.usage.cache_creation_input_tokens,
|
|
327
|
+
cacheReadTokens: message.usage.cache_read_input_tokens,
|
|
328
|
+
outputTokens: message.usage.output_tokens,
|
|
329
|
+
turns: 1
|
|
330
|
+
},
|
|
331
|
+
model
|
|
332
|
+
).total;
|
|
333
|
+
}
|
|
334
|
+
const turnTools = [];
|
|
335
|
+
for (const block of message.content ?? []) {
|
|
336
|
+
if (block.type !== "tool_use" || !block.name) continue;
|
|
337
|
+
if (block.id) {
|
|
338
|
+
if (seenToolIds.has(block.id)) continue;
|
|
339
|
+
seenToolIds.add(block.id);
|
|
340
|
+
}
|
|
341
|
+
stats.toolCalls[block.name] = (stats.toolCalls[block.name] ?? 0) + 1;
|
|
342
|
+
turnTools.push(block.name);
|
|
343
|
+
}
|
|
344
|
+
if (turnCost > 0 && turnTools.length > 0) {
|
|
345
|
+
const share = turnCost / turnTools.length;
|
|
346
|
+
for (const name of turnTools) {
|
|
347
|
+
stats.toolCostUsd[name] = (stats.toolCostUsd[name] ?? 0) + share;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return stats;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/format.ts
|
|
355
|
+
var usd = (n) => `$${n.toFixed(2)}`;
|
|
356
|
+
var tok = (n) => n.toLocaleString("en-US");
|
|
357
|
+
function compact(n) {
|
|
358
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
|
|
359
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
360
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
361
|
+
return String(n);
|
|
362
|
+
}
|
|
363
|
+
var pct = (fraction) => `${Math.round(fraction * 100)}%`;
|
|
364
|
+
var pct1 = (fraction) => `${(fraction * 100).toFixed(1)}%`;
|
|
365
|
+
var shortModel = (id) => id.replace(/^claude-/, "").replace(/-\d{8}$/, "");
|
|
366
|
+
function duration(startIso, endIso) {
|
|
367
|
+
if (!startIso || !endIso) return "\u2014";
|
|
368
|
+
const mins = Math.round((Date.parse(endIso) - Date.parse(startIso)) / 6e4);
|
|
369
|
+
if (mins < 1) return "<1m";
|
|
370
|
+
if (mins < 60) return `${mins}m`;
|
|
371
|
+
const h = Math.floor(mins / 60);
|
|
372
|
+
const m = mins % 60;
|
|
373
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
374
|
+
}
|
|
375
|
+
function relativeDate(iso, now = Date.now()) {
|
|
376
|
+
if (!iso) return "\u2014";
|
|
377
|
+
const date = iso.slice(0, 10);
|
|
378
|
+
const days = Math.floor((now - Date.parse(date)) / 864e5);
|
|
379
|
+
if (days <= 0) return "today";
|
|
380
|
+
if (days === 1) return "yesterday";
|
|
381
|
+
if (days < 30) return `${days}d ago`;
|
|
382
|
+
return date;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export {
|
|
386
|
+
emptyUsage,
|
|
387
|
+
costUsd,
|
|
388
|
+
addUsage,
|
|
389
|
+
cacheSavings,
|
|
390
|
+
cacheHitRate,
|
|
391
|
+
buildReport,
|
|
392
|
+
sanitizeProjectPath,
|
|
393
|
+
findTranscripts,
|
|
394
|
+
findMcpServers,
|
|
395
|
+
usd,
|
|
396
|
+
tok,
|
|
397
|
+
compact,
|
|
398
|
+
pct,
|
|
399
|
+
pct1,
|
|
400
|
+
shortModel,
|
|
401
|
+
duration,
|
|
402
|
+
relativeDate,
|
|
403
|
+
parseTranscript
|
|
404
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildReport,
|
|
4
|
+
findMcpServers,
|
|
5
|
+
findTranscripts,
|
|
6
|
+
parseTranscript,
|
|
7
|
+
pct1,
|
|
8
|
+
tok,
|
|
9
|
+
usd
|
|
10
|
+
} from "./chunk-MO4Y7IF6.js";
|
|
11
|
+
|
|
12
|
+
// src/cli.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
// src/report.ts
|
|
16
|
+
import Table from "cli-table3";
|
|
17
|
+
import pc from "picocolors";
|
|
18
|
+
function renderReport(report) {
|
|
19
|
+
const parts = [];
|
|
20
|
+
const span = report.spanStart && report.spanEnd ? `${report.spanStart} \u2192 ${report.spanEnd}` : `${report.spanDays} days`;
|
|
21
|
+
parts.push(
|
|
22
|
+
pc.bold(
|
|
23
|
+
`tokz audit \u2014 ${report.sessionCount} sessions, ${span}: ${usd(report.totalCostUsd)} API-equivalent cost, projected ${usd(report.monthlyProjectionUsd)}/month`
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
parts.push(
|
|
27
|
+
pc.dim(
|
|
28
|
+
"Cost = what these tokens would bill at Anthropic API pay-as-you-go rates. On a Pro/Max subscription you pay a flat fee, not this \u2014 treat it as value received, not a bill."
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
parts.push(
|
|
32
|
+
`Cache hit rate ${pct1(report.cacheHitRate)} \u2014 prompt caching saved ${usd(report.cacheSavingsUsd)} vs uncached input pricing.`
|
|
33
|
+
);
|
|
34
|
+
const costTable = new Table({ head: ["Model", "Input", "Cache read", "Cache write", "Output", "Cost"] });
|
|
35
|
+
for (const [model, u] of Object.entries(report.usageByModel)) {
|
|
36
|
+
const c = report.costByModel[model];
|
|
37
|
+
costTable.push([model, tok(u.inputTokens), tok(u.cacheReadTokens), tok(u.cacheCreationTokens), tok(u.outputTokens), usd(c.total)]);
|
|
38
|
+
}
|
|
39
|
+
parts.push(costTable.toString());
|
|
40
|
+
if (report.servers.length > 0) {
|
|
41
|
+
const serverTable = new Table({ head: ["MCP server", "Calls observed", "Status", "Configured in"] });
|
|
42
|
+
for (const s of report.servers) {
|
|
43
|
+
serverTable.push([
|
|
44
|
+
s.name,
|
|
45
|
+
String(s.callsObserved),
|
|
46
|
+
s.unused ? pc.red("UNUSED \u2014 schema loaded every turn for nothing") : pc.green("used"),
|
|
47
|
+
s.source
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
parts.push(serverTable.toString());
|
|
51
|
+
}
|
|
52
|
+
const topTools = Object.entries(report.toolCalls).sort(([, a], [, b]) => b - a).slice(0, 10);
|
|
53
|
+
if (topTools.length > 0) {
|
|
54
|
+
const toolTable = new Table({ head: ["Tool", "Calls"] });
|
|
55
|
+
for (const [name, n] of topTools) toolTable.push([name, String(n)]);
|
|
56
|
+
parts.push(toolTable.toString());
|
|
57
|
+
}
|
|
58
|
+
return parts.join("\n\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/cli.ts
|
|
62
|
+
var program = new Command();
|
|
63
|
+
program.name("tokz").description("Audit where your coding agent's context window and API dollars go.").version("0.1.0");
|
|
64
|
+
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").action(async (project, opts) => {
|
|
65
|
+
const projectPath = project ?? process.cwd();
|
|
66
|
+
const transcripts = await findTranscripts(opts.all ? void 0 : projectPath);
|
|
67
|
+
if (transcripts.length === 0) {
|
|
68
|
+
console.error(`No Claude Code transcripts found for ${opts.all ? "any project" : projectPath}.`);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
73
|
+
const seenToolIds = /* @__PURE__ */ new Set();
|
|
74
|
+
const sessions = await Promise.all(
|
|
75
|
+
transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
|
|
76
|
+
);
|
|
77
|
+
const servers = opts.all ? [] : await findMcpServers(projectPath);
|
|
78
|
+
const days = opts.days ? Number.parseInt(opts.days, 10) : void 0;
|
|
79
|
+
const isoDay = (offset) => new Date(Date.now() - offset * 864e5).toISOString().slice(0, 10);
|
|
80
|
+
const range = days && days > 0 ? { from: isoDay(days - 1), to: isoDay(0) } : void 0;
|
|
81
|
+
const report = buildReport(sessions, servers, range);
|
|
82
|
+
console.log(opts.json ? JSON.stringify(report, null, 2) : renderReport(report));
|
|
83
|
+
});
|
|
84
|
+
program.action(async () => {
|
|
85
|
+
if (!process.stdout.isTTY) {
|
|
86
|
+
const transcripts = await findTranscripts(void 0);
|
|
87
|
+
if (transcripts.length === 0) {
|
|
88
|
+
console.error("No Claude Code transcripts found.");
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
93
|
+
const seenToolIds = /* @__PURE__ */ new Set();
|
|
94
|
+
const sessions = await Promise.all(
|
|
95
|
+
transcripts.map((f) => parseTranscript(f, seenMessageIds, seenToolIds))
|
|
96
|
+
);
|
|
97
|
+
console.log(renderReport(buildReport(sessions, [])));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const [{ render }, React, { Root }, { Fullscreen }] = await Promise.all([
|
|
101
|
+
import("ink"),
|
|
102
|
+
import("react"),
|
|
103
|
+
import("./Root-T6AZELSZ.js"),
|
|
104
|
+
import("./Fullscreen-GK2ZYXHV.js")
|
|
105
|
+
]);
|
|
106
|
+
render(React.createElement(Fullscreen, null, React.createElement(Root)));
|
|
107
|
+
});
|
|
108
|
+
program.parseAsync();
|
package/package.json
CHANGED
|
@@ -1,8 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tokz/cli",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Audit where your coding agent's context window
|
|
5
|
-
"
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokz/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Audit where your coding agent's context window and API dollars go.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": { "node": ">=22.0.0" },
|
|
8
|
+
"bin": { "tokz": "./dist/cli.js" },
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"publishConfig": { "access": "public" },
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"dev": "tsx src/cli.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"cli-table3": "^0.6.5",
|
|
18
|
+
"commander": "^13.0.0",
|
|
19
|
+
"ink": "^5.0.1",
|
|
20
|
+
"ink-select-input": "^6.0.0",
|
|
21
|
+
"picocolors": "^1.1.0",
|
|
22
|
+
"react": "^18.3.1",
|
|
23
|
+
"tinyglobby": "^0.2.10",
|
|
24
|
+
"zod": "^3.24.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"@types/react": "^18.3.12",
|
|
29
|
+
"ink-testing-library": "^4.0.0",
|
|
30
|
+
"tsup": "^8.3.0",
|
|
31
|
+
"tsx": "^4.19.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/cli.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("tokz: context-window audit for coding agents. v0 coming soon - https://tokz.dev");
|