@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/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 labelForPath(projectPath, projectName) {
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(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
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 segments[segments.length - 1] ?? projectPath;
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 g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
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(label, g);
1107
+ groups.set(key, g);
1076
1108
  }
1077
1109
  const result = [];
1078
- for (const [label, g] of groups.entries()) {
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 WHERE source_request_id = ? AND agent = ? AND id != ?
1451
- `).run(row.source_request_id, row.agent, row.keep_id);
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 readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
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 (!existsSync3(projectsDir)) {
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 = readFileSync2(filePath, "utf-8").split(`
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 existsSync4, readFileSync as readFileSync3 } from "fs";
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 BunDatabase } from "bun:sqlite";
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 (!existsSync4(configPath))
2398
+ if (!existsSync5(configPath))
2070
2399
  return "gpt-5-codex";
2071
2400
  try {
2072
- const content = readFileSync3(configPath, "utf-8");
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 || !existsSync4(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 readFileSync3(rolloutPath, "utf-8").split(`
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 (!existsSync4(dbPath)) {
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 = new BunDatabase(dbPath, { readonly: true });
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 readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
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 (!existsSync5(root))
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 (existsSync5(rootFile))
2289
- return readFileSync4(rootFile, "utf-8").trim();
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 (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
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 (!existsSync5(chatsDir))
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(readFileSync4(filePath, "utf-8"));
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;AAoFD,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA4FhH;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
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"}