@hasna/economy 0.2.25 → 0.2.27
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/index.js +377 -25
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +382 -31
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/lib/peer-sync.d.ts +21 -0
- package/dist/lib/peer-sync.d.ts.map +1 -0
- package/dist/mcp/index.js +71 -14
- package/dist/server/index.js +71 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1037,9 +1037,7 @@ function queryAgentBreakdown(db, period = "all") {
|
|
|
1037
1037
|
}
|
|
1038
1038
|
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1039
1039
|
}
|
|
1040
|
-
function
|
|
1041
|
-
if (projectName && projectName.trim() !== "")
|
|
1042
|
-
return projectName;
|
|
1040
|
+
function pathProjectLabel(projectPath) {
|
|
1043
1041
|
if (!projectPath)
|
|
1044
1042
|
return "";
|
|
1045
1043
|
const segments = projectPath.split("/").filter(Boolean);
|
|
@@ -1048,12 +1046,45 @@ function labelForPath(projectPath, projectName) {
|
|
|
1048
1046
|
if (projectPrefix.test(seg))
|
|
1049
1047
|
return seg;
|
|
1050
1048
|
}
|
|
1051
|
-
const generic = new Set([
|
|
1049
|
+
const generic = new Set([
|
|
1050
|
+
"web",
|
|
1051
|
+
"app",
|
|
1052
|
+
"apps",
|
|
1053
|
+
"packages",
|
|
1054
|
+
"src",
|
|
1055
|
+
"lib",
|
|
1056
|
+
"server",
|
|
1057
|
+
"client",
|
|
1058
|
+
"api",
|
|
1059
|
+
"frontend",
|
|
1060
|
+
"backend",
|
|
1061
|
+
"home",
|
|
1062
|
+
"users",
|
|
1063
|
+
"workspace",
|
|
1064
|
+
"workspaces",
|
|
1065
|
+
"hasna"
|
|
1066
|
+
]);
|
|
1052
1067
|
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1053
1068
|
if (!generic.has(segments[i].toLowerCase()))
|
|
1054
1069
|
return segments[i];
|
|
1055
1070
|
}
|
|
1056
|
-
return
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
function isRepoLikeLabel(label) {
|
|
1074
|
+
return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
|
|
1075
|
+
}
|
|
1076
|
+
function labelForPath(projectPath, projectName) {
|
|
1077
|
+
const pathLabel = pathProjectLabel(projectPath);
|
|
1078
|
+
if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
|
|
1079
|
+
return pathLabel;
|
|
1080
|
+
if (projectName && projectName.trim() !== "")
|
|
1081
|
+
return projectName;
|
|
1082
|
+
if (pathLabel)
|
|
1083
|
+
return pathLabel;
|
|
1084
|
+
return projectPath;
|
|
1085
|
+
}
|
|
1086
|
+
function groupKeyForPath(projectPath, projectName) {
|
|
1087
|
+
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1057
1088
|
}
|
|
1058
1089
|
function queryProjectBreakdown(db, period = "all") {
|
|
1059
1090
|
const requestWhere = requestPeriodWhere(period);
|
|
@@ -1068,14 +1099,15 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1068
1099
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1069
1100
|
if (!label)
|
|
1070
1101
|
continue;
|
|
1071
|
-
const
|
|
1102
|
+
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1103
|
+
const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
|
|
1072
1104
|
g.sessionIds.push(s.id);
|
|
1073
1105
|
if (!g.samplePath)
|
|
1074
1106
|
g.samplePath = s.project_path;
|
|
1075
|
-
groups.set(
|
|
1107
|
+
groups.set(key, g);
|
|
1076
1108
|
}
|
|
1077
1109
|
const result = [];
|
|
1078
|
-
for (const
|
|
1110
|
+
for (const g of groups.values()) {
|
|
1079
1111
|
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1080
1112
|
const reqStats = placeholders.length ? db.prepare(`
|
|
1081
1113
|
SELECT
|
|
@@ -1106,7 +1138,7 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1106
1138
|
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1107
1139
|
result.push({
|
|
1108
1140
|
project_path: g.samplePath,
|
|
1109
|
-
project_name: label,
|
|
1141
|
+
project_name: g.label,
|
|
1110
1142
|
sessions: totalSessions,
|
|
1111
1143
|
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1112
1144
|
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
@@ -1438,17 +1470,21 @@ function listMachineRegistry(db) {
|
|
|
1438
1470
|
}
|
|
1439
1471
|
function dedupeRequests(db) {
|
|
1440
1472
|
const dupes = db.prepare(`
|
|
1441
|
-
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1473
|
+
SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1442
1474
|
FROM requests
|
|
1443
1475
|
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1444
|
-
GROUP BY source_request_id, agent
|
|
1476
|
+
GROUP BY source_request_id, agent, COALESCE(machine_id, '')
|
|
1445
1477
|
HAVING cnt > 1
|
|
1446
1478
|
`).all();
|
|
1447
1479
|
let removed = 0;
|
|
1448
1480
|
for (const row of dupes) {
|
|
1449
1481
|
const result = db.prepare(`
|
|
1450
|
-
DELETE FROM requests
|
|
1451
|
-
|
|
1482
|
+
DELETE FROM requests
|
|
1483
|
+
WHERE source_request_id = ?
|
|
1484
|
+
AND agent = ?
|
|
1485
|
+
AND COALESCE(machine_id, '') = ?
|
|
1486
|
+
AND id != ?
|
|
1487
|
+
`).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
|
|
1452
1488
|
removed += result.changes;
|
|
1453
1489
|
}
|
|
1454
1490
|
return removed;
|
|
@@ -1731,10 +1767,303 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
|
1731
1767
|
}
|
|
1732
1768
|
return { imported, skipped };
|
|
1733
1769
|
}
|
|
1770
|
+
// src/lib/peer-sync.ts
|
|
1771
|
+
init_database();
|
|
1772
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
1773
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1774
|
+
|
|
1775
|
+
// src/lib/package-metadata.ts
|
|
1776
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1777
|
+
var cachedMetadata = null;
|
|
1778
|
+
function getPackageMetadata() {
|
|
1779
|
+
if (cachedMetadata)
|
|
1780
|
+
return cachedMetadata;
|
|
1781
|
+
const raw = readFileSync2(new URL("../../package.json", import.meta.url), "utf8");
|
|
1782
|
+
const parsed = JSON.parse(raw);
|
|
1783
|
+
cachedMetadata = {
|
|
1784
|
+
name: parsed.name ?? "@hasna/economy",
|
|
1785
|
+
version: parsed.version ?? "0.0.0"
|
|
1786
|
+
};
|
|
1787
|
+
return cachedMetadata;
|
|
1788
|
+
}
|
|
1789
|
+
var packageMetadata = getPackageMetadata();
|
|
1790
|
+
|
|
1791
|
+
// src/lib/peer-sync.ts
|
|
1792
|
+
var GENERIC_PEER_TABLES = [
|
|
1793
|
+
"usage_snapshots",
|
|
1794
|
+
"subscriptions",
|
|
1795
|
+
"billing_daily",
|
|
1796
|
+
"savings_daily",
|
|
1797
|
+
"budgets",
|
|
1798
|
+
"goals",
|
|
1799
|
+
"model_pricing",
|
|
1800
|
+
"machines"
|
|
1801
|
+
];
|
|
1802
|
+
function quoteIdent(identifier) {
|
|
1803
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1804
|
+
}
|
|
1805
|
+
function tableExists(db, table) {
|
|
1806
|
+
const row = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table);
|
|
1807
|
+
return Boolean(row);
|
|
1808
|
+
}
|
|
1809
|
+
function tableColumns(db, table) {
|
|
1810
|
+
if (!tableExists(db, table))
|
|
1811
|
+
return [];
|
|
1812
|
+
return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
1813
|
+
}
|
|
1814
|
+
function commonColumns(source, target, table) {
|
|
1815
|
+
const sourceCols = new Set(tableColumns(source, table).map((c) => c.name));
|
|
1816
|
+
return tableColumns(target, table).map((c) => c.name).filter((c) => sourceCols.has(c));
|
|
1817
|
+
}
|
|
1818
|
+
function primaryKeyColumns(db, table) {
|
|
1819
|
+
return tableColumns(db, table).filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
1820
|
+
}
|
|
1821
|
+
function selectRows(source, table, columns) {
|
|
1822
|
+
if (columns.length === 0)
|
|
1823
|
+
return [];
|
|
1824
|
+
const select = columns.map(quoteIdent).join(", ");
|
|
1825
|
+
return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
|
|
1826
|
+
}
|
|
1827
|
+
function rowByKey(target, table, keyColumns, row) {
|
|
1828
|
+
if (keyColumns.length === 0)
|
|
1829
|
+
return null;
|
|
1830
|
+
if (keyColumns.some((c) => row[c] == null))
|
|
1831
|
+
return null;
|
|
1832
|
+
const where = keyColumns.map((c) => `${quoteIdent(c)} = ?`).join(" AND ");
|
|
1833
|
+
return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
|
|
1834
|
+
}
|
|
1835
|
+
function hasId(target, table, id) {
|
|
1836
|
+
return target.prepare(`SELECT id, machine_id FROM ${quoteIdent(table)} WHERE id = ?`).get(id);
|
|
1837
|
+
}
|
|
1838
|
+
function shouldReplace(source, existing) {
|
|
1839
|
+
if (!existing)
|
|
1840
|
+
return true;
|
|
1841
|
+
const sourceUpdated = source["updated_at"];
|
|
1842
|
+
const existingUpdated = existing["updated_at"];
|
|
1843
|
+
if (typeof sourceUpdated === "string" && typeof existingUpdated === "string" && existingUpdated !== "") {
|
|
1844
|
+
return sourceUpdated >= existingUpdated;
|
|
1845
|
+
}
|
|
1846
|
+
return true;
|
|
1847
|
+
}
|
|
1848
|
+
function normalizeRow(row, columns, sourceMachine, now) {
|
|
1849
|
+
const next = { ...row };
|
|
1850
|
+
if (columns.includes("machine_id") && (!next["machine_id"] || next["machine_id"] === "")) {
|
|
1851
|
+
next["machine_id"] = sourceMachine;
|
|
1852
|
+
}
|
|
1853
|
+
if (columns.includes("updated_at") && (!next["updated_at"] || next["updated_at"] === "")) {
|
|
1854
|
+
next["updated_at"] = next["timestamp"] ?? next["started_at"] ?? next["created_at"] ?? now;
|
|
1855
|
+
}
|
|
1856
|
+
if (columns.includes("synced_at") && next["synced_at"] == null)
|
|
1857
|
+
next["synced_at"] = "";
|
|
1858
|
+
if (columns.includes("attribution_tag") && next["attribution_tag"] == null)
|
|
1859
|
+
next["attribution_tag"] = "";
|
|
1860
|
+
return next;
|
|
1861
|
+
}
|
|
1862
|
+
function insertOrReplace(target, table, columns, row) {
|
|
1863
|
+
const colSql = columns.map(quoteIdent).join(", ");
|
|
1864
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
1865
|
+
target.prepare(`
|
|
1866
|
+
INSERT OR REPLACE INTO ${quoteIdent(table)} (${colSql})
|
|
1867
|
+
VALUES (${placeholders})
|
|
1868
|
+
`).run(...columns.map((c) => row[c] ?? null));
|
|
1869
|
+
}
|
|
1870
|
+
function collisionId(target, table, machine, originalId) {
|
|
1871
|
+
const base = `${machine || "peer"}:${originalId}`;
|
|
1872
|
+
const baseRow = hasId(target, table, base);
|
|
1873
|
+
if (!baseRow || String(baseRow["machine_id"] ?? "") === machine)
|
|
1874
|
+
return base;
|
|
1875
|
+
for (let i = 2;; i++) {
|
|
1876
|
+
const candidate = `${base}:${i}`;
|
|
1877
|
+
const row = hasId(target, table, candidate);
|
|
1878
|
+
if (!row || String(row["machine_id"] ?? "") === machine)
|
|
1879
|
+
return candidate;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function mergeIdentityTable(target, source, table, sourceMachine, now, sessionIdMap) {
|
|
1883
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
1884
|
+
const columns = commonColumns(source, target, table);
|
|
1885
|
+
const rows = selectRows(source, table, columns);
|
|
1886
|
+
const idMap = new Map;
|
|
1887
|
+
for (const raw of rows) {
|
|
1888
|
+
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
1889
|
+
const originalId = String(row["id"] ?? "");
|
|
1890
|
+
if (!originalId) {
|
|
1891
|
+
stats.skipped++;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
const machine = String(row["machine_id"] ?? "");
|
|
1895
|
+
const directExisting = hasId(target, table, originalId);
|
|
1896
|
+
if (directExisting && String(directExisting["machine_id"] ?? "") !== machine) {
|
|
1897
|
+
row["id"] = collisionId(target, table, machine, originalId);
|
|
1898
|
+
stats.collisions++;
|
|
1899
|
+
}
|
|
1900
|
+
if (table === "requests" && sessionIdMap) {
|
|
1901
|
+
const originalSessionId = String(row["session_id"] ?? "");
|
|
1902
|
+
row["session_id"] = sessionIdMap.get(originalSessionId) ?? originalSessionId;
|
|
1903
|
+
}
|
|
1904
|
+
const existing = hasId(target, table, String(row["id"]));
|
|
1905
|
+
idMap.set(originalId, String(row["id"]));
|
|
1906
|
+
if (existing && !shouldReplace(row, existing)) {
|
|
1907
|
+
stats.skipped++;
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
insertOrReplace(target, table, columns, row);
|
|
1911
|
+
if (existing)
|
|
1912
|
+
stats.updated++;
|
|
1913
|
+
else
|
|
1914
|
+
stats.inserted++;
|
|
1915
|
+
}
|
|
1916
|
+
return { stats, idMap };
|
|
1917
|
+
}
|
|
1918
|
+
function mergeProjects(target, source) {
|
|
1919
|
+
const table = "projects";
|
|
1920
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
1921
|
+
const columns = commonColumns(source, target, table);
|
|
1922
|
+
const rows = selectRows(source, table, columns);
|
|
1923
|
+
for (const raw of rows) {
|
|
1924
|
+
const row = { ...raw };
|
|
1925
|
+
const path = String(row["path"] ?? "");
|
|
1926
|
+
const id = String(row["id"] ?? "");
|
|
1927
|
+
if (!path || !id) {
|
|
1928
|
+
stats.skipped++;
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const existingByPath = target.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
1932
|
+
if (existingByPath) {
|
|
1933
|
+
row["id"] = existingByPath["id"] ?? id;
|
|
1934
|
+
insertOrReplace(target, table, columns, row);
|
|
1935
|
+
stats.updated++;
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
const existingById = target.prepare(`SELECT * FROM projects WHERE id = ?`).get(id);
|
|
1939
|
+
if (existingById && String(existingById["path"] ?? "") !== path) {
|
|
1940
|
+
row["id"] = `peer:${id}`;
|
|
1941
|
+
stats.collisions++;
|
|
1942
|
+
while (target.prepare(`SELECT id FROM projects WHERE id = ?`).get(row["id"])) {
|
|
1943
|
+
row["id"] = `peer:${String(row["id"])}`;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
insertOrReplace(target, table, columns, row);
|
|
1947
|
+
stats.inserted++;
|
|
1948
|
+
}
|
|
1949
|
+
return stats;
|
|
1950
|
+
}
|
|
1951
|
+
function mergeGenericTable(target, source, table, sourceMachine, now) {
|
|
1952
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
1953
|
+
const columns = commonColumns(source, target, table);
|
|
1954
|
+
const keyColumns = primaryKeyColumns(target, table).filter((c) => columns.includes(c));
|
|
1955
|
+
const rows = selectRows(source, table, columns);
|
|
1956
|
+
for (const raw of rows) {
|
|
1957
|
+
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
1958
|
+
const existing = rowByKey(target, table, keyColumns, row);
|
|
1959
|
+
if (existing && !shouldReplace(row, existing)) {
|
|
1960
|
+
stats.skipped++;
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
insertOrReplace(target, table, columns, row);
|
|
1964
|
+
if (existing)
|
|
1965
|
+
stats.updated++;
|
|
1966
|
+
else
|
|
1967
|
+
stats.inserted++;
|
|
1968
|
+
}
|
|
1969
|
+
return stats;
|
|
1970
|
+
}
|
|
1971
|
+
function detectSourceMachine(source, fallback) {
|
|
1972
|
+
if (fallback && fallback.trim())
|
|
1973
|
+
return fallback.trim();
|
|
1974
|
+
const counts = new Map;
|
|
1975
|
+
for (const table of ["sessions", "requests", "usage_snapshots"]) {
|
|
1976
|
+
if (!tableExists(source, table))
|
|
1977
|
+
continue;
|
|
1978
|
+
const rows = source.prepare(`
|
|
1979
|
+
SELECT machine_id, COUNT(*) as cnt
|
|
1980
|
+
FROM ${quoteIdent(table)}
|
|
1981
|
+
WHERE machine_id != '' AND machine_id IS NOT NULL
|
|
1982
|
+
GROUP BY machine_id
|
|
1983
|
+
`).all();
|
|
1984
|
+
for (const row of rows) {
|
|
1985
|
+
counts.set(row.machine_id, (counts.get(row.machine_id) ?? 0) + row.cnt);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
let best = "";
|
|
1989
|
+
let bestCount = -1;
|
|
1990
|
+
for (const [machine, count] of counts.entries()) {
|
|
1991
|
+
if (count > bestCount) {
|
|
1992
|
+
best = machine;
|
|
1993
|
+
bestCount = count;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
return best || "peer";
|
|
1997
|
+
}
|
|
1998
|
+
function ensureMachineRegistry(target, machine, now) {
|
|
1999
|
+
if (!machine)
|
|
2000
|
+
return;
|
|
2001
|
+
target.prepare(`
|
|
2002
|
+
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
2003
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?)
|
|
2004
|
+
ON CONFLICT(machine_id) DO UPDATE SET
|
|
2005
|
+
hostname = COALESCE(NULLIF(machines.hostname, ''), excluded.hostname),
|
|
2006
|
+
last_seen_at = CASE
|
|
2007
|
+
WHEN machines.last_seen_at IS NULL OR machines.last_seen_at < excluded.last_seen_at THEN excluded.last_seen_at
|
|
2008
|
+
ELSE machines.last_seen_at
|
|
2009
|
+
END,
|
|
2010
|
+
last_pull_at = excluded.last_pull_at,
|
|
2011
|
+
economy_version = excluded.economy_version,
|
|
2012
|
+
updated_at = excluded.updated_at
|
|
2013
|
+
`).run(machine, machine, now, now, packageMetadata.version, now);
|
|
2014
|
+
}
|
|
2015
|
+
function openSourceDatabase(path) {
|
|
2016
|
+
try {
|
|
2017
|
+
return new BunDatabase(path, { readonly: true });
|
|
2018
|
+
} catch {
|
|
2019
|
+
return new BunDatabase(path);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
2023
|
+
if (!existsSync3(sourcePath))
|
|
2024
|
+
throw new Error(`source database does not exist: ${sourcePath}`);
|
|
2025
|
+
const source = openSourceDatabase(sourcePath);
|
|
2026
|
+
const now = opts.now ?? new Date().toISOString();
|
|
2027
|
+
const sourceMachine = detectSourceMachine(source, opts.sourceMachine);
|
|
2028
|
+
const tables = [];
|
|
2029
|
+
try {
|
|
2030
|
+
target.exec("PRAGMA foreign_keys = OFF");
|
|
2031
|
+
target.exec("BEGIN IMMEDIATE");
|
|
2032
|
+
try {
|
|
2033
|
+
tables.push(mergeProjects(target, source));
|
|
2034
|
+
const sessionMerge = mergeIdentityTable(target, source, "sessions", sourceMachine, now);
|
|
2035
|
+
tables.push(sessionMerge.stats);
|
|
2036
|
+
tables.push(mergeIdentityTable(target, source, "requests", sourceMachine, now, sessionMerge.idMap).stats);
|
|
2037
|
+
for (const table of GENERIC_PEER_TABLES) {
|
|
2038
|
+
tables.push(mergeGenericTable(target, source, table, sourceMachine, now));
|
|
2039
|
+
}
|
|
2040
|
+
ensureMachineRegistry(target, sourceMachine, now);
|
|
2041
|
+
target.exec("COMMIT");
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
target.exec("ROLLBACK");
|
|
2044
|
+
throw err;
|
|
2045
|
+
} finally {
|
|
2046
|
+
target.exec("PRAGMA foreign_keys = ON");
|
|
2047
|
+
}
|
|
2048
|
+
} finally {
|
|
2049
|
+
source.close();
|
|
2050
|
+
}
|
|
2051
|
+
const deduped = dedupeRequests(target);
|
|
2052
|
+
const rowsWritten = tables.reduce((sum, table) => sum + table.inserted + table.updated, 0);
|
|
2053
|
+
const collisions = tables.reduce((sum, table) => sum + table.collisions, 0);
|
|
2054
|
+
return {
|
|
2055
|
+
source_path: sourcePath,
|
|
2056
|
+
source_machine: sourceMachine,
|
|
2057
|
+
rows_written: rowsWritten,
|
|
2058
|
+
collisions,
|
|
2059
|
+
deduped,
|
|
2060
|
+
tables: tables.filter((t) => t.inserted || t.updated || t.skipped || t.collisions)
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
1734
2063
|
// src/ingest/claude.ts
|
|
1735
2064
|
init_database();
|
|
1736
2065
|
init_pricing();
|
|
1737
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
2066
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
1738
2067
|
import { homedir as homedir2 } from "os";
|
|
1739
2068
|
import { join as join3, basename } from "path";
|
|
1740
2069
|
|
|
@@ -1903,7 +2232,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
1903
2232
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
1904
2233
|
}
|
|
1905
2234
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
1906
|
-
if (!
|
|
2235
|
+
if (!existsSync4(projectsDir)) {
|
|
1907
2236
|
if (verbose)
|
|
1908
2237
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
1909
2238
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -1932,7 +2261,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
1932
2261
|
continue;
|
|
1933
2262
|
let lines;
|
|
1934
2263
|
try {
|
|
1935
|
-
lines =
|
|
2264
|
+
lines = readFileSync3(filePath, "utf-8").split(`
|
|
1936
2265
|
`).filter((l) => l.trim());
|
|
1937
2266
|
} catch {
|
|
1938
2267
|
continue;
|
|
@@ -2051,10 +2380,10 @@ function supportsClaudeDataResidencyPricing(model) {
|
|
|
2051
2380
|
// src/ingest/codex.ts
|
|
2052
2381
|
init_database();
|
|
2053
2382
|
init_pricing();
|
|
2054
|
-
import { existsSync as
|
|
2383
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2055
2384
|
import { homedir as homedir3 } from "os";
|
|
2056
2385
|
import { join as join4, basename as basename2 } from "path";
|
|
2057
|
-
import { Database as
|
|
2386
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
2058
2387
|
var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
|
|
2059
2388
|
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
|
|
2060
2389
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
@@ -2066,10 +2395,10 @@ function codexConfigPath() {
|
|
|
2066
2395
|
}
|
|
2067
2396
|
function readCodexModel() {
|
|
2068
2397
|
const configPath = codexConfigPath();
|
|
2069
|
-
if (!
|
|
2398
|
+
if (!existsSync5(configPath))
|
|
2070
2399
|
return "gpt-5-codex";
|
|
2071
2400
|
try {
|
|
2072
|
-
const content =
|
|
2401
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
2073
2402
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2074
2403
|
return match?.[1] ?? "gpt-5-codex";
|
|
2075
2404
|
} catch {
|
|
@@ -2087,13 +2416,32 @@ function buildThreadQuery(codexDb) {
|
|
|
2087
2416
|
FROM threads WHERE tokens_used > 0
|
|
2088
2417
|
`;
|
|
2089
2418
|
}
|
|
2419
|
+
function openCodexDb(dbPath, verbose) {
|
|
2420
|
+
let lastError;
|
|
2421
|
+
for (const readonly of [true, false]) {
|
|
2422
|
+
let codexDb = null;
|
|
2423
|
+
try {
|
|
2424
|
+
codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
2425
|
+
codexDb.prepare("PRAGMA schema_version").get();
|
|
2426
|
+
return codexDb;
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
lastError = error;
|
|
2429
|
+
codexDb?.close();
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (verbose) {
|
|
2433
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
2434
|
+
console.log("Codex DB unreadable:", dbPath, message);
|
|
2435
|
+
}
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2090
2438
|
function readTokenEvents(rolloutPath) {
|
|
2091
|
-
if (!rolloutPath || !
|
|
2439
|
+
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
2092
2440
|
return [];
|
|
2093
2441
|
const fallbackUsages = new Map;
|
|
2094
2442
|
let fallbackTimestamp;
|
|
2095
2443
|
let aggregate = null;
|
|
2096
|
-
for (const line of
|
|
2444
|
+
for (const line of readFileSync4(rolloutPath, "utf-8").split(`
|
|
2097
2445
|
`)) {
|
|
2098
2446
|
if (!line.trim())
|
|
2099
2447
|
continue;
|
|
@@ -2165,7 +2513,7 @@ function fallbackEvents(totalTokens) {
|
|
|
2165
2513
|
}
|
|
2166
2514
|
async function ingestCodex(db, verbose = false) {
|
|
2167
2515
|
const dbPath = codexDbPath();
|
|
2168
|
-
if (!
|
|
2516
|
+
if (!existsSync5(dbPath)) {
|
|
2169
2517
|
if (verbose)
|
|
2170
2518
|
console.log("Codex DB not found:", dbPath);
|
|
2171
2519
|
return { sessions: 0, requests: 0 };
|
|
@@ -2176,7 +2524,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2176
2524
|
let requests = 0;
|
|
2177
2525
|
const account = await resolveAccountForAgent("codex");
|
|
2178
2526
|
try {
|
|
2179
|
-
codexDb =
|
|
2527
|
+
codexDb = openCodexDb(dbPath, verbose);
|
|
2528
|
+
if (!codexDb)
|
|
2529
|
+
return { sessions: 0, requests: 0 };
|
|
2180
2530
|
const threads = codexDb.prepare(buildThreadQuery(codexDb)).all();
|
|
2181
2531
|
for (const thread of threads) {
|
|
2182
2532
|
const model = thread.model ?? readCodexModel();
|
|
@@ -2246,7 +2596,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2246
2596
|
// src/ingest/gemini.ts
|
|
2247
2597
|
init_database();
|
|
2248
2598
|
init_pricing();
|
|
2249
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2599
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
2250
2600
|
import { homedir as homedir4 } from "os";
|
|
2251
2601
|
import { join as join5, basename as basename3 } from "path";
|
|
2252
2602
|
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
@@ -2267,7 +2617,7 @@ function numberField(...values) {
|
|
|
2267
2617
|
function listProjectDirs(...roots) {
|
|
2268
2618
|
const dirs = new Set;
|
|
2269
2619
|
for (const root of roots) {
|
|
2270
|
-
if (!
|
|
2620
|
+
if (!existsSync6(root))
|
|
2271
2621
|
continue;
|
|
2272
2622
|
try {
|
|
2273
2623
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
@@ -2285,15 +2635,15 @@ function projectRoot(projectDir, chatData) {
|
|
|
2285
2635
|
return chatData.project_path;
|
|
2286
2636
|
const rootFile = join5(projectDir, ".project_root");
|
|
2287
2637
|
try {
|
|
2288
|
-
if (
|
|
2289
|
-
return
|
|
2638
|
+
if (existsSync6(rootFile))
|
|
2639
|
+
return readFileSync5(rootFile, "utf-8").trim();
|
|
2290
2640
|
} catch {}
|
|
2291
2641
|
return "";
|
|
2292
2642
|
}
|
|
2293
2643
|
async function ingestGemini(db, verbose) {
|
|
2294
2644
|
const tmpDir = geminiTmpDir();
|
|
2295
2645
|
const historyDir = geminiHistoryDir();
|
|
2296
|
-
if (!
|
|
2646
|
+
if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
|
|
2297
2647
|
if (verbose)
|
|
2298
2648
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2299
2649
|
return { sessions: 0, requests: 0 };
|
|
@@ -2306,7 +2656,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2306
2656
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2307
2657
|
for (const projectDir of projectDirs) {
|
|
2308
2658
|
const chatsDir = join5(projectDir, "chats");
|
|
2309
|
-
if (!
|
|
2659
|
+
if (!existsSync6(chatsDir))
|
|
2310
2660
|
continue;
|
|
2311
2661
|
let chatFiles = [];
|
|
2312
2662
|
try {
|
|
@@ -2327,7 +2677,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2327
2677
|
continue;
|
|
2328
2678
|
let chatData;
|
|
2329
2679
|
try {
|
|
2330
|
-
chatData = JSON.parse(
|
|
2680
|
+
chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
2331
2681
|
} catch {
|
|
2332
2682
|
continue;
|
|
2333
2683
|
}
|
|
@@ -2430,6 +2780,7 @@ export {
|
|
|
2430
2780
|
queryAccountBreakdown,
|
|
2431
2781
|
openDatabase,
|
|
2432
2782
|
normalizeModelName,
|
|
2783
|
+
mergePeerDatabase,
|
|
2433
2784
|
listSubscriptions,
|
|
2434
2785
|
listProjects,
|
|
2435
2786
|
listModelPricing,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA6C7D,iBAAS,cAAc,IAAI,MAAM,CAUhC;AAwGD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SqliteAdapter as Database } from '@hasna/cloud';
|
|
2
|
+
export interface PeerTableMergeStats {
|
|
3
|
+
table: string;
|
|
4
|
+
inserted: number;
|
|
5
|
+
updated: number;
|
|
6
|
+
skipped: number;
|
|
7
|
+
collisions: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PeerMergeResult {
|
|
10
|
+
source_path: string;
|
|
11
|
+
source_machine: string;
|
|
12
|
+
rows_written: number;
|
|
13
|
+
collisions: number;
|
|
14
|
+
deduped: number;
|
|
15
|
+
tables: PeerTableMergeStats[];
|
|
16
|
+
}
|
|
17
|
+
export declare function mergePeerDatabase(target: Database, sourcePath: string, opts?: {
|
|
18
|
+
sourceMachine?: string;
|
|
19
|
+
now?: string;
|
|
20
|
+
}): PeerMergeResult;
|
|
21
|
+
//# sourceMappingURL=peer-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-sync.d.ts","sourceRoot":"","sources":["../../src/lib/peer-sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAc7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAC9B;AAwQD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,eAAe,CA0CjB"}
|