@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/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 {
@@ -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 BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
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 || !existsSync4(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 readFileSync3(rolloutPath, "utf-8").split(`
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 (!existsSync4(dbPath)) {
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 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";
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 (!existsSync5(root))
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 (existsSync5(rootFile))
2310
- return readFileSync4(rootFile, "utf-8").trim();
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 (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
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 (!existsSync5(chatsDir))
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(readFileSync4(filePath, "utf-8"));
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 labelForPath(projectPath, projectName) {
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(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
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 segments[segments.length - 1] ?? projectPath;
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 g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
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(label, g);
1108
+ groups.set(key, g);
1077
1109
  }
1078
1110
  const result = [];
1079
- for (const [label, g] of groups.entries()) {
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 WHERE source_request_id = ? AND agent = ? AND id != ?
1419
- `).run(row.source_request_id, row.agent, row.keep_id);
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;