@hasna/economy 0.2.31 → 0.2.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/index.js +184 -832
- package/dist/db/database.d.ts +1 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -1049
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +1 -1
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/cloud-sync.d.ts +2 -9
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +2 -3
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/peer-sync.d.ts +1 -1
- package/dist/lib/peer-sync.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +1 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +34 -514
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +15 -442
- package/dist/server/index.js +46 -492
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/db/storage-adapter.d.ts +0 -34
- package/dist/db/storage-adapter.d.ts.map +0 -1
- package/dist/lib/remote-storage.d.ts +0 -15
- package/dist/lib/remote-storage.d.ts.map +0 -1
- package/dist/lib/storage-sync.d.ts +0 -27
- 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
|
|
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 =
|
|
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
|
|
2092
|
-
if (!
|
|
1847
|
+
let listProjects2 = listActiveProjects;
|
|
1848
|
+
if (!listProjects2) {
|
|
2093
1849
|
const projectsApi = await import("@hasna/projects");
|
|
2094
|
-
|
|
1850
|
+
listProjects2 = projectsApi.listProjects;
|
|
2095
1851
|
}
|
|
2096
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2119
|
+
return new BunDatabase(path, { readonly: true });
|
|
2368
2120
|
} catch {
|
|
2369
|
-
return new
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
3146
|
-
import { homedir as
|
|
3147
|
-
import { join as
|
|
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 =
|
|
3295
|
-
var TAKUMI_PROJECTS_DIR =
|
|
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(
|
|
2328
|
+
walk(join3(dir, entry.name));
|
|
3306
2329
|
else if (entry.name.endsWith(".jsonl"))
|
|
3307
|
-
files.push(
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
|
3470
|
-
import { homedir as
|
|
3471
|
-
import { join as
|
|
3472
|
-
import { Database as
|
|
3473
|
-
var DEFAULT_CODEX_DB_PATH =
|
|
3474
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
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 (!
|
|
2507
|
+
if (!existsSync5(configPath))
|
|
3485
2508
|
return "gpt-5-codex";
|
|
3486
2509
|
try {
|
|
3487
|
-
const content =
|
|
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
|
|
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 || !
|
|
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
|
|
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 (!
|
|
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
|
|
3686
|
-
import { homedir as
|
|
3687
|
-
import { join as
|
|
3688
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
3689
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
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 (!
|
|
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(
|
|
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 =
|
|
2745
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
3723
2746
|
try {
|
|
3724
|
-
if (
|
|
3725
|
-
return
|
|
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 (!
|
|
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 =
|
|
3745
|
-
if (!
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
};
|