devrage 0.5.4 → 0.5.6
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 +500 -42
- package/dist/cli.js.map +4 -4
- package/dist/lib/adapters/codex.d.ts.map +1 -1
- package/dist/lib/adapters/codex.js +38 -4
- package/dist/lib/adapters/codex.js.map +1 -1
- package/dist/lib/adapters/index.d.ts.map +1 -1
- package/dist/lib/adapters/index.js +2 -0
- package/dist/lib/adapters/index.js.map +1 -1
- package/dist/lib/adapters/t3code.d.ts +3 -0
- package/dist/lib/adapters/t3code.d.ts.map +1 -0
- package/dist/lib/adapters/t3code.js +436 -0
- package/dist/lib/adapters/t3code.js.map +1 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/commands/scan.ts
|
|
4
4
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
5
|
-
import { dirname as dirname2, join as
|
|
5
|
+
import { dirname as dirname2, join as join11 } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/adapters/amp.ts
|
|
@@ -569,11 +569,19 @@ function codexAdapter() {
|
|
|
569
569
|
}
|
|
570
570
|
},
|
|
571
571
|
async *usage(options) {
|
|
572
|
+
const seenUsage = /* @__PURE__ */ new Set();
|
|
572
573
|
for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
|
|
573
|
-
|
|
574
|
+
for await (const record of parseCodexUsageJsonl(file.filePath, {
|
|
574
575
|
session: file.session,
|
|
575
576
|
since: options?.since
|
|
576
|
-
})
|
|
577
|
+
})) {
|
|
578
|
+
const key = codexUsageRecordKey(record);
|
|
579
|
+
if (seenUsage.has(key)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
seenUsage.add(key);
|
|
583
|
+
yield record;
|
|
584
|
+
}
|
|
577
585
|
}
|
|
578
586
|
}
|
|
579
587
|
};
|
|
@@ -591,10 +599,15 @@ async function* discoverCodexSessionFiles(dir) {
|
|
|
591
599
|
if (entryStat.isDirectory()) {
|
|
592
600
|
yield* discoverCodexSessionFiles(fullPath);
|
|
593
601
|
} else if (entry.endsWith(".jsonl")) {
|
|
594
|
-
yield { filePath: fullPath, session: entry
|
|
602
|
+
yield { filePath: fullPath, session: sessionFromRolloutFileName(entry) };
|
|
595
603
|
}
|
|
596
604
|
}
|
|
597
605
|
}
|
|
606
|
+
function sessionFromRolloutFileName(fileName) {
|
|
607
|
+
return fileName.match(
|
|
608
|
+
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i
|
|
609
|
+
)?.[1] ?? fileName.replace(".jsonl", "");
|
|
610
|
+
}
|
|
598
611
|
async function* parseCodexJsonl(filePath, context) {
|
|
599
612
|
const rl = createInterface2({
|
|
600
613
|
input: createReadStream2(filePath, { encoding: "utf-8" }),
|
|
@@ -655,6 +668,8 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
655
668
|
let model;
|
|
656
669
|
let previousTotal = null;
|
|
657
670
|
let previousUsageSignature = null;
|
|
671
|
+
let session = context.session;
|
|
672
|
+
let sawSessionMeta = false;
|
|
658
673
|
for await (const line of rl) {
|
|
659
674
|
if (!line.trim()) {
|
|
660
675
|
continue;
|
|
@@ -662,6 +677,14 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
662
677
|
try {
|
|
663
678
|
const entry = JSON.parse(line);
|
|
664
679
|
const payload = asRecord3(entry["payload"]);
|
|
680
|
+
if (entry["type"] === "session_meta") {
|
|
681
|
+
const metaSession = stringValue2(payload?.["id"]) ?? stringValue2(entry["id"]);
|
|
682
|
+
if (metaSession && !sawSessionMeta) {
|
|
683
|
+
session = metaSession;
|
|
684
|
+
sawSessionMeta = true;
|
|
685
|
+
}
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
665
688
|
if (entry["type"] === "turn_context") {
|
|
666
689
|
model = stringValue2(payload?.["model"]) ?? model;
|
|
667
690
|
continue;
|
|
@@ -710,7 +733,7 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
710
733
|
provider: "openai",
|
|
711
734
|
model,
|
|
712
735
|
timestamp,
|
|
713
|
-
session
|
|
736
|
+
session,
|
|
714
737
|
inputTokens: Math.max(usage2.inputTokens - usage2.cachedInputTokens, 0),
|
|
715
738
|
outputTokens: Math.max(usage2.outputTokens - reasoningTokens, 0),
|
|
716
739
|
reasoningTokens,
|
|
@@ -721,6 +744,19 @@ async function* parseCodexUsageJsonl(filePath, context) {
|
|
|
721
744
|
}
|
|
722
745
|
}
|
|
723
746
|
}
|
|
747
|
+
function codexUsageRecordKey(record) {
|
|
748
|
+
return JSON.stringify([
|
|
749
|
+
record.session ?? "",
|
|
750
|
+
record.timestamp ?? "",
|
|
751
|
+
record.provider ?? "",
|
|
752
|
+
record.model ?? "",
|
|
753
|
+
record.inputTokens,
|
|
754
|
+
record.outputTokens,
|
|
755
|
+
record.reasoningTokens,
|
|
756
|
+
record.cacheReadTokens,
|
|
757
|
+
record.cacheWriteTokens
|
|
758
|
+
]);
|
|
759
|
+
}
|
|
724
760
|
function parseCodexTokenUsage(value) {
|
|
725
761
|
const usage2 = asRecord3(value);
|
|
726
762
|
if (!usage2) {
|
|
@@ -1653,23 +1689,444 @@ function asRecord5(value) {
|
|
|
1653
1689
|
return value;
|
|
1654
1690
|
}
|
|
1655
1691
|
|
|
1656
|
-
// src/adapters/
|
|
1657
|
-
import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
|
|
1692
|
+
// src/adapters/t3code.ts
|
|
1658
1693
|
import { existsSync as existsSync4 } from "node:fs";
|
|
1659
1694
|
import { homedir as homedir8 } from "node:os";
|
|
1660
|
-
import { join as join8 } from "node:path";
|
|
1695
|
+
import { isAbsolute, join as join8, resolve } from "node:path";
|
|
1696
|
+
function t3codeAdapter() {
|
|
1697
|
+
return {
|
|
1698
|
+
name: "t3code",
|
|
1699
|
+
async *messages(options) {
|
|
1700
|
+
for (const location of discoverT3Databases()) {
|
|
1701
|
+
const db = await openT3Db(location.path);
|
|
1702
|
+
if (!db) {
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
yield* queryUserMessages2(db, location, options);
|
|
1707
|
+
} finally {
|
|
1708
|
+
db.close();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
async *usage(options) {
|
|
1713
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1714
|
+
for (const location of discoverT3Databases()) {
|
|
1715
|
+
const db = await openT3Db(location.path);
|
|
1716
|
+
if (!db) {
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
yield* queryUsageRecords2(db, location, seen, options);
|
|
1721
|
+
} finally {
|
|
1722
|
+
db.close();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function discoverT3Databases() {
|
|
1729
|
+
const locations = [];
|
|
1730
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1731
|
+
const stateDir = stringValue6(process.env["T3CODE_STATE_DIR"]);
|
|
1732
|
+
if (stateDir) {
|
|
1733
|
+
addLocation(locations, seen, join8(resolveHomePath(stateDir), "state.sqlite"), "state");
|
|
1734
|
+
}
|
|
1735
|
+
for (const baseDir of uniqueStrings2([
|
|
1736
|
+
stringValue6(process.env["T3CODE_HOME"]),
|
|
1737
|
+
join8(homedir8(), ".t3")
|
|
1738
|
+
])) {
|
|
1739
|
+
addLocation(
|
|
1740
|
+
locations,
|
|
1741
|
+
seen,
|
|
1742
|
+
join8(resolveHomePath(baseDir), "userdata", "state.sqlite"),
|
|
1743
|
+
"userdata"
|
|
1744
|
+
);
|
|
1745
|
+
addLocation(locations, seen, join8(resolveHomePath(baseDir), "dev", "state.sqlite"), "dev");
|
|
1746
|
+
}
|
|
1747
|
+
return locations;
|
|
1748
|
+
}
|
|
1749
|
+
function addLocation(locations, seen, dbPath, scope) {
|
|
1750
|
+
if (seen.has(dbPath) || !existsSync4(dbPath)) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
seen.add(dbPath);
|
|
1754
|
+
locations.push({ path: dbPath, scope });
|
|
1755
|
+
}
|
|
1756
|
+
function resolveHomePath(value) {
|
|
1757
|
+
if (value === "~") {
|
|
1758
|
+
return homedir8();
|
|
1759
|
+
}
|
|
1760
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
1761
|
+
return join8(homedir8(), value.slice(2));
|
|
1762
|
+
}
|
|
1763
|
+
return isAbsolute(value) ? value : resolve(value);
|
|
1764
|
+
}
|
|
1765
|
+
async function openT3Db(dbPath) {
|
|
1766
|
+
try {
|
|
1767
|
+
const BetterSqlite3 = await import("better-sqlite3");
|
|
1768
|
+
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
1769
|
+
return new Ctor(
|
|
1770
|
+
dbPath,
|
|
1771
|
+
{ readonly: true, fileMustExist: true }
|
|
1772
|
+
);
|
|
1773
|
+
} catch {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
function* queryUserMessages2(db, location, options) {
|
|
1778
|
+
if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const orderColumn = hasColumns(db, "projection_thread_messages", ["message_id"]) ? "message_id" : "created_at";
|
|
1782
|
+
let query = `
|
|
1783
|
+
SELECT thread_id, created_at, text
|
|
1784
|
+
FROM projection_thread_messages
|
|
1785
|
+
WHERE role = 'user'
|
|
1786
|
+
`;
|
|
1787
|
+
const params = [];
|
|
1788
|
+
if (options?.since) {
|
|
1789
|
+
query += ` AND created_at >= ?`;
|
|
1790
|
+
params.push(options.since.toISOString());
|
|
1791
|
+
}
|
|
1792
|
+
query += ` ORDER BY created_at ASC, ${orderColumn} ASC`;
|
|
1793
|
+
let rows;
|
|
1794
|
+
try {
|
|
1795
|
+
rows = db.prepare(query).all(...params);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
for (const row of rows) {
|
|
1800
|
+
const text = stringValue6(row.text);
|
|
1801
|
+
if (!text) {
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
yield {
|
|
1805
|
+
text,
|
|
1806
|
+
timestamp: stringValue6(row.created_at),
|
|
1807
|
+
session: stringValue6(row.thread_id),
|
|
1808
|
+
project: location.scope
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function* queryUsageRecords2(db, location, seen, options) {
|
|
1813
|
+
if (!hasColumns(db, "orchestration_events", [
|
|
1814
|
+
"event_id",
|
|
1815
|
+
"stream_id",
|
|
1816
|
+
"event_type",
|
|
1817
|
+
"occurred_at",
|
|
1818
|
+
"payload_json"
|
|
1819
|
+
])) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const threadInfo = readThreadInfo(db);
|
|
1823
|
+
const orderColumn = hasColumns(db, "orchestration_events", ["sequence"]) ? "sequence" : "event_id";
|
|
1824
|
+
let query = `
|
|
1825
|
+
SELECT event_id, stream_id, occurred_at, payload_json
|
|
1826
|
+
FROM orchestration_events
|
|
1827
|
+
WHERE event_type = 'thread.activity-appended'
|
|
1828
|
+
`;
|
|
1829
|
+
const params = [];
|
|
1830
|
+
if (options?.since) {
|
|
1831
|
+
query += ` AND occurred_at >= ?`;
|
|
1832
|
+
params.push(options.since.toISOString());
|
|
1833
|
+
}
|
|
1834
|
+
query += ` ORDER BY occurred_at ASC, ${orderColumn} ASC`;
|
|
1835
|
+
let rows;
|
|
1836
|
+
try {
|
|
1837
|
+
rows = db.prepare(query).all(...params);
|
|
1838
|
+
} catch {
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
for (const row of rows) {
|
|
1842
|
+
const payload = asRecord6(parseJson(row.payload_json));
|
|
1843
|
+
const activity = asRecord6(payload?.["activity"]);
|
|
1844
|
+
if (activity?.["kind"] !== "context-window.updated") {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const usage2 = parseUsageSnapshot(activity["payload"]);
|
|
1848
|
+
if (!usage2 || !hasBillableUsage2(usage2)) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const threadId = stringValue6(payload?.["threadId"]) ?? stringValue6(row.stream_id);
|
|
1852
|
+
const timestamp = stringValue6(activity["createdAt"]) ?? stringValue6(row.occurred_at);
|
|
1853
|
+
const turnId = stringValue6(activity["turnId"]);
|
|
1854
|
+
const info = threadId ? threadInfo.get(threadId) : void 0;
|
|
1855
|
+
const provider = normalizeT3Provider(info?.provider, info?.model);
|
|
1856
|
+
const dedupeKey = t3UsageDedupeKey({
|
|
1857
|
+
scope: location.scope,
|
|
1858
|
+
threadId,
|
|
1859
|
+
turnId,
|
|
1860
|
+
provider,
|
|
1861
|
+
model: info?.model,
|
|
1862
|
+
usage: usage2
|
|
1863
|
+
});
|
|
1864
|
+
if (seen.has(dedupeKey)) {
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
seen.add(dedupeKey);
|
|
1868
|
+
yield {
|
|
1869
|
+
agent: "t3code",
|
|
1870
|
+
provider,
|
|
1871
|
+
model: info?.model,
|
|
1872
|
+
timestamp,
|
|
1873
|
+
session: threadId,
|
|
1874
|
+
inputTokens: usage2.inputTokens,
|
|
1875
|
+
outputTokens: usage2.outputTokens,
|
|
1876
|
+
reasoningTokens: usage2.reasoningTokens,
|
|
1877
|
+
cacheReadTokens: usage2.cacheReadTokens,
|
|
1878
|
+
cacheWriteTokens: usage2.cacheWriteTokens
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function normalizeT3Provider(provider, model) {
|
|
1883
|
+
const normalized = provider?.trim().toLowerCase();
|
|
1884
|
+
const key = normalized?.replace(/[^a-z0-9]/g, "");
|
|
1885
|
+
switch (key) {
|
|
1886
|
+
case "codex":
|
|
1887
|
+
return "openai";
|
|
1888
|
+
case "claudeagent":
|
|
1889
|
+
case "claudecode":
|
|
1890
|
+
return "anthropic";
|
|
1891
|
+
case "cursor":
|
|
1892
|
+
case "opencode":
|
|
1893
|
+
return void 0;
|
|
1894
|
+
default:
|
|
1895
|
+
if (key?.includes("codex")) {
|
|
1896
|
+
return "openai";
|
|
1897
|
+
}
|
|
1898
|
+
if (key?.includes("claude")) {
|
|
1899
|
+
return "anthropic";
|
|
1900
|
+
}
|
|
1901
|
+
if (normalized === "openai" || normalized === "anthropic") {
|
|
1902
|
+
return normalized;
|
|
1903
|
+
}
|
|
1904
|
+
return providerFromModel(model);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function providerFromModel(model) {
|
|
1908
|
+
const slash = model?.indexOf("/") ?? -1;
|
|
1909
|
+
if (!model || slash <= 0) {
|
|
1910
|
+
return void 0;
|
|
1911
|
+
}
|
|
1912
|
+
return model.slice(0, slash);
|
|
1913
|
+
}
|
|
1914
|
+
function readThreadInfo(db) {
|
|
1915
|
+
const info = /* @__PURE__ */ new Map();
|
|
1916
|
+
readProjectionThreadModels(db, info);
|
|
1917
|
+
readProjectionThreadProviders(db, info);
|
|
1918
|
+
return info;
|
|
1919
|
+
}
|
|
1920
|
+
function readProjectionThreadModels(db, info) {
|
|
1921
|
+
if (!tableExists(db, "projection_threads")) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
const columns = tableColumns(db, "projection_threads");
|
|
1925
|
+
if (!columns.has("thread_id")) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
if (columns.has("model_selection_json")) {
|
|
1930
|
+
const rows = db.prepare("SELECT thread_id, model_selection_json FROM projection_threads").all();
|
|
1931
|
+
for (const row of rows) {
|
|
1932
|
+
const threadId = stringValue6(row.thread_id);
|
|
1933
|
+
const modelSelection = asRecord6(parseJson(row.model_selection_json));
|
|
1934
|
+
if (!threadId || !modelSelection) {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
const entry = info.get(threadId) ?? {};
|
|
1938
|
+
entry.model = stringValue6(modelSelection["model"]) ?? entry.model;
|
|
1939
|
+
entry.provider = stringValue6(modelSelection["provider"]) ?? stringValue6(modelSelection["instanceId"]) ?? entry.provider;
|
|
1940
|
+
info.set(threadId, entry);
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (columns.has("model")) {
|
|
1945
|
+
const rows = db.prepare("SELECT thread_id, model FROM projection_threads").all();
|
|
1946
|
+
for (const row of rows) {
|
|
1947
|
+
const threadId = stringValue6(row.thread_id);
|
|
1948
|
+
const model = stringValue6(row.model);
|
|
1949
|
+
if (threadId && model) {
|
|
1950
|
+
info.set(threadId, { ...info.get(threadId), model });
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
} catch {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function readProjectionThreadProviders(db, info) {
|
|
1959
|
+
if (!hasColumns(db, "projection_thread_sessions", ["thread_id", "provider_name"])) {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
const rows = db.prepare("SELECT thread_id, provider_name FROM projection_thread_sessions").all();
|
|
1964
|
+
for (const row of rows) {
|
|
1965
|
+
const threadId = stringValue6(row.thread_id);
|
|
1966
|
+
const provider = stringValue6(row.provider_name);
|
|
1967
|
+
if (!threadId || !provider) {
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
info.set(threadId, { ...info.get(threadId), provider });
|
|
1971
|
+
}
|
|
1972
|
+
} catch {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function parseUsageSnapshot(value) {
|
|
1977
|
+
const usage2 = asRecord6(value);
|
|
1978
|
+
if (!usage2) {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
const lastInputTokens = tokenValue(usage2["lastInputTokens"] ?? usage2["last_input_tokens"]);
|
|
1982
|
+
const lastCachedInputTokens = tokenValue(
|
|
1983
|
+
usage2["lastCachedInputTokens"] ?? usage2["last_cached_input_tokens"]
|
|
1984
|
+
);
|
|
1985
|
+
const lastOutputTokens = tokenValue(usage2["lastOutputTokens"] ?? usage2["last_output_tokens"]);
|
|
1986
|
+
const lastReasoningOutputTokens = tokenValue(
|
|
1987
|
+
usage2["lastReasoningOutputTokens"] ?? usage2["last_reasoning_output_tokens"]
|
|
1988
|
+
);
|
|
1989
|
+
const hasLastDetails = [
|
|
1990
|
+
lastInputTokens,
|
|
1991
|
+
lastCachedInputTokens,
|
|
1992
|
+
lastOutputTokens,
|
|
1993
|
+
lastReasoningOutputTokens
|
|
1994
|
+
].some((token) => token !== void 0);
|
|
1995
|
+
if (hasLastDetails) {
|
|
1996
|
+
return splitTokenUsage({
|
|
1997
|
+
inputTokens: lastInputTokens ?? 0,
|
|
1998
|
+
cachedInputTokens: lastCachedInputTokens ?? 0,
|
|
1999
|
+
outputTokens: lastOutputTokens ?? 0,
|
|
2000
|
+
reasoningOutputTokens: lastReasoningOutputTokens ?? 0
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
const inputTokens = tokenValue(usage2["inputTokens"] ?? usage2["input_tokens"]);
|
|
2004
|
+
const cachedInputTokens = tokenValue(usage2["cachedInputTokens"] ?? usage2["cached_input_tokens"]);
|
|
2005
|
+
const outputTokens = tokenValue(usage2["outputTokens"] ?? usage2["output_tokens"]);
|
|
2006
|
+
const reasoningOutputTokens = tokenValue(
|
|
2007
|
+
usage2["reasoningOutputTokens"] ?? usage2["reasoning_output_tokens"]
|
|
2008
|
+
);
|
|
2009
|
+
const hasSnapshotDetails = [
|
|
2010
|
+
inputTokens,
|
|
2011
|
+
cachedInputTokens,
|
|
2012
|
+
outputTokens,
|
|
2013
|
+
reasoningOutputTokens
|
|
2014
|
+
].some((token) => token !== void 0);
|
|
2015
|
+
if (!hasSnapshotDetails) {
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
return splitTokenUsage({
|
|
2019
|
+
inputTokens: inputTokens ?? 0,
|
|
2020
|
+
cachedInputTokens: cachedInputTokens ?? 0,
|
|
2021
|
+
outputTokens: outputTokens ?? 0,
|
|
2022
|
+
reasoningOutputTokens: reasoningOutputTokens ?? 0
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
function splitTokenUsage(input) {
|
|
2026
|
+
const reasoningTokens = Math.min(input.reasoningOutputTokens, input.outputTokens);
|
|
2027
|
+
return {
|
|
2028
|
+
inputTokens: Math.max(input.inputTokens - input.cachedInputTokens, 0),
|
|
2029
|
+
outputTokens: Math.max(input.outputTokens - reasoningTokens, 0),
|
|
2030
|
+
reasoningTokens,
|
|
2031
|
+
cacheReadTokens: input.cachedInputTokens,
|
|
2032
|
+
cacheWriteTokens: 0
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
function hasBillableUsage2(usage2) {
|
|
2036
|
+
return usage2.inputTokens + usage2.outputTokens + usage2.reasoningTokens + usage2.cacheReadTokens + usage2.cacheWriteTokens > 0;
|
|
2037
|
+
}
|
|
2038
|
+
function t3UsageDedupeKey(input) {
|
|
2039
|
+
return JSON.stringify([
|
|
2040
|
+
input.scope,
|
|
2041
|
+
input.threadId ?? "",
|
|
2042
|
+
input.turnId ?? "",
|
|
2043
|
+
input.provider ?? "",
|
|
2044
|
+
input.model ?? "",
|
|
2045
|
+
input.usage.inputTokens,
|
|
2046
|
+
input.usage.outputTokens,
|
|
2047
|
+
input.usage.reasoningTokens,
|
|
2048
|
+
input.usage.cacheReadTokens,
|
|
2049
|
+
input.usage.cacheWriteTokens
|
|
2050
|
+
]);
|
|
2051
|
+
}
|
|
2052
|
+
function hasColumns(db, table, requiredColumns) {
|
|
2053
|
+
const columns = tableColumns(db, table);
|
|
2054
|
+
return requiredColumns.every((column) => columns.has(column));
|
|
2055
|
+
}
|
|
2056
|
+
function tableExists(db, table) {
|
|
2057
|
+
try {
|
|
2058
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(table);
|
|
2059
|
+
return Boolean(row);
|
|
2060
|
+
} catch {
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
function tableColumns(db, table) {
|
|
2065
|
+
if (!tableExists(db, table)) {
|
|
2066
|
+
return /* @__PURE__ */ new Set();
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
const rows = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
2070
|
+
return new Set(rows.flatMap((row) => stringValue6(row.name) ?? []));
|
|
2071
|
+
} catch {
|
|
2072
|
+
return /* @__PURE__ */ new Set();
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function parseJson(value) {
|
|
2076
|
+
if (typeof value !== "string") {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
return JSON.parse(value);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function tokenValue(value) {
|
|
2086
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2087
|
+
return void 0;
|
|
2088
|
+
}
|
|
2089
|
+
return Math.max(Math.round(value), 0);
|
|
2090
|
+
}
|
|
2091
|
+
function uniqueStrings2(values) {
|
|
2092
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2093
|
+
const unique = [];
|
|
2094
|
+
for (const value of values) {
|
|
2095
|
+
if (!value || seen.has(value)) {
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
seen.add(value);
|
|
2099
|
+
unique.push(value);
|
|
2100
|
+
}
|
|
2101
|
+
return unique;
|
|
2102
|
+
}
|
|
2103
|
+
function stringValue6(value) {
|
|
2104
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
2105
|
+
}
|
|
2106
|
+
function asRecord6(value) {
|
|
2107
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
return value;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// src/adapters/zed.ts
|
|
2114
|
+
import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
|
|
2115
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2116
|
+
import { homedir as homedir9 } from "node:os";
|
|
2117
|
+
import { join as join9 } from "node:path";
|
|
1661
2118
|
function getZedPaths() {
|
|
1662
2119
|
if (process.platform === "darwin") {
|
|
1663
|
-
const base2 =
|
|
2120
|
+
const base2 = join9(homedir9(), "Library", "Application Support", "Zed");
|
|
1664
2121
|
return {
|
|
1665
|
-
conversations:
|
|
1666
|
-
db:
|
|
2122
|
+
conversations: join9(base2, "conversations"),
|
|
2123
|
+
db: join9(base2, "db")
|
|
1667
2124
|
};
|
|
1668
2125
|
}
|
|
1669
|
-
const base =
|
|
2126
|
+
const base = join9(process.env["XDG_DATA_HOME"] ?? join9(homedir9(), ".local", "share"), "zed");
|
|
1670
2127
|
return {
|
|
1671
|
-
conversations:
|
|
1672
|
-
db:
|
|
2128
|
+
conversations: join9(base, "conversations"),
|
|
2129
|
+
db: join9(base, "db")
|
|
1673
2130
|
};
|
|
1674
2131
|
}
|
|
1675
2132
|
function zedAdapter() {
|
|
@@ -1683,7 +2140,7 @@ function zedAdapter() {
|
|
|
1683
2140
|
};
|
|
1684
2141
|
}
|
|
1685
2142
|
async function* parseTextThreads(dir, _options) {
|
|
1686
|
-
if (!
|
|
2143
|
+
if (!existsSync5(dir)) {
|
|
1687
2144
|
return;
|
|
1688
2145
|
}
|
|
1689
2146
|
let files;
|
|
@@ -1694,7 +2151,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
1694
2151
|
}
|
|
1695
2152
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
1696
2153
|
for (const file of jsonFiles) {
|
|
1697
|
-
const filePath =
|
|
2154
|
+
const filePath = join9(dir, file);
|
|
1698
2155
|
const session = file.replace(".json", "");
|
|
1699
2156
|
try {
|
|
1700
2157
|
const raw = await readFile3(filePath, "utf-8");
|
|
@@ -1720,7 +2177,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
1720
2177
|
}
|
|
1721
2178
|
}
|
|
1722
2179
|
async function* parseAgentThreads(dbDir, _options) {
|
|
1723
|
-
if (!
|
|
2180
|
+
if (!existsSync5(dbDir)) {
|
|
1724
2181
|
return;
|
|
1725
2182
|
}
|
|
1726
2183
|
let dbFiles;
|
|
@@ -1741,7 +2198,7 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
1741
2198
|
return;
|
|
1742
2199
|
}
|
|
1743
2200
|
for (const dbFile of dbFiles) {
|
|
1744
|
-
const dbPath =
|
|
2201
|
+
const dbPath = join9(dbDir, dbFile);
|
|
1745
2202
|
let db;
|
|
1746
2203
|
try {
|
|
1747
2204
|
db = new Database(dbPath, {
|
|
@@ -1792,6 +2249,7 @@ var ADAPTERS = {
|
|
|
1792
2249
|
amp: ampAdapter,
|
|
1793
2250
|
cline: clineAdapter,
|
|
1794
2251
|
pi: piAdapter,
|
|
2252
|
+
t3code: t3codeAdapter,
|
|
1795
2253
|
zed: zedAdapter
|
|
1796
2254
|
};
|
|
1797
2255
|
function createAdapter(name) {
|
|
@@ -1961,8 +2419,8 @@ function runPattern(_originalText, searchText, matches, seen) {
|
|
|
1961
2419
|
|
|
1962
2420
|
// src/pricing/index.ts
|
|
1963
2421
|
import { mkdir, readFile as readFile4, writeFile } from "node:fs/promises";
|
|
1964
|
-
import { homedir as
|
|
1965
|
-
import { dirname, join as
|
|
2422
|
+
import { homedir as homedir10 } from "node:os";
|
|
2423
|
+
import { dirname, join as join10 } from "node:path";
|
|
1966
2424
|
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
1967
2425
|
var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1968
2426
|
var FETCH_TIMEOUT_MS = 2e3;
|
|
@@ -2062,16 +2520,16 @@ async function summarizeUsage(records, pricing) {
|
|
|
2062
2520
|
}
|
|
2063
2521
|
function getPricingCachePath() {
|
|
2064
2522
|
if (process.env["XDG_CACHE_HOME"]) {
|
|
2065
|
-
return
|
|
2523
|
+
return join10(process.env["XDG_CACHE_HOME"], "devrage", "models.dev.json");
|
|
2066
2524
|
}
|
|
2067
2525
|
if (process.platform === "darwin") {
|
|
2068
|
-
return
|
|
2526
|
+
return join10(homedir10(), "Library", "Caches", "devrage", "models.dev.json");
|
|
2069
2527
|
}
|
|
2070
2528
|
if (process.platform === "win32") {
|
|
2071
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
2072
|
-
return
|
|
2529
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join10(homedir10(), "AppData", "Local");
|
|
2530
|
+
return join10(localAppData, "devrage", "models.dev.json");
|
|
2073
2531
|
}
|
|
2074
|
-
return
|
|
2532
|
+
return join10(homedir10(), ".cache", "devrage", "models.dev.json");
|
|
2075
2533
|
}
|
|
2076
2534
|
function createCostAccumulator() {
|
|
2077
2535
|
return {
|
|
@@ -2180,7 +2638,7 @@ async function readPricingCache(cachePath) {
|
|
|
2180
2638
|
try {
|
|
2181
2639
|
const raw = await readFile4(cachePath, "utf-8");
|
|
2182
2640
|
const parsed = JSON.parse(raw);
|
|
2183
|
-
const cache =
|
|
2641
|
+
const cache = asRecord7(parsed);
|
|
2184
2642
|
if (cache?.["source"] !== "models.dev" || cache["schemaVersion"] !== 1 || typeof cache["fetchedAt"] !== "string" || !isModelsDevCatalog(cache["catalog"])) {
|
|
2185
2643
|
return null;
|
|
2186
2644
|
}
|
|
@@ -2321,10 +2779,10 @@ function inferProvider(model) {
|
|
|
2321
2779
|
return void 0;
|
|
2322
2780
|
}
|
|
2323
2781
|
function getCatalogRates(catalog, provider, model) {
|
|
2324
|
-
const root =
|
|
2325
|
-
const providerEntry =
|
|
2326
|
-
const models =
|
|
2327
|
-
const modelEntry =
|
|
2782
|
+
const root = asRecord7(catalog);
|
|
2783
|
+
const providerEntry = asRecord7(root?.[provider]);
|
|
2784
|
+
const models = asRecord7(providerEntry?.["models"]);
|
|
2785
|
+
const modelEntry = asRecord7(models?.[model]);
|
|
2328
2786
|
return toRateTable(modelEntry?.["cost"]);
|
|
2329
2787
|
}
|
|
2330
2788
|
function selectContextRates(rates, record) {
|
|
@@ -2332,8 +2790,8 @@ function selectContextRates(rates, record) {
|
|
|
2332
2790
|
let selected = rates;
|
|
2333
2791
|
let selectedSize = 0;
|
|
2334
2792
|
for (const tier of rates.tiers ?? []) {
|
|
2335
|
-
const tierRecord =
|
|
2336
|
-
const tierInfo =
|
|
2793
|
+
const tierRecord = asRecord7(tier);
|
|
2794
|
+
const tierInfo = asRecord7(tierRecord?.["tier"]);
|
|
2337
2795
|
const size = typeof tierInfo?.["size"] === "number" ? tierInfo["size"] : 0;
|
|
2338
2796
|
if (tierInfo?.["type"] !== "context" || contextTokens < size || size < selectedSize) {
|
|
2339
2797
|
continue;
|
|
@@ -2350,7 +2808,7 @@ function selectContextRates(rates, record) {
|
|
|
2350
2808
|
return selected;
|
|
2351
2809
|
}
|
|
2352
2810
|
function toRateTable(value) {
|
|
2353
|
-
const record =
|
|
2811
|
+
const record = asRecord7(value);
|
|
2354
2812
|
if (!record) {
|
|
2355
2813
|
return null;
|
|
2356
2814
|
}
|
|
@@ -2384,10 +2842,10 @@ function numberValue6(value) {
|
|
|
2384
2842
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2385
2843
|
}
|
|
2386
2844
|
function isModelsDevCatalog(value) {
|
|
2387
|
-
const catalog =
|
|
2388
|
-
const openai =
|
|
2389
|
-
const anthropic =
|
|
2390
|
-
return Boolean(
|
|
2845
|
+
const catalog = asRecord7(value);
|
|
2846
|
+
const openai = asRecord7(catalog?.["openai"]);
|
|
2847
|
+
const anthropic = asRecord7(catalog?.["anthropic"]);
|
|
2848
|
+
return Boolean(asRecord7(openai?.["models"]) || asRecord7(anthropic?.["models"]));
|
|
2391
2849
|
}
|
|
2392
2850
|
function isFresh(fetchedAt, ttlMs) {
|
|
2393
2851
|
const fetchedTime = new Date(fetchedAt).getTime();
|
|
@@ -2396,7 +2854,7 @@ function isFresh(fetchedAt, ttlMs) {
|
|
|
2396
2854
|
function mergePricingSource(left, right) {
|
|
2397
2855
|
return left === right ? left : "mixed";
|
|
2398
2856
|
}
|
|
2399
|
-
function
|
|
2857
|
+
function asRecord7(value) {
|
|
2400
2858
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2401
2859
|
return null;
|
|
2402
2860
|
}
|
|
@@ -2501,7 +2959,7 @@ function parseArgs(args) {
|
|
|
2501
2959
|
console.log(`devrage scan \u2014 scan sessions for profanity
|
|
2502
2960
|
|
|
2503
2961
|
Options:
|
|
2504
|
-
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, zed)
|
|
2962
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
2505
2963
|
--since, -s <date> Only scan messages after this date (ISO 8601)
|
|
2506
2964
|
--day, --days [n] Only scan the last n days (default: 1)
|
|
2507
2965
|
--week Only scan the last 7 days
|
|
@@ -2542,7 +3000,7 @@ Usage:
|
|
|
2542
3000
|
devrage cost [options]
|
|
2543
3001
|
|
|
2544
3002
|
Options:
|
|
2545
|
-
--agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi)
|
|
3003
|
+
--agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi, t3code)
|
|
2546
3004
|
--refresh-prices Refresh models.dev pricing before estimating cost
|
|
2547
3005
|
--since, -s <date> Only include usage after this date (ISO 8601)
|
|
2548
3006
|
--day, --days [n] Only include the last n days (default: 1)
|
|
@@ -2756,7 +3214,7 @@ function printCostCommandUnavailable(options) {
|
|
|
2756
3214
|
}
|
|
2757
3215
|
async function writeCostHtmlReport(totals, options) {
|
|
2758
3216
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2759
|
-
const reportPath =
|
|
3217
|
+
const reportPath = join11(
|
|
2760
3218
|
dirname2(getPricingCachePath()),
|
|
2761
3219
|
`cost-report-${safeTimestamp(generatedAt)}.html`
|
|
2762
3220
|
);
|
|
@@ -3209,7 +3667,7 @@ async function main() {
|
|
|
3209
3667
|
process.exit(0);
|
|
3210
3668
|
}
|
|
3211
3669
|
if (command === "--version") {
|
|
3212
|
-
console.log("0.5.
|
|
3670
|
+
console.log("0.5.6");
|
|
3213
3671
|
process.exit(0);
|
|
3214
3672
|
}
|
|
3215
3673
|
const parsed = parseCommand(args);
|