@hasna/economy 0.2.30 → 0.2.31

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.
Files changed (64) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/commands/tui.d.ts +1 -1
  3. package/dist/cli/commands/tui.d.ts.map +1 -1
  4. package/dist/cli/index.js +850 -187
  5. package/dist/db/database.d.ts +4 -2
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/storage-adapter.d.ts +34 -0
  8. package/dist/db/storage-adapter.d.ts.map +1 -0
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1057 -54
  12. package/dist/ingest/billing.d.ts +1 -1
  13. package/dist/ingest/billing.d.ts.map +1 -1
  14. package/dist/ingest/claude-quota.d.ts +1 -1
  15. package/dist/ingest/claude-quota.d.ts.map +1 -1
  16. package/dist/ingest/claude.d.ts +1 -1
  17. package/dist/ingest/claude.d.ts.map +1 -1
  18. package/dist/ingest/codex-quota.d.ts +1 -1
  19. package/dist/ingest/codex-quota.d.ts.map +1 -1
  20. package/dist/ingest/codex.d.ts +1 -1
  21. package/dist/ingest/codex.d.ts.map +1 -1
  22. package/dist/ingest/cursor.d.ts +1 -1
  23. package/dist/ingest/cursor.d.ts.map +1 -1
  24. package/dist/ingest/gemini.d.ts +1 -1
  25. package/dist/ingest/gemini.d.ts.map +1 -1
  26. package/dist/ingest/hermes.d.ts +1 -1
  27. package/dist/ingest/hermes.d.ts.map +1 -1
  28. package/dist/ingest/opencode.d.ts +1 -1
  29. package/dist/ingest/opencode.d.ts.map +1 -1
  30. package/dist/ingest/otel.d.ts +1 -1
  31. package/dist/ingest/otel.d.ts.map +1 -1
  32. package/dist/ingest/pi.d.ts +1 -1
  33. package/dist/ingest/pi.d.ts.map +1 -1
  34. package/dist/ingest/plugin.d.ts +1 -1
  35. package/dist/ingest/plugin.d.ts.map +1 -1
  36. package/dist/lib/billing-diff.d.ts +1 -1
  37. package/dist/lib/billing-diff.d.ts.map +1 -1
  38. package/dist/lib/cloud-sync.d.ts +9 -2
  39. package/dist/lib/cloud-sync.d.ts.map +1 -1
  40. package/dist/lib/open-projects.d.ts +3 -2
  41. package/dist/lib/open-projects.d.ts.map +1 -1
  42. package/dist/lib/peer-sync.d.ts +1 -1
  43. package/dist/lib/peer-sync.d.ts.map +1 -1
  44. package/dist/lib/pricing.d.ts +1 -1
  45. package/dist/lib/pricing.d.ts.map +1 -1
  46. package/dist/lib/remote-storage.d.ts +15 -0
  47. package/dist/lib/remote-storage.d.ts.map +1 -0
  48. package/dist/lib/savings.d.ts +1 -1
  49. package/dist/lib/savings.d.ts.map +1 -1
  50. package/dist/lib/spikes.d.ts +1 -1
  51. package/dist/lib/spikes.d.ts.map +1 -1
  52. package/dist/lib/storage-sync.d.ts +27 -0
  53. package/dist/lib/storage-sync.d.ts.map +1 -0
  54. package/dist/lib/sync-all.d.ts +1 -1
  55. package/dist/lib/sync-all.d.ts.map +1 -1
  56. package/dist/lib/webhooks.d.ts +1 -1
  57. package/dist/lib/webhooks.d.ts.map +1 -1
  58. package/dist/mcp/index.js +514 -38
  59. package/dist/mcp/server.d.ts.map +1 -1
  60. package/dist/otel/index.js +442 -15
  61. package/dist/server/index.js +510 -51
  62. package/dist/server/serve.d.ts +1 -1
  63. package/dist/server/serve.d.ts.map +1 -1
  64. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -16,6 +16,60 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
+ // src/db/storage-adapter.ts
20
+ import { Database as BunDatabase } from "bun:sqlite";
21
+
22
+ class SqliteAdapter {
23
+ db;
24
+ constructor(path) {
25
+ this.db = new BunDatabase(path, { create: true });
26
+ }
27
+ run(sql, ...params) {
28
+ const result = this.db.prepare(sql).run(...params);
29
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
30
+ }
31
+ get(sql, ...params) {
32
+ return this.db.prepare(sql).get(...params);
33
+ }
34
+ all(sql, ...params) {
35
+ return this.db.prepare(sql).all(...params);
36
+ }
37
+ exec(sql) {
38
+ this.db.exec(sql);
39
+ }
40
+ query(sql) {
41
+ return this.db.query(sql);
42
+ }
43
+ prepare(sql) {
44
+ const statement = this.db.prepare(sql);
45
+ return {
46
+ run(...params) {
47
+ const result = statement.run(...params);
48
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
49
+ },
50
+ get(...params) {
51
+ return statement.get(...params);
52
+ },
53
+ all(...params) {
54
+ return statement.all(...params);
55
+ },
56
+ finalize() {
57
+ statement.finalize();
58
+ }
59
+ };
60
+ }
61
+ close() {
62
+ this.db.close();
63
+ }
64
+ transaction(fn) {
65
+ return this.db.transaction(fn)();
66
+ }
67
+ get raw() {
68
+ return this.db;
69
+ }
70
+ }
71
+ var init_storage_adapter = () => {};
72
+
19
73
  // src/lib/pricing.ts
20
74
  var exports_pricing = {};
