@hasna/economy 0.2.31 → 0.2.32

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 +184 -832
  5. package/dist/db/database.d.ts +1 -3
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/index.d.ts +0 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +50 -1049
  10. package/dist/ingest/billing.d.ts +1 -1
  11. package/dist/ingest/billing.d.ts.map +1 -1
  12. package/dist/ingest/claude-quota.d.ts +1 -1
  13. package/dist/ingest/claude-quota.d.ts.map +1 -1
  14. package/dist/ingest/claude.d.ts +1 -1
  15. package/dist/ingest/claude.d.ts.map +1 -1
  16. package/dist/ingest/codex-quota.d.ts +1 -1
  17. package/dist/ingest/codex-quota.d.ts.map +1 -1
  18. package/dist/ingest/codex.d.ts +1 -1
  19. package/dist/ingest/codex.d.ts.map +1 -1
  20. package/dist/ingest/cursor.d.ts +1 -1
  21. package/dist/ingest/cursor.d.ts.map +1 -1
  22. package/dist/ingest/gemini.d.ts +1 -1
  23. package/dist/ingest/gemini.d.ts.map +1 -1
  24. package/dist/ingest/hermes.d.ts +1 -1
  25. package/dist/ingest/hermes.d.ts.map +1 -1
  26. package/dist/ingest/opencode.d.ts +1 -1
  27. package/dist/ingest/opencode.d.ts.map +1 -1
  28. package/dist/ingest/otel.d.ts +1 -1
  29. package/dist/ingest/otel.d.ts.map +1 -1
  30. package/dist/ingest/pi.d.ts +1 -1
  31. package/dist/ingest/pi.d.ts.map +1 -1
  32. package/dist/ingest/plugin.d.ts +1 -1
  33. package/dist/ingest/plugin.d.ts.map +1 -1
  34. package/dist/lib/billing-diff.d.ts +1 -1
  35. package/dist/lib/billing-diff.d.ts.map +1 -1
  36. package/dist/lib/cloud-sync.d.ts +2 -9
  37. package/dist/lib/cloud-sync.d.ts.map +1 -1
  38. package/dist/lib/open-projects.d.ts +2 -3
  39. package/dist/lib/open-projects.d.ts.map +1 -1
  40. package/dist/lib/peer-sync.d.ts +1 -1
  41. package/dist/lib/peer-sync.d.ts.map +1 -1
  42. package/dist/lib/pricing.d.ts +1 -1
  43. package/dist/lib/pricing.d.ts.map +1 -1
  44. package/dist/lib/savings.d.ts +1 -1
  45. package/dist/lib/savings.d.ts.map +1 -1
  46. package/dist/lib/spikes.d.ts +1 -1
  47. package/dist/lib/spikes.d.ts.map +1 -1
  48. package/dist/lib/sync-all.d.ts +1 -1
  49. package/dist/lib/sync-all.d.ts.map +1 -1
  50. package/dist/lib/webhooks.d.ts +1 -1
  51. package/dist/lib/webhooks.d.ts.map +1 -1
  52. package/dist/mcp/index.js +34 -514
  53. package/dist/mcp/server.d.ts.map +1 -1
  54. package/dist/otel/index.js +15 -442
  55. package/dist/server/index.js +46 -492
  56. package/dist/server/serve.d.ts +1 -1
  57. package/dist/server/serve.d.ts.map +1 -1
  58. package/package.json +6 -5
  59. package/dist/db/storage-adapter.d.ts +0 -34
  60. package/dist/db/storage-adapter.d.ts.map +0 -1
  61. package/dist/lib/remote-storage.d.ts +0 -15
  62. package/dist/lib/remote-storage.d.ts.map +0 -1
  63. package/dist/lib/storage-sync.d.ts +0 -27
  64. package/dist/lib/storage-sync.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -16,60 +16,6 @@ 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
-
73
19
  // src/lib/pricing.ts
74
20
  var exports_pricing = {};
