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 +195 -83
- package/dist/cli.js.map +3 -3
- package/dist/lib/adapters/codex.js +56 -14
- package/dist/lib/adapters/codex.js.map +1 -1
- package/dist/lib/adapters/cursor.js +61 -45
- package/dist/lib/adapters/cursor.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
673
|
-
if (!total) {
|
|
673
|
+
if (!info) {
|
|
674
674
|
continue;
|
|
675
675
|
}
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
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(
|
|
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(
|
|
696
|
-
outputTokens: Math.max(
|
|
714
|
+
inputTokens: Math.max(usage2.inputTokens - usage2.cachedInputTokens, 0),
|
|
715
|
+
outputTokens: Math.max(usage2.outputTokens - reasoningTokens, 0),
|
|
697
716
|
reasoningTokens,
|
|
698
|
-
cacheReadTokens:
|
|
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
|
|
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
|
|
732
|
-
return usage2.inputTokens + usage2.cachedInputTokens + usage2.outputTokens + usage2.
|
|
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(
|
|
811
|
-
join5(
|
|
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
|
-
|
|
824
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
870
|
-
|
|
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
|
-
|
|
884
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
}
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
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
|
-
|
|
2592
|
-
|
|
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
|
-
|
|
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.
|
|
3212
|
+
console.log("0.5.4");
|
|
3120
3213
|
process.exit(0);
|
|
3121
3214
|
}
|
|
3122
|
-
const
|
|
3123
|
-
if (
|
|
3124
|
-
await handler(args
|
|
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);
|