devrage 0.0.5 → 0.5.4

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/dist/cli.js CHANGED
@@ -654,6 +654,7 @@ async function* parseCodexUsageJsonl(filePath, context) {
654
654
  });
655
655
  let model;
656
656
  let previousTotal = null;
657
+ let previousUsageSignature = null;
657
658
  for await (const line of rl) {
658
659
  if (!line.trim()) {
659
660
  continue;
@@ -669,13 +670,31 @@ async function* parseCodexUsageJsonl(filePath, context) {
669
670
  continue;
670
671
  }
671
672
  const info = asRecord3(payload["info"]);
672
- const total = parseCodexTokenUsage(info?.["total_token_usage"]);
673
- if (!total) {
673
+ if (!info) {
674
674
  continue;
675
675
  }
676
- const delta = previousTotal ? subtractCodexUsage(total, previousTotal) : total;
677
- previousTotal = total;
678
- if (!hasPositiveUsage(delta)) {
676
+ const lastUsageValue = info["last_token_usage"];
677
+ const lastUsage = parseCodexTokenUsage(lastUsageValue);
678
+ const total = parseCodexTokenUsage(info["total_token_usage"]);
679
+ let usage2 = null;
680
+ if (lastUsageValue !== void 0) {
681
+ if (lastUsage && hasBillableUsage(lastUsage)) {
682
+ const signature = codexUsageSignature(lastUsage, total);
683
+ if (signature !== previousUsageSignature) {
684
+ usage2 = lastUsage;
685
+ }
686
+ previousUsageSignature = signature;
687
+ }
688
+ } else if (total) {
689
+ const delta = previousTotal ? subtractCodexUsage(total, previousTotal) : total;
690
+ if (hasBillableUsage(delta)) {
691
+ usage2 = delta;
692
+ }
693
+ }
694
+ if (total && hasBillableUsage(total)) {
695
+ previousTotal = total;
696
+ }
697
+ if (!usage2) {
679
698
  continue;
680
699
  }
681
700
  const timestamp = stringValue2(entry["timestamp"]);
@@ -685,17 +704,17 @@ async function* parseCodexUsageJsonl(filePath, context) {
685
704
  continue;
686
705
  }
687
706
  }
688
- const reasoningTokens = Math.min(delta.reasoningOutputTokens, delta.outputTokens);
707
+ const reasoningTokens = Math.min(usage2.reasoningOutputTokens, usage2.outputTokens);
689
708
  yield {
690
709
  agent: "codex",
691
710
  provider: "openai",
692
711
  model,
693
712
  timestamp,
694
713
  session: context.session,
695
- inputTokens: Math.max(delta.inputTokens - delta.cachedInputTokens, 0),
696
- outputTokens: Math.max(delta.outputTokens - reasoningTokens, 0),
714
+ inputTokens: Math.max(usage2.inputTokens - usage2.cachedInputTokens, 0),
715
+ outputTokens: Math.max(usage2.outputTokens - reasoningTokens, 0),
697
716
  reasoningTokens,
698
- cacheReadTokens: delta.cachedInputTokens,
717
+ cacheReadTokens: usage2.cachedInputTokens,
699
718
  cacheWriteTokens: 0
700
719
  };
701
720
  } catch {
@@ -707,14 +726,23 @@ function parseCodexTokenUsage(value) {
707
726
  if (!usage2) {
708
727
  return null;
709
728
  }
710
- const parsed = {
729
+ const hasUsageField = [
730
+ "input_tokens",
731
+ "cached_input_tokens",
732
+ "output_tokens",
733
+ "reasoning_output_tokens",
734
+ "total_tokens"
735
+ ].some((key) => typeof usage2[key] === "number" && Number.isFinite(usage2[key]));
736
+ if (!hasUsageField) {
737
+ return null;
738
+ }
739
+ return {
711
740
  inputTokens: numberValue2(usage2["input_tokens"]),
712
741
  cachedInputTokens: numberValue2(usage2["cached_input_tokens"]),
713
742
  outputTokens: numberValue2(usage2["output_tokens"]),
714
743
  reasoningOutputTokens: numberValue2(usage2["reasoning_output_tokens"]),
715
744
  totalTokens: numberValue2(usage2["total_tokens"])
716
745
  };
717
- return hasPositiveUsage(parsed) ? parsed : null;
718
746
  }
719
747
  function subtractCodexUsage(current, previous) {
720
748
  return {
@@ -728,8 +756,20 @@ function subtractCodexUsage(current, previous) {
728
756
  totalTokens: Math.max(current.totalTokens - previous.totalTokens, 0)
729
757
  };
730
758
  }
731
- function hasPositiveUsage(usage2) {
732
- return usage2.inputTokens + usage2.cachedInputTokens + usage2.outputTokens + usage2.totalTokens > 0;
759
+ function hasBillableUsage(usage2) {
760
+ return usage2.inputTokens + usage2.cachedInputTokens + usage2.outputTokens + usage2.reasoningOutputTokens > 0;
761
+ }
762
+ function codexUsageSignature(usage2, total) {
763
+ return [
764
+ usage2.inputTokens,
765
+ usage2.cachedInputTokens,
766
+ usage2.outputTokens,
767
+ usage2.reasoningOutputTokens,
768
+ total?.inputTokens ?? "",
769
+ total?.cachedInputTokens ?? "",
770
+ total?.outputTokens ?? "",
771
+ total?.reasoningOutputTokens ?? ""
772
+ ].join(":");
733
773
  }
734
774
  function numberValue2(value) {
735
775
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -805,10 +845,12 @@ async function discoverCursorStateStores() {
805
845
  return stores;
806
846
  }
807
847
  function getCursorUserDirs() {
848
+ const configHome = envOrDefault("XDG_CONFIG_HOME", join5(homedir5(), ".config"));
849
+ const appData = envOrDefault("APPDATA", join5(homedir5(), "AppData", "Roaming"));
808
850
  return uniqueStrings([
809
851
  join5(homedir5(), "Library", "Application Support", "Cursor", "User"),
810
- join5(process.env["XDG_CONFIG_HOME"] ?? join5(homedir5(), ".config"), "Cursor", "User"),
811
- join5(process.env["APPDATA"] ?? join5(homedir5(), "AppData", "Roaming"), "Cursor", "User")
852
+ join5(configHome, "Cursor", "User"),
853
+ join5(appData, "Cursor", "User")
812
854
  ]);
813
855
  }
814
856
  async function* parseCursorStore(store, options) {
@@ -820,36 +862,40 @@ async function* parseCursorStore(store, options) {
820
862
  const rows = readStateRows(db);
821
863
  const seen = /* @__PURE__ */ new Set();
822
864
  for (const row of rows) {
823
- if (!isCandidateKey(row.key)) {
824
- continue;
825
- }
826
- const parsed = parseJsonValue(row.value);
827
- if (parsed === void 0) {
828
- continue;
829
- }
830
- for (const message of extractCursorMessages(parsed, row.key)) {
831
- const text = message.text.trim();
832
- if (!isLikelyMessageText(text)) {
865
+ try {
866
+ if (!isCandidateKey(row.key)) {
833
867
  continue;
834
868
  }
835
- if (options?.since && message.timestamp) {
836
- const timestamp = new Date(message.timestamp);
837
- if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
869
+ const parsed = parseJsonValue(row.value);
870
+ if (parsed === void 0) {
871
+ continue;
872
+ }
873
+ for (const message of extractCursorMessages(parsed, row.key)) {
874
+ const text = message.text.trim();
875
+ if (!isLikelyMessageText(text)) {
838
876
  continue;
839
877
  }
878
+ if (options?.since && message.timestamp) {
879
+ const timestamp = new Date(message.timestamp);
880
+ if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
881
+ continue;
882
+ }
883
+ }
884
+ const session = message.session ?? `${store.scope}:${row.key}`;
885
+ const dedupeKey = `${session}\0${message.timestamp ?? ""}\0${text}`;
886
+ if (seen.has(dedupeKey)) {
887
+ continue;
888
+ }
889
+ seen.add(dedupeKey);
890
+ yield {
891
+ text,
892
+ timestamp: message.timestamp,
893
+ session,
894
+ project: store.project
895
+ };
840
896
  }
841
- const session = message.session ?? `${store.scope}:${row.key}`;
842
- const dedupeKey = `${session}\0${message.timestamp ?? ""}\0${text}`;
843
- if (seen.has(dedupeKey)) {
844
- continue;
845
- }
846
- seen.add(dedupeKey);
847
- yield {
848
- text,
849
- timestamp: message.timestamp,
850
- session,
851
- project: store.project
852
- };
897
+ } catch {
898
+ continue;
853
899
  }
854
900
  }
855
901
  } finally {
@@ -866,26 +912,30 @@ async function* parseCursorUsageStore(store, options) {
866
912
  const composerModels = collectComposerModels(rows);
867
913
  const seen = /* @__PURE__ */ new Set();
868
914
  for (const row of rows) {
869
- if (!row.key.startsWith("bubbleId:")) {
870
- continue;
871
- }
872
- const parsed = parseJsonValue(row.value);
873
- const usage2 = extractCursorBubbleUsage(parsed, row.key, composerModels);
874
- if (!usage2) {
875
- continue;
876
- }
877
- if (options?.since && usage2.timestamp) {
878
- const timestamp = new Date(usage2.timestamp);
879
- if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
915
+ try {
916
+ if (!row.key.startsWith("bubbleId:")) {
880
917
  continue;
881
918
  }
882
- }
883
- const dedupeKey = `${usage2.session ?? ""}\0${usage2.timestamp ?? ""}\0${usage2.model ?? ""}\0${usage2.inputTokens}\0${usage2.outputTokens}`;
884
- if (seen.has(dedupeKey)) {
919
+ const parsed = parseJsonValue(row.value);
920
+ const usage2 = extractCursorBubbleUsage(parsed, row.key, composerModels);
921
+ if (!usage2) {
922
+ continue;
923
+ }
924
+ if (options?.since && usage2.timestamp) {
925
+ const timestamp = new Date(usage2.timestamp);
926
+ if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
927
+ continue;
928
+ }
929
+ }
930
+ const dedupeKey = `${usage2.session ?? ""}\0${usage2.timestamp ?? ""}\0${usage2.model ?? ""}\0${usage2.inputTokens}\0${usage2.outputTokens}`;
931
+ if (seen.has(dedupeKey)) {
932
+ continue;
933
+ }
934
+ seen.add(dedupeKey);
935
+ yield usage2;
936
+ } catch {
885
937
  continue;
886
938
  }
887
- seen.add(dedupeKey);
888
- yield usage2;
889
939
  }
890
940
  } finally {
891
941
  db.close();
@@ -907,18 +957,22 @@ function readStateRows(db) {
907
957
  const rows = [];
908
958
  try {
909
959
  const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
910
- const availableTables = new Set(tables.map((table) => table.name));
960
+ const availableTables = new Set(tables.flatMap((table) => stringValue3(table.name) ?? []));
911
961
  for (const table of STATE_TABLES) {
912
962
  if (!availableTables.has(table)) {
913
963
  continue;
914
964
  }
915
965
  const columns = db.prepare(`PRAGMA table_info("${table}")`).all();
916
- const columnNames = new Set(columns.map((column) => column.name));
966
+ const columnNames = new Set(columns.flatMap((column) => stringValue3(column.name) ?? []));
917
967
  if (!columnNames.has("key") || !columnNames.has("value")) {
918
968
  continue;
919
969
  }
920
970
  const tableRows = db.prepare(`SELECT key, value FROM "${table}"`).all();
921
- rows.push(...tableRows);
971
+ rows.push(
972
+ ...tableRows.flatMap(
973
+ (row) => typeof row.key === "string" ? [{ key: row.key, value: row.value }] : []
974
+ )
975
+ );
922
976
  }
923
977
  } catch {
924
978
  return rows;
@@ -1250,6 +1304,10 @@ function uniqueMessages(messages) {
1250
1304
  function uniqueStrings(values) {
1251
1305
  return Array.from(new Set(values));
1252
1306
  }
1307
+ function envOrDefault(name, fallback) {
1308
+ const value = process.env[name];
1309
+ return value && value.trim() ? value : fallback;
1310
+ }
1253
1311
  function numberValue3(value) {
1254
1312
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1255
1313
  }
@@ -2372,30 +2430,49 @@ var SPINNER_MESSAGES = [
2372
2430
  "Auditing your language",
2373
2431
  "Tabulating regrets"
2374
2432
  ];
2433
+ var COST_SPINNER_MESSAGES = [
2434
+ "Loading price catalog",
2435
+ "Reading local usage",
2436
+ "Scanning transcript stores",
2437
+ "Crunching token counts",
2438
+ "Still working through local history"
2439
+ ];
2375
2440
  var DAY_MS = 24 * 60 * 60 * 1e3;
2376
2441
  function createSpinner(messages = SPINNER_MESSAGES) {
2377
2442
  let messageIdx = 0;
2378
2443
  let dotCount = 0;
2379
2444
  let timer = null;
2445
+ let messageOverride = null;
2446
+ function render() {
2447
+ dotCount = (dotCount + 1) % 4;
2448
+ const msg = messageOverride ?? messages[messageIdx % messages.length];
2449
+ const dots = ".".repeat(dotCount || 1);
2450
+ process.stdout.write(`\r ${c.dim}${msg}${dots}${c.reset} `);
2451
+ }
2380
2452
  return {
2381
- start() {
2453
+ start(message) {
2382
2454
  messageIdx = Math.floor(Math.random() * messages.length);
2455
+ messageOverride = message ?? null;
2456
+ render();
2383
2457
  timer = setInterval(() => {
2384
- dotCount = (dotCount + 1) % 4;
2385
- const msg = messages[messageIdx % messages.length];
2386
- const dots = ".".repeat(dotCount || 1);
2387
- process.stdout.write(`\r ${c.dim}${msg}${dots}${c.reset} `);
2458
+ render();
2388
2459
  }, 300);
2389
2460
  },
2390
- update() {
2391
- messageIdx++;
2461
+ update(message) {
2462
+ if (message) {
2463
+ messageOverride = message;
2464
+ } else {
2465
+ messageOverride = null;
2466
+ messageIdx++;
2467
+ }
2468
+ render();
2392
2469
  },
2393
2470
  stop() {
2394
2471
  if (timer) {
2395
2472
  clearInterval(timer);
2396
2473
  timer = null;
2397
2474
  }
2398
- process.stdout.write("\r" + " ".repeat(60) + "\r");
2475
+ process.stdout.write("\r" + " ".repeat(80) + "\r");
2399
2476
  }
2400
2477
  };
2401
2478
  }
@@ -2578,23 +2655,38 @@ async function cost(args) {
2578
2655
  const options = parseCostArgs(args);
2579
2656
  const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
2580
2657
  const costByAgent = {};
2581
- const pricing = await loadPricingCatalog({ refresh: options.refreshPrices });
2582
- for (const adapter of adapters) {
2583
- if (!adapter.usage) {
2584
- continue;
2585
- }
2586
- const summary = await summarizeUsage(adapter.usage({ since: options.since }), pricing);
2587
- if (summary.requests > 0) {
2588
- costByAgent[adapter.name] = summary;
2658
+ const spinner = createSpinner(COST_SPINNER_MESSAGES);
2659
+ let totals = null;
2660
+ spinner.start("Loading price catalog");
2661
+ try {
2662
+ const pricing = await loadPricingCatalog({ refresh: options.refreshPrices });
2663
+ for (const adapter of adapters) {
2664
+ if (!adapter.usage) {
2665
+ continue;
2666
+ }
2667
+ spinner.update(`Reading ${adapter.name} usage`);
2668
+ const summary = await summarizeUsage(adapter.usage({ since: options.since }), pricing);
2669
+ if (summary.requests > 0) {
2670
+ costByAgent[adapter.name] = summary;
2671
+ }
2589
2672
  }
2673
+ totals = getCostTotals(costByAgent);
2674
+ } finally {
2675
+ spinner.stop();
2590
2676
  }
2591
- const totals = getCostTotals(costByAgent);
2592
- console.log("");
2593
- if (totals.entries.length === 0) {
2677
+ if (!totals || totals.entries.length === 0) {
2678
+ console.log("");
2594
2679
  printCostCommandUnavailable(options);
2595
2680
  return;
2596
2681
  }
2597
- const reportUrl = await writeCostHtmlReport(totals, options);
2682
+ spinner.start("Writing cost report");
2683
+ let reportUrl;
2684
+ try {
2685
+ reportUrl = await writeCostHtmlReport(totals, options);
2686
+ } finally {
2687
+ spinner.stop();
2688
+ }
2689
+ console.log("");
2598
2690
  printCostCommand(totals, options, reportUrl);
2599
2691
  }
2600
2692
  function printCostCommand(totals, options, reportUrl) {
@@ -3088,6 +3180,7 @@ var COMMANDS = {
3088
3180
  cost,
3089
3181
  scan
3090
3182
  };
3183
+ var OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--agent", "-a", "--since", "-s"]);
3091
3184
  function usage() {
3092
3185
  console.log(`devrage \u2014 count how many times you swear at your coding agents
3093
3186
 
@@ -3116,16 +3209,35 @@ async function main() {
3116
3209
  process.exit(0);
3117
3210
  }
3118
3211
  if (command === "--version") {
3119
- console.log("0.0.5");
3212
+ console.log("0.5.4");
3120
3213
  process.exit(0);
3121
3214
  }
3122
- const handler = command ? COMMANDS[command] : void 0;
3123
- if (handler) {
3124
- await handler(args.slice(1));
3215
+ const parsed = parseCommand(args);
3216
+ if (parsed) {
3217
+ await parsed.handler(parsed.args);
3125
3218
  } else {
3126
3219
  await scan(args);
3127
3220
  }
3128
3221
  }
3222
+ function parseCommand(args) {
3223
+ for (let index = 0; index < args.length; index++) {
3224
+ const arg = args[index];
3225
+ if (!arg) {
3226
+ continue;
3227
+ }
3228
+ const handler = COMMANDS[arg];
3229
+ if (handler) {
3230
+ return {
3231
+ handler,
3232
+ args: [...args.slice(0, index), ...args.slice(index + 1)]
3233
+ };
3234
+ }
3235
+ if (OPTIONS_WITH_VALUES.has(arg) && index + 1 < args.length) {
3236
+ index++;
3237
+ }
3238
+ }
3239
+ return null;
3240
+ }
3129
3241
  main().catch((err) => {
3130
3242
  console.error(err);
3131
3243
  process.exit(1);