75
21
  __export(exports_pricing, {
@@ -566,6 +512,7 @@ var init_pricing = __esm(() => {
566
512
  });
567
513
 
568
514
  // src/db/database.ts
515
+ import { SqliteAdapter as Database } from "@hasna/cloud";
569
516
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
570
517
  import { hostname } from "os";
571
518
  import { homedir } from "os";
@@ -608,7 +555,7 @@ function openDatabase(dbPath, skipSeed = false) {
608
555
  if (dir && !existsSync(dir))
609
556
  mkdirSync(dir, { recursive: true });
610
557
  }
611
- const db = new SqliteAdapter(path);
558
+ const db = new Database(path);
612
559
  db.exec("PRAGMA journal_mode = WAL");
613
560
  db.exec("PRAGMA busy_timeout = 5000");
614
561
  db.exec("PRAGMA foreign_keys = ON");
@@ -1644,198 +1591,7 @@ function dedupeRequests(db) {
1644
1591
  }
1645
1592
  return removed;
1646
1593
  }
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
- });
1594
+ var init_database = () => {};
1839
1595
 
1840
1596
  // src/lib/agents.ts
1841
1597
  var AGENTS = [
@@ -2088,26 +1844,22 @@ function clearActiveModel() {
2088
1844
  // src/lib/open-projects.ts
2089
1845
  init_database();
2090
1846
  async function syncOpenProjectsRegistry(db, listActiveProjects) {
2091
- let listOpenProjects = listActiveProjects;
2092
- if (!listOpenProjects) {
1847
+ let listProjects2 = listActiveProjects;
1848
+ if (!listProjects2) {
2093
1849
  const projectsApi = await import("@hasna/projects");
2094
- listOpenProjects = projectsApi.listProjects ?? projectsApi.listWorkspaces;
1850
+ listProjects2 = projectsApi.listProjects;
2095
1851
  }
2096
- if (!listOpenProjects) {
2097
- throw new Error("@hasna/projects does not expose listWorkspaces or listProjects");
2098
- }
2099
- const projects = listOpenProjects({ status: "active", limit: 5000 });
1852
+ const projects = listProjects2({ status: "active", limit: 5000 });
2100
1853
  let imported = 0;
2101
1854
  let skipped = 0;
2102
1855
  for (const project of projects) {
2103
- const path = project.path ?? project.primary_path ?? "";
2104
- if (!path) {
1856
+ if (!project.path) {
2105
1857
  skipped++;
2106
1858
  continue;
2107
1859
  }
2108
1860
  upsertProject(db, {
2109
1861
  id: project.id,
2110
- path,
1862
+ path: project.path,
2111
1863
  name: project.name,
2112
1864
  description: project.description,
2113
1865
  tags: project.tags ?? [],
@@ -2119,7 +1871,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
2119
1871
  }
2120
1872
  // src/lib/peer-sync.ts
2121
1873
  init_database();
2122
- import { Database as BunDatabase2 } from "bun:sqlite";
1874
+ import { Database as BunDatabase } from "bun:sqlite";
2123
1875
  import { existsSync as existsSync3 } from "fs";
2124
1876
 
2125
1877
  // src/lib/package-metadata.ts
@@ -2364,9 +2116,9 @@ function ensureMachineRegistry(target, machine, now) {
2364
2116
  }
2365
2117
  function openSourceDatabase(path) {
2366
2118
  try {
2367
- return new BunDatabase2(path, { readonly: true });
2119
+ return new BunDatabase(path, { readonly: true });
2368
2120
  } catch {
2369
- return new BunDatabase2(path);
2121
+ return new BunDatabase(path);
2370
2122
  }
2371
2123
  }
2372
2124
  function mergePeerDatabase(target, sourcePath, opts = {}) {
@@ -2410,741 +2162,12 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
2410
2162
  tables: tables.filter((t) => t.inserted || t.updated || t.skipped || t.collisions)
2411
2163
  };
2412
2164
  }
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
- }
3142
2165
  // src/ingest/claude.ts
3143
2166
  init_database();
3144
2167
  init_pricing();
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";
2168
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
2169
+ import { homedir as homedir2 } from "os";
2170
+ import { join as join3, basename } from "path";
3148
2171
 
3149
2172
  // src/lib/savings.ts
3150
2173
  function defaultCostBasisForAgent(agent) {
@@ -3291,8 +2314,8 @@ function withAccount(record, account) {
3291
2314
  function autoDetectProject(cwd, projects) {
3292
2315
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
3293
2316
  }
3294
- var CLAUDE_PROJECTS_DIR = join4(homedir3(), ".claude", "projects");
3295
- var TAKUMI_PROJECTS_DIR = join4(homedir3(), ".takumi", "projects");
2317
+ var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
2318
+ var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
3296
2319
  function dirNameToPath(dirName) {
3297
2320
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
3298
2321
  }
@@ -3302,9 +2325,9 @@ function collectJsonlFiles(projectDir) {
3302
2325
  try {
3303
2326
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
3304
2327
  if (entry.isDirectory())
3305
- walk(join4(dir, entry.name));
2328
+ walk(join3(dir, entry.name));
3306
2329
  else if (entry.name.endsWith(".jsonl"))
3307
- files.push(join4(dir, entry.name));
2330
+ files.push(join3(dir, entry.name));
3308
2331
  }
3309
2332
  } catch {}
3310
2333
  }
@@ -3318,7 +2341,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
3318
2341
  return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
3319
2342
  }
3320
2343
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
3321
- if (!existsSync5(projectsDir)) {
2344
+ if (!existsSync4(projectsDir)) {
3322
2345
  if (verbose)
3323
2346
  console.log(`${agentName} projects dir not found:`, projectsDir);
3324
2347
  return { files: 0, requests: 0, sessions: 0 };
@@ -3331,7 +2354,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3331
2354
  const account = await resolveAccountForAgent(agentName);
3332
2355
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
3333
2356
  for (const projectDirEntry of projectDirs) {
3334
- const projectDirPath = join4(projectsDir, projectDirEntry.name);
2357
+ const projectDirPath = join3(projectsDir, projectDirEntry.name);
3335
2358
  const projectPath = dirNameToPath(projectDirEntry.name);
3336
2359
  const jsonlFiles = collectJsonlFiles(projectDirPath);
3337
2360
  for (const filePath of jsonlFiles) {
@@ -3347,7 +2370,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
3347
2370
  continue;
3348
2371
  let lines;
3349
2372
  try {
3350
- lines = readFileSync4(filePath, "utf-8").split(`
2373
+ lines = readFileSync3(filePath, "utf-8").split(`
3351
2374
  `).filter((l) => l.trim());
3352
2375
  } catch {
3353
2376
  continue;
@@ -3466,12 +2489,12 @@ function supportsClaudeDataResidencyPricing(model) {
3466
2489
  // src/ingest/codex.ts
3467
2490
  init_database();
3468
2491
  init_pricing();
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");
2492
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2493
+ import { homedir as homedir3 } from "os";
2494
+ import { join as join4, basename as basename2 } from "path";
2495
+ import { Database as BunDatabase2 } from "bun:sqlite";
2496
+ var DEFAULT_CODEX_DB_PATH = join4(homedir3(), ".codex", "state_5.sqlite");
2497
+ var DEFAULT_CODEX_CONFIG_PATH = join4(homedir3(), ".codex", "config.toml");
3475
2498
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
3476
2499
  function codexDbPath() {
3477
2500
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
@@ -3481,10 +2504,10 @@ function codexConfigPath() {
3481
2504
  }
3482
2505
  function readCodexModel() {
3483
2506
  const configPath = codexConfigPath();
3484
- if (!existsSync6(configPath))
2507
+ if (!existsSync5(configPath))
3485
2508
  return "gpt-5-codex";
3486
2509
  try {
3487
- const content = readFileSync5(configPath, "utf-8");
2510
+ const content = readFileSync4(configPath, "utf-8");
3488
2511
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
3489
2512
  return match?.[1] ?? "gpt-5-codex";
3490
2513
  } catch {
@@ -3507,7 +2530,7 @@ function openCodexDb(dbPath, verbose) {
3507
2530
  for (const readonly of [true, false]) {
3508
2531
  let codexDb = null;
3509
2532
  try {
3510
- codexDb = readonly ? new BunDatabase3(dbPath, { readonly: true }) : new BunDatabase3(dbPath);
2533
+ codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
3511
2534
  codexDb.prepare("PRAGMA schema_version").get();
3512
2535
  return codexDb;
3513
2536
  } catch (error) {
@@ -3522,12 +2545,12 @@ function openCodexDb(dbPath, verbose) {
3522
2545
  return null;
3523
2546
  }
3524
2547
  function readTokenEvents(rolloutPath) {
3525
- if (!rolloutPath || !existsSync6(rolloutPath))
2548
+ if (!rolloutPath || !existsSync5(rolloutPath))
3526
2549
  return [];
3527
2550
  const fallbackUsages = new Map;
3528
2551
  let fallbackTimestamp;
3529
2552
  let aggregate = null;
3530
- for (const line of readFileSync5(rolloutPath, "utf-8").split(`
2553
+ for (const line of readFileSync4(rolloutPath, "utf-8").split(`
3531
2554
  `)) {
3532
2555
  if (!line.trim())
3533
2556
  continue;
@@ -3599,7 +2622,7 @@ function fallbackEvents(totalTokens) {
3599
2622
  }
3600
2623
  async function ingestCodex(db, verbose = false) {
3601
2624
  const dbPath = codexDbPath();
3602
- if (!existsSync6(dbPath)) {
2625
+ if (!existsSync5(dbPath)) {
3603
2626
  if (verbose)
3604
2627
  console.log("Codex DB not found:", dbPath);
3605
2628
  return { sessions: 0, requests: 0 };
@@ -3682,11 +2705,11 @@ async function ingestCodex(db, verbose = false) {
3682
2705
  // src/ingest/gemini.ts
3683
2706
  init_database();
3684
2707
  init_pricing();
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");
2708
+ import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
2709
+ import { homedir as homedir4 } from "os";
2710
+ import { join as join5, basename as basename3 } from "path";
2711
+ var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
2712
+ var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
3690
2713
  function geminiTmpDir() {
3691
2714
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
3692
2715
  }
@@ -3703,12 +2726,12 @@ function numberField(...values) {
3703
2726
  function listProjectDirs(...roots) {
3704
2727
  const dirs = new Set;
3705
2728
  for (const root of roots) {
3706
- if (!existsSync7(root))
2729
+ if (!existsSync6(root))
3707
2730
  continue;
3708
2731
  try {
3709
2732
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
3710
2733
  if (entry.isDirectory())
3711
- dirs.add(join6(root, entry.name));
2734
+ dirs.add(join5(root, entry.name));
3712
2735
  }
3713
2736
  } catch {}
3714
2737
  }
@@ -3719,17 +2742,17 @@ function projectRoot(projectDir, chatData) {
3719
2742
  return chatData.projectPath;
3720
2743
  if (chatData.project_path)
3721
2744
  return chatData.project_path;
3722
- const rootFile = join6(projectDir, ".project_root");
2745
+ const rootFile = join5(projectDir, ".project_root");
3723
2746
  try {
3724
- if (existsSync7(rootFile))
3725
- return readFileSync6(rootFile, "utf-8").trim();
2747
+ if (existsSync6(rootFile))
2748
+ return readFileSync5(rootFile, "utf-8").trim();
3726
2749
  } catch {}
3727
2750
  return "";
3728
2751
  }
3729
2752
  async function ingestGemini(db, verbose) {
3730
2753
  const tmpDir = geminiTmpDir();
3731
2754
  const historyDir = geminiHistoryDir();
3732
- if (!existsSync7(tmpDir) && !existsSync7(historyDir)) {
2755
+ if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
3733
2756
  if (verbose)
3734
2757
  console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
3735
2758
  return { sessions: 0, requests: 0 };
@@ -3741,17 +2764,17 @@ async function ingestGemini(db, verbose) {
3741
2764
  const account = await resolveAccountForAgent("gemini");
3742
2765
  const projectDirs = listProjectDirs(tmpDir, historyDir);
3743
2766
  for (const projectDir of projectDirs) {
3744
- const chatsDir = join6(projectDir, "chats");
3745
- if (!existsSync7(chatsDir))
2767
+ const chatsDir = join5(projectDir, "chats");
2768
+ if (!existsSync6(chatsDir))
3746
2769
  continue;
3747
2770
  let chatFiles = [];
3748
2771
  try {
3749
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
2772
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
3750
2773
  } catch {
3751
2774
  continue;
3752
2775
  }
3753
2776
  for (const filePath of chatFiles) {
3754
- const stateKey = filePath.replace(homedir5(), "~");
2777
+ const stateKey = filePath.replace(homedir4(), "~");
3755
2778
  let fileMtime = "0";
3756
2779
  try {
3757
2780
  fileMtime = statSync3(filePath).mtimeMs.toString();
@@ -3763,7 +2786,7 @@ async function ingestGemini(db, verbose) {
3763
2786
  continue;
3764
2787
  let chatData;
3765
2788
  try {
3766
- chatData = JSON.parse(readFileSync6(filePath, "utf-8"));
2789
+ chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
3767
2790
  } catch {
3768
2791
  continue;
3769
2792
  }
@@ -3847,18 +2870,11 @@ export {
3847
2870
  upsertGoal,
3848
2871
  upsertBudget,
3849
2872
  upsertBillingDaily,
3850
- syncPush,
3851
- syncPull,
3852
2873
  syncOpenProjectsRegistry,
3853
- shouldPullFromCloud,
3854
- setLastCloudPull,
3855
2874
  setIngestState,
3856
2875
  setActiveModel,
3857
2876
  seedModelPricing,
3858
- runCloudMigrations,
3859
2877
  rollupSession,
3860
- removeCloudSchedule,
3861
- registerCloudSchedule,
3862
2878
  readCodexModel,
3863
2879
  queryUsageSnapshots,
3864
2880
  queryTopSessions,
@@ -3875,8 +2891,6 @@ export {
3875
2891
  openDatabase,
3876
2892
  normalizeModelName,
3877
2893
  mergePeerDatabase,
3878
- maybePushAfterIngest,
3879
- maybePullFromCloud,
3880
2894
  listSubscriptions,
3881
2895
  listProjects,
3882
2896
  listModelPricing,
@@ -3884,8 +2898,6 @@ export {
3884
2898
  listMachineRegistry,
3885
2899
  listGoals,
3886
2900
  listBudgets,
3887
- isCloudIncrementalEnabled,
3888
- isCloudAutoEnabled,
3889
2901
  isAgent,
3890
2902
  ingestTakumi,
3891
2903
  ingestJsonlProjects,
@@ -3897,15 +2909,10 @@ export {
3897
2909
  getPricing,
3898
2910
  getModelPricing,
3899
2911
  getMachineId,
3900
- getLastCloudPull,
3901
2912
  getIngestState,
3902
2913
  getGoalStatuses,
3903
2914
  getDbPath,
3904
2915
  getDataDir,
3905
- getCloudScheduleStatus,
3906
- getCloudPullIntervalMinutes,
3907
- getCloudPg,
3908
- getCloudDatabaseUrl,
3909
2916
  getBudgetStatuses,
3910
2917
  getActiveModel,
3911
2918
  gatherTrainingData,
@@ -3918,16 +2925,10 @@ export {
3918
2925
  dedupeRequests,
3919
2926
  computeCostFromDb,
3920
2927
  computeCost,
3921
- cloudSyncFull,
3922
- cloudPush,
3923
- cloudPull,
3924
2928
  clearBillingRange,
3925
2929
  clearActiveModel,
3926
- SqliteAdapter,
3927
- PgAdapterAsync,
3928
2930
  DEFAULT_PRICING,
3929
2931
  DEFAULT_MODEL,
3930
2932
  COST_BASIS,
3931
- CLOUD_TABLES,
3932
2933
  AGENTS
3933
2934
  };