@hasna/economy 0.2.26 → 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 +355 -24
- 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 +361 -31
- package/dist/lib/peer-sync.d.ts +21 -0
- package/dist/lib/peer-sync.d.ts.map +1 -0
- package/dist/mcp/index.js +49 -13
- package/dist/server/index.js +49 -13
- 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 {
|
|
@@ -2092,7 +2421,7 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2092
2421
|
for (const readonly of [true, false]) {
|
|
2093
2422
|
let codexDb = null;
|
|
2094
2423
|
try {
|
|
2095
|
-
codexDb = readonly ? new
|
|
2424
|
+
codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
2096
2425
|
codexDb.prepare("PRAGMA schema_version").get();
|
|
2097
2426
|
return codexDb;
|
|
2098
2427
|
} catch (error) {
|
|
@@ -2107,12 +2436,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2107
2436
|
return null;
|
|
2108
2437
|
}
|
|
2109
2438
|
function readTokenEvents(rolloutPath) {
|
|
2110
|
-
if (!rolloutPath || !
|
|
2439
|
+
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
2111
2440
|
return [];
|
|
2112
2441
|
const fallbackUsages = new Map;
|
|
2113
2442
|
let fallbackTimestamp;
|
|
2114
2443
|
let aggregate = null;
|
|
2115
|
-
for (const line of
|
|
2444
|
+
for (const line of readFileSync4(rolloutPath, "utf-8").split(`
|
|
2116
2445
|
`)) {
|
|
2117
2446
|
if (!line.trim())
|
|
2118
2447
|
continue;
|
|
@@ -2184,7 +2513,7 @@ function fallbackEvents(totalTokens) {
|
|
|
2184
2513
|
}
|
|
2185
2514
|
async function ingestCodex(db, verbose = false) {
|
|
2186
2515
|
const dbPath = codexDbPath();
|
|
2187
|
-
if (!
|
|
2516
|
+
if (!existsSync5(dbPath)) {
|
|
2188
2517
|
if (verbose)
|
|
2189
2518
|
console.log("Codex DB not found:", dbPath);
|
|
2190
2519
|
return { sessions: 0, requests: 0 };
|
|
@@ -2267,7 +2596,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2267
2596
|
// src/ingest/gemini.ts
|
|
2268
2597
|
init_database();
|
|
2269
2598
|
init_pricing();
|
|
2270
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2599
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
2271
2600
|
import { homedir as homedir4 } from "os";
|
|
2272
2601
|
import { join as join5, basename as basename3 } from "path";
|
|
2273
2602
|
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
@@ -2288,7 +2617,7 @@ function numberField(...values) {
|
|
|
2288
2617
|
function listProjectDirs(...roots) {
|
|
2289
2618
|
const dirs = new Set;
|
|
2290
2619
|
for (const root of roots) {
|
|
2291
|
-
if (!
|
|
2620
|
+
if (!existsSync6(root))
|
|
2292
2621
|
continue;
|
|
2293
2622
|
try {
|
|
2294
2623
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
@@ -2306,15 +2635,15 @@ function projectRoot(projectDir, chatData) {
|
|
|
2306
2635
|
return chatData.project_path;
|
|
2307
2636
|
const rootFile = join5(projectDir, ".project_root");
|
|
2308
2637
|
try {
|
|
2309
|
-
if (
|
|
2310
|
-
return
|
|
2638
|
+
if (existsSync6(rootFile))
|
|
2639
|
+
return readFileSync5(rootFile, "utf-8").trim();
|
|
2311
2640
|
} catch {}
|
|
2312
2641
|
return "";
|
|
2313
2642
|
}
|
|
2314
2643
|
async function ingestGemini(db, verbose) {
|
|
2315
2644
|
const tmpDir = geminiTmpDir();
|
|
2316
2645
|
const historyDir = geminiHistoryDir();
|
|
2317
|
-
if (!
|
|
2646
|
+
if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
|
|
2318
2647
|
if (verbose)
|
|
2319
2648
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2320
2649
|
return { sessions: 0, requests: 0 };
|
|
@@ -2327,7 +2656,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2327
2656
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2328
2657
|
for (const projectDir of projectDirs) {
|
|
2329
2658
|
const chatsDir = join5(projectDir, "chats");
|
|
2330
|
-
if (!
|
|
2659
|
+
if (!existsSync6(chatsDir))
|
|
2331
2660
|
continue;
|
|
2332
2661
|
let chatFiles = [];
|
|
2333
2662
|
try {
|
|
@@ -2348,7 +2677,7 @@ async function ingestGemini(db, verbose) {
|
|
|
2348
2677
|
continue;
|
|
2349
2678
|
let chatData;
|
|
2350
2679
|
try {
|
|
2351
|
-
chatData = JSON.parse(
|
|
2680
|
+
chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
2352
2681
|
} catch {
|
|
2353
2682
|
continue;
|
|
2354
2683
|
}
|
|
@@ -2451,6 +2780,7 @@ export {
|
|
|
2451
2780
|
queryAccountBreakdown,
|
|
2452
2781
|
openDatabase,
|
|
2453
2782
|
normalizeModelName,
|
|
2783
|
+
mergePeerDatabase,
|
|
2454
2784
|
listSubscriptions,
|
|
2455
2785
|
listProjects,
|
|
2456
2786
|
listModelPricing,
|
|
@@ -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"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -1038,9 +1038,7 @@ function queryAgentBreakdown(db, period = "all") {
|
|
|
1038
1038
|
}
|
|
1039
1039
|
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1040
1040
|
}
|
|
1041
|
-
function
|
|
1042
|
-
if (projectName && projectName.trim() !== "")
|
|
1043
|
-
return projectName;
|
|
1041
|
+
function pathProjectLabel(projectPath) {
|
|
1044
1042
|
if (!projectPath)
|
|
1045
1043
|
return "";
|
|
1046
1044
|
const segments = projectPath.split("/").filter(Boolean);
|
|
@@ -1049,12 +1047,45 @@ function labelForPath(projectPath, projectName) {
|
|
|
1049
1047
|
if (projectPrefix.test(seg))
|
|
1050
1048
|
return seg;
|
|
1051
1049
|
}
|
|
1052
|
-
const generic = new Set([
|
|
1050
|
+
const generic = new Set([
|
|
1051
|
+
"web",
|
|
1052
|
+
"app",
|
|
1053
|
+
"apps",
|
|
1054
|
+
"packages",
|
|
1055
|
+
"src",
|
|
1056
|
+
"lib",
|
|
1057
|
+
"server",
|
|
1058
|
+
"client",
|
|
1059
|
+
"api",
|
|
1060
|
+
"frontend",
|
|
1061
|
+
"backend",
|
|
1062
|
+
"home",
|
|
1063
|
+
"users",
|
|
1064
|
+
"workspace",
|
|
1065
|
+
"workspaces",
|
|
1066
|
+
"hasna"
|
|
1067
|
+
]);
|
|
1053
1068
|
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1054
1069
|
if (!generic.has(segments[i].toLowerCase()))
|
|
1055
1070
|
return segments[i];
|
|
1056
1071
|
}
|
|
1057
|
-
return
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
function isRepoLikeLabel(label) {
|
|
1075
|
+
return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
|
|
1076
|
+
}
|
|
1077
|
+
function labelForPath(projectPath, projectName) {
|
|
1078
|
+
const pathLabel = pathProjectLabel(projectPath);
|
|
1079
|
+
if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
|
|
1080
|
+
return pathLabel;
|
|
1081
|
+
if (projectName && projectName.trim() !== "")
|
|
1082
|
+
return projectName;
|
|
1083
|
+
if (pathLabel)
|
|
1084
|
+
return pathLabel;
|
|
1085
|
+
return projectPath;
|
|
1086
|
+
}
|
|
1087
|
+
function groupKeyForPath(projectPath, projectName) {
|
|
1088
|
+
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1058
1089
|
}
|
|
1059
1090
|
function queryProjectBreakdown(db, period = "all") {
|
|
1060
1091
|
const requestWhere = requestPeriodWhere(period);
|
|
@@ -1069,14 +1100,15 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1069
1100
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1070
1101
|
if (!label)
|
|
1071
1102
|
continue;
|
|
1072
|
-
const
|
|
1103
|
+
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1104
|
+
const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
|
|
1073
1105
|
g.sessionIds.push(s.id);
|
|
1074
1106
|
if (!g.samplePath)
|
|
1075
1107
|
g.samplePath = s.project_path;
|
|
1076
|
-
groups.set(
|
|
1108
|
+
groups.set(key, g);
|
|
1077
1109
|
}
|
|
1078
1110
|
const result = [];
|
|
1079
|
-
for (const
|
|
1111
|
+
for (const g of groups.values()) {
|
|
1080
1112
|
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1081
1113
|
const reqStats = placeholders.length ? db.prepare(`
|
|
1082
1114
|
SELECT
|
|
@@ -1107,7 +1139,7 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1107
1139
|
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1108
1140
|
result.push({
|
|
1109
1141
|
project_path: g.samplePath,
|
|
1110
|
-
project_name: label,
|
|
1142
|
+
project_name: g.label,
|
|
1111
1143
|
sessions: totalSessions,
|
|
1112
1144
|
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1113
1145
|
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
@@ -1406,17 +1438,21 @@ function queryUsageSnapshots(db, opts = {}) {
|
|
|
1406
1438
|
}
|
|
1407
1439
|
function dedupeRequests(db) {
|
|
1408
1440
|
const dupes = db.prepare(`
|
|
1409
|
-
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1441
|
+
SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1410
1442
|
FROM requests
|
|
1411
1443
|
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1412
|
-
GROUP BY source_request_id, agent
|
|
1444
|
+
GROUP BY source_request_id, agent, COALESCE(machine_id, '')
|
|
1413
1445
|
HAVING cnt > 1
|
|
1414
1446
|
`).all();
|
|
1415
1447
|
let removed = 0;
|
|
1416
1448
|
for (const row of dupes) {
|
|
1417
1449
|
const result = db.prepare(`
|
|
1418
|
-
DELETE FROM requests
|
|
1419
|
-
|
|
1450
|
+
DELETE FROM requests
|
|
1451
|
+
WHERE source_request_id = ?
|
|
1452
|
+
AND agent = ?
|
|
1453
|
+
AND COALESCE(machine_id, '') = ?
|
|
1454
|
+
AND id != ?
|
|
1455
|
+
`).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
|
|
1420
1456
|
removed += result.changes;
|
|
1421
1457
|
}
|
|
1422
1458
|
return removed;
|