tokenleak 0.1.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 +166 -0
- package/package.json +35 -0
- package/tokenleak.js +2382 -0
package/tokenleak.js
ADDED
|
@@ -0,0 +1,2382 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __require = import.meta.require;
|
|
4
|
+
|
|
5
|
+
// node_modules/citty/dist/_chunks/libs/scule.mjs
|
|
6
|
+
var NUMBER_CHAR_RE = /\d/;
|
|
7
|
+
var STR_SPLITTERS = [
|
|
8
|
+
"-",
|
|
9
|
+
"_",
|
|
10
|
+
"/",
|
|
11
|
+
"."
|
|
12
|
+
];
|
|
13
|
+
function isUppercase(char = "") {
|
|
14
|
+
if (NUMBER_CHAR_RE.test(char))
|
|
15
|
+
return;
|
|
16
|
+
return char !== char.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
function splitByCase(str, separators) {
|
|
19
|
+
const splitters = separators ?? STR_SPLITTERS;
|
|
20
|
+
const parts = [];
|
|
21
|
+
if (!str || typeof str !== "string")
|
|
22
|
+
return parts;
|
|
23
|
+
let buff = "";
|
|
24
|
+
let previousUpper;
|
|
25
|
+
let previousSplitter;
|
|
26
|
+
for (const char of str) {
|
|
27
|
+
const isSplitter = splitters.includes(char);
|
|
28
|
+
if (isSplitter === true) {
|
|
29
|
+
parts.push(buff);
|
|
30
|
+
buff = "";
|
|
31
|
+
previousUpper = undefined;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const isUpper = isUppercase(char);
|
|
35
|
+
if (previousSplitter === false) {
|
|
36
|
+
if (previousUpper === false && isUpper === true) {
|
|
37
|
+
parts.push(buff);
|
|
38
|
+
buff = char;
|
|
39
|
+
previousUpper = isUpper;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (previousUpper === true && isUpper === false && buff.length > 1) {
|
|
43
|
+
const lastChar = buff.at(-1);
|
|
44
|
+
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
|
|
45
|
+
buff = lastChar + char;
|
|
46
|
+
previousUpper = isUpper;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
buff += char;
|
|
51
|
+
previousUpper = isUpper;
|
|
52
|
+
previousSplitter = isSplitter;
|
|
53
|
+
}
|
|
54
|
+
parts.push(buff);
|
|
55
|
+
return parts;
|
|
56
|
+
}
|
|
57
|
+
function upperFirst(str) {
|
|
58
|
+
return str ? str[0].toUpperCase() + str.slice(1) : "";
|
|
59
|
+
}
|
|
60
|
+
function lowerFirst(str) {
|
|
61
|
+
return str ? str[0].toLowerCase() + str.slice(1) : "";
|
|
62
|
+
}
|
|
63
|
+
function pascalCase(str, opts) {
|
|
64
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
|
|
65
|
+
}
|
|
66
|
+
function camelCase(str, opts) {
|
|
67
|
+
return lowerFirst(pascalCase(str || "", opts));
|
|
68
|
+
}
|
|
69
|
+
function kebabCase(str, joiner) {
|
|
70
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// node_modules/citty/dist/index.mjs
|
|
74
|
+
import { parseArgs as parseArgs$1 } from "util";
|
|
75
|
+
function toArray(val) {
|
|
76
|
+
if (Array.isArray(val))
|
|
77
|
+
return val;
|
|
78
|
+
return val === undefined ? [] : [val];
|
|
79
|
+
}
|
|
80
|
+
function formatLineColumns(lines, linePrefix = "") {
|
|
81
|
+
const maxLength = [];
|
|
82
|
+
for (const line of lines)
|
|
83
|
+
for (const [i, element] of line.entries())
|
|
84
|
+
maxLength[i] = Math.max(maxLength[i] || 0, element.length);
|
|
85
|
+
return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i])).join(" ")).join(`
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
function resolveValue(input) {
|
|
89
|
+
return typeof input === "function" ? input() : input;
|
|
90
|
+
}
|
|
91
|
+
var CLIError = class extends Error {
|
|
92
|
+
code;
|
|
93
|
+
constructor(message, code) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "CLIError";
|
|
96
|
+
this.code = code;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function parseRawArgs(args = [], opts = {}) {
|
|
100
|
+
const booleans = new Set(opts.boolean || []);
|
|
101
|
+
const strings = new Set(opts.string || []);
|
|
102
|
+
const aliasMap = opts.alias || {};
|
|
103
|
+
const defaults = opts.default || {};
|
|
104
|
+
const aliasToMain = /* @__PURE__ */ new Map;
|
|
105
|
+
const mainToAliases = /* @__PURE__ */ new Map;
|
|
106
|
+
for (const [key, value] of Object.entries(aliasMap)) {
|
|
107
|
+
const targets = value;
|
|
108
|
+
for (const target of targets) {
|
|
109
|
+
aliasToMain.set(key, target);
|
|
110
|
+
if (!mainToAliases.has(target))
|
|
111
|
+
mainToAliases.set(target, []);
|
|
112
|
+
mainToAliases.get(target).push(key);
|
|
113
|
+
aliasToMain.set(target, key);
|
|
114
|
+
if (!mainToAliases.has(key))
|
|
115
|
+
mainToAliases.set(key, []);
|
|
116
|
+
mainToAliases.get(key).push(target);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const options = {};
|
|
120
|
+
function getType(name) {
|
|
121
|
+
if (booleans.has(name))
|
|
122
|
+
return "boolean";
|
|
123
|
+
const aliases = mainToAliases.get(name) || [];
|
|
124
|
+
for (const alias of aliases)
|
|
125
|
+
if (booleans.has(alias))
|
|
126
|
+
return "boolean";
|
|
127
|
+
return "string";
|
|
128
|
+
}
|
|
129
|
+
const allOptions = new Set([
|
|
130
|
+
...booleans,
|
|
131
|
+
...strings,
|
|
132
|
+
...Object.keys(aliasMap),
|
|
133
|
+
...Object.values(aliasMap).flat(),
|
|
134
|
+
...Object.keys(defaults)
|
|
135
|
+
]);
|
|
136
|
+
for (const name of allOptions)
|
|
137
|
+
if (!options[name])
|
|
138
|
+
options[name] = {
|
|
139
|
+
type: getType(name),
|
|
140
|
+
default: defaults[name]
|
|
141
|
+
};
|
|
142
|
+
for (const [alias, main] of aliasToMain.entries())
|
|
143
|
+
if (alias.length === 1 && options[main] && !options[main].short)
|
|
144
|
+
options[main].short = alias;
|
|
145
|
+
const processedArgs = [];
|
|
146
|
+
const negatedFlags = {};
|
|
147
|
+
for (let i = 0;i < args.length; i++) {
|
|
148
|
+
const arg = args[i];
|
|
149
|
+
if (arg === "--") {
|
|
150
|
+
processedArgs.push(...args.slice(i));
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (arg.startsWith("--no-")) {
|
|
154
|
+
const flagName = arg.slice(5);
|
|
155
|
+
negatedFlags[flagName] = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
processedArgs.push(arg);
|
|
159
|
+
}
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = parseArgs$1({
|
|
163
|
+
args: processedArgs,
|
|
164
|
+
options: Object.keys(options).length > 0 ? options : undefined,
|
|
165
|
+
allowPositionals: true,
|
|
166
|
+
strict: false
|
|
167
|
+
});
|
|
168
|
+
} catch {
|
|
169
|
+
parsed = {
|
|
170
|
+
values: {},
|
|
171
|
+
positionals: processedArgs
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const out = { _: [] };
|
|
175
|
+
out._ = parsed.positionals;
|
|
176
|
+
for (const [key, value] of Object.entries(parsed.values))
|
|
177
|
+
out[key] = value;
|
|
178
|
+
for (const [name] of Object.entries(negatedFlags)) {
|
|
179
|
+
out[name] = false;
|
|
180
|
+
const mainName = aliasToMain.get(name);
|
|
181
|
+
if (mainName)
|
|
182
|
+
out[mainName] = false;
|
|
183
|
+
const aliases = mainToAliases.get(name);
|
|
184
|
+
if (aliases)
|
|
185
|
+
for (const alias of aliases)
|
|
186
|
+
out[alias] = false;
|
|
187
|
+
}
|
|
188
|
+
for (const [alias, main] of aliasToMain.entries()) {
|
|
189
|
+
if (out[alias] !== undefined && out[main] === undefined)
|
|
190
|
+
out[main] = out[alias];
|
|
191
|
+
if (out[main] !== undefined && out[alias] === undefined)
|
|
192
|
+
out[alias] = out[main];
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
var noColor = /* @__PURE__ */ (() => {
|
|
197
|
+
const env = globalThis.process?.env ?? {};
|
|
198
|
+
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
|
|
199
|
+
})();
|
|
200
|
+
var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
|
|
201
|
+
var bold = /* @__PURE__ */ _c(1, 22);
|
|
202
|
+
var cyan = /* @__PURE__ */ _c(36);
|
|
203
|
+
var gray = /* @__PURE__ */ _c(90);
|
|
204
|
+
var underline = /* @__PURE__ */ _c(4, 24);
|
|
205
|
+
function parseArgs(rawArgs, argsDef) {
|
|
206
|
+
const parseOptions = {
|
|
207
|
+
boolean: [],
|
|
208
|
+
string: [],
|
|
209
|
+
alias: {},
|
|
210
|
+
default: {}
|
|
211
|
+
};
|
|
212
|
+
const args = resolveArgs(argsDef);
|
|
213
|
+
for (const arg of args) {
|
|
214
|
+
if (arg.type === "positional")
|
|
215
|
+
continue;
|
|
216
|
+
if (arg.type === "string" || arg.type === "enum")
|
|
217
|
+
parseOptions.string.push(arg.name);
|
|
218
|
+
else if (arg.type === "boolean")
|
|
219
|
+
parseOptions.boolean.push(arg.name);
|
|
220
|
+
if (arg.default !== undefined)
|
|
221
|
+
parseOptions.default[arg.name] = arg.default;
|
|
222
|
+
if (arg.alias)
|
|
223
|
+
parseOptions.alias[arg.name] = arg.alias;
|
|
224
|
+
const camelName = camelCase(arg.name);
|
|
225
|
+
const kebabName = kebabCase(arg.name);
|
|
226
|
+
if (camelName !== arg.name || kebabName !== arg.name) {
|
|
227
|
+
const existingAliases = toArray(parseOptions.alias[arg.name] || []);
|
|
228
|
+
if (camelName !== arg.name && !existingAliases.includes(camelName))
|
|
229
|
+
existingAliases.push(camelName);
|
|
230
|
+
if (kebabName !== arg.name && !existingAliases.includes(kebabName))
|
|
231
|
+
existingAliases.push(kebabName);
|
|
232
|
+
if (existingAliases.length > 0)
|
|
233
|
+
parseOptions.alias[arg.name] = existingAliases;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const parsed = parseRawArgs(rawArgs, parseOptions);
|
|
237
|
+
const [...positionalArguments] = parsed._;
|
|
238
|
+
const parsedArgsProxy = new Proxy(parsed, { get(target, prop) {
|
|
239
|
+
return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
|
|
240
|
+
} });
|
|
241
|
+
for (const [, arg] of args.entries())
|
|
242
|
+
if (arg.type === "positional") {
|
|
243
|
+
const nextPositionalArgument = positionalArguments.shift();
|
|
244
|
+
if (nextPositionalArgument !== undefined)
|
|
245
|
+
parsedArgsProxy[arg.name] = nextPositionalArgument;
|
|
246
|
+
else if (arg.default === undefined && arg.required !== false)
|
|
247
|
+
throw new CLIError(`Missing required positional argument: ${arg.name.toUpperCase()}`, "EARG");
|
|
248
|
+
else
|
|
249
|
+
parsedArgsProxy[arg.name] = arg.default;
|
|
250
|
+
} else if (arg.type === "enum") {
|
|
251
|
+
const argument = parsedArgsProxy[arg.name];
|
|
252
|
+
const options = arg.options || [];
|
|
253
|
+
if (argument !== undefined && options.length > 0 && !options.includes(argument))
|
|
254
|
+
throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
|
|
255
|
+
} else if (arg.required && parsedArgsProxy[arg.name] === undefined)
|
|
256
|
+
throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
|
|
257
|
+
return parsedArgsProxy;
|
|
258
|
+
}
|
|
259
|
+
function resolveArgs(argsDef) {
|
|
260
|
+
const args = [];
|
|
261
|
+
for (const [name, argDef] of Object.entries(argsDef || {}))
|
|
262
|
+
args.push({
|
|
263
|
+
...argDef,
|
|
264
|
+
name,
|
|
265
|
+
alias: toArray(argDef.alias)
|
|
266
|
+
});
|
|
267
|
+
return args;
|
|
268
|
+
}
|
|
269
|
+
function defineCommand(def) {
|
|
270
|
+
return def;
|
|
271
|
+
}
|
|
272
|
+
async function runCommand(cmd, opts) {
|
|
273
|
+
const cmdArgs = await resolveValue(cmd.args || {});
|
|
274
|
+
const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
|
|
275
|
+
const context = {
|
|
276
|
+
rawArgs: opts.rawArgs,
|
|
277
|
+
args: parsedArgs,
|
|
278
|
+
data: opts.data,
|
|
279
|
+
cmd
|
|
280
|
+
};
|
|
281
|
+
if (typeof cmd.setup === "function")
|
|
282
|
+
await cmd.setup(context);
|
|
283
|
+
let result;
|
|
284
|
+
try {
|
|
285
|
+
const subCommands = await resolveValue(cmd.subCommands);
|
|
286
|
+
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
287
|
+
const subCommandArgIndex = opts.rawArgs.findIndex((arg) => !arg.startsWith("-"));
|
|
288
|
+
const subCommandName = opts.rawArgs[subCommandArgIndex];
|
|
289
|
+
if (subCommandName) {
|
|
290
|
+
if (!subCommands[subCommandName])
|
|
291
|
+
throw new CLIError(`Unknown command ${cyan(subCommandName)}`, "E_UNKNOWN_COMMAND");
|
|
292
|
+
const subCommand = await resolveValue(subCommands[subCommandName]);
|
|
293
|
+
if (subCommand)
|
|
294
|
+
await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
295
|
+
} else if (!cmd.run)
|
|
296
|
+
throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
|
297
|
+
}
|
|
298
|
+
if (typeof cmd.run === "function")
|
|
299
|
+
result = await cmd.run(context);
|
|
300
|
+
} finally {
|
|
301
|
+
if (typeof cmd.cleanup === "function")
|
|
302
|
+
await cmd.cleanup(context);
|
|
303
|
+
}
|
|
304
|
+
return { result };
|
|
305
|
+
}
|
|
306
|
+
async function resolveSubCommand(cmd, rawArgs, parent) {
|
|
307
|
+
const subCommands = await resolveValue(cmd.subCommands);
|
|
308
|
+
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
309
|
+
const subCommandArgIndex = rawArgs.findIndex((arg) => !arg.startsWith("-"));
|
|
310
|
+
const subCommandName = rawArgs[subCommandArgIndex];
|
|
311
|
+
const subCommand = await resolveValue(subCommands[subCommandName]);
|
|
312
|
+
if (subCommand)
|
|
313
|
+
return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
|
|
314
|
+
}
|
|
315
|
+
return [cmd, parent];
|
|
316
|
+
}
|
|
317
|
+
async function showUsage(cmd, parent) {
|
|
318
|
+
try {
|
|
319
|
+
console.log(await renderUsage(cmd, parent) + `
|
|
320
|
+
`);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
var negativePrefixRe = /^no[-A-Z]/;
|
|
326
|
+
async function renderUsage(cmd, parent) {
|
|
327
|
+
const cmdMeta = await resolveValue(cmd.meta || {});
|
|
328
|
+
const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
|
|
329
|
+
const parentMeta = await resolveValue(parent?.meta || {});
|
|
330
|
+
const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
|
|
331
|
+
const argLines = [];
|
|
332
|
+
const posLines = [];
|
|
333
|
+
const commandsLines = [];
|
|
334
|
+
const usageLine = [];
|
|
335
|
+
for (const arg of cmdArgs)
|
|
336
|
+
if (arg.type === "positional") {
|
|
337
|
+
const name = arg.name.toUpperCase();
|
|
338
|
+
const isRequired = arg.required !== false && arg.default === undefined;
|
|
339
|
+
const defaultHint = arg.default ? `="${arg.default}"` : "";
|
|
340
|
+
posLines.push([
|
|
341
|
+
cyan(name + defaultHint),
|
|
342
|
+
arg.description || "",
|
|
343
|
+
arg.valueHint ? `<${arg.valueHint}>` : ""
|
|
344
|
+
]);
|
|
345
|
+
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
|
346
|
+
} else {
|
|
347
|
+
const isRequired = arg.required === true && arg.default === undefined;
|
|
348
|
+
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + (arg.type === "string" && (arg.valueHint || arg.default) ? `=${arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`}` : "") + (arg.type === "enum" && arg.options ? `=<${arg.options.join("|")}>` : "");
|
|
349
|
+
argLines.push([cyan(argStr + (isRequired ? " (required)" : "")), arg.description || ""]);
|
|
350
|
+
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
|
351
|
+
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
|
352
|
+
argLines.push([cyan(negativeArgStr + (isRequired ? " (required)" : "")), arg.negativeDescription || ""]);
|
|
353
|
+
}
|
|
354
|
+
if (isRequired)
|
|
355
|
+
usageLine.push(argStr);
|
|
356
|
+
}
|
|
357
|
+
if (cmd.subCommands) {
|
|
358
|
+
const commandNames = [];
|
|
359
|
+
const subCommands = await resolveValue(cmd.subCommands);
|
|
360
|
+
for (const [name, sub] of Object.entries(subCommands)) {
|
|
361
|
+
const meta = await resolveValue((await resolveValue(sub))?.meta);
|
|
362
|
+
if (meta?.hidden)
|
|
363
|
+
continue;
|
|
364
|
+
commandsLines.push([cyan(name), meta?.description || ""]);
|
|
365
|
+
commandNames.push(name);
|
|
366
|
+
}
|
|
367
|
+
usageLine.push(commandNames.join("|"));
|
|
368
|
+
}
|
|
369
|
+
const usageLines = [];
|
|
370
|
+
const version = cmdMeta.version || parentMeta.version;
|
|
371
|
+
usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
|
|
372
|
+
const hasOptions = argLines.length > 0 || posLines.length > 0;
|
|
373
|
+
usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
|
|
374
|
+
if (posLines.length > 0) {
|
|
375
|
+
usageLines.push(underline(bold("ARGUMENTS")), "");
|
|
376
|
+
usageLines.push(formatLineColumns(posLines, " "));
|
|
377
|
+
usageLines.push("");
|
|
378
|
+
}
|
|
379
|
+
if (argLines.length > 0) {
|
|
380
|
+
usageLines.push(underline(bold("OPTIONS")), "");
|
|
381
|
+
usageLines.push(formatLineColumns(argLines, " "));
|
|
382
|
+
usageLines.push("");
|
|
383
|
+
}
|
|
384
|
+
if (commandsLines.length > 0) {
|
|
385
|
+
usageLines.push(underline(bold("COMMANDS")), "");
|
|
386
|
+
usageLines.push(formatLineColumns(commandsLines, " "));
|
|
387
|
+
usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
|
|
388
|
+
}
|
|
389
|
+
return usageLines.filter((l) => typeof l === "string").join(`
|
|
390
|
+
`);
|
|
391
|
+
}
|
|
392
|
+
async function runMain(cmd, opts = {}) {
|
|
393
|
+
const rawArgs = opts.rawArgs || process.argv.slice(2);
|
|
394
|
+
const showUsage$1 = opts.showUsage || showUsage;
|
|
395
|
+
try {
|
|
396
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
397
|
+
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
|
398
|
+
process.exit(0);
|
|
399
|
+
} else if (rawArgs.length === 1 && rawArgs[0] === "--version") {
|
|
400
|
+
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
|
|
401
|
+
if (!meta?.version)
|
|
402
|
+
throw new CLIError("No version specified", "E_NO_VERSION");
|
|
403
|
+
console.log(meta.version);
|
|
404
|
+
} else
|
|
405
|
+
await runCommand(cmd, { rawArgs });
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error instanceof CLIError) {
|
|
408
|
+
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
|
409
|
+
console.error(error.message);
|
|
410
|
+
} else
|
|
411
|
+
console.error(error, `
|
|
412
|
+
`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// packages/cli/src/cli.ts
|
|
418
|
+
import { writeFileSync } from "fs";
|
|
419
|
+
|
|
420
|
+
// packages/core/dist/constants.js
|
|
421
|
+
var DEFAULT_DAYS = 90;
|
|
422
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
423
|
+
var MAX_JSONL_RECORD_BYTES = 10 * 1024 * 1024;
|
|
424
|
+
var SCHEMA_VERSION = 1;
|
|
425
|
+
// packages/core/dist/aggregation/streaks.js
|
|
426
|
+
function calculateStreaks(daily) {
|
|
427
|
+
if (daily.length === 0) {
|
|
428
|
+
return { current: 0, longest: 0 };
|
|
429
|
+
}
|
|
430
|
+
const sorted = [...daily].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
431
|
+
const ONE_DAY_MS = 86400000;
|
|
432
|
+
let longest = 1;
|
|
433
|
+
let currentRun = 1;
|
|
434
|
+
for (let i = 1;i < sorted.length; i++) {
|
|
435
|
+
const prev = new Date(sorted[i - 1].date).getTime();
|
|
436
|
+
const curr = new Date(sorted[i].date).getTime();
|
|
437
|
+
const diff = curr - prev;
|
|
438
|
+
if (diff === ONE_DAY_MS) {
|
|
439
|
+
currentRun++;
|
|
440
|
+
} else {
|
|
441
|
+
currentRun = 1;
|
|
442
|
+
}
|
|
443
|
+
if (currentRun > longest) {
|
|
444
|
+
longest = currentRun;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let current = 1;
|
|
448
|
+
for (let i = sorted.length - 1;i > 0; i--) {
|
|
449
|
+
const curr = new Date(sorted[i].date).getTime();
|
|
450
|
+
const prev = new Date(sorted[i - 1].date).getTime();
|
|
451
|
+
if (curr - prev === ONE_DAY_MS) {
|
|
452
|
+
current++;
|
|
453
|
+
} else {
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { current, longest };
|
|
458
|
+
}
|
|
459
|
+
// packages/core/dist/aggregation/rolling-window.js
|
|
460
|
+
function rollingWindow(daily, days, referenceDate) {
|
|
461
|
+
if (daily.length === 0 || days <= 0) {
|
|
462
|
+
return { tokens: 0, cost: 0 };
|
|
463
|
+
}
|
|
464
|
+
const refTime = new Date(referenceDate).getTime();
|
|
465
|
+
const ONE_DAY_MS = 86400000;
|
|
466
|
+
const windowStart = refTime - (days - 1) * ONE_DAY_MS;
|
|
467
|
+
let tokens = 0;
|
|
468
|
+
let cost = 0;
|
|
469
|
+
for (const entry of daily) {
|
|
470
|
+
const entryTime = new Date(entry.date).getTime();
|
|
471
|
+
if (entryTime >= windowStart && entryTime <= refTime) {
|
|
472
|
+
tokens += entry.totalTokens;
|
|
473
|
+
cost += entry.cost;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { tokens, cost };
|
|
477
|
+
}
|
|
478
|
+
// packages/core/dist/aggregation/peaks.js
|
|
479
|
+
function findPeakDay(daily) {
|
|
480
|
+
if (daily.length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
let peak = null;
|
|
484
|
+
for (const entry of daily) {
|
|
485
|
+
if (peak === null || entry.totalTokens > peak.tokens || entry.totalTokens === peak.tokens && entry.date > peak.date) {
|
|
486
|
+
peak = { date: entry.date, tokens: entry.totalTokens };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return peak;
|
|
490
|
+
}
|
|
491
|
+
// packages/core/dist/aggregation/day-of-week.js
|
|
492
|
+
var DAY_LABELS = [
|
|
493
|
+
"Sunday",
|
|
494
|
+
"Monday",
|
|
495
|
+
"Tuesday",
|
|
496
|
+
"Wednesday",
|
|
497
|
+
"Thursday",
|
|
498
|
+
"Friday",
|
|
499
|
+
"Saturday"
|
|
500
|
+
];
|
|
501
|
+
function dayOfWeekBreakdown(daily) {
|
|
502
|
+
const buckets = DAY_LABELS.map((label, i) => ({
|
|
503
|
+
day: i,
|
|
504
|
+
label,
|
|
505
|
+
tokens: 0,
|
|
506
|
+
cost: 0,
|
|
507
|
+
count: 0
|
|
508
|
+
}));
|
|
509
|
+
for (const entry of daily) {
|
|
510
|
+
const dayIndex = new Date(entry.date + "T00:00:00").getUTCDay();
|
|
511
|
+
const bucket = buckets[dayIndex];
|
|
512
|
+
bucket.tokens += entry.totalTokens;
|
|
513
|
+
bucket.cost += entry.cost;
|
|
514
|
+
bucket.count += 1;
|
|
515
|
+
}
|
|
516
|
+
return buckets;
|
|
517
|
+
}
|
|
518
|
+
// packages/core/dist/aggregation/cache-rate.js
|
|
519
|
+
function cacheHitRate(daily) {
|
|
520
|
+
let totalCacheRead = 0;
|
|
521
|
+
let totalInput = 0;
|
|
522
|
+
for (const entry of daily) {
|
|
523
|
+
totalCacheRead += entry.cacheReadTokens;
|
|
524
|
+
totalInput += entry.inputTokens;
|
|
525
|
+
}
|
|
526
|
+
const denominator = totalInput + totalCacheRead;
|
|
527
|
+
if (denominator === 0) {
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
return totalCacheRead / denominator;
|
|
531
|
+
}
|
|
532
|
+
// packages/core/dist/aggregation/averages.js
|
|
533
|
+
function calculateAverages(daily, totalDays) {
|
|
534
|
+
if (totalDays <= 0 || daily.length === 0) {
|
|
535
|
+
return { tokens: 0, cost: 0 };
|
|
536
|
+
}
|
|
537
|
+
let totalTokens = 0;
|
|
538
|
+
let totalCost = 0;
|
|
539
|
+
for (const entry of daily) {
|
|
540
|
+
totalTokens += entry.totalTokens;
|
|
541
|
+
totalCost += entry.cost;
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
tokens: totalTokens / totalDays,
|
|
545
|
+
cost: totalCost / totalDays
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
// packages/core/dist/aggregation/top-models.js
|
|
549
|
+
var DEFAULT_LIMIT = 10;
|
|
550
|
+
function topModels(daily, limit = DEFAULT_LIMIT) {
|
|
551
|
+
const modelMap = new Map;
|
|
552
|
+
for (const entry of daily) {
|
|
553
|
+
for (const m of entry.models) {
|
|
554
|
+
const existing = modelMap.get(m.model);
|
|
555
|
+
if (existing) {
|
|
556
|
+
existing.tokens += m.totalTokens;
|
|
557
|
+
existing.cost += m.cost;
|
|
558
|
+
} else {
|
|
559
|
+
modelMap.set(m.model, { tokens: m.totalTokens, cost: m.cost });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
let grandTotal = 0;
|
|
564
|
+
for (const v of modelMap.values()) {
|
|
565
|
+
grandTotal += v.tokens;
|
|
566
|
+
}
|
|
567
|
+
const entries = [];
|
|
568
|
+
for (const [model, { tokens, cost }] of modelMap) {
|
|
569
|
+
entries.push({
|
|
570
|
+
model,
|
|
571
|
+
tokens,
|
|
572
|
+
cost,
|
|
573
|
+
percentage: grandTotal > 0 ? tokens / grandTotal : 0
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
entries.sort((a, b) => b.tokens - a.tokens);
|
|
577
|
+
return entries.slice(0, limit);
|
|
578
|
+
}
|
|
579
|
+
// packages/core/dist/aggregation/aggregate.js
|
|
580
|
+
function aggregate(daily, referenceDate) {
|
|
581
|
+
const streaks = calculateStreaks(daily);
|
|
582
|
+
const rolling30 = rollingWindow(daily, 30, referenceDate);
|
|
583
|
+
const rolling7 = rollingWindow(daily, 7, referenceDate);
|
|
584
|
+
const peak = findPeakDay(daily);
|
|
585
|
+
const dow = dayOfWeekBreakdown(daily);
|
|
586
|
+
const cache = cacheHitRate(daily);
|
|
587
|
+
const models = topModels(daily);
|
|
588
|
+
let totalTokens = 0;
|
|
589
|
+
let totalCost = 0;
|
|
590
|
+
for (const entry of daily) {
|
|
591
|
+
totalTokens += entry.totalTokens;
|
|
592
|
+
totalCost += entry.cost;
|
|
593
|
+
}
|
|
594
|
+
const activeDays = daily.length;
|
|
595
|
+
let totalDays = 0;
|
|
596
|
+
if (daily.length > 0) {
|
|
597
|
+
const sorted = [...daily].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
598
|
+
const first = new Date(sorted[0].date).getTime();
|
|
599
|
+
const last = new Date(sorted[sorted.length - 1].date).getTime();
|
|
600
|
+
const ONE_DAY_MS = 86400000;
|
|
601
|
+
totalDays = Math.round((last - first) / ONE_DAY_MS) + 1;
|
|
602
|
+
}
|
|
603
|
+
const averages = calculateAverages(daily, totalDays);
|
|
604
|
+
return {
|
|
605
|
+
currentStreak: streaks.current,
|
|
606
|
+
longestStreak: streaks.longest,
|
|
607
|
+
rolling30dTokens: rolling30.tokens,
|
|
608
|
+
rolling30dCost: rolling30.cost,
|
|
609
|
+
rolling7dTokens: rolling7.tokens,
|
|
610
|
+
rolling7dCost: rolling7.cost,
|
|
611
|
+
peakDay: peak,
|
|
612
|
+
averageDailyTokens: averages.tokens,
|
|
613
|
+
averageDailyCost: averages.cost,
|
|
614
|
+
cacheHitRate: cache,
|
|
615
|
+
totalTokens,
|
|
616
|
+
totalCost,
|
|
617
|
+
totalDays,
|
|
618
|
+
activeDays,
|
|
619
|
+
dayOfWeek: dow,
|
|
620
|
+
topModels: models
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// packages/core/dist/aggregation/merge.js
|
|
624
|
+
function mergeProviderData(providers) {
|
|
625
|
+
const dateMap = new Map;
|
|
626
|
+
for (const provider of providers) {
|
|
627
|
+
for (const entry of provider.daily) {
|
|
628
|
+
const existing = dateMap.get(entry.date);
|
|
629
|
+
if (existing) {
|
|
630
|
+
existing.inputTokens += entry.inputTokens;
|
|
631
|
+
existing.outputTokens += entry.outputTokens;
|
|
632
|
+
existing.cacheReadTokens += entry.cacheReadTokens;
|
|
633
|
+
existing.cacheWriteTokens += entry.cacheWriteTokens;
|
|
634
|
+
existing.totalTokens += entry.totalTokens;
|
|
635
|
+
existing.cost += entry.cost;
|
|
636
|
+
existing.models = [...existing.models, ...entry.models];
|
|
637
|
+
} else {
|
|
638
|
+
dateMap.set(entry.date, {
|
|
639
|
+
date: entry.date,
|
|
640
|
+
inputTokens: entry.inputTokens,
|
|
641
|
+
outputTokens: entry.outputTokens,
|
|
642
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
643
|
+
cacheWriteTokens: entry.cacheWriteTokens,
|
|
644
|
+
totalTokens: entry.totalTokens,
|
|
645
|
+
cost: entry.cost,
|
|
646
|
+
models: [...entry.models]
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return [...dateMap.values()].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
|
652
|
+
}
|
|
653
|
+
// packages/core/dist/aggregation/compare.js
|
|
654
|
+
function computeDeltas(statsA, statsB) {
|
|
655
|
+
return {
|
|
656
|
+
tokens: statsB.totalTokens - statsA.totalTokens,
|
|
657
|
+
cost: statsB.totalCost - statsA.totalCost,
|
|
658
|
+
streak: statsB.currentStreak - statsA.currentStreak,
|
|
659
|
+
activeDays: statsB.activeDays - statsA.activeDays,
|
|
660
|
+
averageDailyTokens: statsB.averageDailyTokens - statsA.averageDailyTokens,
|
|
661
|
+
cacheHitRate: statsB.cacheHitRate - statsA.cacheHitRate
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function buildCompareOutput(periodA, periodB) {
|
|
665
|
+
return {
|
|
666
|
+
schemaVersion: SCHEMA_VERSION,
|
|
667
|
+
generated: new Date().toISOString(),
|
|
668
|
+
periodA,
|
|
669
|
+
periodB,
|
|
670
|
+
deltas: computeDeltas(periodA.stats, periodB.stats)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function parseCompareRange(rangeStr) {
|
|
674
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
675
|
+
const parts = rangeStr.split("..");
|
|
676
|
+
if (parts.length !== 2)
|
|
677
|
+
return null;
|
|
678
|
+
const [since, until] = parts;
|
|
679
|
+
if (!DATE_PATTERN.test(since) || !DATE_PATTERN.test(until))
|
|
680
|
+
return null;
|
|
681
|
+
if (since > until)
|
|
682
|
+
return null;
|
|
683
|
+
return { since, until };
|
|
684
|
+
}
|
|
685
|
+
function computePreviousPeriod(current) {
|
|
686
|
+
const ONE_DAY_MS = 86400000;
|
|
687
|
+
const sinceMs = new Date(current.since).getTime();
|
|
688
|
+
const untilMs = new Date(current.until).getTime();
|
|
689
|
+
const periodDays = Math.round((untilMs - sinceMs) / ONE_DAY_MS);
|
|
690
|
+
const prevUntil = new Date(sinceMs - ONE_DAY_MS);
|
|
691
|
+
const prevSince = new Date(prevUntil.getTime() - periodDays * ONE_DAY_MS);
|
|
692
|
+
return {
|
|
693
|
+
since: prevSince.toISOString().slice(0, 10),
|
|
694
|
+
until: prevUntil.toISOString().slice(0, 10)
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
// packages/core/dist/index.js
|
|
698
|
+
var VERSION = "0.1.0";
|
|
699
|
+
|
|
700
|
+
// packages/registry/dist/models/normalizer.js
|
|
701
|
+
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
702
|
+
function normalizeModelName(model) {
|
|
703
|
+
return model.replace(DATE_SUFFIX_PATTERN, "");
|
|
704
|
+
}
|
|
705
|
+
// packages/registry/dist/models/pricing.js
|
|
706
|
+
var TOKENS_PER_MILLION = 1e6;
|
|
707
|
+
var MODEL_PRICING = {
|
|
708
|
+
"claude-3-haiku": {
|
|
709
|
+
input: 0.25,
|
|
710
|
+
output: 1.25,
|
|
711
|
+
cacheRead: 0.03,
|
|
712
|
+
cacheWrite: 0.3
|
|
713
|
+
},
|
|
714
|
+
"claude-3-sonnet": {
|
|
715
|
+
input: 3,
|
|
716
|
+
output: 15,
|
|
717
|
+
cacheRead: 0.3,
|
|
718
|
+
cacheWrite: 3.75
|
|
719
|
+
},
|
|
720
|
+
"claude-3-opus": {
|
|
721
|
+
input: 15,
|
|
722
|
+
output: 75,
|
|
723
|
+
cacheRead: 1.5,
|
|
724
|
+
cacheWrite: 18.75
|
|
725
|
+
},
|
|
726
|
+
"claude-3.5-haiku": {
|
|
727
|
+
input: 0.8,
|
|
728
|
+
output: 4,
|
|
729
|
+
cacheRead: 0.08,
|
|
730
|
+
cacheWrite: 1
|
|
731
|
+
},
|
|
732
|
+
"claude-3.5-sonnet": {
|
|
733
|
+
input: 3,
|
|
734
|
+
output: 15,
|
|
735
|
+
cacheRead: 0.3,
|
|
736
|
+
cacheWrite: 3.75
|
|
737
|
+
},
|
|
738
|
+
"claude-sonnet-4": {
|
|
739
|
+
input: 3,
|
|
740
|
+
output: 15,
|
|
741
|
+
cacheRead: 0.3,
|
|
742
|
+
cacheWrite: 3.75
|
|
743
|
+
},
|
|
744
|
+
"claude-opus-4": {
|
|
745
|
+
input: 15,
|
|
746
|
+
output: 75,
|
|
747
|
+
cacheRead: 1.5,
|
|
748
|
+
cacheWrite: 18.75
|
|
749
|
+
},
|
|
750
|
+
"gpt-4o": {
|
|
751
|
+
input: 2.5,
|
|
752
|
+
output: 10,
|
|
753
|
+
cacheRead: 1.25,
|
|
754
|
+
cacheWrite: 2.5
|
|
755
|
+
},
|
|
756
|
+
"gpt-4o-mini": {
|
|
757
|
+
input: 0.15,
|
|
758
|
+
output: 0.6,
|
|
759
|
+
cacheRead: 0.075,
|
|
760
|
+
cacheWrite: 0.15
|
|
761
|
+
},
|
|
762
|
+
o1: {
|
|
763
|
+
input: 15,
|
|
764
|
+
output: 60,
|
|
765
|
+
cacheRead: 7.5,
|
|
766
|
+
cacheWrite: 15
|
|
767
|
+
},
|
|
768
|
+
"o1-mini": {
|
|
769
|
+
input: 3,
|
|
770
|
+
output: 12,
|
|
771
|
+
cacheRead: 1.5,
|
|
772
|
+
cacheWrite: 3
|
|
773
|
+
},
|
|
774
|
+
o3: {
|
|
775
|
+
input: 10,
|
|
776
|
+
output: 40,
|
|
777
|
+
cacheRead: 5,
|
|
778
|
+
cacheWrite: 10
|
|
779
|
+
},
|
|
780
|
+
"o3-mini": {
|
|
781
|
+
input: 1.1,
|
|
782
|
+
output: 4.4,
|
|
783
|
+
cacheRead: 0.55,
|
|
784
|
+
cacheWrite: 1.1
|
|
785
|
+
},
|
|
786
|
+
"o4-mini": {
|
|
787
|
+
input: 1.1,
|
|
788
|
+
output: 4.4,
|
|
789
|
+
cacheRead: 0.55,
|
|
790
|
+
cacheWrite: 1.1
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
function getModelPricing(model) {
|
|
794
|
+
return MODEL_PRICING[model];
|
|
795
|
+
}
|
|
796
|
+
// packages/registry/dist/models/cost.js
|
|
797
|
+
function estimateCost(model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) {
|
|
798
|
+
const normalized = normalizeModelName(model);
|
|
799
|
+
const pricing = getModelPricing(normalized);
|
|
800
|
+
if (!pricing) {
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
803
|
+
const inputCost = inputTokens / TOKENS_PER_MILLION * pricing.input;
|
|
804
|
+
const outputCost = outputTokens / TOKENS_PER_MILLION * pricing.output;
|
|
805
|
+
const cacheReadCost = cacheReadTokens / TOKENS_PER_MILLION * pricing.cacheRead;
|
|
806
|
+
const cacheWriteCost = cacheWriteTokens / TOKENS_PER_MILLION * pricing.cacheWrite;
|
|
807
|
+
return inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
808
|
+
}
|
|
809
|
+
// packages/registry/dist/registry.js
|
|
810
|
+
class ProviderRegistry {
|
|
811
|
+
providers = new Map;
|
|
812
|
+
register(provider) {
|
|
813
|
+
if (this.providers.has(provider.name)) {
|
|
814
|
+
throw new Error(`Provider "${provider.name}" is already registered`);
|
|
815
|
+
}
|
|
816
|
+
this.providers.set(provider.name, provider);
|
|
817
|
+
}
|
|
818
|
+
getAll() {
|
|
819
|
+
return [...this.providers.values()];
|
|
820
|
+
}
|
|
821
|
+
async getAvailable() {
|
|
822
|
+
const results = await Promise.all(this.getAll().map(async (p) => ({
|
|
823
|
+
provider: p,
|
|
824
|
+
available: await p.isAvailable()
|
|
825
|
+
})));
|
|
826
|
+
return results.filter((r) => r.available).map((r) => r.provider);
|
|
827
|
+
}
|
|
828
|
+
async loadAll(range, concurrency = DEFAULT_CONCURRENCY) {
|
|
829
|
+
const available = await this.getAvailable();
|
|
830
|
+
const results = [];
|
|
831
|
+
const queue = [...available];
|
|
832
|
+
const runNext = async () => {
|
|
833
|
+
while (queue.length > 0) {
|
|
834
|
+
const provider = queue.shift();
|
|
835
|
+
try {
|
|
836
|
+
const data = await provider.load(range);
|
|
837
|
+
results.push({ provider: provider.name, data, error: null });
|
|
838
|
+
} catch (err) {
|
|
839
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
840
|
+
results.push({ provider: provider.name, data: null, error: message });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
const workers = Array.from({ length: Math.min(concurrency, available.length) }, () => runNext());
|
|
845
|
+
await Promise.all(workers);
|
|
846
|
+
return results;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// packages/registry/dist/parsers/jsonl-splitter.js
|
|
850
|
+
function getMaxRecordBytes() {
|
|
851
|
+
const envValue = process.env["TOKENLEAK_MAX_JSONL_RECORD_BYTES"];
|
|
852
|
+
if (envValue !== undefined && envValue !== "") {
|
|
853
|
+
const parsed = Number(envValue);
|
|
854
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
855
|
+
throw new Error(`Invalid TOKENLEAK_MAX_JSONL_RECORD_BYTES value: "${envValue}". Must be a positive number.`);
|
|
856
|
+
}
|
|
857
|
+
return parsed;
|
|
858
|
+
}
|
|
859
|
+
return MAX_JSONL_RECORD_BYTES;
|
|
860
|
+
}
|
|
861
|
+
async function* splitJsonlRecords(filePath) {
|
|
862
|
+
const maxBytes = getMaxRecordBytes();
|
|
863
|
+
const file = Bun.file(filePath);
|
|
864
|
+
const text = await file.text();
|
|
865
|
+
const lines = text.split(`
|
|
866
|
+
`);
|
|
867
|
+
let lineNumber = 0;
|
|
868
|
+
for (const line of lines) {
|
|
869
|
+
lineNumber++;
|
|
870
|
+
if (line.trim() === "") {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
const byteLength = new TextEncoder().encode(line).byteLength;
|
|
874
|
+
if (byteLength > maxBytes) {
|
|
875
|
+
throw new Error(`Oversized JSONL record in ${filePath} at line ${lineNumber}: ${byteLength} bytes exceeds limit of ${maxBytes} bytes`);
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
yield JSON.parse(line);
|
|
879
|
+
} catch {
|
|
880
|
+
throw new Error(`Malformed JSON in ${filePath} at line ${lineNumber}: unable to parse`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// packages/registry/dist/providers/claude-code.js
|
|
885
|
+
import { existsSync, readdirSync, statSync } from "fs";
|
|
886
|
+
import { join } from "path";
|
|
887
|
+
import { homedir } from "os";
|
|
888
|
+
var DEFAULT_BASE_DIR = join(homedir(), ".claude", "projects");
|
|
889
|
+
var CLAUDE_CODE_COLORS = {
|
|
890
|
+
primary: "#ff6b35",
|
|
891
|
+
secondary: "#ffa366",
|
|
892
|
+
gradient: ["#ff6b35", "#ffa366"]
|
|
893
|
+
};
|
|
894
|
+
function collectJsonlFiles(dir) {
|
|
895
|
+
const results = [];
|
|
896
|
+
if (!existsSync(dir)) {
|
|
897
|
+
return results;
|
|
898
|
+
}
|
|
899
|
+
const entries = readdirSync(dir);
|
|
900
|
+
for (const entry of entries) {
|
|
901
|
+
const fullPath = join(dir, entry);
|
|
902
|
+
const stat = statSync(fullPath);
|
|
903
|
+
if (stat.isDirectory()) {
|
|
904
|
+
results.push(...collectJsonlFiles(fullPath));
|
|
905
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
906
|
+
results.push(fullPath);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return results;
|
|
910
|
+
}
|
|
911
|
+
function extractUsage(record) {
|
|
912
|
+
if (typeof record !== "object" || record === null) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
const rec = record;
|
|
916
|
+
if (rec["type"] !== "assistant") {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
const timestamp = rec["timestamp"];
|
|
920
|
+
if (typeof timestamp !== "string") {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
const message = rec["message"];
|
|
924
|
+
if (typeof message !== "object" || message === null) {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
const msg = message;
|
|
928
|
+
const usage = msg["usage"];
|
|
929
|
+
if (typeof usage !== "object" || usage === null) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const model = msg["model"];
|
|
933
|
+
if (typeof model !== "string") {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const u = usage;
|
|
937
|
+
const inputTokens = typeof u["input_tokens"] === "number" ? u["input_tokens"] : 0;
|
|
938
|
+
const outputTokens = typeof u["output_tokens"] === "number" ? u["output_tokens"] : 0;
|
|
939
|
+
const cacheReadTokens = typeof u["cache_read_input_tokens"] === "number" ? u["cache_read_input_tokens"] : 0;
|
|
940
|
+
const cacheWriteTokens = typeof u["cache_creation_input_tokens"] === "number" ? u["cache_creation_input_tokens"] : 0;
|
|
941
|
+
const date = timestamp.slice(0, 10);
|
|
942
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
date,
|
|
947
|
+
model,
|
|
948
|
+
inputTokens,
|
|
949
|
+
outputTokens,
|
|
950
|
+
cacheReadTokens,
|
|
951
|
+
cacheWriteTokens
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function isInRange(date, range) {
|
|
955
|
+
return date >= range.since && date <= range.until;
|
|
956
|
+
}
|
|
957
|
+
function buildDailyUsage(records) {
|
|
958
|
+
const byDate = new Map;
|
|
959
|
+
for (const rec of records) {
|
|
960
|
+
const normalizedModel = normalizeModelName(rec.model);
|
|
961
|
+
const cost = estimateCost(rec.model, rec.inputTokens, rec.outputTokens, rec.cacheReadTokens, rec.cacheWriteTokens);
|
|
962
|
+
let dateModels = byDate.get(rec.date);
|
|
963
|
+
if (!dateModels) {
|
|
964
|
+
dateModels = new Map;
|
|
965
|
+
byDate.set(rec.date, dateModels);
|
|
966
|
+
}
|
|
967
|
+
let mb = dateModels.get(normalizedModel);
|
|
968
|
+
if (!mb) {
|
|
969
|
+
mb = {
|
|
970
|
+
model: normalizedModel,
|
|
971
|
+
inputTokens: 0,
|
|
972
|
+
outputTokens: 0,
|
|
973
|
+
cacheReadTokens: 0,
|
|
974
|
+
cacheWriteTokens: 0,
|
|
975
|
+
totalTokens: 0,
|
|
976
|
+
cost: 0
|
|
977
|
+
};
|
|
978
|
+
dateModels.set(normalizedModel, mb);
|
|
979
|
+
}
|
|
980
|
+
mb.inputTokens += rec.inputTokens;
|
|
981
|
+
mb.outputTokens += rec.outputTokens;
|
|
982
|
+
mb.cacheReadTokens += rec.cacheReadTokens;
|
|
983
|
+
mb.cacheWriteTokens += rec.cacheWriteTokens;
|
|
984
|
+
mb.totalTokens += rec.inputTokens + rec.outputTokens;
|
|
985
|
+
mb.cost += cost;
|
|
986
|
+
}
|
|
987
|
+
const daily = [];
|
|
988
|
+
for (const [date, dateModels] of byDate) {
|
|
989
|
+
const models = [...dateModels.values()];
|
|
990
|
+
const inputTokens = models.reduce((sum, m) => sum + m.inputTokens, 0);
|
|
991
|
+
const outputTokens = models.reduce((sum, m) => sum + m.outputTokens, 0);
|
|
992
|
+
const cacheReadTokens = models.reduce((sum, m) => sum + m.cacheReadTokens, 0);
|
|
993
|
+
const cacheWriteTokens = models.reduce((sum, m) => sum + m.cacheWriteTokens, 0);
|
|
994
|
+
const totalTokens = models.reduce((sum, m) => sum + m.totalTokens, 0);
|
|
995
|
+
const cost = models.reduce((sum, m) => sum + m.cost, 0);
|
|
996
|
+
daily.push({
|
|
997
|
+
date,
|
|
998
|
+
inputTokens,
|
|
999
|
+
outputTokens,
|
|
1000
|
+
cacheReadTokens,
|
|
1001
|
+
cacheWriteTokens,
|
|
1002
|
+
totalTokens,
|
|
1003
|
+
cost,
|
|
1004
|
+
models
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
daily.sort((a, b) => a.date.localeCompare(b.date));
|
|
1008
|
+
return daily;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
class ClaudeCodeProvider {
|
|
1012
|
+
name = "claude-code";
|
|
1013
|
+
displayName = "Claude Code";
|
|
1014
|
+
colors = CLAUDE_CODE_COLORS;
|
|
1015
|
+
baseDir;
|
|
1016
|
+
constructor(baseDir) {
|
|
1017
|
+
this.baseDir = baseDir ?? DEFAULT_BASE_DIR;
|
|
1018
|
+
}
|
|
1019
|
+
async isAvailable() {
|
|
1020
|
+
try {
|
|
1021
|
+
return existsSync(this.baseDir);
|
|
1022
|
+
} catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
async load(range) {
|
|
1027
|
+
const files = collectJsonlFiles(this.baseDir);
|
|
1028
|
+
const allRecords = [];
|
|
1029
|
+
for (const file of files) {
|
|
1030
|
+
for await (const record of splitJsonlRecords(file)) {
|
|
1031
|
+
const usage = extractUsage(record);
|
|
1032
|
+
if (usage !== null && isInRange(usage.date, range)) {
|
|
1033
|
+
allRecords.push(usage);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const daily = buildDailyUsage(allRecords);
|
|
1038
|
+
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
1039
|
+
const totalCost = daily.reduce((sum, d) => sum + d.cost, 0);
|
|
1040
|
+
return {
|
|
1041
|
+
provider: this.name,
|
|
1042
|
+
displayName: this.displayName,
|
|
1043
|
+
daily,
|
|
1044
|
+
totalTokens,
|
|
1045
|
+
totalCost,
|
|
1046
|
+
colors: this.colors
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
// packages/registry/dist/providers/codex.js
|
|
1051
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
1052
|
+
import { join as join2 } from "path";
|
|
1053
|
+
import { homedir as homedir2 } from "os";
|
|
1054
|
+
var CODEX_COLORS = {
|
|
1055
|
+
primary: "#10a37f",
|
|
1056
|
+
secondary: "#4ade80",
|
|
1057
|
+
gradient: ["#10a37f", "#4ade80"]
|
|
1058
|
+
};
|
|
1059
|
+
var DEFAULT_SESSIONS_DIR = join2(homedir2(), ".codex", "sessions");
|
|
1060
|
+
function parseResponseEvent(record) {
|
|
1061
|
+
if (typeof record !== "object" || record === null || !("type" in record)) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
1064
|
+
const obj = record;
|
|
1065
|
+
if (obj["type"] !== "response") {
|
|
1066
|
+
return null;
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof obj["timestamp"] !== "string" || typeof obj["model"] !== "string" || typeof obj["usage"] !== "object" || obj["usage"] === null) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
const usage = obj["usage"];
|
|
1072
|
+
if (typeof usage["input_tokens"] !== "number" || typeof usage["output_tokens"] !== "number" || typeof usage["total_tokens"] !== "number") {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
type: "response",
|
|
1077
|
+
timestamp: obj["timestamp"],
|
|
1078
|
+
model: obj["model"],
|
|
1079
|
+
usage: {
|
|
1080
|
+
input_tokens: usage["input_tokens"],
|
|
1081
|
+
output_tokens: usage["output_tokens"],
|
|
1082
|
+
total_tokens: usage["total_tokens"]
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
var DASHED_DATE_SUFFIX = /-(\d{4})-(\d{2})-(\d{2})$/;
|
|
1087
|
+
function compactModelDateSuffix(model) {
|
|
1088
|
+
return model.replace(DASHED_DATE_SUFFIX, "-$1$2$3");
|
|
1089
|
+
}
|
|
1090
|
+
function extractDate(timestamp) {
|
|
1091
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
1092
|
+
return match ? match[1] : null;
|
|
1093
|
+
}
|
|
1094
|
+
function isInRange2(date, range) {
|
|
1095
|
+
return date >= range.since && date <= range.until;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
class CodexProvider {
|
|
1099
|
+
name = "codex";
|
|
1100
|
+
displayName = "Codex";
|
|
1101
|
+
colors = CODEX_COLORS;
|
|
1102
|
+
sessionsDir;
|
|
1103
|
+
constructor(baseDir) {
|
|
1104
|
+
this.sessionsDir = baseDir ?? DEFAULT_SESSIONS_DIR;
|
|
1105
|
+
}
|
|
1106
|
+
async isAvailable() {
|
|
1107
|
+
try {
|
|
1108
|
+
return existsSync2(this.sessionsDir);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async load(range) {
|
|
1114
|
+
const dailyMap = new Map;
|
|
1115
|
+
let files;
|
|
1116
|
+
try {
|
|
1117
|
+
files = readdirSync2(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1118
|
+
} catch {
|
|
1119
|
+
files = [];
|
|
1120
|
+
}
|
|
1121
|
+
for (const file of files) {
|
|
1122
|
+
const filePath = join2(this.sessionsDir, file);
|
|
1123
|
+
for await (const record of splitJsonlRecords(filePath)) {
|
|
1124
|
+
const event = parseResponseEvent(record);
|
|
1125
|
+
if (!event) {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const date = extractDate(event.timestamp);
|
|
1129
|
+
if (!date || !isInRange2(date, range)) {
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
|
|
1133
|
+
const inputTokens = event.usage.input_tokens;
|
|
1134
|
+
const outputTokens = event.usage.output_tokens;
|
|
1135
|
+
const cacheReadTokens = 0;
|
|
1136
|
+
const cacheWriteTokens = 0;
|
|
1137
|
+
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1138
|
+
if (!dailyMap.has(date)) {
|
|
1139
|
+
dailyMap.set(date, new Map);
|
|
1140
|
+
}
|
|
1141
|
+
const modelMap = dailyMap.get(date);
|
|
1142
|
+
if (!modelMap.has(normalizedModel)) {
|
|
1143
|
+
modelMap.set(normalizedModel, {
|
|
1144
|
+
model: normalizedModel,
|
|
1145
|
+
inputTokens: 0,
|
|
1146
|
+
outputTokens: 0,
|
|
1147
|
+
cacheReadTokens: 0,
|
|
1148
|
+
cacheWriteTokens: 0,
|
|
1149
|
+
totalTokens: 0,
|
|
1150
|
+
cost: 0
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
const breakdown = modelMap.get(normalizedModel);
|
|
1154
|
+
breakdown.inputTokens += inputTokens;
|
|
1155
|
+
breakdown.outputTokens += outputTokens;
|
|
1156
|
+
breakdown.totalTokens += inputTokens + outputTokens;
|
|
1157
|
+
breakdown.cost += cost;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const daily = [...dailyMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, modelMap]) => {
|
|
1161
|
+
const models = [...modelMap.values()];
|
|
1162
|
+
const inputTokens = models.reduce((s, m) => s + m.inputTokens, 0);
|
|
1163
|
+
const outputTokens = models.reduce((s, m) => s + m.outputTokens, 0);
|
|
1164
|
+
const cacheReadTokens = models.reduce((s, m) => s + m.cacheReadTokens, 0);
|
|
1165
|
+
const cacheWriteTokens = models.reduce((s, m) => s + m.cacheWriteTokens, 0);
|
|
1166
|
+
const totalTokens2 = models.reduce((s, m) => s + m.totalTokens, 0);
|
|
1167
|
+
const cost = models.reduce((s, m) => s + m.cost, 0);
|
|
1168
|
+
return {
|
|
1169
|
+
date,
|
|
1170
|
+
inputTokens,
|
|
1171
|
+
outputTokens,
|
|
1172
|
+
cacheReadTokens,
|
|
1173
|
+
cacheWriteTokens,
|
|
1174
|
+
totalTokens: totalTokens2,
|
|
1175
|
+
cost,
|
|
1176
|
+
models
|
|
1177
|
+
};
|
|
1178
|
+
});
|
|
1179
|
+
const totalTokens = daily.reduce((s, d) => s + d.totalTokens, 0);
|
|
1180
|
+
const totalCost = daily.reduce((s, d) => s + d.cost, 0);
|
|
1181
|
+
return {
|
|
1182
|
+
provider: this.name,
|
|
1183
|
+
displayName: this.displayName,
|
|
1184
|
+
daily,
|
|
1185
|
+
totalTokens,
|
|
1186
|
+
totalCost,
|
|
1187
|
+
colors: this.colors
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// packages/registry/dist/providers/open-code.js
|
|
1192
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync } from "fs";
|
|
1193
|
+
import { join as join3 } from "path";
|
|
1194
|
+
import { homedir as homedir3 } from "os";
|
|
1195
|
+
import { Database } from "bun:sqlite";
|
|
1196
|
+
var PROVIDER_NAME = "open-code";
|
|
1197
|
+
var DISPLAY_NAME = "Open Code";
|
|
1198
|
+
var COLORS = {
|
|
1199
|
+
primary: "#6366f1",
|
|
1200
|
+
secondary: "#a78bfa",
|
|
1201
|
+
gradient: ["#6366f1", "#a78bfa"]
|
|
1202
|
+
};
|
|
1203
|
+
function extractDate2(createdAt) {
|
|
1204
|
+
if (typeof createdAt === "number") {
|
|
1205
|
+
return new Date(createdAt * 1000).toISOString().slice(0, 10);
|
|
1206
|
+
}
|
|
1207
|
+
const asNum = Number(createdAt);
|
|
1208
|
+
if (!Number.isNaN(asNum) && String(asNum) === String(createdAt).trim()) {
|
|
1209
|
+
return new Date(asNum * 1000).toISOString().slice(0, 10);
|
|
1210
|
+
}
|
|
1211
|
+
return new Date(createdAt).toISOString().slice(0, 10);
|
|
1212
|
+
}
|
|
1213
|
+
function isWithinRange(date, range) {
|
|
1214
|
+
return date >= range.since && date <= range.until;
|
|
1215
|
+
}
|
|
1216
|
+
function buildProviderData(records) {
|
|
1217
|
+
const byDate = new Map;
|
|
1218
|
+
for (const record of records) {
|
|
1219
|
+
let dateMap = byDate.get(record.date);
|
|
1220
|
+
if (!dateMap) {
|
|
1221
|
+
dateMap = new Map;
|
|
1222
|
+
byDate.set(record.date, dateMap);
|
|
1223
|
+
}
|
|
1224
|
+
const normalized = normalizeModelName(record.model);
|
|
1225
|
+
const existing = dateMap.get(normalized);
|
|
1226
|
+
if (existing) {
|
|
1227
|
+
existing.inputTokens += record.inputTokens;
|
|
1228
|
+
existing.outputTokens += record.outputTokens;
|
|
1229
|
+
} else {
|
|
1230
|
+
dateMap.set(normalized, {
|
|
1231
|
+
inputTokens: record.inputTokens,
|
|
1232
|
+
outputTokens: record.outputTokens
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
let totalTokens = 0;
|
|
1237
|
+
let totalCost = 0;
|
|
1238
|
+
const daily = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, modelMap]) => {
|
|
1239
|
+
const models = [];
|
|
1240
|
+
let dayInput = 0;
|
|
1241
|
+
let dayOutput = 0;
|
|
1242
|
+
let dayCost = 0;
|
|
1243
|
+
for (const [model, usage] of modelMap) {
|
|
1244
|
+
const cost = estimateCost(model, usage.inputTokens, usage.outputTokens, 0, 0);
|
|
1245
|
+
const modelTotal = usage.inputTokens + usage.outputTokens;
|
|
1246
|
+
models.push({
|
|
1247
|
+
model,
|
|
1248
|
+
inputTokens: usage.inputTokens,
|
|
1249
|
+
outputTokens: usage.outputTokens,
|
|
1250
|
+
cacheReadTokens: 0,
|
|
1251
|
+
cacheWriteTokens: 0,
|
|
1252
|
+
totalTokens: modelTotal,
|
|
1253
|
+
cost
|
|
1254
|
+
});
|
|
1255
|
+
dayInput += usage.inputTokens;
|
|
1256
|
+
dayOutput += usage.outputTokens;
|
|
1257
|
+
dayCost += cost;
|
|
1258
|
+
}
|
|
1259
|
+
const dayTotal = dayInput + dayOutput;
|
|
1260
|
+
totalTokens += dayTotal;
|
|
1261
|
+
totalCost += dayCost;
|
|
1262
|
+
return {
|
|
1263
|
+
date,
|
|
1264
|
+
inputTokens: dayInput,
|
|
1265
|
+
outputTokens: dayOutput,
|
|
1266
|
+
cacheReadTokens: 0,
|
|
1267
|
+
cacheWriteTokens: 0,
|
|
1268
|
+
totalTokens: dayTotal,
|
|
1269
|
+
cost: dayCost,
|
|
1270
|
+
models
|
|
1271
|
+
};
|
|
1272
|
+
});
|
|
1273
|
+
return {
|
|
1274
|
+
provider: PROVIDER_NAME,
|
|
1275
|
+
displayName: DISPLAY_NAME,
|
|
1276
|
+
daily,
|
|
1277
|
+
totalTokens,
|
|
1278
|
+
totalCost,
|
|
1279
|
+
colors: COLORS
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function loadFromSqlite(dbPath, range) {
|
|
1283
|
+
const db = new Database(dbPath, { readonly: true });
|
|
1284
|
+
try {
|
|
1285
|
+
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1286
|
+
const records = [];
|
|
1287
|
+
for (const row of rows) {
|
|
1288
|
+
const date = extractDate2(row.created_at);
|
|
1289
|
+
if (isWithinRange(date, range)) {
|
|
1290
|
+
records.push({
|
|
1291
|
+
date,
|
|
1292
|
+
model: row.model,
|
|
1293
|
+
inputTokens: row.input_tokens,
|
|
1294
|
+
outputTokens: row.output_tokens
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return records;
|
|
1299
|
+
} finally {
|
|
1300
|
+
db.close();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function loadFromJson(sessionsDir, range) {
|
|
1304
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1305
|
+
const records = [];
|
|
1306
|
+
for (const file of files) {
|
|
1307
|
+
const content = readFileSync(join3(sessionsDir, file), "utf-8");
|
|
1308
|
+
const session = JSON.parse(content);
|
|
1309
|
+
if (!Array.isArray(session.messages)) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
for (const msg of session.messages) {
|
|
1313
|
+
if (msg.role !== "assistant" || !msg.usage) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const date = extractDate2(msg.created_at);
|
|
1317
|
+
if (isWithinRange(date, range)) {
|
|
1318
|
+
records.push({
|
|
1319
|
+
date,
|
|
1320
|
+
model: msg.model,
|
|
1321
|
+
inputTokens: msg.usage.input_tokens,
|
|
1322
|
+
outputTokens: msg.usage.output_tokens
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return records;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
class OpenCodeProvider {
|
|
1331
|
+
name = PROVIDER_NAME;
|
|
1332
|
+
displayName = DISPLAY_NAME;
|
|
1333
|
+
colors = COLORS;
|
|
1334
|
+
baseDir;
|
|
1335
|
+
constructor(baseDir) {
|
|
1336
|
+
this.baseDir = baseDir ?? join3(homedir3(), ".opencode");
|
|
1337
|
+
}
|
|
1338
|
+
async isAvailable() {
|
|
1339
|
+
try {
|
|
1340
|
+
if (!existsSync3(this.baseDir)) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
const hasDb = existsSync3(join3(this.baseDir, "sessions.db"));
|
|
1344
|
+
const hasSessionsDir = existsSync3(join3(this.baseDir, "sessions"));
|
|
1345
|
+
return hasDb || hasSessionsDir;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
async load(range) {
|
|
1351
|
+
const dbPath = join3(this.baseDir, "sessions.db");
|
|
1352
|
+
const sessionsDir = join3(this.baseDir, "sessions");
|
|
1353
|
+
let records;
|
|
1354
|
+
if (existsSync3(dbPath)) {
|
|
1355
|
+
records = loadFromSqlite(dbPath, range);
|
|
1356
|
+
} else if (existsSync3(sessionsDir)) {
|
|
1357
|
+
records = loadFromJson(sessionsDir, range);
|
|
1358
|
+
} else {
|
|
1359
|
+
records = [];
|
|
1360
|
+
}
|
|
1361
|
+
return buildProviderData(records);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
// packages/renderers/dist/json/json-renderer.js
|
|
1365
|
+
class JsonRenderer {
|
|
1366
|
+
format = "json";
|
|
1367
|
+
async render(output, _options) {
|
|
1368
|
+
return JSON.stringify(output, null, 2);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// packages/renderers/dist/svg/theme.js
|
|
1372
|
+
var DARK_THEME = {
|
|
1373
|
+
background: "#0d1117",
|
|
1374
|
+
foreground: "#e6edf3",
|
|
1375
|
+
muted: "#7d8590",
|
|
1376
|
+
border: "#30363d",
|
|
1377
|
+
cardBackground: "#161b22",
|
|
1378
|
+
heatmap: ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
|
|
1379
|
+
accent: "#58a6ff",
|
|
1380
|
+
accentSecondary: "#bc8cff",
|
|
1381
|
+
barFill: "#58a6ff",
|
|
1382
|
+
barBackground: "#21262d"
|
|
1383
|
+
};
|
|
1384
|
+
var LIGHT_THEME = {
|
|
1385
|
+
background: "#ffffff",
|
|
1386
|
+
foreground: "#1f2328",
|
|
1387
|
+
muted: "#656d76",
|
|
1388
|
+
border: "#d0d7de",
|
|
1389
|
+
cardBackground: "#f6f8fa",
|
|
1390
|
+
heatmap: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
|
1391
|
+
accent: "#0969da",
|
|
1392
|
+
accentSecondary: "#8250df",
|
|
1393
|
+
barFill: "#0969da",
|
|
1394
|
+
barBackground: "#eaeef2"
|
|
1395
|
+
};
|
|
1396
|
+
function getTheme(mode) {
|
|
1397
|
+
return mode === "dark" ? DARK_THEME : LIGHT_THEME;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// packages/renderers/dist/svg/layout.js
|
|
1401
|
+
var PADDING = 24;
|
|
1402
|
+
var CELL_SIZE = 12;
|
|
1403
|
+
var CELL_GAP = 3;
|
|
1404
|
+
var HEADER_HEIGHT = 60;
|
|
1405
|
+
var MONTH_LABEL_HEIGHT = 20;
|
|
1406
|
+
var DAY_LABEL_WIDTH = 32;
|
|
1407
|
+
var HEATMAP_ROWS = 7;
|
|
1408
|
+
var SECTION_GAP = 28;
|
|
1409
|
+
var STAT_ROW_HEIGHT = 28;
|
|
1410
|
+
var BAR_HEIGHT = 20;
|
|
1411
|
+
var BAR_GAP = 8;
|
|
1412
|
+
var BAR_LABEL_WIDTH = 120;
|
|
1413
|
+
var FONT_SIZE_TITLE = 20;
|
|
1414
|
+
var FONT_SIZE_SUBTITLE = 14;
|
|
1415
|
+
var FONT_SIZE_BODY = 12;
|
|
1416
|
+
var FONT_SIZE_SMALL = 10;
|
|
1417
|
+
var FONT_FAMILY = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif";
|
|
1418
|
+
|
|
1419
|
+
// packages/renderers/dist/svg/utils.js
|
|
1420
|
+
function escapeXml(str) {
|
|
1421
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1422
|
+
}
|
|
1423
|
+
function rect(x, y, w, h, fill, rx) {
|
|
1424
|
+
const rxAttr = rx !== undefined ? ` rx="${rx}"` : "";
|
|
1425
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${escapeXml(fill)}"${rxAttr}/>`;
|
|
1426
|
+
}
|
|
1427
|
+
function text(x, y, content, attrs) {
|
|
1428
|
+
const attrStr = attrs ? Object.entries(attrs).map(([k, v]) => ` ${k}="${typeof v === "string" ? escapeXml(v) : v}"`).join("") : "";
|
|
1429
|
+
return `<text x="${x}" y="${y}"${attrStr}>${escapeXml(content)}</text>`;
|
|
1430
|
+
}
|
|
1431
|
+
function group(children, transform) {
|
|
1432
|
+
const transformAttr = transform ? ` transform="${escapeXml(transform)}"` : "";
|
|
1433
|
+
return `<g${transformAttr}>${children.join("")}</g>`;
|
|
1434
|
+
}
|
|
1435
|
+
function formatNumber(n) {
|
|
1436
|
+
if (n >= 1e6) {
|
|
1437
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1438
|
+
}
|
|
1439
|
+
if (n >= 1000) {
|
|
1440
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
1441
|
+
}
|
|
1442
|
+
return n.toFixed(0);
|
|
1443
|
+
}
|
|
1444
|
+
function formatCost(cost) {
|
|
1445
|
+
if (cost >= 100) {
|
|
1446
|
+
return `$${cost.toFixed(0)}`;
|
|
1447
|
+
}
|
|
1448
|
+
if (cost >= 1) {
|
|
1449
|
+
return `$${cost.toFixed(2)}`;
|
|
1450
|
+
}
|
|
1451
|
+
return `$${cost.toFixed(4)}`;
|
|
1452
|
+
}
|
|
1453
|
+
function formatPercent(rate) {
|
|
1454
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// packages/renderers/dist/svg/heatmap.js
|
|
1458
|
+
var DAY_LABELS2 = ["", "Mon", "", "Wed", "", "Fri", ""];
|
|
1459
|
+
var MONTH_NAMES = [
|
|
1460
|
+
"Jan",
|
|
1461
|
+
"Feb",
|
|
1462
|
+
"Mar",
|
|
1463
|
+
"Apr",
|
|
1464
|
+
"May",
|
|
1465
|
+
"Jun",
|
|
1466
|
+
"Jul",
|
|
1467
|
+
"Aug",
|
|
1468
|
+
"Sep",
|
|
1469
|
+
"Oct",
|
|
1470
|
+
"Nov",
|
|
1471
|
+
"Dec"
|
|
1472
|
+
];
|
|
1473
|
+
function getLevel(tokens, quantiles) {
|
|
1474
|
+
if (tokens <= 0)
|
|
1475
|
+
return 0;
|
|
1476
|
+
if (tokens <= quantiles[0])
|
|
1477
|
+
return 1;
|
|
1478
|
+
if (tokens <= quantiles[1])
|
|
1479
|
+
return 2;
|
|
1480
|
+
if (tokens <= quantiles[2])
|
|
1481
|
+
return 3;
|
|
1482
|
+
return 4;
|
|
1483
|
+
}
|
|
1484
|
+
function computeQuantiles(values) {
|
|
1485
|
+
const nonZero = values.filter((v) => v > 0).sort((a, b) => a - b);
|
|
1486
|
+
if (nonZero.length === 0)
|
|
1487
|
+
return [0, 0, 0];
|
|
1488
|
+
const q = (p) => {
|
|
1489
|
+
const idx = Math.floor(p * (nonZero.length - 1));
|
|
1490
|
+
return nonZero[idx] ?? 0;
|
|
1491
|
+
};
|
|
1492
|
+
return [q(0.25), q(0.5), q(0.75)];
|
|
1493
|
+
}
|
|
1494
|
+
function renderHeatmap(daily, theme, options = {}) {
|
|
1495
|
+
const tokenMap = new Map;
|
|
1496
|
+
for (const d of daily) {
|
|
1497
|
+
const existing = tokenMap.get(d.date) ?? 0;
|
|
1498
|
+
tokenMap.set(d.date, existing + d.totalTokens);
|
|
1499
|
+
}
|
|
1500
|
+
const dates = daily.map((d) => d.date).sort();
|
|
1501
|
+
const endStr = options.endDate ?? dates[dates.length - 1] ?? new Date().toISOString().slice(0, 10);
|
|
1502
|
+
const startStr = options.startDate ?? dates[0] ?? endStr;
|
|
1503
|
+
const end = new Date(endStr);
|
|
1504
|
+
const start = new Date(startStr);
|
|
1505
|
+
const startDay = start.getDay();
|
|
1506
|
+
start.setDate(start.getDate() - startDay);
|
|
1507
|
+
const cells = [];
|
|
1508
|
+
const allTokens = Array.from(tokenMap.values());
|
|
1509
|
+
const quantiles = computeQuantiles(allTokens);
|
|
1510
|
+
const current = new Date(start);
|
|
1511
|
+
let col = 0;
|
|
1512
|
+
const monthLabels = [];
|
|
1513
|
+
let lastMonth = -1;
|
|
1514
|
+
while (current <= end) {
|
|
1515
|
+
const row = current.getDay();
|
|
1516
|
+
const dateStr = current.toISOString().slice(0, 10);
|
|
1517
|
+
const tokens = tokenMap.get(dateStr) ?? 0;
|
|
1518
|
+
const level = getLevel(tokens, quantiles);
|
|
1519
|
+
const x = DAY_LABEL_WIDTH + col * (CELL_SIZE + CELL_GAP);
|
|
1520
|
+
const y = MONTH_LABEL_HEIGHT + row * (CELL_SIZE + CELL_GAP);
|
|
1521
|
+
const title = `${dateStr}: ${tokens.toLocaleString()} tokens`;
|
|
1522
|
+
cells.push(`<rect x="${x}" y="${y}" width="${CELL_SIZE}" height="${CELL_SIZE}" fill="${escapeXml(theme.heatmap[level])}" rx="2"><title>${escapeXml(title)}</title></rect>`);
|
|
1523
|
+
const month = current.getMonth();
|
|
1524
|
+
if (month !== lastMonth && row === 0) {
|
|
1525
|
+
lastMonth = month;
|
|
1526
|
+
monthLabels.push(text(x, MONTH_LABEL_HEIGHT - 6, MONTH_NAMES[month] ?? "", {
|
|
1527
|
+
fill: theme.muted,
|
|
1528
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1529
|
+
"font-family": FONT_FAMILY
|
|
1530
|
+
}));
|
|
1531
|
+
}
|
|
1532
|
+
if (row === 6) {
|
|
1533
|
+
col++;
|
|
1534
|
+
}
|
|
1535
|
+
current.setDate(current.getDate() + 1);
|
|
1536
|
+
}
|
|
1537
|
+
const dayLabels = DAY_LABELS2.map((label, i) => {
|
|
1538
|
+
if (!label)
|
|
1539
|
+
return "";
|
|
1540
|
+
const y = MONTH_LABEL_HEIGHT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE - 1;
|
|
1541
|
+
return text(0, y, label, {
|
|
1542
|
+
fill: theme.muted,
|
|
1543
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1544
|
+
"font-family": FONT_FAMILY
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
const totalCols = col + 1;
|
|
1548
|
+
const width = DAY_LABEL_WIDTH + totalCols * (CELL_SIZE + CELL_GAP);
|
|
1549
|
+
const height = MONTH_LABEL_HEIGHT + HEATMAP_ROWS * (CELL_SIZE + CELL_GAP);
|
|
1550
|
+
const svg = group([...monthLabels, ...dayLabels, ...cells]);
|
|
1551
|
+
return { svg, width, height };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// packages/renderers/dist/svg/stats-panel.js
|
|
1555
|
+
function buildStatItems(stats) {
|
|
1556
|
+
return [
|
|
1557
|
+
{ label: "Current Streak", value: `${stats.currentStreak} days` },
|
|
1558
|
+
{ label: "Longest Streak", value: `${stats.longestStreak} days` },
|
|
1559
|
+
{ label: "Total Tokens", value: formatNumber(stats.totalTokens) },
|
|
1560
|
+
{ label: "Total Cost", value: formatCost(stats.totalCost) },
|
|
1561
|
+
{ label: "30-Day Tokens", value: formatNumber(stats.rolling30dTokens) },
|
|
1562
|
+
{ label: "30-Day Cost", value: formatCost(stats.rolling30dCost) },
|
|
1563
|
+
{ label: "Avg Daily Tokens", value: formatNumber(stats.averageDailyTokens) },
|
|
1564
|
+
{ label: "Cache Hit Rate", value: formatPercent(stats.cacheHitRate) },
|
|
1565
|
+
{ label: "Active Days", value: `${stats.activeDays} / ${stats.totalDays}` }
|
|
1566
|
+
];
|
|
1567
|
+
}
|
|
1568
|
+
function renderStatsPanel(stats, theme) {
|
|
1569
|
+
const items = buildStatItems(stats);
|
|
1570
|
+
const width = 280;
|
|
1571
|
+
const children = [];
|
|
1572
|
+
for (let i = 0;i < items.length; i++) {
|
|
1573
|
+
const item = items[i];
|
|
1574
|
+
if (!item)
|
|
1575
|
+
continue;
|
|
1576
|
+
const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
|
|
1577
|
+
children.push(text(0, y, item.label, {
|
|
1578
|
+
fill: theme.muted,
|
|
1579
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1580
|
+
"font-family": FONT_FAMILY
|
|
1581
|
+
}));
|
|
1582
|
+
children.push(text(width - 8, y, item.value, {
|
|
1583
|
+
fill: theme.foreground,
|
|
1584
|
+
"font-size": FONT_SIZE_BODY,
|
|
1585
|
+
"font-family": FONT_FAMILY,
|
|
1586
|
+
"text-anchor": "end"
|
|
1587
|
+
}));
|
|
1588
|
+
}
|
|
1589
|
+
const height = items.length * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
|
|
1590
|
+
return { svg: group(children), width, height };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// packages/renderers/dist/svg/insights-panel.js
|
|
1594
|
+
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1595
|
+
function buildInsights(stats, providers) {
|
|
1596
|
+
const items = [];
|
|
1597
|
+
if (stats.peakDay) {
|
|
1598
|
+
items.push({
|
|
1599
|
+
label: "Peak Day",
|
|
1600
|
+
value: `${stats.peakDay.date} (${formatNumber(stats.peakDay.tokens)} tokens)`
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
if (stats.dayOfWeek.length > 0) {
|
|
1604
|
+
const sorted = [...stats.dayOfWeek].sort((a, b) => b.tokens - a.tokens);
|
|
1605
|
+
const top = sorted[0];
|
|
1606
|
+
if (top) {
|
|
1607
|
+
items.push({
|
|
1608
|
+
label: "Most Active Day",
|
|
1609
|
+
value: DAY_NAMES[top.day] ?? top.label
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (stats.topModels.length > 0) {
|
|
1614
|
+
const top = stats.topModels[0];
|
|
1615
|
+
if (top) {
|
|
1616
|
+
items.push({
|
|
1617
|
+
label: "Top Model",
|
|
1618
|
+
value: `${top.model} (${top.percentage.toFixed(1)}%)`
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (providers.length > 0) {
|
|
1623
|
+
const sorted = [...providers].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1624
|
+
const top = sorted[0];
|
|
1625
|
+
if (top) {
|
|
1626
|
+
items.push({
|
|
1627
|
+
label: "Top Provider",
|
|
1628
|
+
value: `${top.displayName} (${formatNumber(top.totalTokens)} tokens)`
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return items;
|
|
1633
|
+
}
|
|
1634
|
+
function renderInsightsPanel(stats, providers, theme) {
|
|
1635
|
+
const items = buildInsights(stats, providers);
|
|
1636
|
+
const width = 360;
|
|
1637
|
+
const children = [];
|
|
1638
|
+
for (let i = 0;i < items.length; i++) {
|
|
1639
|
+
const item = items[i];
|
|
1640
|
+
if (!item)
|
|
1641
|
+
continue;
|
|
1642
|
+
const y = i * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT;
|
|
1643
|
+
children.push(text(0, y, item.label, {
|
|
1644
|
+
fill: theme.muted,
|
|
1645
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1646
|
+
"font-family": FONT_FAMILY
|
|
1647
|
+
}));
|
|
1648
|
+
children.push(text(width - 8, y, item.value, {
|
|
1649
|
+
fill: theme.foreground,
|
|
1650
|
+
"font-size": FONT_SIZE_BODY,
|
|
1651
|
+
"font-family": FONT_FAMILY,
|
|
1652
|
+
"text-anchor": "end"
|
|
1653
|
+
}));
|
|
1654
|
+
}
|
|
1655
|
+
const height = Math.max(items.length * STAT_ROW_HEIGHT + STAT_ROW_HEIGHT, STAT_ROW_HEIGHT);
|
|
1656
|
+
return { svg: group(children), width, height };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// packages/renderers/dist/svg/day-of-week-chart.js
|
|
1660
|
+
function renderDayOfWeekChart(dayOfWeek, theme) {
|
|
1661
|
+
const chartWidth = 300;
|
|
1662
|
+
const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
|
|
1663
|
+
const maxTokens = Math.max(...dayOfWeek.map((d) => d.tokens), 1);
|
|
1664
|
+
const children = [];
|
|
1665
|
+
for (let i = 0;i < dayOfWeek.length; i++) {
|
|
1666
|
+
const entry = dayOfWeek[i];
|
|
1667
|
+
if (!entry)
|
|
1668
|
+
continue;
|
|
1669
|
+
const y = i * (BAR_HEIGHT + BAR_GAP);
|
|
1670
|
+
const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
|
|
1671
|
+
children.push(text(0, y + BAR_HEIGHT - 4, entry.label, {
|
|
1672
|
+
fill: theme.muted,
|
|
1673
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1674
|
+
"font-family": FONT_FAMILY
|
|
1675
|
+
}));
|
|
1676
|
+
children.push(rect(BAR_LABEL_WIDTH, y, barAreaWidth, BAR_HEIGHT, theme.barBackground, 3));
|
|
1677
|
+
if (barWidth > 0) {
|
|
1678
|
+
children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.barFill, 3));
|
|
1679
|
+
}
|
|
1680
|
+
children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, formatNumber(entry.tokens), {
|
|
1681
|
+
fill: theme.foreground,
|
|
1682
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1683
|
+
"font-family": FONT_FAMILY
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
const height = dayOfWeek.length * (BAR_HEIGHT + BAR_GAP);
|
|
1687
|
+
const width = chartWidth + 60;
|
|
1688
|
+
return { svg: group(children), width, height };
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// packages/renderers/dist/svg/model-chart.js
|
|
1692
|
+
function renderModelChart(topModels2, theme) {
|
|
1693
|
+
const chartWidth = 360;
|
|
1694
|
+
const barAreaWidth = chartWidth - BAR_LABEL_WIDTH;
|
|
1695
|
+
const maxTokens = Math.max(...topModels2.map((m) => m.tokens), 1);
|
|
1696
|
+
const children = [];
|
|
1697
|
+
for (let i = 0;i < topModels2.length; i++) {
|
|
1698
|
+
const entry = topModels2[i];
|
|
1699
|
+
if (!entry)
|
|
1700
|
+
continue;
|
|
1701
|
+
const y = i * (BAR_HEIGHT + BAR_GAP);
|
|
1702
|
+
const barWidth = Math.max(entry.tokens / maxTokens * barAreaWidth, 0);
|
|
1703
|
+
const label = entry.model.length > 18 ? entry.model.slice(0, 17) + "\u2026" : entry.model;
|
|
1704
|
+
children.push(text(0, y + BAR_HEIGHT - 4, label, {
|
|
1705
|
+
fill: theme.muted,
|
|
1706
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1707
|
+
"font-family": FONT_FAMILY
|
|
1708
|
+
}));
|
|
1709
|
+
children.push(rect(BAR_LABEL_WIDTH, y, barAreaWidth, BAR_HEIGHT, theme.barBackground, 3));
|
|
1710
|
+
if (barWidth > 0) {
|
|
1711
|
+
children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.accentSecondary, 3));
|
|
1712
|
+
}
|
|
1713
|
+
const valueStr = `${formatNumber(entry.tokens)} (${entry.percentage.toFixed(1)}%)`;
|
|
1714
|
+
children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, valueStr, {
|
|
1715
|
+
fill: theme.foreground,
|
|
1716
|
+
"font-size": FONT_SIZE_SMALL,
|
|
1717
|
+
"font-family": FONT_FAMILY
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
const height = Math.max(topModels2.length * (BAR_HEIGHT + BAR_GAP), BAR_HEIGHT);
|
|
1721
|
+
const width = chartWidth + 100;
|
|
1722
|
+
return { svg: group(children), width, height };
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// packages/renderers/dist/svg/svg-renderer.js
|
|
1726
|
+
class SvgRenderer {
|
|
1727
|
+
format = "svg";
|
|
1728
|
+
async render(output, options) {
|
|
1729
|
+
const theme = getTheme(options.theme);
|
|
1730
|
+
const contentWidth = options.width - PADDING * 2;
|
|
1731
|
+
let y = PADDING;
|
|
1732
|
+
const sections = [];
|
|
1733
|
+
sections.push(group([
|
|
1734
|
+
text(PADDING, y + FONT_SIZE_TITLE + 4, "Tokenleak", {
|
|
1735
|
+
fill: theme.foreground,
|
|
1736
|
+
"font-size": FONT_SIZE_TITLE,
|
|
1737
|
+
"font-family": FONT_FAMILY,
|
|
1738
|
+
"font-weight": "bold"
|
|
1739
|
+
}),
|
|
1740
|
+
text(PADDING, y + FONT_SIZE_TITLE + 4 + 20, `${output.dateRange.since} \u2014 ${output.dateRange.until}`, {
|
|
1741
|
+
fill: theme.muted,
|
|
1742
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1743
|
+
"font-family": FONT_FAMILY
|
|
1744
|
+
})
|
|
1745
|
+
]));
|
|
1746
|
+
y += HEADER_HEIGHT + SECTION_GAP;
|
|
1747
|
+
if (output.providers.length > 0) {
|
|
1748
|
+
const providerNames = output.providers.map((p) => p.displayName).join(" \xB7 ");
|
|
1749
|
+
sections.push(text(PADDING, y, providerNames, {
|
|
1750
|
+
fill: theme.accent,
|
|
1751
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1752
|
+
"font-family": FONT_FAMILY
|
|
1753
|
+
}));
|
|
1754
|
+
y += SECTION_GAP;
|
|
1755
|
+
}
|
|
1756
|
+
const allDaily = output.providers.flatMap((p) => p.daily);
|
|
1757
|
+
if (allDaily.length > 0) {
|
|
1758
|
+
sections.push(text(PADDING, y, "Activity", {
|
|
1759
|
+
fill: theme.foreground,
|
|
1760
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1761
|
+
"font-family": FONT_FAMILY,
|
|
1762
|
+
"font-weight": "bold"
|
|
1763
|
+
}));
|
|
1764
|
+
y += 16;
|
|
1765
|
+
const heatmap = renderHeatmap(allDaily, theme, {
|
|
1766
|
+
startDate: output.dateRange.since,
|
|
1767
|
+
endDate: output.dateRange.until
|
|
1768
|
+
});
|
|
1769
|
+
sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
|
|
1770
|
+
y += heatmap.height + SECTION_GAP;
|
|
1771
|
+
}
|
|
1772
|
+
sections.push(text(PADDING, y, "Statistics", {
|
|
1773
|
+
fill: theme.foreground,
|
|
1774
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1775
|
+
"font-family": FONT_FAMILY,
|
|
1776
|
+
"font-weight": "bold"
|
|
1777
|
+
}));
|
|
1778
|
+
y += 16;
|
|
1779
|
+
const stats = renderStatsPanel(output.aggregated, theme);
|
|
1780
|
+
sections.push(group([stats.svg], `translate(${PADDING}, ${y})`));
|
|
1781
|
+
y += stats.height + SECTION_GAP;
|
|
1782
|
+
if (output.aggregated.dayOfWeek.length > 0) {
|
|
1783
|
+
sections.push(text(PADDING, y, "Day of Week", {
|
|
1784
|
+
fill: theme.foreground,
|
|
1785
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1786
|
+
"font-family": FONT_FAMILY,
|
|
1787
|
+
"font-weight": "bold"
|
|
1788
|
+
}));
|
|
1789
|
+
y += 16;
|
|
1790
|
+
const dowChart = renderDayOfWeekChart(output.aggregated.dayOfWeek, theme);
|
|
1791
|
+
sections.push(group([dowChart.svg], `translate(${PADDING}, ${y})`));
|
|
1792
|
+
y += dowChart.height + SECTION_GAP;
|
|
1793
|
+
}
|
|
1794
|
+
if (output.aggregated.topModels.length > 0) {
|
|
1795
|
+
sections.push(text(PADDING, y, "Top Models", {
|
|
1796
|
+
fill: theme.foreground,
|
|
1797
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1798
|
+
"font-family": FONT_FAMILY,
|
|
1799
|
+
"font-weight": "bold"
|
|
1800
|
+
}));
|
|
1801
|
+
y += 16;
|
|
1802
|
+
const modelChart = renderModelChart(output.aggregated.topModels, theme);
|
|
1803
|
+
sections.push(group([modelChart.svg], `translate(${PADDING}, ${y})`));
|
|
1804
|
+
y += modelChart.height + SECTION_GAP;
|
|
1805
|
+
}
|
|
1806
|
+
if (options.showInsights) {
|
|
1807
|
+
sections.push(text(PADDING, y, "Insights", {
|
|
1808
|
+
fill: theme.foreground,
|
|
1809
|
+
"font-size": FONT_SIZE_SUBTITLE,
|
|
1810
|
+
"font-family": FONT_FAMILY,
|
|
1811
|
+
"font-weight": "bold"
|
|
1812
|
+
}));
|
|
1813
|
+
y += 16;
|
|
1814
|
+
const insights = renderInsightsPanel(output.aggregated, output.providers, theme);
|
|
1815
|
+
sections.push(group([insights.svg], `translate(${PADDING}, ${y})`));
|
|
1816
|
+
y += insights.height + SECTION_GAP;
|
|
1817
|
+
}
|
|
1818
|
+
const totalHeight = y + PADDING;
|
|
1819
|
+
const svgContent = [
|
|
1820
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${options.width}" height="${totalHeight}" viewBox="0 0 ${options.width} ${totalHeight}">`,
|
|
1821
|
+
rect(0, 0, options.width, totalHeight, theme.background, 8),
|
|
1822
|
+
...sections,
|
|
1823
|
+
"</svg>"
|
|
1824
|
+
].join(`
|
|
1825
|
+
`);
|
|
1826
|
+
return svgContent;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// packages/renderers/dist/png/png-renderer.js
|
|
1830
|
+
import sharp from "sharp";
|
|
1831
|
+
|
|
1832
|
+
class PngRenderer {
|
|
1833
|
+
format = "png";
|
|
1834
|
+
svgRenderer = new SvgRenderer;
|
|
1835
|
+
async render(output, options) {
|
|
1836
|
+
const svgString = await this.svgRenderer.render(output, options);
|
|
1837
|
+
const pngBuffer = await sharp(Buffer.from(svgString)).png().toBuffer();
|
|
1838
|
+
return pngBuffer;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
// packages/renderers/dist/terminal/ansi.js
|
|
1842
|
+
var ESC = "\x1B[";
|
|
1843
|
+
var CODES = {
|
|
1844
|
+
green: `${ESC}32m`,
|
|
1845
|
+
yellow: `${ESC}33m`,
|
|
1846
|
+
red: `${ESC}31m`,
|
|
1847
|
+
cyan: `${ESC}36m`,
|
|
1848
|
+
bold: `${ESC}1m`,
|
|
1849
|
+
dim: `${ESC}2m`,
|
|
1850
|
+
reset: `${ESC}0m`
|
|
1851
|
+
};
|
|
1852
|
+
// packages/cli/src/config.ts
|
|
1853
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1854
|
+
import { join as join4 } from "path";
|
|
1855
|
+
import { homedir as homedir4 } from "os";
|
|
1856
|
+
var CONFIG_FILENAME = ".tokenleakrc";
|
|
1857
|
+
function loadConfig() {
|
|
1858
|
+
try {
|
|
1859
|
+
const configPath = join4(homedir4(), CONFIG_FILENAME);
|
|
1860
|
+
const raw = readFileSync2(configPath, "utf-8");
|
|
1861
|
+
const parsed = JSON.parse(raw);
|
|
1862
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1863
|
+
return parsed;
|
|
1864
|
+
}
|
|
1865
|
+
return {};
|
|
1866
|
+
} catch {
|
|
1867
|
+
return {};
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// packages/cli/src/env.ts
|
|
1872
|
+
var VALID_FORMATS = new Set(["json", "svg", "png", "terminal"]);
|
|
1873
|
+
var VALID_THEMES = new Set(["dark", "light"]);
|
|
1874
|
+
function loadEnvOverrides() {
|
|
1875
|
+
const overrides = {};
|
|
1876
|
+
const format = process.env["TOKENLEAK_FORMAT"];
|
|
1877
|
+
if (format && VALID_FORMATS.has(format)) {
|
|
1878
|
+
overrides.format = format;
|
|
1879
|
+
}
|
|
1880
|
+
const theme = process.env["TOKENLEAK_THEME"];
|
|
1881
|
+
if (theme && VALID_THEMES.has(theme)) {
|
|
1882
|
+
overrides.theme = theme;
|
|
1883
|
+
}
|
|
1884
|
+
const days = process.env["TOKENLEAK_DAYS"];
|
|
1885
|
+
if (days !== undefined && days !== "") {
|
|
1886
|
+
const parsed = Number(days);
|
|
1887
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1888
|
+
overrides.days = parsed;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return overrides;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// packages/cli/src/errors.ts
|
|
1895
|
+
class TokenleakError extends Error {
|
|
1896
|
+
constructor(message) {
|
|
1897
|
+
super(message);
|
|
1898
|
+
this.name = "TokenleakError";
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
function handleError(error) {
|
|
1902
|
+
if (error instanceof TokenleakError) {
|
|
1903
|
+
process.stderr.write(`Error: ${error.message}
|
|
1904
|
+
`);
|
|
1905
|
+
} else if (error instanceof Error) {
|
|
1906
|
+
process.stderr.write(`Error: ${error.message}
|
|
1907
|
+
`);
|
|
1908
|
+
} else {
|
|
1909
|
+
process.stderr.write(`Error: ${String(error)}
|
|
1910
|
+
`);
|
|
1911
|
+
}
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// packages/cli/src/sharing/clipboard.ts
|
|
1916
|
+
var PLATFORM_COMMANDS = {
|
|
1917
|
+
darwin: ["pbcopy"],
|
|
1918
|
+
linux: ["xclip", "-selection", "clipboard"],
|
|
1919
|
+
win32: ["clip"]
|
|
1920
|
+
};
|
|
1921
|
+
function getClipboardCommand(platform = process.platform) {
|
|
1922
|
+
const command = PLATFORM_COMMANDS[platform];
|
|
1923
|
+
if (!command) {
|
|
1924
|
+
throw new Error(`Clipboard is not supported on platform "${platform}". Supported: macOS, Linux, Windows.`);
|
|
1925
|
+
}
|
|
1926
|
+
return command;
|
|
1927
|
+
}
|
|
1928
|
+
async function copyToClipboard(content, platform = process.platform) {
|
|
1929
|
+
const [cmd, ...args] = getClipboardCommand(platform);
|
|
1930
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
1931
|
+
stdin: "pipe",
|
|
1932
|
+
stdout: "ignore",
|
|
1933
|
+
stderr: "pipe"
|
|
1934
|
+
});
|
|
1935
|
+
proc.stdin.write(content);
|
|
1936
|
+
proc.stdin.end();
|
|
1937
|
+
const exitCode = await proc.exited;
|
|
1938
|
+
if (exitCode !== 0) {
|
|
1939
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1940
|
+
throw new Error(`Clipboard command "${cmd}" failed with exit code ${exitCode}: ${stderr.trim()}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
// packages/cli/src/sharing/open.ts
|
|
1944
|
+
var PLATFORM_COMMANDS2 = {
|
|
1945
|
+
darwin: "open",
|
|
1946
|
+
linux: "xdg-open",
|
|
1947
|
+
win32: "start"
|
|
1948
|
+
};
|
|
1949
|
+
function getOpenCommand(platform = process.platform) {
|
|
1950
|
+
const command = PLATFORM_COMMANDS2[platform];
|
|
1951
|
+
if (!command) {
|
|
1952
|
+
throw new Error(`Opening files is not supported on platform "${platform}". Supported: macOS, Linux, Windows.`);
|
|
1953
|
+
}
|
|
1954
|
+
return command;
|
|
1955
|
+
}
|
|
1956
|
+
async function openFile(filePath, platform = process.platform) {
|
|
1957
|
+
const cmd = getOpenCommand(platform);
|
|
1958
|
+
const spawnArgs = platform === "win32" ? ["cmd", "/c", "start", "", filePath] : [cmd, filePath];
|
|
1959
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
1960
|
+
stdout: "ignore",
|
|
1961
|
+
stderr: "pipe"
|
|
1962
|
+
});
|
|
1963
|
+
const exitCode = await proc.exited;
|
|
1964
|
+
if (exitCode !== 0) {
|
|
1965
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1966
|
+
throw new Error(`Failed to open "${filePath}" with "${cmd}": exit code ${exitCode}: ${stderr.trim()}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
// packages/cli/src/sharing/gist.ts
|
|
1970
|
+
async function isGhAvailable() {
|
|
1971
|
+
try {
|
|
1972
|
+
const proc = Bun.spawn(["gh", "auth", "status"], {
|
|
1973
|
+
stdout: "ignore",
|
|
1974
|
+
stderr: "ignore"
|
|
1975
|
+
});
|
|
1976
|
+
const exitCode = await proc.exited;
|
|
1977
|
+
return exitCode === 0;
|
|
1978
|
+
} catch {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
async function uploadToGist(content, filename, description) {
|
|
1983
|
+
const available = await isGhAvailable();
|
|
1984
|
+
if (!available) {
|
|
1985
|
+
throw new Error("GitHub CLI (gh) is not installed or not authenticated. " + "Install it from https://cli.github.com and run `gh auth login`.");
|
|
1986
|
+
}
|
|
1987
|
+
const { join: join5 } = await import("path");
|
|
1988
|
+
const { tmpdir } = await import("os");
|
|
1989
|
+
const { writeFileSync, unlinkSync } = await import("fs");
|
|
1990
|
+
const tmpPath = join5(tmpdir(), `tokenleak-gist-${Date.now()}-${filename}`);
|
|
1991
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
1992
|
+
try {
|
|
1993
|
+
const proc = Bun.spawn(["gh", "gist", "create", tmpPath, "--desc", description, "--public"], {
|
|
1994
|
+
stdout: "pipe",
|
|
1995
|
+
stderr: "pipe"
|
|
1996
|
+
});
|
|
1997
|
+
const exitCode = await proc.exited;
|
|
1998
|
+
const stdout = (await new Response(proc.stdout).text()).trim();
|
|
1999
|
+
const stderr = (await new Response(proc.stderr).text()).trim();
|
|
2000
|
+
if (exitCode !== 0) {
|
|
2001
|
+
throw new Error(`Failed to create gist: ${stderr || "unknown error"} (exit code ${exitCode})`);
|
|
2002
|
+
}
|
|
2003
|
+
if (!stdout.startsWith("http")) {
|
|
2004
|
+
throw new Error(`Unexpected gh output: ${stdout}`);
|
|
2005
|
+
}
|
|
2006
|
+
return stdout;
|
|
2007
|
+
} finally {
|
|
2008
|
+
try {
|
|
2009
|
+
unlinkSync(tmpPath);
|
|
2010
|
+
} catch {}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
// packages/cli/src/cli.ts
|
|
2014
|
+
var FORMAT_VALUES = ["json", "svg", "png", "terminal"];
|
|
2015
|
+
var THEME_VALUES = ["dark", "light"];
|
|
2016
|
+
function inferFormatFromPath(filePath) {
|
|
2017
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
2018
|
+
switch (ext) {
|
|
2019
|
+
case "json":
|
|
2020
|
+
return "json";
|
|
2021
|
+
case "svg":
|
|
2022
|
+
return "svg";
|
|
2023
|
+
case "png":
|
|
2024
|
+
return "png";
|
|
2025
|
+
default:
|
|
2026
|
+
return null;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
function computeDateRange(args) {
|
|
2030
|
+
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
2031
|
+
let since;
|
|
2032
|
+
if (args.since) {
|
|
2033
|
+
since = args.since;
|
|
2034
|
+
} else {
|
|
2035
|
+
const daysBack = args.days ?? DEFAULT_DAYS;
|
|
2036
|
+
const d = new Date(until);
|
|
2037
|
+
d.setDate(d.getDate() - daysBack);
|
|
2038
|
+
since = d.toISOString().slice(0, 10);
|
|
2039
|
+
}
|
|
2040
|
+
return { since, until };
|
|
2041
|
+
}
|
|
2042
|
+
function resolveConfig(cliArgs) {
|
|
2043
|
+
const fileConfig = loadConfig();
|
|
2044
|
+
const envConfig = loadEnvOverrides();
|
|
2045
|
+
const merged = {
|
|
2046
|
+
format: "terminal",
|
|
2047
|
+
theme: "dark",
|
|
2048
|
+
days: DEFAULT_DAYS,
|
|
2049
|
+
output: null,
|
|
2050
|
+
width: 80,
|
|
2051
|
+
noColor: false,
|
|
2052
|
+
noInsights: false,
|
|
2053
|
+
clipboard: false,
|
|
2054
|
+
open: false
|
|
2055
|
+
};
|
|
2056
|
+
if (fileConfig.format && FORMAT_VALUES.includes(fileConfig.format)) {
|
|
2057
|
+
merged.format = fileConfig.format;
|
|
2058
|
+
}
|
|
2059
|
+
if (fileConfig.theme && THEME_VALUES.includes(fileConfig.theme)) {
|
|
2060
|
+
merged.theme = fileConfig.theme;
|
|
2061
|
+
}
|
|
2062
|
+
if (fileConfig.days !== undefined)
|
|
2063
|
+
merged.days = fileConfig.days;
|
|
2064
|
+
if (fileConfig.width !== undefined)
|
|
2065
|
+
merged.width = fileConfig.width;
|
|
2066
|
+
if (fileConfig.noColor !== undefined)
|
|
2067
|
+
merged.noColor = fileConfig.noColor;
|
|
2068
|
+
if (fileConfig.noInsights !== undefined)
|
|
2069
|
+
merged.noInsights = fileConfig.noInsights;
|
|
2070
|
+
if (envConfig.format)
|
|
2071
|
+
merged.format = envConfig.format;
|
|
2072
|
+
if (envConfig.theme)
|
|
2073
|
+
merged.theme = envConfig.theme;
|
|
2074
|
+
if (envConfig.days !== undefined)
|
|
2075
|
+
merged.days = envConfig.days;
|
|
2076
|
+
const result = { ...merged };
|
|
2077
|
+
if (cliArgs["format"] !== undefined) {
|
|
2078
|
+
result.format = cliArgs["format"];
|
|
2079
|
+
}
|
|
2080
|
+
if (cliArgs["theme"] !== undefined) {
|
|
2081
|
+
result.theme = cliArgs["theme"];
|
|
2082
|
+
}
|
|
2083
|
+
if (cliArgs["since"] !== undefined) {
|
|
2084
|
+
result.since = cliArgs["since"];
|
|
2085
|
+
}
|
|
2086
|
+
if (cliArgs["until"] !== undefined) {
|
|
2087
|
+
result.until = cliArgs["until"];
|
|
2088
|
+
}
|
|
2089
|
+
if (cliArgs["days"] !== undefined) {
|
|
2090
|
+
result.days = cliArgs["days"];
|
|
2091
|
+
}
|
|
2092
|
+
if (cliArgs["output"] !== undefined) {
|
|
2093
|
+
const outputPath = cliArgs["output"];
|
|
2094
|
+
result.output = outputPath;
|
|
2095
|
+
if (cliArgs["format"] === undefined) {
|
|
2096
|
+
const inferred = inferFormatFromPath(outputPath);
|
|
2097
|
+
if (inferred) {
|
|
2098
|
+
result.format = inferred;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (cliArgs["width"] !== undefined) {
|
|
2103
|
+
result.width = cliArgs["width"];
|
|
2104
|
+
}
|
|
2105
|
+
if (cliArgs["noColor"] !== undefined) {
|
|
2106
|
+
result.noColor = cliArgs["noColor"];
|
|
2107
|
+
}
|
|
2108
|
+
if (cliArgs["noInsights"] !== undefined) {
|
|
2109
|
+
result.noInsights = cliArgs["noInsights"];
|
|
2110
|
+
}
|
|
2111
|
+
if (cliArgs["compare"] !== undefined) {
|
|
2112
|
+
result.compare = cliArgs["compare"];
|
|
2113
|
+
}
|
|
2114
|
+
if (cliArgs["provider"] !== undefined) {
|
|
2115
|
+
result.provider = cliArgs["provider"];
|
|
2116
|
+
}
|
|
2117
|
+
if (cliArgs["clipboard"] !== undefined) {
|
|
2118
|
+
result.clipboard = cliArgs["clipboard"];
|
|
2119
|
+
}
|
|
2120
|
+
if (cliArgs["open"] !== undefined) {
|
|
2121
|
+
result.open = cliArgs["open"];
|
|
2122
|
+
}
|
|
2123
|
+
if (cliArgs["upload"] !== undefined) {
|
|
2124
|
+
result.upload = cliArgs["upload"];
|
|
2125
|
+
}
|
|
2126
|
+
return result;
|
|
2127
|
+
}
|
|
2128
|
+
function getRenderer(format) {
|
|
2129
|
+
switch (format) {
|
|
2130
|
+
case "json":
|
|
2131
|
+
return new JsonRenderer;
|
|
2132
|
+
default:
|
|
2133
|
+
throw new TokenleakError(`Format "${format}" is not yet supported. Available formats: json`);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function loadAndAggregate(range, providers) {
|
|
2137
|
+
const results = await Promise.all(providers.map(async (p) => {
|
|
2138
|
+
try {
|
|
2139
|
+
return await p.load(range);
|
|
2140
|
+
} catch {
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
}));
|
|
2144
|
+
const data = results.filter((r) => r !== null);
|
|
2145
|
+
const merged = data.length > 0 ? mergeProviderData(data) : [];
|
|
2146
|
+
const stats = aggregate(merged, range.until);
|
|
2147
|
+
return { data, stats };
|
|
2148
|
+
}
|
|
2149
|
+
async function runCompare(compareStr, currentRange, _registry, available) {
|
|
2150
|
+
let previousRange;
|
|
2151
|
+
if (compareStr === "auto" || compareStr === "true" || compareStr === "") {
|
|
2152
|
+
previousRange = computePreviousPeriod(currentRange);
|
|
2153
|
+
} else {
|
|
2154
|
+
const parsed = parseCompareRange(compareStr);
|
|
2155
|
+
if (!parsed) {
|
|
2156
|
+
throw new TokenleakError(`Invalid --compare format: "${compareStr}". Use YYYY-MM-DD..YYYY-MM-DD or "auto".`);
|
|
2157
|
+
}
|
|
2158
|
+
previousRange = parsed;
|
|
2159
|
+
}
|
|
2160
|
+
const [currentResult, previousResult] = await Promise.all([
|
|
2161
|
+
loadAndAggregate(currentRange, available),
|
|
2162
|
+
loadAndAggregate(previousRange, available)
|
|
2163
|
+
]);
|
|
2164
|
+
return buildCompareOutput({ range: currentRange, stats: currentResult.stats }, { range: previousRange, stats: previousResult.stats });
|
|
2165
|
+
}
|
|
2166
|
+
async function run(cliArgs) {
|
|
2167
|
+
const config = resolveConfig(cliArgs);
|
|
2168
|
+
const dateRange = computeDateRange({
|
|
2169
|
+
since: config.since,
|
|
2170
|
+
until: config.until,
|
|
2171
|
+
days: config.days
|
|
2172
|
+
});
|
|
2173
|
+
const registry = new ProviderRegistry;
|
|
2174
|
+
registry.register(new ClaudeCodeProvider);
|
|
2175
|
+
registry.register(new CodexProvider);
|
|
2176
|
+
registry.register(new OpenCodeProvider);
|
|
2177
|
+
let available = await registry.getAvailable();
|
|
2178
|
+
if (config.provider) {
|
|
2179
|
+
const requested = new Set(config.provider.split(",").map((s) => s.trim().toLowerCase()));
|
|
2180
|
+
available = available.filter((p) => requested.has(p.name.toLowerCase()) || requested.has(p.displayName.toLowerCase()));
|
|
2181
|
+
}
|
|
2182
|
+
if (available.length === 0) {
|
|
2183
|
+
throw new TokenleakError("No provider data found");
|
|
2184
|
+
}
|
|
2185
|
+
if (config.compare) {
|
|
2186
|
+
const compareOutput = await runCompare(config.compare, dateRange, registry, available);
|
|
2187
|
+
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
2188
|
+
if (config.output) {
|
|
2189
|
+
writeFileSync(config.output, rendered2);
|
|
2190
|
+
} else {
|
|
2191
|
+
process.stdout.write(rendered2 + `
|
|
2192
|
+
`);
|
|
2193
|
+
}
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const results = await Promise.all(available.map(async (p) => {
|
|
2197
|
+
try {
|
|
2198
|
+
return await p.load(dateRange);
|
|
2199
|
+
} catch {
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2202
|
+
}));
|
|
2203
|
+
const providerDataList = results.filter((r) => r !== null);
|
|
2204
|
+
if (providerDataList.length === 0) {
|
|
2205
|
+
throw new TokenleakError("No provider data found");
|
|
2206
|
+
}
|
|
2207
|
+
const mergedDaily = mergeProviderData(providerDataList);
|
|
2208
|
+
const stats = aggregate(mergedDaily, dateRange.until);
|
|
2209
|
+
const output = {
|
|
2210
|
+
schemaVersion: SCHEMA_VERSION,
|
|
2211
|
+
generated: new Date().toISOString(),
|
|
2212
|
+
dateRange,
|
|
2213
|
+
providers: providerDataList,
|
|
2214
|
+
aggregated: stats
|
|
2215
|
+
};
|
|
2216
|
+
const renderer = getRenderer(config.format);
|
|
2217
|
+
const renderOptions = {
|
|
2218
|
+
format: config.format,
|
|
2219
|
+
theme: config.theme,
|
|
2220
|
+
width: config.width,
|
|
2221
|
+
showInsights: !config.noInsights,
|
|
2222
|
+
noColor: config.noColor,
|
|
2223
|
+
output: config.output
|
|
2224
|
+
};
|
|
2225
|
+
const rendered = await renderer.render(output, renderOptions);
|
|
2226
|
+
if (config.output) {
|
|
2227
|
+
const data = typeof rendered === "string" ? rendered : Buffer.from(rendered);
|
|
2228
|
+
writeFileSync(config.output, data);
|
|
2229
|
+
} else {
|
|
2230
|
+
const text2 = typeof rendered === "string" ? rendered : rendered.toString("utf-8");
|
|
2231
|
+
process.stdout.write(text2 + `
|
|
2232
|
+
`);
|
|
2233
|
+
}
|
|
2234
|
+
if (config.clipboard) {
|
|
2235
|
+
const text2 = typeof rendered === "string" ? rendered : rendered.toString("utf-8");
|
|
2236
|
+
await copyToClipboard(text2);
|
|
2237
|
+
process.stderr.write(`Copied output to clipboard.
|
|
2238
|
+
`);
|
|
2239
|
+
}
|
|
2240
|
+
if (config.open) {
|
|
2241
|
+
if (!config.output) {
|
|
2242
|
+
throw new TokenleakError("--open requires --output to specify a file path");
|
|
2243
|
+
}
|
|
2244
|
+
await openFile(config.output);
|
|
2245
|
+
process.stderr.write(`Opened ${config.output} in default application.
|
|
2246
|
+
`);
|
|
2247
|
+
}
|
|
2248
|
+
if (config.upload === "gist") {
|
|
2249
|
+
const text2 = typeof rendered === "string" ? rendered : rendered.toString("utf-8");
|
|
2250
|
+
const ext = config.format === "json" ? "json" : config.format === "svg" ? "svg" : "txt";
|
|
2251
|
+
const filename = `tokenleak.${ext}`;
|
|
2252
|
+
const description = `Tokenleak report (${dateRange.since} to ${dateRange.until})`;
|
|
2253
|
+
const url = await uploadToGist(text2, filename, description);
|
|
2254
|
+
process.stderr.write(`Uploaded to gist: ${url}
|
|
2255
|
+
`);
|
|
2256
|
+
} else if (config.upload !== undefined) {
|
|
2257
|
+
throw new TokenleakError(`Unknown upload target "${config.upload}". Supported: gist`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
var main = defineCommand({
|
|
2261
|
+
meta: {
|
|
2262
|
+
name: "tokenleak",
|
|
2263
|
+
version: VERSION,
|
|
2264
|
+
description: "Visualise your AI coding-assistant token usage across providers"
|
|
2265
|
+
},
|
|
2266
|
+
args: {
|
|
2267
|
+
format: {
|
|
2268
|
+
type: "string",
|
|
2269
|
+
alias: "f",
|
|
2270
|
+
description: "Output format: json, svg, png, terminal"
|
|
2271
|
+
},
|
|
2272
|
+
theme: {
|
|
2273
|
+
type: "string",
|
|
2274
|
+
alias: "t",
|
|
2275
|
+
description: "Color theme: dark, light"
|
|
2276
|
+
},
|
|
2277
|
+
since: {
|
|
2278
|
+
type: "string",
|
|
2279
|
+
alias: "s",
|
|
2280
|
+
description: "Start date (YYYY-MM-DD)"
|
|
2281
|
+
},
|
|
2282
|
+
until: {
|
|
2283
|
+
type: "string",
|
|
2284
|
+
alias: "u",
|
|
2285
|
+
description: "End date (YYYY-MM-DD), defaults to today"
|
|
2286
|
+
},
|
|
2287
|
+
days: {
|
|
2288
|
+
type: "string",
|
|
2289
|
+
alias: "d",
|
|
2290
|
+
description: `Number of days to look back (default: ${DEFAULT_DAYS}, overridden by --since)`
|
|
2291
|
+
},
|
|
2292
|
+
output: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
alias: "o",
|
|
2295
|
+
description: "Output file path"
|
|
2296
|
+
},
|
|
2297
|
+
width: {
|
|
2298
|
+
type: "string",
|
|
2299
|
+
alias: "w",
|
|
2300
|
+
description: "Terminal width (default: 80)"
|
|
2301
|
+
},
|
|
2302
|
+
noColor: {
|
|
2303
|
+
type: "boolean",
|
|
2304
|
+
description: "Disable ANSI colors",
|
|
2305
|
+
default: false
|
|
2306
|
+
},
|
|
2307
|
+
noInsights: {
|
|
2308
|
+
type: "boolean",
|
|
2309
|
+
description: "Hide insights panel",
|
|
2310
|
+
default: false
|
|
2311
|
+
},
|
|
2312
|
+
compare: {
|
|
2313
|
+
type: "string",
|
|
2314
|
+
description: "Compare two date ranges (YYYY-MM-DD..YYYY-MM-DD)"
|
|
2315
|
+
},
|
|
2316
|
+
provider: {
|
|
2317
|
+
type: "string",
|
|
2318
|
+
alias: "p",
|
|
2319
|
+
description: "Filter to specific provider(s), comma-separated"
|
|
2320
|
+
},
|
|
2321
|
+
clipboard: {
|
|
2322
|
+
type: "boolean",
|
|
2323
|
+
description: "Copy output to clipboard after rendering",
|
|
2324
|
+
default: false
|
|
2325
|
+
},
|
|
2326
|
+
open: {
|
|
2327
|
+
type: "boolean",
|
|
2328
|
+
description: "Open output file in default application (requires --output)",
|
|
2329
|
+
default: false
|
|
2330
|
+
},
|
|
2331
|
+
upload: {
|
|
2332
|
+
type: "string",
|
|
2333
|
+
description: "Upload output to a service (supported: gist)"
|
|
2334
|
+
}
|
|
2335
|
+
},
|
|
2336
|
+
async run({ args }) {
|
|
2337
|
+
try {
|
|
2338
|
+
const cliArgs = {};
|
|
2339
|
+
if (args.format !== undefined)
|
|
2340
|
+
cliArgs["format"] = args.format;
|
|
2341
|
+
if (args.theme !== undefined)
|
|
2342
|
+
cliArgs["theme"] = args.theme;
|
|
2343
|
+
if (args.since !== undefined)
|
|
2344
|
+
cliArgs["since"] = args.since;
|
|
2345
|
+
if (args.until !== undefined)
|
|
2346
|
+
cliArgs["until"] = args.until;
|
|
2347
|
+
if (args.days !== undefined)
|
|
2348
|
+
cliArgs["days"] = Number(args.days);
|
|
2349
|
+
if (args.output !== undefined)
|
|
2350
|
+
cliArgs["output"] = args.output;
|
|
2351
|
+
if (args.width !== undefined)
|
|
2352
|
+
cliArgs["width"] = Number(args.width);
|
|
2353
|
+
if (args.noColor)
|
|
2354
|
+
cliArgs["noColor"] = true;
|
|
2355
|
+
if (args.noInsights)
|
|
2356
|
+
cliArgs["noInsights"] = true;
|
|
2357
|
+
if (args.compare !== undefined)
|
|
2358
|
+
cliArgs["compare"] = args.compare;
|
|
2359
|
+
if (args.provider !== undefined)
|
|
2360
|
+
cliArgs["provider"] = args.provider;
|
|
2361
|
+
if (args.clipboard)
|
|
2362
|
+
cliArgs["clipboard"] = true;
|
|
2363
|
+
if (args.open)
|
|
2364
|
+
cliArgs["open"] = true;
|
|
2365
|
+
if (args.upload !== undefined)
|
|
2366
|
+
cliArgs["upload"] = args.upload;
|
|
2367
|
+
await run(cliArgs);
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
handleError(error);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
var isDirectExecution = typeof Bun !== "undefined" ? Bun.main === import.meta.path : process.argv[1] !== undefined && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/"));
|
|
2374
|
+
if (isDirectExecution) {
|
|
2375
|
+
runMain(main);
|
|
2376
|
+
}
|
|
2377
|
+
export {
|
|
2378
|
+
run,
|
|
2379
|
+
resolveConfig,
|
|
2380
|
+
inferFormatFromPath,
|
|
2381
|
+
computeDateRange
|
|
2382
|
+
};
|