21
75
  __export(exports_pricing, {
@@ -512,7 +566,6 @@ var init_pricing = __esm(() => {
512
566
  });
513
567
 
514
568
  // src/db/database.ts
515
- import { SqliteAdapter as Database } from "@hasna/cloud";
516
569
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
517
570
  import { hostname } from "os";
518
571
  import { homedir } from "os";
@@ -555,7 +608,7 @@ function openDatabase(dbPath, skipSeed = false) {
555
608
  if (dir && !existsSync(dir))
556
609
  mkdirSync(dir, { recursive: true });
557
610
  }
558
- const db = new Database(path);
611
+ const db = new SqliteAdapter(path);
559
612
  db.exec("PRAGMA journal_mode = WAL");
560
613
  db.exec("PRAGMA busy_timeout = 5000");
561
614
  db.exec("PRAGMA foreign_keys = ON");
@@ -1307,13 +1360,17 @@ function queryDailyBreakdown(db, days = 30, machine) {
1307
1360
  ORDER BY date ASC
1308
1361
  `).all(...params);
1309
1362
  }
1310
- function queryHourlyBreakdown(db, machine) {
1311
- const machineClause = machine ? " AND machine_id = ?" : "";
1312
- const params = machine ? [machine] : [];
1363
+ function queryHourlyBreakdown(db, machine, hours) {
1364
+ const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
1365
+ const params = hours == null ? [] : [`-${hours} hours`];
1366
+ if (machine) {
1367
+ clauses.push("machine_id = ?");
1368
+ params.push(machine);
1369
+ }
1313
1370
  return db.prepare(`
1314
1371
  SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
1315
1372
  FROM requests
1316
- WHERE DATE(timestamp) = DATE('now')${machineClause}
1373
+ WHERE ${clauses.join(" AND ")}
1317
1374
  GROUP BY STRFTIME('%H', timestamp), agent
1318
1375
  ORDER BY hour ASC
1319
1376
  `).all(...params);
@@ -1587,7 +1644,198 @@ function dedupeRequests(db) {
1587
1644
  }
1588
1645
  return removed;
1589
1646
  }
1590
- var init_database = () => {};
1647
+ var init_database = __esm(() => {
1648
+ init_storage_adapter();
1649
+ init_storage_adapter();
1650
+ });
1651
+
1652
+ // src/db/pg-migrations.ts
1653
+ var exports_pg_migrations = {};
1654
+ __export(exports_pg_migrations, {
1655
+ PG_MIGRATIONS: () => PG_MIGRATIONS
1656
+ });
1657
+ var PG_MIGRATIONS;
1658
+ var init_pg_migrations = __esm(() => {
1659
+ PG_MIGRATIONS = [
1660
+ `CREATE TABLE IF NOT EXISTS requests (
1661
+ id TEXT PRIMARY KEY,
1662
+ agent TEXT NOT NULL,
1663
+ session_id TEXT NOT NULL,
1664
+ model TEXT NOT NULL,
1665
+ input_tokens INTEGER DEFAULT 0,
1666
+ output_tokens INTEGER DEFAULT 0,
1667
+ cache_read_tokens INTEGER DEFAULT 0,
1668
+ cache_create_tokens INTEGER DEFAULT 0,
1669
+ cache_create_5m_tokens INTEGER DEFAULT 0,
1670
+ cache_create_1h_tokens INTEGER DEFAULT 0,
1671
+ cost_usd REAL NOT NULL DEFAULT 0,
1672
+ duration_ms INTEGER DEFAULT 0,
1673
+ timestamp TEXT NOT NULL,
1674
+ source_request_id TEXT,
1675
+ machine_id TEXT DEFAULT '',
1676
+ account_key TEXT DEFAULT '',
1677
+ account_tool TEXT DEFAULT '',
1678
+ account_name TEXT DEFAULT '',
1679
+ account_email TEXT DEFAULT '',
1680
+ account_source TEXT DEFAULT ''
1681
+ )`,
1682
+ `CREATE TABLE IF NOT EXISTS sessions (
1683
+ id TEXT PRIMARY KEY,
1684
+ agent TEXT NOT NULL,
1685
+ project_path TEXT DEFAULT '',
1686
+ project_name TEXT DEFAULT '',
1687
+ started_at TEXT NOT NULL,
1688
+ ended_at TEXT,
1689
+ total_cost_usd REAL DEFAULT 0,
1690
+ total_tokens INTEGER DEFAULT 0,
1691
+ request_count INTEGER DEFAULT 0,
1692
+ machine_id TEXT DEFAULT '',
1693
+ account_key TEXT DEFAULT '',
1694
+ account_tool TEXT DEFAULT '',
1695
+ account_name TEXT DEFAULT '',
1696
+ account_email TEXT DEFAULT '',
1697
+ account_source TEXT DEFAULT ''
1698
+ )`,
1699
+ `CREATE TABLE IF NOT EXISTS projects (
1700
+ id TEXT PRIMARY KEY,
1701
+ path TEXT UNIQUE NOT NULL,
1702
+ name TEXT NOT NULL,
1703
+ description TEXT,
1704
+ tags TEXT DEFAULT '[]',
1705
+ created_at TEXT NOT NULL
1706
+ )`,
1707
+ `CREATE TABLE IF NOT EXISTS budgets (
1708
+ id TEXT PRIMARY KEY,
1709
+ project_path TEXT,
1710
+ agent TEXT,
1711
+ period TEXT NOT NULL,
1712
+ limit_usd REAL NOT NULL,
1713
+ alert_at_percent INTEGER DEFAULT 80,
1714
+ created_at TEXT NOT NULL,
1715
+ updated_at TEXT NOT NULL
1716
+ )`,
1717
+ `CREATE TABLE IF NOT EXISTS goals (
1718
+ id TEXT PRIMARY KEY,
1719
+ period TEXT NOT NULL,
1720
+ project_path TEXT,
1721
+ agent TEXT,
1722
+ limit_usd REAL NOT NULL,
1723
+ created_at TEXT NOT NULL,
1724
+ updated_at TEXT NOT NULL
1725
+ )`,
1726
+ `CREATE TABLE IF NOT EXISTS ingest_state (
1727
+ source TEXT NOT NULL,
1728
+ key TEXT NOT NULL,
1729
+ value TEXT NOT NULL,
1730
+ PRIMARY KEY (source, key)
1731
+ )`,
1732
+ `CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
1733
+ `CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
1734
+ `CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
1735
+ `CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
1736
+ `CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
1737
+ `CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
1738
+ `CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
1739
+ `CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
1740
+ `CREATE TABLE IF NOT EXISTS model_pricing (
1741
+ model TEXT PRIMARY KEY,
1742
+ input_per_1m REAL NOT NULL DEFAULT 0,
1743
+ output_per_1m REAL NOT NULL DEFAULT 0,
1744
+ cache_read_per_1m REAL NOT NULL DEFAULT 0,
1745
+ cache_write_per_1m REAL NOT NULL DEFAULT 0,
1746
+ cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
1747
+ cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
1748
+ updated_at TEXT NOT NULL
1749
+ )`,
1750
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
1751
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
1752
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
1753
+ `ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
1754
+ `CREATE TABLE IF NOT EXISTS feedback (
1755
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1756
+ message TEXT NOT NULL,
1757
+ email TEXT,
1758
+ category TEXT DEFAULT 'general',
1759
+ version TEXT,
1760
+ machine_id TEXT,
1761
+ created_at TEXT NOT NULL DEFAULT NOW()::text
1762
+ )`,
1763
+ `CREATE TABLE IF NOT EXISTS billing_daily (
1764
+ date TEXT NOT NULL,
1765
+ provider TEXT NOT NULL,
1766
+ description TEXT DEFAULT '',
1767
+ cost_usd REAL NOT NULL DEFAULT 0,
1768
+ updated_at TEXT NOT NULL,
1769
+ PRIMARY KEY (date, provider, description)
1770
+ )`,
1771
+ `CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
1772
+ `CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
1773
+ `CREATE TABLE IF NOT EXISTS subscriptions (
1774
+ id TEXT PRIMARY KEY,
1775
+ agent TEXT,
1776
+ provider TEXT NOT NULL,
1777
+ plan TEXT NOT NULL,
1778
+ monthly_fee_usd REAL NOT NULL DEFAULT 0,
1779
+ included_usage_usd REAL NOT NULL DEFAULT 0,
1780
+ billing_cycle_start TEXT,
1781
+ reset_policy TEXT DEFAULT 'monthly',
1782
+ active INTEGER NOT NULL DEFAULT 1,
1783
+ created_at TEXT NOT NULL,
1784
+ updated_at TEXT NOT NULL
1785
+ )`,
1786
+ `CREATE TABLE IF NOT EXISTS usage_snapshots (
1787
+ id TEXT PRIMARY KEY,
1788
+ agent TEXT NOT NULL,
1789
+ date TEXT NOT NULL,
1790
+ metric TEXT NOT NULL,
1791
+ value REAL NOT NULL DEFAULT 0,
1792
+ unit TEXT DEFAULT '',
1793
+ machine_id TEXT DEFAULT '',
1794
+ updated_at TEXT NOT NULL
1795
+ )`,
1796
+ `CREATE TABLE IF NOT EXISTS savings_daily (
1797
+ date TEXT NOT NULL,
1798
+ agent TEXT DEFAULT '',
1799
+ api_equivalent_usd REAL NOT NULL DEFAULT 0,
1800
+ subscription_fee_usd REAL NOT NULL DEFAULT 0,
1801
+ included_consumed_usd REAL NOT NULL DEFAULT 0,
1802
+ on_demand_usd REAL NOT NULL DEFAULT 0,
1803
+ saved_usd REAL NOT NULL DEFAULT 0,
1804
+ updated_at TEXT NOT NULL,
1805
+ PRIMARY KEY (date, agent)
1806
+ )`,
1807
+ `CREATE TABLE IF NOT EXISTS machines (
1808
+ machine_id TEXT PRIMARY KEY,
1809
+ hostname TEXT NOT NULL,
1810
+ last_seen_at TEXT,
1811
+ last_push_at TEXT,
1812
+ last_pull_at TEXT,
1813
+ economy_version TEXT,
1814
+ updated_at TEXT NOT NULL
1815
+ )`,
1816
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
1817
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1818
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1819
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1820
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1821
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1822
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1823
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1824
+ `ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1825
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
1826
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
1827
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
1828
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
1829
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
1830
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
1831
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
1832
+ `ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
1833
+ `CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
1834
+ `CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
1835
+ `CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
1836
+ `CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
1837
+ ];
1838
+ });
1591
1839
 
