devrage 0.5.5 → 0.5.7
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 +555 -73
- package/dist/cli.js.map +4 -4
- package/dist/lib/adapters/cursor.d.ts.map +1 -1
- package/dist/lib/adapters/cursor.js +8 -8
- package/dist/lib/adapters/cursor.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/opencode.d.ts.map +1 -1
- package/dist/lib/adapters/opencode.js +5 -9
- package/dist/lib/adapters/opencode.js.map +1 -1
- package/dist/lib/adapters/sqlite.d.ts +15 -0
- package/dist/lib/adapters/sqlite.d.ts.map +1 -0
- package/dist/lib/adapters/sqlite.js +95 -0
- package/dist/lib/adapters/sqlite.js.map +1 -0
- 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 +430 -0
- package/dist/lib/adapters/t3code.js.map +1 -0
- package/dist/lib/adapters/zed.d.ts.map +1 -1
- package/dist/lib/adapters/zed.js +6 -20
- package/dist/lib/adapters/zed.js.map +1 -1
- 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
|
|
@@ -825,6 +825,97 @@ import { existsSync as existsSync2 } from "node:fs";
|
|
|
825
825
|
import { readdir as readdir5 } from "node:fs/promises";
|
|
826
826
|
import { homedir as homedir5 } from "node:os";
|
|
827
827
|
import { join as join5 } from "node:path";
|
|
828
|
+
|
|
829
|
+
// src/adapters/sqlite.ts
|
|
830
|
+
async function openReadonlySqliteDatabase(dbPath) {
|
|
831
|
+
const requestedDriver = process.env["DEVRAGE_SQLITE_DRIVER"];
|
|
832
|
+
if (requestedDriver) {
|
|
833
|
+
const loader = driverLoader(requestedDriver);
|
|
834
|
+
if (!loader) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
return await loader(dbPath);
|
|
839
|
+
} catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const loaders = isBunRuntime() ? [openWithBunSqlite, openWithNodeSqlite, openWithBetterSqlite3] : [openWithNodeSqlite, openWithBetterSqlite3];
|
|
844
|
+
for (const loader of loaders) {
|
|
845
|
+
try {
|
|
846
|
+
return await loader(dbPath);
|
|
847
|
+
} catch {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
function driverLoader(driver) {
|
|
854
|
+
switch (driver) {
|
|
855
|
+
case "bun":
|
|
856
|
+
case "bun:sqlite":
|
|
857
|
+
return openWithBunSqlite;
|
|
858
|
+
case "node":
|
|
859
|
+
case "node:sqlite":
|
|
860
|
+
return openWithNodeSqlite;
|
|
861
|
+
case "better-sqlite3":
|
|
862
|
+
return openWithBetterSqlite3;
|
|
863
|
+
default:
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function isBunRuntime() {
|
|
868
|
+
return Boolean(process.versions["bun"]);
|
|
869
|
+
}
|
|
870
|
+
async function openWithBunSqlite(dbPath) {
|
|
871
|
+
const specifier = "bun:sqlite";
|
|
872
|
+
const sqlite = await import(specifier);
|
|
873
|
+
const db = new sqlite.Database(dbPath, { readonly: true, create: false });
|
|
874
|
+
return {
|
|
875
|
+
prepare(sql) {
|
|
876
|
+
const prepare = db.prepare ?? db.query;
|
|
877
|
+
return prepare.call(db, sql);
|
|
878
|
+
},
|
|
879
|
+
close() {
|
|
880
|
+
db.close();
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
async function openWithNodeSqlite(dbPath) {
|
|
885
|
+
const sqlite = await importNodeSqlite();
|
|
886
|
+
return new sqlite.DatabaseSync(dbPath, { open: true, readOnly: true, timeout: 5e3 });
|
|
887
|
+
}
|
|
888
|
+
async function openWithBetterSqlite3(dbPath) {
|
|
889
|
+
const BetterSqlite3 = await import("better-sqlite3");
|
|
890
|
+
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
891
|
+
return new Ctor(dbPath, {
|
|
892
|
+
readonly: true,
|
|
893
|
+
fileMustExist: true
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async function importNodeSqlite() {
|
|
897
|
+
const originalEmitWarning = process.emitWarning;
|
|
898
|
+
process.emitWarning = ((warning, ...args) => {
|
|
899
|
+
const message = typeof warning === "string" ? warning : warning.message;
|
|
900
|
+
const type = typeof args[0] === "string" ? args[0] : void 0;
|
|
901
|
+
if (message === "SQLite is an experimental feature and might change at any time" && type === "ExperimentalWarning") {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
originalEmitWarning(warning, ...args);
|
|
905
|
+
});
|
|
906
|
+
try {
|
|
907
|
+
const specifier = "node:sqlite";
|
|
908
|
+
const sqlite = await import(specifier);
|
|
909
|
+
if (typeof sqlite.DatabaseSync !== "function") {
|
|
910
|
+
throw new Error("node:sqlite DatabaseSync is unavailable");
|
|
911
|
+
}
|
|
912
|
+
return sqlite;
|
|
913
|
+
} finally {
|
|
914
|
+
process.emitWarning = originalEmitWarning;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/adapters/cursor.ts
|
|
828
919
|
var CANDIDATE_KEY_PREFIXES = [
|
|
829
920
|
"bubbleId:",
|
|
830
921
|
"composerData:",
|
|
@@ -978,16 +1069,7 @@ async function* parseCursorUsageStore(store, options) {
|
|
|
978
1069
|
}
|
|
979
1070
|
}
|
|
980
1071
|
async function openCursorDb(dbPath) {
|
|
981
|
-
|
|
982
|
-
const BetterSqlite3 = await import("better-sqlite3");
|
|
983
|
-
const Ctor = BetterSqlite3.default ?? BetterSqlite3;
|
|
984
|
-
return new Ctor(
|
|
985
|
-
dbPath,
|
|
986
|
-
{ readonly: true }
|
|
987
|
-
);
|
|
988
|
-
} catch {
|
|
989
|
-
return null;
|
|
990
|
-
}
|
|
1072
|
+
return openReadonlySqliteDatabase(dbPath);
|
|
991
1073
|
}
|
|
992
1074
|
function readStateRows(db) {
|
|
993
1075
|
const rows = [];
|
|
@@ -1048,6 +1130,12 @@ function decodeStateValue(value) {
|
|
|
1048
1130
|
if (Buffer.isBuffer(value)) {
|
|
1049
1131
|
return value.toString("utf-8");
|
|
1050
1132
|
}
|
|
1133
|
+
if (value instanceof Uint8Array) {
|
|
1134
|
+
return Buffer.from(value).toString("utf-8");
|
|
1135
|
+
}
|
|
1136
|
+
if (ArrayBuffer.isView(value)) {
|
|
1137
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("utf-8");
|
|
1138
|
+
}
|
|
1051
1139
|
return null;
|
|
1052
1140
|
}
|
|
1053
1141
|
function extractCursorMessages(root, rowKey) {
|
|
@@ -1410,17 +1498,11 @@ async function openOpencodeDb() {
|
|
|
1410
1498
|
if (!dbPath) {
|
|
1411
1499
|
return null;
|
|
1412
1500
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
return new Ctor(
|
|
1417
|
-
dbPath,
|
|
1418
|
-
{ readonly: true }
|
|
1419
|
-
);
|
|
1420
|
-
} catch {
|
|
1421
|
-
console.warn("devrage: better-sqlite3 not available, skipping OpenCode sessions");
|
|
1422
|
-
return null;
|
|
1501
|
+
const db = await openReadonlySqliteDatabase(dbPath);
|
|
1502
|
+
if (!db) {
|
|
1503
|
+
console.warn("devrage: SQLite support not available, skipping OpenCode sessions");
|
|
1423
1504
|
}
|
|
1505
|
+
return db;
|
|
1424
1506
|
}
|
|
1425
1507
|
function* queryUserMessages(db, options) {
|
|
1426
1508
|
let query = `
|
|
@@ -1689,23 +1771,435 @@ function asRecord5(value) {
|
|
|
1689
1771
|
return value;
|
|
1690
1772
|
}
|
|
1691
1773
|
|
|
1692
|
-
// src/adapters/
|
|
1693
|
-
import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
|
|
1774
|
+
// src/adapters/t3code.ts
|
|
1694
1775
|
import { existsSync as existsSync4 } from "node:fs";
|
|
1695
1776
|
import { homedir as homedir8 } from "node:os";
|
|
1696
|
-
import { join as join8 } from "node:path";
|
|
1777
|
+
import { isAbsolute, join as join8, resolve } from "node:path";
|
|
1778
|
+
function t3codeAdapter() {
|
|
1779
|
+
return {
|
|
1780
|
+
name: "t3code",
|
|
1781
|
+
async *messages(options) {
|
|
1782
|
+
for (const location of discoverT3Databases()) {
|
|
1783
|
+
const db = await openT3Db(location.path);
|
|
1784
|
+
if (!db) {
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
yield* queryUserMessages2(db, location, options);
|
|
1789
|
+
} finally {
|
|
1790
|
+
db.close();
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
},
|
|
1794
|
+
async *usage(options) {
|
|
1795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1796
|
+
for (const location of discoverT3Databases()) {
|
|
1797
|
+
const db = await openT3Db(location.path);
|
|
1798
|
+
if (!db) {
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
try {
|
|
1802
|
+
yield* queryUsageRecords2(db, location, seen, options);
|
|
1803
|
+
} finally {
|
|
1804
|
+
db.close();
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function discoverT3Databases() {
|
|
1811
|
+
const locations = [];
|
|
1812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1813
|
+
const stateDir = stringValue6(process.env["T3CODE_STATE_DIR"]);
|
|
1814
|
+
if (stateDir) {
|
|
1815
|
+
addLocation(locations, seen, join8(resolveHomePath(stateDir), "state.sqlite"), "state");
|
|
1816
|
+
}
|
|
1817
|
+
for (const baseDir of uniqueStrings2([
|
|
1818
|
+
stringValue6(process.env["T3CODE_HOME"]),
|
|
1819
|
+
join8(homedir8(), ".t3")
|
|
1820
|
+
])) {
|
|
1821
|
+
addLocation(
|
|
1822
|
+
locations,
|
|
1823
|
+
seen,
|
|
1824
|
+
join8(resolveHomePath(baseDir), "userdata", "state.sqlite"),
|
|
1825
|
+
"userdata"
|
|
1826
|
+
);
|
|
1827
|
+
addLocation(locations, seen, join8(resolveHomePath(baseDir), "dev", "state.sqlite"), "dev");
|
|
1828
|
+
}
|
|
1829
|
+
return locations;
|
|
1830
|
+
}
|
|
1831
|
+
function addLocation(locations, seen, dbPath, scope) {
|
|
1832
|
+
if (seen.has(dbPath) || !existsSync4(dbPath)) {
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
seen.add(dbPath);
|
|
1836
|
+
locations.push({ path: dbPath, scope });
|
|
1837
|
+
}
|
|
1838
|
+
function resolveHomePath(value) {
|
|
1839
|
+
if (value === "~") {
|
|
1840
|
+
return homedir8();
|
|
1841
|
+
}
|
|
1842
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
1843
|
+
return join8(homedir8(), value.slice(2));
|
|
1844
|
+
}
|
|
1845
|
+
return isAbsolute(value) ? value : resolve(value);
|
|
1846
|
+
}
|
|
1847
|
+
async function openT3Db(dbPath) {
|
|
1848
|
+
return openReadonlySqliteDatabase(dbPath);
|
|
1849
|
+
}
|
|
1850
|
+
function* queryUserMessages2(db, location, options) {
|
|
1851
|
+
if (!hasColumns(db, "projection_thread_messages", ["thread_id", "role", "text", "created_at"])) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const orderColumn = hasColumns(db, "projection_thread_messages", ["message_id"]) ? "message_id" : "created_at";
|
|
1855
|
+
let query = `
|
|
1856
|
+
SELECT thread_id, created_at, text
|
|
1857
|
+
FROM projection_thread_messages
|
|
1858
|
+
WHERE role = 'user'
|
|
1859
|
+
`;
|
|
1860
|
+
const params = [];
|
|
1861
|
+
if (options?.since) {
|
|
1862
|
+
query += ` AND created_at >= ?`;
|
|
1863
|
+
params.push(options.since.toISOString());
|
|
1864
|
+
}
|
|
1865
|
+
query += ` ORDER BY created_at ASC, ${orderColumn} ASC`;
|
|
1866
|
+
let rows;
|
|
1867
|
+
try {
|
|
1868
|
+
rows = db.prepare(query).all(...params);
|
|
1869
|
+
} catch {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
for (const row of rows) {
|
|
1873
|
+
const text = stringValue6(row.text);
|
|
1874
|
+
if (!text) {
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
yield {
|
|
1878
|
+
text,
|
|
1879
|
+
timestamp: stringValue6(row.created_at),
|
|
1880
|
+
session: stringValue6(row.thread_id),
|
|
1881
|
+
project: location.scope
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
function* queryUsageRecords2(db, location, seen, options) {
|
|
1886
|
+
if (!hasColumns(db, "orchestration_events", [
|
|
1887
|
+
"event_id",
|
|
1888
|
+
"stream_id",
|
|
1889
|
+
"event_type",
|
|
1890
|
+
"occurred_at",
|
|
1891
|
+
"payload_json"
|
|
1892
|
+
])) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const threadInfo = readThreadInfo(db);
|
|
1896
|
+
const orderColumn = hasColumns(db, "orchestration_events", ["sequence"]) ? "sequence" : "event_id";
|
|
1897
|
+
let query = `
|
|
1898
|
+
SELECT event_id, stream_id, occurred_at, payload_json
|
|
1899
|
+
FROM orchestration_events
|
|
1900
|
+
WHERE event_type = 'thread.activity-appended'
|
|
1901
|
+
`;
|
|
1902
|
+
const params = [];
|
|
1903
|
+
if (options?.since) {
|
|
1904
|
+
query += ` AND occurred_at >= ?`;
|
|
1905
|
+
params.push(options.since.toISOString());
|
|
1906
|
+
}
|
|
1907
|
+
query += ` ORDER BY occurred_at ASC, ${orderColumn} ASC`;
|
|
1908
|
+
let rows;
|
|
1909
|
+
try {
|
|
1910
|
+
rows = db.prepare(query).all(...params);
|
|
1911
|
+
} catch {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
for (const row of rows) {
|
|
1915
|
+
const payload = asRecord6(parseJson(row.payload_json));
|
|
1916
|
+
const activity = asRecord6(payload?.["activity"]);
|
|
1917
|
+
if (activity?.["kind"] !== "context-window.updated") {
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
const usage2 = parseUsageSnapshot(activity["payload"]);
|
|
1921
|
+
if (!usage2 || !hasBillableUsage2(usage2)) {
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
const threadId = stringValue6(payload?.["threadId"]) ?? stringValue6(row.stream_id);
|
|
1925
|
+
const timestamp = stringValue6(activity["createdAt"]) ?? stringValue6(row.occurred_at);
|
|
1926
|
+
const turnId = stringValue6(activity["turnId"]);
|
|
1927
|
+
const info = threadId ? threadInfo.get(threadId) : void 0;
|
|
1928
|
+
const provider = normalizeT3Provider(info?.provider, info?.model);
|
|
1929
|
+
const dedupeKey = t3UsageDedupeKey({
|
|
1930
|
+
scope: location.scope,
|
|
1931
|
+
threadId,
|
|
1932
|
+
turnId,
|
|
1933
|
+
provider,
|
|
1934
|
+
model: info?.model,
|
|
1935
|
+
usage: usage2
|
|
1936
|
+
});
|
|
1937
|
+
if (seen.has(dedupeKey)) {
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
seen.add(dedupeKey);
|
|
1941
|
+
yield {
|
|
1942
|
+
agent: "t3code",
|
|
1943
|
+
provider,
|
|
1944
|
+
model: info?.model,
|
|
1945
|
+
timestamp,
|
|
1946
|
+
session: threadId,
|
|
1947
|
+
inputTokens: usage2.inputTokens,
|
|
1948
|
+
outputTokens: usage2.outputTokens,
|
|
1949
|
+
reasoningTokens: usage2.reasoningTokens,
|
|
1950
|
+
cacheReadTokens: usage2.cacheReadTokens,
|
|
1951
|
+
cacheWriteTokens: usage2.cacheWriteTokens
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function normalizeT3Provider(provider, model) {
|
|
1956
|
+
const normalized = provider?.trim().toLowerCase();
|
|
1957
|
+
const key = normalized?.replace(/[^a-z0-9]/g, "");
|
|
1958
|
+
switch (key) {
|
|
1959
|
+
case "codex":
|
|
1960
|
+
return "openai";
|
|
1961
|
+
case "claudeagent":
|
|
1962
|
+
case "claudecode":
|
|
1963
|
+
return "anthropic";
|
|
1964
|
+
case "cursor":
|
|
1965
|
+
case "opencode":
|
|
1966
|
+
return void 0;
|
|
1967
|
+
default:
|
|
1968
|
+
if (key?.includes("codex")) {
|
|
1969
|
+
return "openai";
|
|
1970
|
+
}
|
|
1971
|
+
if (key?.includes("claude")) {
|
|
1972
|
+
return "anthropic";
|
|
1973
|
+
}
|
|
1974
|
+
if (normalized === "openai" || normalized === "anthropic") {
|
|
1975
|
+
return normalized;
|
|
1976
|
+
}
|
|
1977
|
+
return providerFromModel(model);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function providerFromModel(model) {
|
|
1981
|
+
const slash = model?.indexOf("/") ?? -1;
|
|
1982
|
+
if (!model || slash <= 0) {
|
|
1983
|
+
return void 0;
|
|
1984
|
+
}
|
|
1985
|
+
return model.slice(0, slash);
|
|
1986
|
+
}
|
|
1987
|
+
function readThreadInfo(db) {
|
|
1988
|
+
const info = /* @__PURE__ */ new Map();
|
|
1989
|
+
readProjectionThreadModels(db, info);
|
|
1990
|
+
readProjectionThreadProviders(db, info);
|
|
1991
|
+
return info;
|
|
1992
|
+
}
|
|
1993
|
+
function readProjectionThreadModels(db, info) {
|
|
1994
|
+
if (!tableExists(db, "projection_threads")) {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const columns = tableColumns(db, "projection_threads");
|
|
1998
|
+
if (!columns.has("thread_id")) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
try {
|
|
2002
|
+
if (columns.has("model_selection_json")) {
|
|
2003
|
+
const rows = db.prepare("SELECT thread_id, model_selection_json FROM projection_threads").all();
|
|
2004
|
+
for (const row of rows) {
|
|
2005
|
+
const threadId = stringValue6(row.thread_id);
|
|
2006
|
+
const modelSelection = asRecord6(parseJson(row.model_selection_json));
|
|
2007
|
+
if (!threadId || !modelSelection) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
const entry = info.get(threadId) ?? {};
|
|
2011
|
+
entry.model = stringValue6(modelSelection["model"]) ?? entry.model;
|
|
2012
|
+
entry.provider = stringValue6(modelSelection["provider"]) ?? stringValue6(modelSelection["instanceId"]) ?? entry.provider;
|
|
2013
|
+
info.set(threadId, entry);
|
|
2014
|
+
}
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
if (columns.has("model")) {
|
|
2018
|
+
const rows = db.prepare("SELECT thread_id, model FROM projection_threads").all();
|
|
2019
|
+
for (const row of rows) {
|
|
2020
|
+
const threadId = stringValue6(row.thread_id);
|
|
2021
|
+
const model = stringValue6(row.model);
|
|
2022
|
+
if (threadId && model) {
|
|
2023
|
+
info.set(threadId, { ...info.get(threadId), model });
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
} catch {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function readProjectionThreadProviders(db, info) {
|
|
2032
|
+
if (!hasColumns(db, "projection_thread_sessions", ["thread_id", "provider_name"])) {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
try {
|
|
2036
|
+
const rows = db.prepare("SELECT thread_id, provider_name FROM projection_thread_sessions").all();
|
|
2037
|
+
for (const row of rows) {
|
|
2038
|
+
const threadId = stringValue6(row.thread_id);
|
|
2039
|
+
const provider = stringValue6(row.provider_name);
|
|
2040
|
+
if (!threadId || !provider) {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
info.set(threadId, { ...info.get(threadId), provider });
|
|
2044
|
+
}
|
|
2045
|
+
} catch {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
function parseUsageSnapshot(value) {
|
|
2050
|
+
const usage2 = asRecord6(value);
|
|
2051
|
+
if (!usage2) {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
const lastInputTokens = tokenValue(usage2["lastInputTokens"] ?? usage2["last_input_tokens"]);
|
|
2055
|
+
const lastCachedInputTokens = tokenValue(
|
|
2056
|
+
usage2["lastCachedInputTokens"] ?? usage2["last_cached_input_tokens"]
|
|
2057
|
+
);
|
|
2058
|
+
const lastOutputTokens = tokenValue(usage2["lastOutputTokens"] ?? usage2["last_output_tokens"]);
|
|
2059
|
+
const lastReasoningOutputTokens = tokenValue(
|
|
2060
|
+
usage2["lastReasoningOutputTokens"] ?? usage2["last_reasoning_output_tokens"]
|
|
2061
|
+
);
|
|
2062
|
+
const hasLastDetails = [
|
|
2063
|
+
lastInputTokens,
|
|
2064
|
+
lastCachedInputTokens,
|
|
2065
|
+
lastOutputTokens,
|
|
2066
|
+
lastReasoningOutputTokens
|
|
2067
|
+
].some((token) => token !== void 0);
|
|
2068
|
+
if (hasLastDetails) {
|
|
2069
|
+
return splitTokenUsage({
|
|
2070
|
+
inputTokens: lastInputTokens ?? 0,
|
|
2071
|
+
cachedInputTokens: lastCachedInputTokens ?? 0,
|
|
2072
|
+
outputTokens: lastOutputTokens ?? 0,
|
|
2073
|
+
reasoningOutputTokens: lastReasoningOutputTokens ?? 0
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
const inputTokens = tokenValue(usage2["inputTokens"] ?? usage2["input_tokens"]);
|
|
2077
|
+
const cachedInputTokens = tokenValue(usage2["cachedInputTokens"] ?? usage2["cached_input_tokens"]);
|
|
2078
|
+
const outputTokens = tokenValue(usage2["outputTokens"] ?? usage2["output_tokens"]);
|
|
2079
|
+
const reasoningOutputTokens = tokenValue(
|
|
2080
|
+
usage2["reasoningOutputTokens"] ?? usage2["reasoning_output_tokens"]
|
|
2081
|
+
);
|
|
2082
|
+
const hasSnapshotDetails = [
|
|
2083
|
+
inputTokens,
|
|
2084
|
+
cachedInputTokens,
|
|
2085
|
+
outputTokens,
|
|
2086
|
+
reasoningOutputTokens
|
|
2087
|
+
].some((token) => token !== void 0);
|
|
2088
|
+
if (!hasSnapshotDetails) {
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
return splitTokenUsage({
|
|
2092
|
+
inputTokens: inputTokens ?? 0,
|
|
2093
|
+
cachedInputTokens: cachedInputTokens ?? 0,
|
|
2094
|
+
outputTokens: outputTokens ?? 0,
|
|
2095
|
+
reasoningOutputTokens: reasoningOutputTokens ?? 0
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
function splitTokenUsage(input) {
|
|
2099
|
+
const reasoningTokens = Math.min(input.reasoningOutputTokens, input.outputTokens);
|
|
2100
|
+
return {
|
|
2101
|
+
inputTokens: Math.max(input.inputTokens - input.cachedInputTokens, 0),
|
|
2102
|
+
outputTokens: Math.max(input.outputTokens - reasoningTokens, 0),
|
|
2103
|
+
reasoningTokens,
|
|
2104
|
+
cacheReadTokens: input.cachedInputTokens,
|
|
2105
|
+
cacheWriteTokens: 0
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
function hasBillableUsage2(usage2) {
|
|
2109
|
+
return usage2.inputTokens + usage2.outputTokens + usage2.reasoningTokens + usage2.cacheReadTokens + usage2.cacheWriteTokens > 0;
|
|
2110
|
+
}
|
|
2111
|
+
function t3UsageDedupeKey(input) {
|
|
2112
|
+
return JSON.stringify([
|
|
2113
|
+
input.scope,
|
|
2114
|
+
input.threadId ?? "",
|
|
2115
|
+
input.turnId ?? "",
|
|
2116
|
+
input.provider ?? "",
|
|
2117
|
+
input.model ?? "",
|
|
2118
|
+
input.usage.inputTokens,
|
|
2119
|
+
input.usage.outputTokens,
|
|
2120
|
+
input.usage.reasoningTokens,
|
|
2121
|
+
input.usage.cacheReadTokens,
|
|
2122
|
+
input.usage.cacheWriteTokens
|
|
2123
|
+
]);
|
|
2124
|
+
}
|
|
2125
|
+
function hasColumns(db, table, requiredColumns) {
|
|
2126
|
+
const columns = tableColumns(db, table);
|
|
2127
|
+
return requiredColumns.every((column) => columns.has(column));
|
|
2128
|
+
}
|
|
2129
|
+
function tableExists(db, table) {
|
|
2130
|
+
try {
|
|
2131
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(table);
|
|
2132
|
+
return Boolean(row);
|
|
2133
|
+
} catch {
|
|
2134
|
+
return false;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
function tableColumns(db, table) {
|
|
2138
|
+
if (!tableExists(db, table)) {
|
|
2139
|
+
return /* @__PURE__ */ new Set();
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
const rows = db.prepare(`PRAGMA table_info("${table}")`).all();
|
|
2143
|
+
return new Set(rows.flatMap((row) => stringValue6(row.name) ?? []));
|
|
2144
|
+
} catch {
|
|
2145
|
+
return /* @__PURE__ */ new Set();
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function parseJson(value) {
|
|
2149
|
+
if (typeof value !== "string") {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
try {
|
|
2153
|
+
return JSON.parse(value);
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
function tokenValue(value) {
|
|
2159
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2160
|
+
return void 0;
|
|
2161
|
+
}
|
|
2162
|
+
return Math.max(Math.round(value), 0);
|
|
2163
|
+
}
|
|
2164
|
+
function uniqueStrings2(values) {
|
|
2165
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2166
|
+
const unique = [];
|
|
2167
|
+
for (const value of values) {
|
|
2168
|
+
if (!value || seen.has(value)) {
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
seen.add(value);
|
|
2172
|
+
unique.push(value);
|
|
2173
|
+
}
|
|
2174
|
+
return unique;
|
|
2175
|
+
}
|
|
2176
|
+
function stringValue6(value) {
|
|
2177
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
2178
|
+
}
|
|
2179
|
+
function asRecord6(value) {
|
|
2180
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
return value;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/adapters/zed.ts
|
|
2187
|
+
import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
|
|
2188
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2189
|
+
import { homedir as homedir9 } from "node:os";
|
|
2190
|
+
import { join as join9 } from "node:path";
|
|
1697
2191
|
function getZedPaths() {
|
|
1698
2192
|
if (process.platform === "darwin") {
|
|
1699
|
-
const base2 =
|
|
2193
|
+
const base2 = join9(homedir9(), "Library", "Application Support", "Zed");
|
|
1700
2194
|
return {
|
|
1701
|
-
conversations:
|
|
1702
|
-
db:
|
|
2195
|
+
conversations: join9(base2, "conversations"),
|
|
2196
|
+
db: join9(base2, "db")
|
|
1703
2197
|
};
|
|
1704
2198
|
}
|
|
1705
|
-
const base =
|
|
2199
|
+
const base = join9(process.env["XDG_DATA_HOME"] ?? join9(homedir9(), ".local", "share"), "zed");
|
|
1706
2200
|
return {
|
|
1707
|
-
conversations:
|
|
1708
|
-
db:
|
|
2201
|
+
conversations: join9(base, "conversations"),
|
|
2202
|
+
db: join9(base, "db")
|
|
1709
2203
|
};
|
|
1710
2204
|
}
|
|
1711
2205
|
function zedAdapter() {
|
|
@@ -1719,7 +2213,7 @@ function zedAdapter() {
|
|
|
1719
2213
|
};
|
|
1720
2214
|
}
|
|
1721
2215
|
async function* parseTextThreads(dir, _options) {
|
|
1722
|
-
if (!
|
|
2216
|
+
if (!existsSync5(dir)) {
|
|
1723
2217
|
return;
|
|
1724
2218
|
}
|
|
1725
2219
|
let files;
|
|
@@ -1730,7 +2224,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
1730
2224
|
}
|
|
1731
2225
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
1732
2226
|
for (const file of jsonFiles) {
|
|
1733
|
-
const filePath =
|
|
2227
|
+
const filePath = join9(dir, file);
|
|
1734
2228
|
const session = file.replace(".json", "");
|
|
1735
2229
|
try {
|
|
1736
2230
|
const raw = await readFile3(filePath, "utf-8");
|
|
@@ -1756,7 +2250,7 @@ async function* parseTextThreads(dir, _options) {
|
|
|
1756
2250
|
}
|
|
1757
2251
|
}
|
|
1758
2252
|
async function* parseAgentThreads(dbDir, _options) {
|
|
1759
|
-
if (!
|
|
2253
|
+
if (!existsSync5(dbDir)) {
|
|
1760
2254
|
return;
|
|
1761
2255
|
}
|
|
1762
2256
|
let dbFiles;
|
|
@@ -1769,21 +2263,10 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
1769
2263
|
if (dbFiles.length === 0) {
|
|
1770
2264
|
return;
|
|
1771
2265
|
}
|
|
1772
|
-
let Database;
|
|
1773
|
-
try {
|
|
1774
|
-
const mod = await import("better-sqlite3");
|
|
1775
|
-
Database = mod.default ?? mod;
|
|
1776
|
-
} catch {
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
2266
|
for (const dbFile of dbFiles) {
|
|
1780
|
-
const dbPath =
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
db = new Database(dbPath, {
|
|
1784
|
-
readonly: true
|
|
1785
|
-
});
|
|
1786
|
-
} catch {
|
|
2267
|
+
const dbPath = join9(dbDir, dbFile);
|
|
2268
|
+
const db = await openReadonlySqliteDatabase(dbPath);
|
|
2269
|
+
if (!db) {
|
|
1787
2270
|
continue;
|
|
1788
2271
|
}
|
|
1789
2272
|
try {
|
|
@@ -1793,14 +2276,12 @@ async function* parseAgentThreads(dbDir, _options) {
|
|
|
1793
2276
|
(t) => t === "messages" || t === "thread_messages" || t.includes("message")
|
|
1794
2277
|
);
|
|
1795
2278
|
if (!msgTable) {
|
|
1796
|
-
db.close();
|
|
1797
2279
|
continue;
|
|
1798
2280
|
}
|
|
1799
2281
|
const columns = db.prepare(`PRAGMA table_info("${msgTable}")`).all();
|
|
1800
2282
|
const colNames = columns.map((c2) => c2.name);
|
|
1801
2283
|
const hasRole = colNames.includes("role");
|
|
1802
2284
|
if (!hasRole) {
|
|
1803
|
-
db.close();
|
|
1804
2285
|
continue;
|
|
1805
2286
|
}
|
|
1806
2287
|
const contentCol = colNames.includes("content") ? "content" : colNames.includes("body") ? "body" : "text";
|
|
@@ -1828,6 +2309,7 @@ var ADAPTERS = {
|
|
|
1828
2309
|
amp: ampAdapter,
|
|
1829
2310
|
cline: clineAdapter,
|
|
1830
2311
|
pi: piAdapter,
|
|
2312
|
+
t3code: t3codeAdapter,
|
|
1831
2313
|
zed: zedAdapter
|
|
1832
2314
|
};
|
|
1833
2315
|
function createAdapter(name) {
|
|
@@ -1997,8 +2479,8 @@ function runPattern(_originalText, searchText, matches, seen) {
|
|
|
1997
2479
|
|
|
1998
2480
|
// src/pricing/index.ts
|
|
1999
2481
|
import { mkdir, readFile as readFile4, writeFile } from "node:fs/promises";
|
|
2000
|
-
import { homedir as
|
|
2001
|
-
import { dirname, join as
|
|
2482
|
+
import { homedir as homedir10 } from "node:os";
|
|
2483
|
+
import { dirname, join as join10 } from "node:path";
|
|
2002
2484
|
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
2003
2485
|
var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2004
2486
|
var FETCH_TIMEOUT_MS = 2e3;
|
|
@@ -2098,16 +2580,16 @@ async function summarizeUsage(records, pricing) {
|
|
|
2098
2580
|
}
|
|
2099
2581
|
function getPricingCachePath() {
|
|
2100
2582
|
if (process.env["XDG_CACHE_HOME"]) {
|
|
2101
|
-
return
|
|
2583
|
+
return join10(process.env["XDG_CACHE_HOME"], "devrage", "models.dev.json");
|
|
2102
2584
|
}
|
|
2103
2585
|
if (process.platform === "darwin") {
|
|
2104
|
-
return
|
|
2586
|
+
return join10(homedir10(), "Library", "Caches", "devrage", "models.dev.json");
|
|
2105
2587
|
}
|
|
2106
2588
|
if (process.platform === "win32") {
|
|
2107
|
-
const localAppData = process.env["LOCALAPPDATA"] ??
|
|
2108
|
-
return
|
|
2589
|
+
const localAppData = process.env["LOCALAPPDATA"] ?? join10(homedir10(), "AppData", "Local");
|
|
2590
|
+
return join10(localAppData, "devrage", "models.dev.json");
|
|
2109
2591
|
}
|
|
2110
|
-
return
|
|
2592
|
+
return join10(homedir10(), ".cache", "devrage", "models.dev.json");
|
|
2111
2593
|
}
|
|
2112
2594
|
function createCostAccumulator() {
|
|
2113
2595
|
return {
|
|
@@ -2216,7 +2698,7 @@ async function readPricingCache(cachePath) {
|
|
|
2216
2698
|
try {
|
|
2217
2699
|
const raw = await readFile4(cachePath, "utf-8");
|
|
2218
2700
|
const parsed = JSON.parse(raw);
|
|
2219
|
-
const cache =
|
|
2701
|
+
const cache = asRecord7(parsed);
|
|
2220
2702
|
if (cache?.["source"] !== "models.dev" || cache["schemaVersion"] !== 1 || typeof cache["fetchedAt"] !== "string" || !isModelsDevCatalog(cache["catalog"])) {
|
|
2221
2703
|
return null;
|
|
2222
2704
|
}
|
|
@@ -2357,10 +2839,10 @@ function inferProvider(model) {
|
|
|
2357
2839
|
return void 0;
|
|
2358
2840
|
}
|
|
2359
2841
|
function getCatalogRates(catalog, provider, model) {
|
|
2360
|
-
const root =
|
|
2361
|
-
const providerEntry =
|
|
2362
|
-
const models =
|
|
2363
|
-
const modelEntry =
|
|
2842
|
+
const root = asRecord7(catalog);
|
|
2843
|
+
const providerEntry = asRecord7(root?.[provider]);
|
|
2844
|
+
const models = asRecord7(providerEntry?.["models"]);
|
|
2845
|
+
const modelEntry = asRecord7(models?.[model]);
|
|
2364
2846
|
return toRateTable(modelEntry?.["cost"]);
|
|
2365
2847
|
}
|
|
2366
2848
|
function selectContextRates(rates, record) {
|
|
@@ -2368,8 +2850,8 @@ function selectContextRates(rates, record) {
|
|
|
2368
2850
|
let selected = rates;
|
|
2369
2851
|
let selectedSize = 0;
|
|
2370
2852
|
for (const tier of rates.tiers ?? []) {
|
|
2371
|
-
const tierRecord =
|
|
2372
|
-
const tierInfo =
|
|
2853
|
+
const tierRecord = asRecord7(tier);
|
|
2854
|
+
const tierInfo = asRecord7(tierRecord?.["tier"]);
|
|
2373
2855
|
const size = typeof tierInfo?.["size"] === "number" ? tierInfo["size"] : 0;
|
|
2374
2856
|
if (tierInfo?.["type"] !== "context" || contextTokens < size || size < selectedSize) {
|
|
2375
2857
|
continue;
|
|
@@ -2386,7 +2868,7 @@ function selectContextRates(rates, record) {
|
|
|
2386
2868
|
return selected;
|
|
2387
2869
|
}
|
|
2388
2870
|
function toRateTable(value) {
|
|
2389
|
-
const record =
|
|
2871
|
+
const record = asRecord7(value);
|
|
2390
2872
|
if (!record) {
|
|
2391
2873
|
return null;
|
|
2392
2874
|
}
|
|
@@ -2420,10 +2902,10 @@ function numberValue6(value) {
|
|
|
2420
2902
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2421
2903
|
}
|
|
2422
2904
|
function isModelsDevCatalog(value) {
|
|
2423
|
-
const catalog =
|
|
2424
|
-
const openai =
|
|
2425
|
-
const anthropic =
|
|
2426
|
-
return Boolean(
|
|
2905
|
+
const catalog = asRecord7(value);
|
|
2906
|
+
const openai = asRecord7(catalog?.["openai"]);
|
|
2907
|
+
const anthropic = asRecord7(catalog?.["anthropic"]);
|
|
2908
|
+
return Boolean(asRecord7(openai?.["models"]) || asRecord7(anthropic?.["models"]));
|
|
2427
2909
|
}
|
|
2428
2910
|
function isFresh(fetchedAt, ttlMs) {
|
|
2429
2911
|
const fetchedTime = new Date(fetchedAt).getTime();
|
|
@@ -2432,7 +2914,7 @@ function isFresh(fetchedAt, ttlMs) {
|
|
|
2432
2914
|
function mergePricingSource(left, right) {
|
|
2433
2915
|
return left === right ? left : "mixed";
|
|
2434
2916
|
}
|
|
2435
|
-
function
|
|
2917
|
+
function asRecord7(value) {
|
|
2436
2918
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2437
2919
|
return null;
|
|
2438
2920
|
}
|
|
@@ -2537,7 +3019,7 @@ function parseArgs(args) {
|
|
|
2537
3019
|
console.log(`devrage scan \u2014 scan sessions for profanity
|
|
2538
3020
|
|
|
2539
3021
|
Options:
|
|
2540
|
-
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, zed)
|
|
3022
|
+
--agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, t3code, zed)
|
|
2541
3023
|
--since, -s <date> Only scan messages after this date (ISO 8601)
|
|
2542
3024
|
--day, --days [n] Only scan the last n days (default: 1)
|
|
2543
3025
|
--week Only scan the last 7 days
|
|
@@ -2578,7 +3060,7 @@ Usage:
|
|
|
2578
3060
|
devrage cost [options]
|
|
2579
3061
|
|
|
2580
3062
|
Options:
|
|
2581
|
-
--agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi)
|
|
3063
|
+
--agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi, t3code)
|
|
2582
3064
|
--refresh-prices Refresh models.dev pricing before estimating cost
|
|
2583
3065
|
--since, -s <date> Only include usage after this date (ISO 8601)
|
|
2584
3066
|
--day, --days [n] Only include the last n days (default: 1)
|
|
@@ -2792,7 +3274,7 @@ function printCostCommandUnavailable(options) {
|
|
|
2792
3274
|
}
|
|
2793
3275
|
async function writeCostHtmlReport(totals, options) {
|
|
2794
3276
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2795
|
-
const reportPath =
|
|
3277
|
+
const reportPath = join11(
|
|
2796
3278
|
dirname2(getPricingCachePath()),
|
|
2797
3279
|
`cost-report-${safeTimestamp(generatedAt)}.html`
|
|
2798
3280
|
);
|
|
@@ -3245,7 +3727,7 @@ async function main() {
|
|
|
3245
3727
|
process.exit(0);
|
|
3246
3728
|
}
|
|
3247
3729
|
if (command === "--version") {
|
|
3248
|
-
console.log("0.5.
|
|
3730
|
+
console.log("0.5.7");
|
|
3249
3731
|
process.exit(0);
|
|
3250
3732
|
}
|
|
3251
3733
|
const parsed = parseCommand(args);
|