token-rats 0.3.0 → 0.3.1

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/dist/index.js +109 -97
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5256,7 +5256,7 @@ var ApiClient = class {
5256
5256
  // package.json
5257
5257
  var package_default = {
5258
5258
  name: "token-rats",
5259
- version: "0.3.0",
5259
+ version: "0.3.1",
5260
5260
  description: "Track Claude Code, Codex, and Cursor usage and compare AI subscriptions.",
5261
5261
  license: "MIT",
5262
5262
  type: "module",
@@ -5414,6 +5414,7 @@ function logoutCommand() {
5414
5414
  // src/lib/collect.ts
5415
5415
  import { createHash } from "node:crypto";
5416
5416
  import * as fs4 from "node:fs";
5417
+ import { createInterface } from "node:readline";
5417
5418
 
5418
5419
  // ../parsers/src/hash.ts
5419
5420
  var FNV_PRIME = 16777619;
@@ -5437,30 +5438,24 @@ function computeDedupeKey(source, model, startedAt, inTokens, outTokens) {
5437
5438
  }
5438
5439
 
5439
5440
  // ../parsers/src/claude-code.ts
5440
- function parseClaudeCode(input) {
5441
- let text;
5442
- if (typeof input === "string") {
5443
- text = input;
5444
- } else {
5445
- text = new TextDecoder().decode(input instanceof ArrayBuffer ? new Uint8Array(input) : input);
5446
- }
5441
+ function createClaudeCodeParser() {
5447
5442
  const sessions = /* @__PURE__ */ new Map();
5448
5443
  const messages = /* @__PURE__ */ new Map();
5449
- for (const rawLine of text.split("\n")) {
5444
+ function push(rawLine) {
5450
5445
  const line = rawLine.trim();
5451
- if (line.length === 0) continue;
5446
+ if (line.length === 0) return;
5452
5447
  let event;
5453
5448
  try {
5454
5449
  event = JSON.parse(line);
5455
5450
  } catch {
5456
- continue;
5451
+ return;
5457
5452
  }
5458
- if (typeof event !== "object" || event === null) continue;
5453
+ if (typeof event !== "object" || event === null) return;
5459
5454
  const ev = event;
5460
5455
  const sidCamel = ev.sessionId;
5461
5456
  const sidSnake = ev.session_id;
5462
5457
  const sessionId = typeof sidCamel === "string" ? sidCamel : typeof sidSnake === "string" ? sidSnake : null;
5463
- if (!sessionId) continue;
5458
+ if (!sessionId) return;
5464
5459
  const rawTs = ev.timestamp;
5465
5460
  let timestamp = 0;
5466
5461
  if (typeof rawTs === "number" && Number.isFinite(rawTs)) {
@@ -5488,9 +5483,9 @@ function parseClaudeCode(input) {
5488
5483
  if (acc.startedAt === 0 || timestamp < acc.startedAt) acc.startedAt = timestamp;
5489
5484
  if (timestamp > acc.endedAt) acc.endedAt = timestamp;
5490
5485
  }
5491
- if (ev.type !== "assistant") continue;
5486
+ if (ev.type !== "assistant") return;
5492
5487
  const message = ev.message;
5493
- if (typeof message !== "object" || message === null) continue;
5488
+ if (typeof message !== "object" || message === null) return;
5494
5489
  const msg = message;
5495
5490
  if (typeof msg.model === "string" && msg.model.length > 0 && timestamp >= acc.modelAt) {
5496
5491
  acc.model = msg.model;
@@ -5520,38 +5515,41 @@ function parseClaudeCode(input) {
5520
5515
  if (key) messages.set(key, next);
5521
5516
  }
5522
5517
  }
5523
- const results = [];
5524
- for (const acc of sessions.values()) {
5525
- const startedAt = acc.startedAt > 0 ? acc.startedAt : acc.endedAt;
5526
- const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
5527
- if (startedAt <= 0 || endedAt <= 0) continue;
5528
- const model = acc.model.length > 0 ? acc.model : "unknown";
5529
- const costUsdCents = 0;
5530
- const dedupeKey = computeDedupeKey(
5531
- "claude-code",
5532
- model,
5533
- startedAt,
5534
- acc.inTokens,
5535
- acc.outTokens
5536
- );
5537
- results.push({
5538
- id: `claude-code:${acc.sessionId}`,
5539
- source: "claude-code",
5540
- provider: "anthropic",
5541
- client: "claude-code",
5542
- channel: "cli",
5543
- model,
5544
- inTokens: acc.inTokens,
5545
- outTokens: acc.outTokens,
5546
- cacheReadTokens: acc.cacheReadTokens,
5547
- cacheWriteTokens: acc.cacheWriteTokens,
5548
- costUsdCents,
5549
- startedAt,
5550
- endedAt,
5551
- dedupeKey
5552
- });
5518
+ function finish() {
5519
+ const results = [];
5520
+ for (const acc of sessions.values()) {
5521
+ const startedAt = acc.startedAt > 0 ? acc.startedAt : acc.endedAt;
5522
+ const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
5523
+ if (startedAt <= 0 || endedAt <= 0) continue;
5524
+ const model = acc.model.length > 0 ? acc.model : "unknown";
5525
+ const costUsdCents = 0;
5526
+ const dedupeKey = computeDedupeKey(
5527
+ "claude-code",
5528
+ model,
5529
+ startedAt,
5530
+ acc.inTokens,
5531
+ acc.outTokens
5532
+ );
5533
+ results.push({
5534
+ id: `claude-code:${acc.sessionId}`,
5535
+ source: "claude-code",
5536
+ provider: "anthropic",
5537
+ client: "claude-code",
5538
+ channel: "cli",
5539
+ model,
5540
+ inTokens: acc.inTokens,
5541
+ outTokens: acc.outTokens,
5542
+ cacheReadTokens: acc.cacheReadTokens,
5543
+ cacheWriteTokens: acc.cacheWriteTokens,
5544
+ costUsdCents,
5545
+ startedAt,
5546
+ endedAt,
5547
+ dedupeKey
5548
+ });
5549
+ }
5550
+ return results;
5553
5551
  }
5554
- return results;
5552
+ return { push, finish };
5555
5553
  }
5556
5554
  function toNonNegInt(v) {
5557
5555
  if (typeof v !== "number" || !Number.isFinite(v)) return 0;
@@ -5559,25 +5557,19 @@ function toNonNegInt(v) {
5559
5557
  }
5560
5558
 
5561
5559
  // ../parsers/src/codex.ts
5562
- function parseCodex(input) {
5563
- let text;
5564
- if (typeof input === "string") {
5565
- text = input;
5566
- } else {
5567
- text = new TextDecoder().decode(input instanceof ArrayBuffer ? new Uint8Array(input) : input);
5568
- }
5560
+ function createCodexParser() {
5569
5561
  const sessions = /* @__PURE__ */ new Map();
5570
5562
  let currentSessionId = null;
5571
- for (const rawLine of text.split("\n")) {
5563
+ function push(rawLine) {
5572
5564
  const line = rawLine.trim();
5573
- if (line.length === 0) continue;
5565
+ if (line.length === 0) return;
5574
5566
  let event;
5575
5567
  try {
5576
5568
  event = JSON.parse(line);
5577
5569
  } catch {
5578
- continue;
5570
+ return;
5579
5571
  }
5580
- if (typeof event !== "object" || event === null) continue;
5572
+ if (typeof event !== "object" || event === null) return;
5581
5573
  const ev = event;
5582
5574
  const ts = parseTimestamp(ev.timestamp);
5583
5575
  const type = typeof ev.type === "string" ? ev.type : null;
@@ -5586,7 +5578,7 @@ function parseCodex(input) {
5586
5578
  const id = typeof payload.id === "string" ? payload.id : null;
5587
5579
  if (!id) {
5588
5580
  currentSessionId = null;
5589
- continue;
5581
+ return;
5590
5582
  }
5591
5583
  currentSessionId = id;
5592
5584
  const metaTs = parseTimestamp(payload.timestamp) || ts;
@@ -5598,9 +5590,9 @@ function parseCodex(input) {
5598
5590
  acc2.startedAt = metaTs;
5599
5591
  }
5600
5592
  if (metaTs > acc2.endedAt) acc2.endedAt = metaTs;
5601
- continue;
5593
+ return;
5602
5594
  }
5603
- if (!currentSessionId) continue;
5595
+ if (!currentSessionId) return;
5604
5596
  const acc = upsert(sessions, currentSessionId);
5605
5597
  if (ts > 0) {
5606
5598
  if (acc.startedAt === 0) acc.startedAt = ts;
@@ -5611,19 +5603,19 @@ function parseCodex(input) {
5611
5603
  acc.model = payload.model;
5612
5604
  acc.modelAt = ts;
5613
5605
  }
5614
- continue;
5606
+ return;
5615
5607
  }
5616
5608
  if (type === "event_msg" && payload && payload.type === "token_count") {
5617
5609
  const info2 = payload.info;
5618
- if (typeof info2 !== "object" || info2 === null) continue;
5610
+ if (typeof info2 !== "object" || info2 === null) return;
5619
5611
  const total = info2.total_token_usage;
5620
- if (typeof total !== "object" || total === null) continue;
5612
+ if (typeof total !== "object" || total === null) return;
5621
5613
  const t = total;
5622
5614
  const inputTotal = toNonNegInt2(t.input_tokens);
5623
5615
  const cachedInput = toNonNegInt2(t.cached_input_tokens);
5624
5616
  const output = toNonNegInt2(t.output_tokens);
5625
5617
  const reasoning = toNonNegInt2(t.reasoning_output_tokens);
5626
- if (ts < acc.usageAt) continue;
5618
+ if (ts < acc.usageAt) return;
5627
5619
  acc.usageAt = ts;
5628
5620
  acc.inTokens = Math.max(0, inputTotal - cachedInput);
5629
5621
  acc.outTokens = output;
@@ -5631,32 +5623,41 @@ function parseCodex(input) {
5631
5623
  acc.reasoningTokens = reasoning;
5632
5624
  }
5633
5625
  }
5634
- const results = [];
5635
- for (const acc of sessions.values()) {
5636
- const startedAt = acc.startedAt > 0 ? acc.startedAt : acc.endedAt;
5637
- const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
5638
- if (startedAt <= 0 || endedAt <= 0) continue;
5639
- const model = acc.model.length > 0 ? acc.model : "unknown";
5640
- const costUsdCents = 0;
5641
- const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
5642
- results.push({
5643
- id: `codex:${acc.sessionId}`,
5644
- source: "codex",
5645
- provider: "openai",
5646
- client: acc.client || "codex-cli",
5647
- channel: acc.channel,
5648
- model,
5649
- inTokens: acc.inTokens,
5650
- outTokens: acc.outTokens,
5651
- cacheReadTokens: acc.cacheReadTokens,
5652
- reasoningTokens: acc.reasoningTokens,
5653
- costUsdCents,
5654
- startedAt,
5655
- endedAt,
5656
- dedupeKey
5657
- });
5626
+ function finish() {
5627
+ const results = [];
5628
+ for (const acc of sessions.values()) {
5629
+ const startedAt = acc.startedAt > 0 ? acc.startedAt : acc.endedAt;
5630
+ const endedAt = acc.endedAt > 0 ? acc.endedAt : acc.startedAt;
5631
+ if (startedAt <= 0 || endedAt <= 0) continue;
5632
+ const model = acc.model.length > 0 ? acc.model : "unknown";
5633
+ const costUsdCents = 0;
5634
+ const dedupeKey = computeDedupeKey("codex", model, startedAt, acc.inTokens, acc.outTokens);
5635
+ results.push({
5636
+ id: `codex:${acc.sessionId}`,
5637
+ source: "codex",
5638
+ provider: "openai",
5639
+ client: acc.client || "codex-cli",
5640
+ channel: acc.channel,
5641
+ model,
5642
+ inTokens: acc.inTokens,
5643
+ outTokens: acc.outTokens,
5644
+ cacheReadTokens: acc.cacheReadTokens,
5645
+ reasoningTokens: acc.reasoningTokens,
5646
+ costUsdCents,
5647
+ startedAt,
5648
+ endedAt,
5649
+ dedupeKey
5650
+ });
5651
+ }
5652
+ return results;
5658
5653
  }
5659
- return results;
5654
+ return {
5655
+ push,
5656
+ finish,
5657
+ startFile() {
5658
+ currentSessionId = null;
5659
+ }
5660
+ };
5660
5661
  }
5661
5662
  function upsert(map, sessionId) {
5662
5663
  let acc = map.get(sessionId);
@@ -5978,12 +5979,23 @@ function discoverCodexFiles() {
5978
5979
  }
5979
5980
 
5980
5981
  // src/lib/collect.ts
5981
- async function collectSessions() {
5982
- function read(files) {
5983
- return files.map((file) => fs4.readFileSync(file, "utf8")).join("\n");
5982
+ async function parseSessionFiles(files, parser) {
5983
+ for (const file of files) {
5984
+ parser.startFile?.();
5985
+ const input = fs4.createReadStream(file, { encoding: "utf8" });
5986
+ const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
5987
+ try {
5988
+ for await (const line of lines) parser.push(line);
5989
+ } finally {
5990
+ lines.close();
5991
+ input.destroy();
5992
+ }
5984
5993
  }
5985
- const claude = parseClaudeCode(read(discoverClaudeCodeFiles()));
5986
- const codex = parseCodex(read(discoverCodexFiles()));
5994
+ return parser.finish();
5995
+ }
5996
+ async function collectSessions() {
5997
+ const claude = await parseSessionFiles(discoverClaudeCodeFiles(), createClaudeCodeParser());
5998
+ const codex = await parseSessionFiles(discoverCodexFiles(), createCodexParser());
5987
5999
  const { rows } = await extractCursorGenerations();
5988
6000
  const cursor = parseCursor(JSON.stringify(rows));
5989
6001
  return [...claude, ...codex, ...cursor].map((record) => ({
@@ -6317,9 +6329,9 @@ function printHelp() {
6317
6329
  --verbose Print file change events and upload detail
6318
6330
 
6319
6331
  \x1B[1mPrivacy:\x1B[0m
6320
- Token Rats reads usage counts only \u2014 never prompts or completions.
6321
- The parser source is in packages/parsers/. We literally can't read
6322
- what you typed.
6332
+ Token Rats reads local logs and uploads usage metadata only.
6333
+ It does not upload prompts or completions.
6334
+ The parser source is in packages/parsers/.
6323
6335
 
6324
6336
  \x1B[1mCursor notes:\x1B[0m
6325
6337
  Cursor doesn't store token counts locally, so per-request tokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-rats",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Track Claude Code, Codex, and Cursor usage and compare AI subscriptions.",
5
5
  "license": "MIT",
6
6
  "type": "module",