1592
1840
  // src/lib/agents.ts
1593
1841
  var AGENTS = [
@@ -1840,22 +2088,26 @@ function clearActiveModel() {
1840
2088
  // src/lib/open-projects.ts
1841
2089
  init_database();
1842
2090
  async function syncOpenProjectsRegistry(db, listActiveProjects) {
1843
- let listProjects2 = listActiveProjects;
1844
- if (!listProjects2) {
2091
+ let listOpenProjects = listActiveProjects;
2092
+ if (!listOpenProjects) {
1845
2093
  const projectsApi = await import("@hasna/projects");
1846
- listProjects2 = projectsApi.listProjects;
2094
+ listOpenProjects = projectsApi.listProjects ?? projectsApi.listWorkspaces;
1847
2095
  }
1848
- const projects = listProjects2({ status: "active", limit: 5000 });
2096
+ if (!listOpenProjects) {
2097
+ throw new Error("@hasna/projects does not expose listWorkspaces or listProjects");
2098
+ }
2099
+ const projects = listOpenProjects({ status: "active", limit: 5000 });
1849
2100
  let imported = 0;
1850
2101
  let skipped = 0;
1851
2102
  for (const project of projects) {
1852
- if (!project.path) {
2103
+ const path = project.path ?? project.primary_path ?? "";
2104
+ if (!path) {
1853
2105
  skipped++;
1854
2106
  continue;
1855
2107
  }
1856
2108
  upsertProject(db, {
1857
2109
  id: project.id,
1858
- path: project.path,
2110
+ path,
1859
2111
  name: project.name,
1860
2112
  description: project.description,
1861
2113
  tags: project.tags ?? [],
@@ -1867,7 +2119,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
1867
2119
  }
1868
2120
  // src/lib/peer-sync.ts
1869
2121
  init_database();
1870
- import { Database as BunDatabase } from "bun:sqlite";
2122
+ import { Database as BunDatabase2 } from "bun:sqlite";
1871
2123
  import { existsSync as existsSync3 } from "fs";
1872
2124
 
1873
2125
  // src/lib/package-metadata.ts
@@ -2112,9 +2364,9 @@ function ensureMachineRegistry(target, machine, now) {
2112
2364
  }
2113
2365
  function openSourceDatabase(path) {
2114
2366
  try {
2115
- return new BunDatabase(path, { readonly: true });
2367
+ return new BunDatabase2(path, { readonly: true });
2116
2368
  } catch {
2117
- return new BunDatabase(path);
2369
+ return new BunDatabase2(path);
2118
2370
  }
2119
2371
  }
2120
2372
  function mergePeerDatabase(target, sourcePath, opts = {}) {
@@ -2158,12 +2410,741 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
2158
2410
  tables: tables.filter((t) => t.inserted || t.updated || t.skipped || t.collisions)
2159
2411
  };
2160
2412
  }
2413
+ // src/lib/cloud-sync.ts
2414
+ init_database();
2415
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2416
+ import { homedir as homedir2, platform } from "os";
2417
+ import { dirname as dirname2, join as join3 } from "path";
2418
+
2419
+ // src/lib/remote-storage.ts
2420
+ import pg from "pg";
2421
+ function translatePlaceholders(sql) {
2422
+ let index = 0;
2423
+ return sql.replace(/\?/g, () => `$${++index}`);
2424
+ }
2425
+ function normalizeParams(params) {
2426
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
2427
+ return flat.map((value) => value === undefined ? null : value);
2428
+ }
2429
+ function sslConfigFor(connectionString) {
2430
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
2431
+ }
2432
+
2433
+ class PgAdapterAsync {
2434
+ pool;
2435
+ constructor(source) {
2436
+ this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
2437
+ }
2438
+ async run(sql, ...params) {
2439
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
2440
+ return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
2441
+ }
2442
+ async get(sql, ...params) {
2443
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
2444
+ return result.rows[0] ?? null;
2445
+ }
2446
+ async all(sql, ...params) {
2447
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
2448
+ return result.rows;
2449
+ }
2450
+ async exec(sql) {
2451
+ await this.pool.query(translatePlaceholders(sql));
2452
+ }
2453
+ async close() {
2454
+ await this.pool.end();
2455
+ }
2456
+ async transaction(fn) {
2457
+ const client = await this.pool.connect();
2458
+ try {
2459
+ await client.query("BEGIN");
2460
+ const result = await fn(client);
2461
+ await client.query("COMMIT");
2462
+ return result;
2463
+ } catch (error) {
2464
+ await client.query("ROLLBACK");
2465
+ throw error;
2466
+ } finally {
2467
+ client.release();
2468
+ }
2469
+ }
2470
+ get raw() {
2471
+ return this.pool;
2472
+ }
2473
+ }
2474
+
2475
+ // src/lib/storage-sync.ts
2476
+ async function syncPush(local, remote, options) {
2477
+ const tables = await getTableOrder(remote, options.tables);
2478
+ return syncTransfer(local, remote, { ...options, tables }, "push");
2479
+ }
2480
+ async function syncPull(remote, local, options) {
2481
+ const tables = await getTableOrder(remote, options.tables);
2482
+ return syncTransfer(remote, local, { ...options, tables }, "pull");
2483
+ }
2484
+ function quoteIdent2(identifier) {
2485
+ return `"${identifier.replace(/"/g, '""')}"`;
2486
+ }
2487
+ async function getTableOrder(remote, tables) {
2488
+ if (tables.length <= 1)
2489
+ return tables;
2490
+ try {
2491
+ const rows = await remote.all(`
2492
+ SELECT DISTINCT
2493
+ tc.table_name AS source_table,
2494
+ ccu.table_name AS referenced_table
2495
+ FROM information_schema.table_constraints tc
2496
+ JOIN information_schema.constraint_column_usage ccu
2497
+ ON tc.constraint_name = ccu.constraint_name
2498
+ AND tc.table_schema = ccu.table_schema
2499
+ WHERE tc.constraint_type = 'FOREIGN KEY'
2500
+ AND tc.table_schema = 'public'
2501
+ `);
2502
+ if (rows.length > 0)
2503
+ return topoSort(tables, rows);
2504
+ } catch {}
2505
+ return tables;
2506
+ }
2507
+ function topoSort(tables, foreignKeys) {
2508
+ const allowed = new Set(tables);
2509
+ const deps = new Map;
2510
+ for (const table of tables)
2511
+ deps.set(table, new Set);
2512
+ for (const fk of foreignKeys) {
2513
+ if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
2514
+ deps.get(fk.source_table)?.add(fk.referenced_table);
2515
+ }
2516
+ }
2517
+ const sorted = [];
2518
+ const visited = new Set;
2519
+ const visiting = new Set;
2520
+ function visit(table) {
2521
+ if (visited.has(table))
2522
+ return;
2523
+ if (visiting.has(table)) {
2524
+ visited.add(table);
2525
+ sorted.push(table);
2526
+ return;
2527
+ }
2528
+ visiting.add(table);
2529
+ for (const dep of deps.get(table) ?? [])
2530
+ visit(dep);
2531
+ visiting.delete(table);
2532
+ visited.add(table);
2533
+ sorted.push(table);
2534
+ }
2535
+ for (const table of tables)
2536
+ visit(table);
2537
+ return sorted;
2538
+ }
2539
+ async function resolvePrimaryKeys(source, target, table, option) {
2540
+ if (option)
2541
+ return Array.isArray(option) ? option : [option];
2542
+ const sourceKeys = await detectPrimaryKeys(source, table);
2543
+ if (sourceKeys.length > 0)
2544
+ return sourceKeys;
2545
+ return detectPrimaryKeys(target, table);
2546
+ }
2547
+ async function detectPrimaryKeys(adapter, table) {
2548
+ if (isAsyncAdapter(adapter)) {
2549
+ try {
2550
+ const rows = await adapter.all(`
2551
+ SELECT kcu.column_name, kcu.ordinal_position
2552
+ FROM information_schema.table_constraints tc
2553
+ JOIN information_schema.key_column_usage kcu
2554
+ ON tc.constraint_name = kcu.constraint_name
2555
+ AND tc.table_schema = kcu.table_schema
2556
+ WHERE tc.constraint_type = 'PRIMARY KEY'
2557
+ AND tc.table_schema = 'public'
2558
+ AND tc.table_name = ?
2559
+ ORDER BY kcu.ordinal_position
2560
+ `, table);
2561
+ return rows.map((row) => row.column_name);
2562
+ } catch {
2563
+ return [];
2564
+ }
2565
+ }
2566
+ try {
2567
+ const rows = adapter.all(`PRAGMA table_info(${quoteIdent2(table)})`);
2568
+ return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
2569
+ } catch {
2570
+ return [];
2571
+ }
2572
+ }
2573
+ async function ensureTablesExist(source, target, tables) {
2574
+ if (!isAsyncAdapter(source) || isAsyncAdapter(target))
2575
+ return;
2576
+ for (const table of tables)
2577
+ await ensureTableInSqliteFromPg(target, source, table);
2578
+ }
2579
+ async function ensureTableInSqliteFromPg(target, source, table) {
2580
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
2581
+ if (existing.length > 0)
2582
+ return;
2583
+ const columns = await source.all(`
2584
+ SELECT column_name, data_type, is_nullable
2585
+ FROM information_schema.columns
2586
+ WHERE table_schema = 'public' AND table_name = ?
2587
+ ORDER BY ordinal_position
2588
+ `, table);
2589
+ if (columns.length === 0)
2590
+ return;
2591
+ const primaryKeys = new Set(await detectPrimaryKeys(source, table));
2592
+ const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
2593
+ const type = pgTypeToSqlite(column.data_type);
2594
+ const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
2595
+ return `${quoteIdent2(column.column_name)} ${type}${notNull}`;
2596
+ });
2597
+ if (primaryKeys.size > 0) {
2598
+ definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent2).join(", ")})`);
2599
+ }
2600
+ target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent2(table)} (${definitions.join(", ")})`);
2601
+ }
2602
+ function pgTypeToSqlite(pgType) {
2603
+ const type = pgType.toLowerCase();
2604
+ if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
2605
+ return "INTEGER";
2606
+ if (type.includes("bool"))
2607
+ return "INTEGER";
2608
+ if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
2609
+ return "REAL";
2610
+ if (type === "bytea")
2611
+ return "BLOB";
2612
+ return "TEXT";
2613
+ }
2614
+ async function filterColumnsForTarget(target, table, columns) {
2615
+ if (columns.includes("machine_id") && table !== "machines")
2616
+ await ensureMachineIdColumnInTarget(target, table);
2617
+ try {
2618
+ if (isAsyncAdapter(target)) {
2619
+ const rows2 = await target.all(`
2620
+ SELECT column_name
2621
+ FROM information_schema.columns
2622
+ WHERE table_schema = 'public' AND table_name = ?
2623
+ `, table);
2624
+ if (rows2.length === 0)
2625
+ return columns;
2626
+ const targetColumns2 = new Set(rows2.map((row) => row.column_name));
2627
+ return columns.filter((column) => targetColumns2.has(column));
2628
+ }
2629
+ const rows = target.all(`PRAGMA table_info(${quoteIdent2(table)})`);
2630
+ if (rows.length === 0)
2631
+ return columns;
2632
+ const targetColumns = new Set(rows.map((row) => row.name));
2633
+ return columns.filter((column) => targetColumns.has(column));
2634
+ } catch {
2635
+ return columns;
2636
+ }
2637
+ }
2638
+ async function ensureMachineIdColumnInTarget(target, table) {
2639
+ if (isAsyncAdapter(target)) {
2640
+ const rows2 = await target.all(`
2641
+ SELECT column_name
2642
+ FROM information_schema.columns
2643
+ WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
2644
+ `, table);
2645
+ if (rows2.length === 0)
2646
+ await target.exec(`ALTER TABLE ${quoteIdent2(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
2647
+ return;
2648
+ }
2649
+ const rows = target.all(`PRAGMA table_info(${quoteIdent2(table)})`);
2650
+ if (!rows.some((row) => row.name === "machine_id")) {
2651
+ target.exec(`ALTER TABLE ${quoteIdent2(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
2652
+ }
2653
+ }
2654
+ async function syncTransfer(source, target, options, _direction) {
2655
+ const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
2656
+ const results = [];
2657
+ const sqliteTarget = isAsyncAdapter(target) ? null : target;
2658
+ await ensureTablesExist(source, target, tables);
2659
+ if (sqliteTarget) {
2660
+ try {
2661
+ sqliteTarget.exec("PRAGMA foreign_keys = OFF");
2662
+ } catch {}
2663
+ }
2664
+ try {
2665
+ for (let i = 0;i < tables.length; i++) {
2666
+ const table = tables[i];
2667
+ const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
2668
+ try {
2669
+ onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2670
+ const rows = await readAll(source, `SELECT * FROM ${quoteIdent2(table)}`);
2671
+ result.rowsRead = rows.length;
2672
+ if (rows.length === 0) {
2673
+ onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2674
+ results.push(result);
2675
+ continue;
2676
+ }
2677
+ const sourceColumns = Object.keys(rows[0]);
2678
+ const columns = await filterColumnsForTarget(target, table, sourceColumns);
2679
+ const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
2680
+ if (primaryKeys.length === 0) {
2681
+ result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
2682
+ for (const batch of batches(rows, batchSize)) {
2683
+ await insertBatch(target, table, columns, batch);
2684
+ result.rowsWritten += batch.length;
2685
+ }
2686
+ results.push(result);
2687
+ continue;
2688
+ }
2689
+ const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
2690
+ if (missingKeys.length > 0) {
2691
+ result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
2692
+ results.push(result);
2693
+ continue;
2694
+ }
2695
+ onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
2696
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
2697
+ const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
2698
+ for (const batch of batches(rows, batchSize)) {
2699
+ await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
2700
+ result.rowsWritten += batch.length;
2701
+ onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
2702
+ }
2703
+ onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
2704
+ } catch (error) {
2705
+ result.errors.push(error instanceof Error ? error.message : String(error));
2706
+ }
2707
+ results.push(result);
2708
+ }
2709
+ } finally {
2710
+ if (sqliteTarget) {
2711
+ try {
2712
+ sqliteTarget.exec("PRAGMA foreign_keys = ON");
2713
+ } catch {}
2714
+ }
2715
+ }
2716
+ return results;
2717
+ }
2718
+ function batches(rows, size) {
2719
+ const result = [];
2720
+ for (let offset = 0;offset < rows.length; offset += size)
2721
+ result.push(rows.slice(offset, offset + size));
2722
+ return result;
2723
+ }
2724
+ async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
2725
+ if (batch.length === 0 || columns.length === 0)
2726
+ return;
2727
+ const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
2728
+ const columnList = columns.map(quoteIdent2).join(", ");
2729
+ const keyList = primaryKeys.map(quoteIdent2).join(", ");
2730
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent2(column)} = EXCLUDED.${quoteIdent2(column)}`).join(", ") : `${quoteIdent2(fallbackKey)} = EXCLUDED.${quoteIdent2(fallbackKey)}`;
2731
+ const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent2(table)}.${quoteIdent2(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent2(conflictColumn)} >= ${quoteIdent2(table)}.${quoteIdent2(conflictColumn)}` : "";
2732
+ if (isAsyncAdapter(target)) {
2733
+ const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
2734
+ const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
2735
+ await target.run(`INSERT INTO ${quoteIdent2(table)} (${columnList}) VALUES ${placeholders2}
2736
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
2737
+ return;
2738
+ }
2739
+ const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
2740
+ const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
2741
+ target.run(`INSERT INTO ${quoteIdent2(table)} (${columnList}) VALUES ${placeholders}
2742
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
2743
+ }
2744
+ async function insertBatch(target, table, columns, batch) {
2745
+ if (batch.length === 0 || columns.length === 0)
2746
+ return;
2747
+ const columnList = columns.map(quoteIdent2).join(", ");
2748
+ if (isAsyncAdapter(target)) {
2749
+ const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
2750
+ const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
2751
+ await target.run(`INSERT INTO ${quoteIdent2(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
2752
+ return;
2753
+ }
2754
+ const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
2755
+ const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
2756
+ target.run(`INSERT INTO ${quoteIdent2(table)} (${columnList}) VALUES ${placeholders}`, ...params);
2757
+ }
2758
+ function coerceForSqlite(value) {
2759
+ if (value === null || value === undefined)
2760
+ return null;
2761
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
2762
+ return value;
2763
+ if (value instanceof Date)
2764
+ return value.toISOString();
2765
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
2766
+ return value;
2767
+ if (typeof value === "object")
2768
+ return JSON.stringify(value);
2769
+ return String(value);
2770
+ }
2771
+ function isAsyncAdapter(adapter) {
2772
+ return adapter instanceof PgAdapterAsync;
2773
+ }
2774
+ async function readAll(adapter, sql) {
2775
+ const rows = adapter.all(sql);
2776
+ return rows instanceof Promise ? await rows : rows;
2777
+ }
2778
+
2779
+ // src/lib/cloud-sync.ts
2780
+ var CLOUD_TABLES = [
2781
+ "requests",
2782
+ "sessions",
2783
+ "projects",
2784
+ "budgets",
2785
+ "goals",
2786
+ "model_pricing",
2787
+ "billing_daily",
2788
+ "subscriptions",
2789
+ "usage_snapshots",
2790
+ "savings_daily",
2791
+ "machines",
2792
+ "ingest_state"
2793
+ ];
2794
+ function getCloudDatabaseUrl() {
2795
+ return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
2796
+ }
2797
+ function isCloudAutoEnabled() {
2798
+ return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
2799
+ }
2800
+ function getCloudPullIntervalMinutes() {
2801
+ const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
2802
+ if (!raw)
2803
+ return 15;
2804
+ const n = Number(raw);
2805
+ return Number.isFinite(n) && n > 0 ? n : 15;
2806
+ }
2807
+ async function getCloudPg() {
2808
+ const url = getCloudDatabaseUrl();
2809
+ if (!url) {
2810
+ throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
2811
+ }
2812
+ return new PgAdapterAsync(url);
2813
+ }
2814
+ async function runCloudMigrations(cloud) {
2815
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2816
+ for (const sql of PG_MIGRATIONS2) {
2817
+ await cloud.run(sql);
2818
+ }
2819
+ }
2820
+ function isCloudIncrementalEnabled() {
2821
+ return process.env["ECONOMY_CLOUD_INCREMENTAL"] === "1" || process.env["ECONOMY_CLOUD_INCREMENTAL"] === "true";
2822
+ }
2823
+ async function cloudPush(opts) {
2824
+ const cloud = await getCloudPg();
2825
+ const local = openDatabase(getDbPath(), true);
2826
+ try {
2827
+ await runCloudMigrations(cloud);
2828
+ touchMachineRegistry(local, "push");
2829
+ const tables = resolveCloudTables(opts?.tables);
2830
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
2831
+ const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
2832
+ return { rows, machine: getMachineId() };
2833
+ } finally {
2834
+ local.close();
2835
+ await cloud.close();
2836
+ }
2837
+ }
2838
+ async function cloudPull(opts) {
2839
+ const cloud = await getCloudPg();
2840
+ const local = openDatabase(getDbPath(), true);
2841
+ try {
2842
+ await runCloudMigrations(cloud);
2843
+ const tables = resolveCloudTables(opts?.tables);
2844
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
2845
+ const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
2846
+ touchMachineRegistry(local, "pull");
2847
+ setLastCloudPull();
2848
+ return { rows, machine: getMachineId() };
2849
+ } finally {
2850
+ local.close();
2851
+ await cloud.close();
2852
+ }
2853
+ }
2854
+ async function cloudSyncFull() {
2855
+ const push = await cloudPush();
2856
+ const pull = await cloudPull();
2857
+ return { push: push.rows, pull: pull.rows, machine: getMachineId() };
2858
+ }
2859
+ function setLastCloudPull(at = new Date().toISOString()) {
2860
+ const db = openDatabase(undefined, true);
2861
+ try {
2862
+ db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
2863
+ } finally {
2864
+ db.close();
2865
+ }
2866
+ }
2867
+ function getLastCloudPull() {
2868
+ const db = openDatabase(undefined, true);
2869
+ try {
2870
+ const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
2871
+ return row?.value ?? null;
2872
+ } finally {
2873
+ db.close();
2874
+ }
2875
+ }
2876
+ function shouldPullFromCloud() {
2877
+ if (!getCloudDatabaseUrl())
2878
+ return false;
2879
+ const last = getLastCloudPull();
2880
+ if (!last)
2881
+ return true;
2882
+ const ageMs = Date.now() - new Date(last).getTime();
2883
+ return ageMs > getCloudPullIntervalMinutes() * 60000;
2884
+ }
2885
+ async function maybePullFromCloud() {
2886
+ if (!shouldPullFromCloud())
2887
+ return false;
2888
+ try {
2889
+ await cloudPull();
2890
+ return true;
2891
+ } catch {
2892
+ return false;
2893
+ }
2894
+ }
2895
+ async function maybePushAfterIngest() {
2896
+ if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
2897
+ return false;
2898
+ try {
2899
+ await cloudPush();
2900
+ return true;
2901
+ } catch {
2902
+ return false;
2903
+ }
2904
+ }
2905
+ function touchMachineRegistry(db, direction) {
2906
+ const now = new Date().toISOString();
2907
+ const machine = getMachineId();
2908
+ db.prepare(`
2909
+ INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
2910
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2911
+ ON CONFLICT(machine_id) DO UPDATE SET
2912
+ hostname = excluded.hostname,
2913
+ last_seen_at = excluded.last_seen_at,
2914
+ last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
2915
+ last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
2916
+ economy_version = excluded.economy_version,
2917
+ updated_at = excluded.updated_at
2918
+ `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
2919
+ }
2920
+ function resolveCloudTables(tables) {
2921
+ if (!tables || tables.length === 0)
2922
+ return [...CLOUD_TABLES];
2923
+ const allowed = new Set(CLOUD_TABLES);
2924
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
2925
+ const invalid = requested.filter((table) => !allowed.has(table));
2926
+ if (invalid.length > 0) {
2927
+ throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
2928
+ }
2929
+ return requested;
2930
+ }
2931
+ var SCHEDULE_SERVICE_NAME = "hasna-economy-cloud-sync";
2932
+ var SCHEDULE_CONFIG_DIR = join3(homedir2(), ".hasna", "economy");
2933
+ var SCHEDULE_CONFIG_PATH = join3(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
2934
+ async function registerCloudSchedule(intervalMinutes) {
2935
+ if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) {
2936
+ throw new Error("Cloud sync interval must be greater than 0 minutes");
2937
+ }
2938
+ mkdirSync3(SCHEDULE_CONFIG_DIR, { recursive: true });
2939
+ if (platform() === "darwin") {
2940
+ await registerLaunchd(intervalMinutes);
2941
+ } else if (platform() === "linux") {
2942
+ await registerSystemd(intervalMinutes);
2943
+ } else {
2944
+ throw new Error(`Automatic economy cloud sync is not supported on ${platform()}`);
2945
+ }
2946
+ writeFileSync2(SCHEDULE_CONFIG_PATH, JSON.stringify({ intervalMinutes, updatedAt: new Date().toISOString() }, null, 2));
2947
+ }
2948
+ async function removeCloudSchedule() {
2949
+ if (platform() === "darwin")
2950
+ await removeLaunchd();
2951
+ if (platform() === "linux")
2952
+ await removeSystemd();
2953
+ try {
2954
+ unlinkSync(SCHEDULE_CONFIG_PATH);
2955
+ } catch {}
2956
+ }
2957
+ async function getCloudScheduleStatus() {
2958
+ const mechanism = platform() === "darwin" ? "launchd" : platform() === "linux" ? "systemd" : "none";
2959
+ const interval = readScheduleInterval();
2960
+ const registered = mechanism === "launchd" ? existsSync4(getLaunchdPlistPath()) : mechanism === "systemd" ? existsSync4(join3(getSystemdDir(), `${SCHEDULE_SERVICE_NAME}.timer`)) : false;
2961
+ return {
2962
+ registered,
2963
+ schedule_minutes: interval,
2964
+ cron_expression: interval > 0 ? minutesToCron(interval) : null,
2965
+ mechanism
2966
+ };
2967
+ }
2968
+ function readScheduleInterval() {
2969
+ try {
2970
+ const parsed = JSON.parse(readFileSync3(SCHEDULE_CONFIG_PATH, "utf8"));
2971
+ return typeof parsed.intervalMinutes === "number" && parsed.intervalMinutes > 0 ? parsed.intervalMinutes : 0;
2972
+ } catch {
2973
+ return 0;
2974
+ }
2975
+ }
2976
+ function minutesToCron(minutes) {
2977
+ if (minutes < 60)
2978
+ return `*/${minutes} * * * *`;
2979
+ const hours = Math.floor(minutes / 60);
2980
+ const remainder = minutes % 60;
2981
+ return remainder === 0 && hours <= 24 ? `0 */${hours} * * *` : `*/${minutes} * * * *`;
2982
+ }
2983
+ function getModuleDir() {
2984
+ return typeof import.meta.dir === "string" ? import.meta.dir : dirname2(new URL(import.meta.url).pathname);
2985
+ }
2986
+ function getBunPath() {
2987
+ const candidates = [
2988
+ join3(homedir2(), ".bun", "bin", "bun"),
2989
+ "/opt/homebrew/bin/bun",
2990
+ "/usr/local/bin/bun",
2991
+ "/usr/bin/bun"
2992
+ ];
2993
+ return candidates.find((candidate) => existsSync4(candidate)) ?? "bun";
2994
+ }
2995
+ function getEconomySyncCommand() {
2996
+ const dir = getModuleDir();
2997
+ const candidates = [
2998
+ join3(dir, "..", "cli", "index.js"),
2999
+ join3(dir, "..", "cli", "index.ts")
3000
+ ];
3001
+ const cliPath = candidates.find((candidate) => existsSync4(candidate));
3002
+ return cliPath ? [getBunPath(), "run", cliPath, "cloud", "sync"] : ["economy", "cloud", "sync"];
3003
+ }
3004
+ function scheduleEnvironment() {
3005
+ const keys = [
3006
+ "ECONOMY_CLOUD_DATABASE_URL",
3007
+ "HASNA_ECONOMY_CLOUD_DATABASE_URL",
3008
+ "HASNA_ECONOMY_DB_PATH",
3009
+ "ECONOMY_DB",
3010
+ "ECONOMY_MACHINE_ID",
3011
+ "ECONOMY_CLOUD_AUTO"
3012
+ ];
3013
+ const env = {
3014
+ HOME: homedir2(),
3015
+ PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin"
3016
+ };
3017
+ for (const key of keys) {
3018
+ const value = process.env[key];
3019
+ if (value)
3020
+ env[key] = value;
3021
+ }
3022
+ return env;
3023
+ }
3024
+ function xmlEscape(value) {
3025
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3026
+ }
3027
+ function getLaunchdPlistPath() {
3028
+ return join3(homedir2(), "Library", "LaunchAgents", "com.hasna.economy-cloud-sync.plist");
3029
+ }
3030
+ function createLaunchdPlist(intervalMinutes) {
3031
+ const args = getEconomySyncCommand();
3032
+ const env = scheduleEnvironment();
3033
+ const stdout = join3(SCHEDULE_CONFIG_DIR, "cloud-sync.log");
3034
+ const stderr = join3(SCHEDULE_CONFIG_DIR, "cloud-sync-error.log");
3035
+ const programArgs = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join(`
3036
+ `);
3037
+ const environment = Object.entries(env).map(([key, value]) => ` <key>${xmlEscape(key)}</key>
3038
+ <string>${xmlEscape(value)}</string>`).join(`
3039
+ `);
3040
+ return `<?xml version="1.0" encoding="UTF-8"?>
3041
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3042
+ <plist version="1.0">
3043
+ <dict>
3044
+ <key>Label</key>
3045
+ <string>com.hasna.economy-cloud-sync</string>
3046
+ <key>ProgramArguments</key>
3047
+ <array>
3048
+ ${programArgs}
3049
+ </array>
3050
+ <key>StartInterval</key>
3051
+ <integer>${Math.round(intervalMinutes * 60)}</integer>
3052
+ <key>RunAtLoad</key>
3053
+ <true/>
3054
+ <key>StandardOutPath</key>
3055
+ <string>${xmlEscape(stdout)}</string>
3056
+ <key>StandardErrorPath</key>
3057
+ <string>${xmlEscape(stderr)}</string>
3058
+ <key>EnvironmentVariables</key>
3059
+ <dict>
3060
+ ${environment}
3061
+ </dict>
3062
+ </dict>
3063
+ </plist>`;
3064
+ }
3065
+ async function registerLaunchd(intervalMinutes) {
3066
+ const plistPath = getLaunchdPlistPath();
3067
+ mkdirSync3(dirname2(plistPath), { recursive: true });
3068
+ try {
3069
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
3070
+ } catch {}
3071
+ writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
3072
+ await Bun.spawn(["launchctl", "load", plistPath]).exited;
3073
+ }
3074
+ async function removeLaunchd() {
3075
+ const plistPath = getLaunchdPlistPath();
3076
+ try {
3077
+ await Bun.spawn(["launchctl", "unload", plistPath]).exited;
3078
+ } catch {}
3079
+ try {
3080
+ unlinkSync(plistPath);
3081
+ } catch {}
3082
+ }
3083
+ function shellArg(value) {
3084
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
3085
+ }
3086
+ function getSystemdDir() {
3087
+ return join3(homedir2(), ".config", "systemd", "user");
3088
+ }
3089
+ function createSystemdService() {
3090
+ const command = getEconomySyncCommand().map(shellArg).join(" ");
3091
+ const environment = Object.entries(scheduleEnvironment()).map(([key, value]) => `Environment=${key}=${shellArg(value)}`).join(`
3092
+ `);
3093
+ return `[Unit]
3094
+ Description=Hasna Economy Cloud Sync
3095
+ After=network.target
3096
+
3097
+ [Service]
3098
+ Type=oneshot
3099
+ ExecStart=${command}
3100
+ ${environment}
3101
+
3102
+ [Install]
3103
+ WantedBy=default.target
3104
+ `;
3105
+ }
3106
+ function createSystemdTimer(intervalMinutes) {
3107
+ return `[Unit]
3108
+ Description=Hasna Economy Cloud Sync Timer
3109
+
3110
+ [Timer]
3111
+ OnBootSec=${intervalMinutes}min
3112
+ OnUnitActiveSec=${intervalMinutes}min
3113
+ Persistent=true
3114
+
3115
+ [Install]
3116
+ WantedBy=timers.target
3117
+ `;
3118
+ }
3119
+ async function registerSystemd(intervalMinutes) {
3120
+ const dir = getSystemdDir();
3121
+ mkdirSync3(dir, { recursive: true });
3122
+ writeFileSync2(join3(dir, `${SCHEDULE_SERVICE_NAME}.service`), createSystemdService());
3123
+ writeFileSync2(join3(dir, `${SCHEDULE_SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
3124
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
3125
+ await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
3126
+ }
3127
+ async function removeSystemd() {
3128
+ try {
3129
+ await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
3130
+ } catch {}
3131
+ const dir = getSystemdDir();
3132
+ try {
3133
+ unlinkSync(join3(dir, `${SCHEDULE_SERVICE_NAME}.service`));
3134
+ } catch {}
3135
+ try {
3136
+ unlinkSync(join3(dir, `${SCHEDULE_SERVICE_NAME}.timer`));
3137
+ } catch {}
3138
+ try {
3139
+ await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
3140
+ } catch {}
3141
+ }
2161
3142
  // src/ingest/claude.ts
2162
3143
  init_database();
2163
3144
  init_pricing();
2164
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
2165
- import { homedir as homedir2 } from "os";
2166
- import { join as join3, basename } from "path";
3145
+ import { readdirSync as readdirSync2, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync2 } from "fs";
3146
+ import { homedir as homedir3 } from "os";
3147
+ import { join as join4, basename } from "path";
2167
3148
 
2168
3149
  // src/lib/savings.ts
2169
3150
  function defaultCostBasisForAgent(agent) {
@@ -2310,8 +3291,8 @@ function withAccount(record, account) {
2310
3291
  function autoDetectProject(cwd, projects) {
2311
3292
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
2312
3293
  }
2313
- var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
2314
- var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
3294
+ var CLAUDE_PROJECTS_DIR = join4(homedir3(), ".claude", "projects");
3295
+ var TAKUMI_PROJECTS_DIR = join4(homedir3(), ".takumi", "projects");
2315
3296
  function dirNameToPath(dirName) {
2316
3297
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
2317
3298
  }
@@ -2321,9 +3302,9 @@ function collectJsonlFiles(projectDir) {
2321
3302
  try {
2322
3303
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2323
3304
  if (entry.isDirectory())
2324
- walk(join3(dir, entry.name));
3305
+ walk(join4(dir, entry.name));
2325
3306
  else if (entry.name.endsWith(".jsonl"))
2326
- files.push(join3(dir, entry.name));
3307
+ files.push(join4(dir, entry.name));
2327
3308
  }
2328
3309
  } catch {}
2329
3310
  }
@@ -2337,7 +3318,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
2337
3318
  return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
2338
3319
  }
2339
3320
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
2340
- if (!existsSync4(projectsDir)) {
3321
+ if (!existsSync5(projectsDir)) {
2341
3322
  if (verbose)
2342
3323
  console.log(`${agentName} projects dir not found:`, projectsDir);
2343
3324
  return { files: 0, requests: 0, sessions: 0 };
@@ -2350,7 +3331,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2350
3331
  const account = await resolveAccountForAgent(agentName);
2351
3332
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2352
3333
  for (const projectDirEntry of projectDirs) {
2353
- const projectDirPath = join3(projectsDir, projectDirEntry.name);
3334
+ const projectDirPath = join4(projectsDir, projectDirEntry.name);
2354
3335
  const projectPath = dirNameToPath(projectDirEntry.name);
2355
3336
  const jsonlFiles = collectJsonlFiles(projectDirPath);
2356
3337
  for (const filePath of jsonlFiles) {
@@ -2366,7 +3347,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2366
3347
  continue;
2367
3348
  let lines;
2368
3349
  try {
2369
- lines = readFileSync3(filePath, "utf-8").split(`
3350
+ lines = readFileSync4(filePath, "utf-8").split(`
2370
3351
  `).filter((l) => l.trim());
2371
3352
  } catch {
2372
3353
  continue;
@@ -2485,12 +3466,12 @@ function supportsClaudeDataResidencyPricing(model) {
2485
3466
  // src/ingest/codex.ts
2486
3467
  init_database();
2487
3468
  init_pricing();
2488
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2489
- import { homedir as homedir3 } from "os";
2490
- import { join as join4, basename as basename2 } from "path";
2491
- import { Database as BunDatabase2 } from "bun:sqlite";
2492
- var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
2493
- var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
3469
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3470
+ import { homedir as homedir4 } from "os";
3471
+ import { join as join5, basename as basename2 } from "path";
3472
+ import { Database as BunDatabase3 } from "bun:sqlite";
3473
+ var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
3474
+ var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
2494
3475
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2495
3476
  function codexDbPath() {
2496
3477
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
@@ -2500,10 +3481,10 @@ function codexConfigPath() {
2500
3481
  }
2501
3482
  function readCodexModel() {
2502
3483
  const configPath = codexConfigPath();
2503
- if (!existsSync5(configPath))
3484
+ if (!existsSync6(configPath))
2504
3485
  return "gpt-5-codex";
2505
3486
  try {
2506
- const content = readFileSync4(configPath, "utf-8");
3487
+ const content = readFileSync5(configPath, "utf-8");
2507
3488
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
2508
3489
  return match?.[1] ?? "gpt-5-codex";
2509
3490
  } catch {
@@ -2526,7 +3507,7 @@ function openCodexDb(dbPath, verbose) {
2526
3507
  for (const readonly of [true, false]) {
2527
3508
  let codexDb = null;
2528
3509
  try {
2529
- codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
3510
+ codexDb = readonly ? new BunDatabase3(dbPath, { readonly: true }) : new BunDatabase3(dbPath);
2530
3511
  codexDb.prepare("PRAGMA schema_version").get();
2531
3512
  return codexDb;
2532
3513
  } catch (error) {
@@ -2541,12 +3522,12 @@ function openCodexDb(dbPath, verbose) {
2541
3522
  return null;
2542
3523
  }
2543
3524
  function readTokenEvents(rolloutPath) {
2544
- if (!rolloutPath || !existsSync5(rolloutPath))
3525
+ if (!rolloutPath || !existsSync6(rolloutPath))
2545
3526
  return [];
2546
3527
  const fallbackUsages = new Map;
2547
3528
  let fallbackTimestamp;
2548
3529
  let aggregate = null;
2549
- for (const line of readFileSync4(rolloutPath, "utf-8").split(`
3530
+ for (const line of readFileSync5(rolloutPath, "utf-8").split(`
2550
3531
  `)) {
2551
3532
  if (!line.trim())
2552
3533
  continue;
@@ -2618,7 +3599,7 @@ function fallbackEvents(totalTokens) {
2618
3599
  }
2619
3600
  async function ingestCodex(db, verbose = false) {
2620
3601
  const dbPath = codexDbPath();
2621
- if (!existsSync5(dbPath)) {
3602
+ if (!existsSync6(dbPath)) {
2622
3603
  if (verbose)
2623
3604
  console.log("Codex DB not found:", dbPath);
2624
3605
  return { sessions: 0, requests: 0 };
@@ -2701,11 +3682,11 @@ async function ingestCodex(db, verbose = false) {
2701
3682
  // src/ingest/gemini.ts
2702
3683
  init_database();
2703
3684
  init_pricing();
2704
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
2705
- import { homedir as homedir4 } from "os";
2706
- import { join as join5, basename as basename3 } from "path";
2707
- var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
2708
- var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
3685
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync3 } from "fs";
3686
+ import { homedir as homedir5 } from "os";
3687
+ import { join as join6, basename as basename3 } from "path";
3688
+ var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
3689
+ var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
2709
3690
  function geminiTmpDir() {
2710
3691
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2711
3692
  }
@@ -2722,12 +3703,12 @@ function numberField(...values) {
2722
3703
  function listProjectDirs(...roots) {
2723
3704
  const dirs = new Set;
2724
3705
  for (const root of roots) {
2725
- if (!existsSync6(root))
3706
+ if (!existsSync7(root))
2726
3707
  continue;
2727
3708
  try {
2728
3709
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
2729
3710
  if (entry.isDirectory())
2730
- dirs.add(join5(root, entry.name));
3711
+ dirs.add(join6(root, entry.name));
2731
3712
  }
2732
3713
  } catch {}
2733
3714
  }
@@ -2738,17 +3719,17 @@ function projectRoot(projectDir, chatData) {
2738
3719
  return chatData.projectPath;
2739
3720
  if (chatData.project_path)
2740
3721
  return chatData.project_path;
2741
- const rootFile = join5(projectDir, ".project_root");
3722
+ const rootFile = join6(projectDir, ".project_root");
2742
3723
  try {
2743
- if (existsSync6(rootFile))
2744
- return readFileSync5(rootFile, "utf-8").trim();
3724
+ if (existsSync7(rootFile))
3725
+ return readFileSync6(rootFile, "utf-8").trim();
2745
3726
  } catch {}
2746
3727
  return "";
2747
3728
  }
2748
3729
  async function ingestGemini(db, verbose) {
2749
3730
  const tmpDir = geminiTmpDir();
2750
3731
  const historyDir = geminiHistoryDir();
2751
- if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
3732
+ if (!existsSync7(tmpDir) && !existsSync7(historyDir)) {
2752
3733
  if (verbose)
2753
3734
  console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
2754
3735
  return { sessions: 0, requests: 0 };
@@ -2760,17 +3741,17 @@ async function ingestGemini(db, verbose) {
2760
3741
  const account = await resolveAccountForAgent("gemini");
2761
3742
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2762
3743
  for (const projectDir of projectDirs) {
2763
- const chatsDir = join5(projectDir, "chats");
2764
- if (!existsSync6(chatsDir))
3744
+ const chatsDir = join6(projectDir, "chats");
3745
+ if (!existsSync7(chatsDir))
2765
3746
  continue;
2766
3747
  let chatFiles = [];
2767
3748
  try {
2768
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
3749
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
2769
3750
  } catch {
2770
3751
  continue;
2771
3752
  }
2772
3753
  for (const filePath of chatFiles) {
2773
- const stateKey = filePath.replace(homedir4(), "~");
3754
+ const stateKey = filePath.replace(homedir5(), "~");
2774
3755
  let fileMtime = "0";
2775
3756
  try {
2776
3757
  fileMtime = statSync3(filePath).mtimeMs.toString();
@@ -2782,7 +3763,7 @@ async function ingestGemini(db, verbose) {
2782
3763
  continue;
2783
3764
  let chatData;
2784
3765
  try {
2785
- chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
3766
+ chatData = JSON.parse(readFileSync6(filePath, "utf-8"));
2786
3767
  } catch {
2787
3768
  continue;
2788
3769
  }
@@ -2866,11 +3847,18 @@ export {
2866
3847
  upsertGoal,
2867
3848
  upsertBudget,
2868
3849
  upsertBillingDaily,
3850
+ syncPush,
3851
+ syncPull,
2869
3852
  syncOpenProjectsRegistry,
3853
+ shouldPullFromCloud,
3854
+ setLastCloudPull,
2870
3855
  setIngestState,
2871
3856
  setActiveModel,
2872
3857
  seedModelPricing,
3858
+ runCloudMigrations,
2873
3859
  rollupSession,
3860
+ removeCloudSchedule,
3861
+ registerCloudSchedule,
2874
3862
  readCodexModel,
2875
3863
  queryUsageSnapshots,
2876
3864
  queryTopSessions,
@@ -2887,6 +3875,8 @@ export {
2887
3875
  openDatabase,
2888
3876
  normalizeModelName,
2889
3877
  mergePeerDatabase,
3878
+ maybePushAfterIngest,
3879
+ maybePullFromCloud,
2890
3880
  listSubscriptions,
2891
3881
  listProjects,
2892
3882
  listModelPricing,
@@ -2894,6 +3884,8 @@ export {
2894
3884
  listMachineRegistry,
2895
3885
  listGoals,
2896
3886
  listBudgets,
3887
+ isCloudIncrementalEnabled,
3888
+ isCloudAutoEnabled,
2897
3889
  isAgent,
2898
3890
  ingestTakumi,
2899
3891
  ingestJsonlProjects,
@@ -2905,10 +3897,15 @@ export {
2905
3897
  getPricing,
2906
3898
  getModelPricing,
2907
3899
  getMachineId,
3900
+ getLastCloudPull,
2908
3901
  getIngestState,
2909
3902
  getGoalStatuses,
2910
3903
  getDbPath,
2911
3904
  getDataDir,
3905
+ getCloudScheduleStatus,
3906
+ getCloudPullIntervalMinutes,
3907
+ getCloudPg,
3908
+ getCloudDatabaseUrl,
2912
3909
  getBudgetStatuses,
2913
3910
  getActiveModel,
2914
3911
  gatherTrainingData,
@@ -2921,10 +3918,16 @@ export {
2921
3918
  dedupeRequests,
2922
3919
  computeCostFromDb,
2923
3920
  computeCost,
3921
+ cloudSyncFull,
3922
+ cloudPush,
3923
+ cloudPull,
2924
3924
  clearBillingRange,
2925
3925
  clearActiveModel,
3926
+ SqliteAdapter,
3927
+ PgAdapterAsync,
2926
3928
  DEFAULT_PRICING,
2927
3929
  DEFAULT_MODEL,
2928
3930
  COST_BASIS,
3931
+ CLOUD_TABLES,
2929
3932
  AGENTS
2930
3933
  };