tokenleak 0.5.0 → 1.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tokenleak.js +86 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenleak",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Visualise your AI coding-assistant token usage across providers — heatmaps, dashboards, and shareable cards.",
5
5
  "type": "module",
6
6
  "bin": {
package/tokenleak.js CHANGED
@@ -663,6 +663,26 @@ function computeTotalDays(daily) {
663
663
  return Math.round((last - first) / ONE_DAY_MS) + 1;
664
664
  }
665
665
  // packages/core/dist/aggregation/merge.js
666
+ function mergeModelArrays(existing, incoming) {
667
+ const map = new Map;
668
+ for (const m of existing) {
669
+ map.set(m.model, { ...m });
670
+ }
671
+ for (const m of incoming) {
672
+ const prev = map.get(m.model);
673
+ if (prev) {
674
+ prev.inputTokens += m.inputTokens;
675
+ prev.outputTokens += m.outputTokens;
676
+ prev.cacheReadTokens += m.cacheReadTokens;
677
+ prev.cacheWriteTokens += m.cacheWriteTokens;
678
+ prev.totalTokens += m.totalTokens;
679
+ prev.cost += m.cost;
680
+ } else {
681
+ map.set(m.model, { ...m });
682
+ }
683
+ }
684
+ return [...map.values()];
685
+ }
666
686
  function mergeProviderData(providers) {
667
687
  const dateMap = new Map;
668
688
  for (const provider of providers) {
@@ -675,7 +695,7 @@ function mergeProviderData(providers) {
675
695
  existing.cacheWriteTokens += entry.cacheWriteTokens;
676
696
  existing.totalTokens += entry.totalTokens;
677
697
  existing.cost += entry.cost;
678
- existing.models = [...existing.models, ...entry.models];
698
+ existing.models = mergeModelArrays(existing.models, entry.models);
679
699
  } else {
680
700
  dateMap.set(entry.date, {
681
701
  date: entry.date,
@@ -736,7 +756,7 @@ function computePreviousPeriod(current) {
736
756
  };
737
757
  }
738
758
  // packages/core/dist/index.js
739
- var VERSION = "0.5.0";
759
+ var VERSION = "1.0.0";
740
760
 
741
761
  // packages/registry/dist/models/normalizer.js
742
762
  var DATE_SUFFIX_PATTERN = /-\d{8}$/;
@@ -1361,8 +1381,17 @@ function buildProviderData(records) {
1361
1381
  };
1362
1382
  }
1363
1383
  function loadFromSqlite(dbPath, range) {
1364
- const db = new Database(dbPath, { readonly: true });
1384
+ let db;
1385
+ try {
1386
+ db = new Database(dbPath, { readonly: true });
1387
+ } catch {
1388
+ return [];
1389
+ }
1365
1390
  try {
1391
+ const tables = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").all();
1392
+ if (tables.length === 0) {
1393
+ return [];
1394
+ }
1366
1395
  const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
1367
1396
  const records = [];
1368
1397
  for (const row of rows) {
@@ -1377,6 +1406,8 @@ function loadFromSqlite(dbPath, range) {
1377
1406
  }
1378
1407
  }
1379
1408
  return records;
1409
+ } catch {
1410
+ return [];
1380
1411
  } finally {
1381
1412
  db.close();
1382
1413
  }
@@ -1385,24 +1416,28 @@ function loadFromJson(sessionsDir, range) {
1385
1416
  const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
1386
1417
  const records = [];
1387
1418
  for (const file of files) {
1388
- const content = readFileSync(join3(sessionsDir, file), "utf-8");
1389
- const session = JSON.parse(content);
1390
- if (!Array.isArray(session.messages)) {
1391
- continue;
1392
- }
1393
- for (const msg of session.messages) {
1394
- if (msg.role !== "assistant" || !msg.usage) {
1419
+ try {
1420
+ const content = readFileSync(join3(sessionsDir, file), "utf-8");
1421
+ const session = JSON.parse(content);
1422
+ if (!Array.isArray(session.messages)) {
1395
1423
  continue;
1396
1424
  }
1397
- const date = extractDate2(msg.created_at);
1398
- if (isInRange(date, range)) {
1399
- records.push({
1400
- date,
1401
- model: msg.model,
1402
- inputTokens: msg.usage.input_tokens,
1403
- outputTokens: msg.usage.output_tokens
1404
- });
1425
+ for (const msg of session.messages) {
1426
+ if (msg.role !== "assistant" || !msg.usage) {
1427
+ continue;
1428
+ }
1429
+ const date = extractDate2(msg.created_at);
1430
+ if (isInRange(date, range)) {
1431
+ records.push({
1432
+ date,
1433
+ model: msg.model,
1434
+ inputTokens: msg.usage.input_tokens,
1435
+ outputTokens: msg.usage.output_tokens
1436
+ });
1437
+ }
1405
1438
  }
1439
+ } catch {
1440
+ continue;
1406
1441
  }
1407
1442
  }
1408
1443
  return records;
@@ -3063,8 +3098,21 @@ function inferFormatFromPath(filePath) {
3063
3098
  return null;
3064
3099
  }
3065
3100
  }
3101
+ var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
3102
+ function isValidDate(dateStr) {
3103
+ if (!DATE_FORMAT.test(dateStr))
3104
+ return false;
3105
+ const d = new Date(dateStr + "T00:00:00Z");
3106
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
3107
+ }
3066
3108
  function computeDateRange(args) {
3067
3109
  const until = args.until ?? new Date().toISOString().slice(0, 10);
3110
+ if (args.until && !isValidDate(args.until)) {
3111
+ throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
3112
+ }
3113
+ if (args.since && !isValidDate(args.since)) {
3114
+ throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
3115
+ }
3068
3116
  let since;
3069
3117
  if (args.since) {
3070
3118
  since = args.since;
@@ -3074,6 +3122,9 @@ function computeDateRange(args) {
3074
3122
  d.setDate(d.getDate() - daysBack);
3075
3123
  since = d.toISOString().slice(0, 10);
3076
3124
  }
3125
+ if (since > until) {
3126
+ throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
3127
+ }
3077
3128
  return { since, until };
3078
3129
  }
3079
3130
  function resolveConfig(cliArgs) {
@@ -3230,6 +3281,10 @@ async function run(cliArgs) {
3230
3281
  throw new TokenleakError("No provider data found");
3231
3282
  }
3232
3283
  if (config.compare) {
3284
+ if (config.format !== "json" && config.format !== "terminal") {
3285
+ process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
3286
+ `);
3287
+ }
3233
3288
  const compareOutput = await runCompare(config.compare, dateRange, registry, available);
3234
3289
  const rendered2 = JSON.stringify(compareOutput, null, 2);
3235
3290
  if (config.output) {
@@ -3261,6 +3316,19 @@ async function run(cliArgs) {
3261
3316
  aggregated: stats
3262
3317
  };
3263
3318
  if (config.liveServer) {
3319
+ const ignoredFlags = [];
3320
+ if (config.output)
3321
+ ignoredFlags.push("--output");
3322
+ if (config.clipboard)
3323
+ ignoredFlags.push("--clipboard");
3324
+ if (config.open)
3325
+ ignoredFlags.push("--open");
3326
+ if (config.upload)
3327
+ ignoredFlags.push("--upload");
3328
+ if (ignoredFlags.length > 0) {
3329
+ process.stderr.write(`Warning: ${ignoredFlags.join(", ")} ignored in --live-server mode.
3330
+ `);
3331
+ }
3264
3332
  const renderOptions2 = {
3265
3333
  format: config.format,
3266
3334
  theme: config.theme,