@hasna/economy 0.2.26 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +440 -81
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +444 -86
- package/dist/lib/peer-sync.d.ts +21 -0
- package/dist/lib/peer-sync.d.ts.map +1 -0
- package/dist/mcp/index.js +132 -68
- package/dist/otel/index.js +35 -43
- package/dist/server/index.js +133 -69
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -615,6 +615,26 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
615
615
|
}
|
|
616
616
|
return db;
|
|
617
617
|
}
|
|
618
|
+
function quoteSqlIdent(identifier) {
|
|
619
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
620
|
+
}
|
|
621
|
+
function hasColumn(db, table, column) {
|
|
622
|
+
const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
|
|
623
|
+
return columns.some((c) => c.name === column);
|
|
624
|
+
}
|
|
625
|
+
function addColumnIfMissing(db, table, column, definition) {
|
|
626
|
+
if (hasColumn(db, table, column))
|
|
627
|
+
return false;
|
|
628
|
+
try {
|
|
629
|
+
db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
|
|
630
|
+
return true;
|
|
631
|
+
} catch (error) {
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
if (/duplicate column name/i.test(message))
|
|
634
|
+
return true;
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
618
638
|
function initSchema(db) {
|
|
619
639
|
db.exec(`
|
|
620
640
|
CREATE TABLE IF NOT EXISTS requests (
|
|
@@ -785,59 +805,31 @@ function initSchema(db) {
|
|
|
785
805
|
CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
|
|
786
806
|
CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
|
|
787
807
|
`);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
792
|
-
}
|
|
793
|
-
if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
|
|
794
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
|
|
808
|
+
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
809
|
+
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
810
|
+
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
795
811
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
796
812
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
|
|
802
|
-
}
|
|
803
|
-
if (!cols.some((c) => c.name === "attribution_tag")) {
|
|
804
|
-
db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
805
|
-
}
|
|
806
|
-
if (!cols.some((c) => c.name === "updated_at")) {
|
|
807
|
-
db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
|
|
813
|
+
addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
|
|
814
|
+
addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
|
|
815
|
+
addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
|
|
816
|
+
if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
|
|
808
817
|
db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
|
|
809
818
|
}
|
|
810
|
-
|
|
811
|
-
db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
812
|
-
}
|
|
819
|
+
addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
|
|
813
820
|
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
814
|
-
|
|
815
|
-
db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
819
|
-
if (!sessionCols.some((c) => c.name === "attribution_tag")) {
|
|
820
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
|
|
821
|
+
addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
|
|
821
822
|
}
|
|
822
|
-
|
|
823
|
-
|
|
823
|
+
addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
|
|
824
|
+
if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
|
|
824
825
|
db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
825
826
|
}
|
|
826
|
-
|
|
827
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
|
|
828
|
-
}
|
|
827
|
+
addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
|
|
829
828
|
for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
|
|
830
|
-
|
|
831
|
-
db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
|
|
835
|
-
if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
|
|
836
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
|
|
837
|
-
}
|
|
838
|
-
if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
|
|
839
|
-
db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
|
|
829
|
+
addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
|
|
840
830
|
}
|
|
831
|
+
addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
|
|
832
|
+
addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
|
|
841
833
|
db.exec(`
|
|
842
834
|
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
843
835
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
@@ -998,17 +990,22 @@ function querySummary(db, period, machine, allMachines = false) {
|
|
|
998
990
|
const codexTotals = db.prepare(`
|
|
999
991
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1000
992
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
993
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1001
994
|
COUNT(*) as sessions
|
|
1002
995
|
FROM sessions
|
|
1003
996
|
WHERE ${sWhere}${machineClause}
|
|
1004
997
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1005
998
|
`).get();
|
|
1006
|
-
const
|
|
999
|
+
const requestSessionCount = db.prepare(`
|
|
1000
|
+
SELECT COUNT(DISTINCT session_id) as sessions
|
|
1001
|
+
FROM requests
|
|
1002
|
+
WHERE ${rWhere}${machineClause}
|
|
1003
|
+
`).get();
|
|
1007
1004
|
return {
|
|
1008
1005
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
1009
|
-
requests: r.requests,
|
|
1006
|
+
requests: r.requests + codexTotals.requests,
|
|
1010
1007
|
tokens: r.tokens + codexTotals.tokens,
|
|
1011
|
-
sessions:
|
|
1008
|
+
sessions: requestSessionCount.sessions + codexTotals.sessions,
|
|
1012
1009
|
period
|
|
1013
1010
|
};
|
|
1014
1011
|
}
|
|
@@ -1087,9 +1084,7 @@ function queryAgentBreakdown(db, period = "all") {
|
|
|
1087
1084
|
}
|
|
1088
1085
|
return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
|
|
1089
1086
|
}
|
|
1090
|
-
function
|
|
1091
|
-
if (projectName && projectName.trim() !== "")
|
|
1092
|
-
return projectName;
|
|
1087
|
+
function pathProjectLabel(projectPath) {
|
|
1093
1088
|
if (!projectPath)
|
|
1094
1089
|
return "";
|
|
1095
1090
|
const segments = projectPath.split("/").filter(Boolean);
|
|
@@ -1098,12 +1093,45 @@ function labelForPath(projectPath, projectName) {
|
|
|
1098
1093
|
if (projectPrefix.test(seg))
|
|
1099
1094
|
return seg;
|
|
1100
1095
|
}
|
|
1101
|
-
const generic = new Set([
|
|
1096
|
+
const generic = new Set([
|
|
1097
|
+
"web",
|
|
1098
|
+
"app",
|
|
1099
|
+
"apps",
|
|
1100
|
+
"packages",
|
|
1101
|
+
"src",
|
|
1102
|
+
"lib",
|
|
1103
|
+
"server",
|
|
1104
|
+
"client",
|
|
1105
|
+
"api",
|
|
1106
|
+
"frontend",
|
|
1107
|
+
"backend",
|
|
1108
|
+
"home",
|
|
1109
|
+
"users",
|
|
1110
|
+
"workspace",
|
|
1111
|
+
"workspaces",
|
|
1112
|
+
"hasna"
|
|
1113
|
+
]);
|
|
1102
1114
|
for (let i = segments.length - 1;i >= 0; i--) {
|
|
1103
1115
|
if (!generic.has(segments[i].toLowerCase()))
|
|
1104
1116
|
return segments[i];
|
|
1105
1117
|
}
|
|
1106
|
-
return
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
function isRepoLikeLabel(label) {
|
|
1121
|
+
return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
|
|
1122
|
+
}
|
|
1123
|
+
function labelForPath(projectPath, projectName) {
|
|
1124
|
+
const pathLabel = pathProjectLabel(projectPath);
|
|
1125
|
+
if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
|
|
1126
|
+
return pathLabel;
|
|
1127
|
+
if (projectName && projectName.trim() !== "")
|
|
1128
|
+
return projectName;
|
|
1129
|
+
if (pathLabel)
|
|
1130
|
+
return pathLabel;
|
|
1131
|
+
return projectPath;
|
|
1132
|
+
}
|
|
1133
|
+
function groupKeyForPath(projectPath, projectName) {
|
|
1134
|
+
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1107
1135
|
}
|
|
1108
1136
|
function queryProjectBreakdown(db, period = "all") {
|
|
1109
1137
|
const requestWhere = requestPeriodWhere(period);
|
|
@@ -1118,14 +1146,15 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1118
1146
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1119
1147
|
if (!label)
|
|
1120
1148
|
continue;
|
|
1121
|
-
const
|
|
1149
|
+
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1150
|
+
const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
|
|
1122
1151
|
g.sessionIds.push(s.id);
|
|
1123
1152
|
if (!g.samplePath)
|
|
1124
1153
|
g.samplePath = s.project_path;
|
|
1125
|
-
groups.set(
|
|
1154
|
+
groups.set(key, g);
|
|
1126
1155
|
}
|
|
1127
1156
|
const result = [];
|
|
1128
|
-
for (const
|
|
1157
|
+
for (const g of groups.values()) {
|
|
1129
1158
|
const placeholders = g.sessionIds.map(() => "?").join(",");
|
|
1130
1159
|
const reqStats = placeholders.length ? db.prepare(`
|
|
1131
1160
|
SELECT
|
|
@@ -1156,7 +1185,7 @@ function queryProjectBreakdown(db, period = "all") {
|
|
|
1156
1185
|
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1157
1186
|
result.push({
|
|
1158
1187
|
project_path: g.samplePath,
|
|
1159
|
-
project_name: label,
|
|
1188
|
+
project_name: g.label,
|
|
1160
1189
|
sessions: totalSessions,
|
|
1161
1190
|
requests: reqStats.requests + sessionOnlyStats.requests,
|
|
1162
1191
|
total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
|
|
@@ -1396,17 +1425,48 @@ function queryBillingSummary(db, period) {
|
|
|
1396
1425
|
}
|
|
1397
1426
|
return { total_usd: total, by_provider };
|
|
1398
1427
|
}
|
|
1399
|
-
function listMachines(db) {
|
|
1428
|
+
function listMachines(db, period = "all") {
|
|
1429
|
+
const rWhere = requestPeriodWhere(period);
|
|
1430
|
+
const sWhere = sessionPeriodWhere(period);
|
|
1400
1431
|
return db.prepare(`
|
|
1432
|
+
WITH request_stats AS (
|
|
1433
|
+
SELECT
|
|
1434
|
+
machine_id,
|
|
1435
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1436
|
+
COUNT(*) as requests,
|
|
1437
|
+
COALESCE(SUM(cost_usd), 0) as total_cost_usd,
|
|
1438
|
+
MAX(timestamp) as last_active
|
|
1439
|
+
FROM requests
|
|
1440
|
+
WHERE machine_id != ''
|
|
1441
|
+
AND ${rWhere}
|
|
1442
|
+
GROUP BY machine_id
|
|
1443
|
+
),
|
|
1444
|
+
session_only_stats AS (
|
|
1445
|
+
SELECT
|
|
1446
|
+
machine_id,
|
|
1447
|
+
COUNT(*) as sessions,
|
|
1448
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1449
|
+
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1450
|
+
MAX(started_at) as last_active
|
|
1451
|
+
FROM sessions
|
|
1452
|
+
WHERE machine_id != ''
|
|
1453
|
+
AND ${sWhere}
|
|
1454
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1455
|
+
GROUP BY machine_id
|
|
1456
|
+
),
|
|
1457
|
+
combined AS (
|
|
1458
|
+
SELECT * FROM request_stats
|
|
1459
|
+
UNION ALL
|
|
1460
|
+
SELECT * FROM session_only_stats
|
|
1461
|
+
)
|
|
1401
1462
|
SELECT
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
COALESCE((
|
|
1405
|
-
COALESCE(SUM(
|
|
1406
|
-
MAX(
|
|
1407
|
-
FROM
|
|
1408
|
-
|
|
1409
|
-
GROUP BY s.machine_id
|
|
1463
|
+
machine_id,
|
|
1464
|
+
COALESCE(SUM(sessions), 0) as sessions,
|
|
1465
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
1466
|
+
COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
|
|
1467
|
+
MAX(last_active) as last_active
|
|
1468
|
+
FROM combined
|
|
1469
|
+
GROUP BY machine_id
|
|
1410
1470
|
ORDER BY total_cost_usd DESC
|
|
1411
1471
|
`).all();
|
|
1412
1472
|
}
|
|
@@ -1488,17 +1548,21 @@ function listMachineRegistry(db) {
|
|
|
1488
1548
|
}
|
|
1489
1549
|
function dedupeRequests(db) {
|
|
1490
1550
|
const dupes = db.prepare(`
|
|
1491
|
-
SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1551
|
+
SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1492
1552
|
FROM requests
|
|
1493
1553
|
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1494
|
-
GROUP BY source_request_id, agent
|
|
1554
|
+
GROUP BY source_request_id, agent, COALESCE(machine_id, '')
|
|
1495
1555
|
HAVING cnt > 1
|
|
1496
1556
|
`).all();
|
|
1497
1557
|
let removed = 0;
|
|
1498
1558
|
for (const row of dupes) {
|
|
1499
1559
|
const result = db.prepare(`
|
|
1500
|
-
DELETE FROM requests
|
|
1501
|
-
|
|
1560
|
+
DELETE FROM requests
|
|
1561
|
+
WHERE source_request_id = ?
|
|
1562
|
+
AND agent = ?
|
|
1563
|
+
AND COALESCE(machine_id, '') = ?
|
|
1564
|
+
AND id != ?
|
|
1565
|
+
`).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
|
|
1502
1566
|
removed += result.changes;
|
|
1503
1567
|
}
|
|
1504
1568
|
return removed;
|
|
@@ -4028,7 +4092,7 @@ __export(exports_config, {
|
|
|
4028
4092
|
loadConfig: () => loadConfig2,
|
|
4029
4093
|
getConfigValue: () => getConfigValue
|
|
4030
4094
|
});
|
|
4031
|
-
import { existsSync as
|
|
4095
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4032
4096
|
import { dirname as dirname2, join as join12 } from "path";
|
|
4033
4097
|
function getConfigPath() {
|
|
4034
4098
|
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
|
|
@@ -4036,7 +4100,7 @@ function getConfigPath() {
|
|
|
4036
4100
|
function loadConfig2() {
|
|
4037
4101
|
try {
|
|
4038
4102
|
const configPath = getConfigPath();
|
|
4039
|
-
if (
|
|
4103
|
+
if (existsSync13(configPath)) {
|
|
4040
4104
|
const raw = readFileSync11(configPath, "utf-8");
|
|
4041
4105
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
4042
4106
|
}
|
|
@@ -4046,7 +4110,7 @@ function loadConfig2() {
|
|
|
4046
4110
|
function saveConfig2(config) {
|
|
4047
4111
|
const configPath = getConfigPath();
|
|
4048
4112
|
const dir = dirname2(configPath);
|
|
4049
|
-
if (!
|
|
4113
|
+
if (!existsSync13(dir))
|
|
4050
4114
|
mkdirSync3(dir, { recursive: true });
|
|
4051
4115
|
writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
|
|
4052
4116
|
`);
|
|
@@ -4190,7 +4254,7 @@ var init_webhooks = __esm(() => {
|
|
|
4190
4254
|
});
|
|
4191
4255
|
|
|
4192
4256
|
// src/lib/watch-paths.ts
|
|
4193
|
-
import { existsSync as
|
|
4257
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4194
4258
|
function getWatchPaths() {
|
|
4195
4259
|
const p = agentPaths();
|
|
4196
4260
|
const candidates = [
|
|
@@ -4203,7 +4267,7 @@ function getWatchPaths() {
|
|
|
4203
4267
|
p.piSessions,
|
|
4204
4268
|
p.hermesDir
|
|
4205
4269
|
];
|
|
4206
|
-
return candidates.filter((path) =>
|
|
4270
|
+
return candidates.filter((path) => existsSync14(path));
|
|
4207
4271
|
}
|
|
4208
4272
|
var init_watch_paths = __esm(() => {
|
|
4209
4273
|
init_paths();
|
|
@@ -4373,7 +4437,7 @@ __export(exports_serve, {
|
|
|
4373
4437
|
createHandler: () => createHandler
|
|
4374
4438
|
});
|
|
4375
4439
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4376
|
-
import { existsSync as
|
|
4440
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4377
4441
|
import { resolve, sep } from "path";
|
|
4378
4442
|
function json(data, status = 200) {
|
|
4379
4443
|
return new Response(JSON.stringify(data), {
|
|
@@ -4438,13 +4502,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
4438
4502
|
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
4439
4503
|
return apiHandler(req);
|
|
4440
4504
|
}
|
|
4441
|
-
if (
|
|
4505
|
+
if (existsSync15(dashboardDir)) {
|
|
4442
4506
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
4443
|
-
if (filePath &&
|
|
4507
|
+
if (filePath && existsSync15(filePath)) {
|
|
4444
4508
|
return new Response(Bun.file(filePath));
|
|
4445
4509
|
}
|
|
4446
4510
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
4447
|
-
if (indexPath &&
|
|
4511
|
+
if (indexPath && existsSync15(indexPath)) {
|
|
4448
4512
|
return new Response(Bun.file(indexPath));
|
|
4449
4513
|
}
|
|
4450
4514
|
}
|
|
@@ -4479,7 +4543,7 @@ function createHandler(db) {
|
|
|
4479
4543
|
const period = url.searchParams.get("period") ?? "month";
|
|
4480
4544
|
return ok({
|
|
4481
4545
|
summary: querySummary(db, period, undefined, true),
|
|
4482
|
-
machines: listMachines(db),
|
|
4546
|
+
machines: listMachines(db, period),
|
|
4483
4547
|
registry: listMachineRegistry(db),
|
|
4484
4548
|
current_machine: getMachineId()
|
|
4485
4549
|
});
|
|
@@ -4841,14 +4905,14 @@ __export(exports_menubar, {
|
|
|
4841
4905
|
});
|
|
4842
4906
|
import chalk6 from "chalk";
|
|
4843
4907
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
4844
|
-
import { cpSync, existsSync as
|
|
4908
|
+
import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4845
4909
|
import { tmpdir, arch } from "os";
|
|
4846
4910
|
import { join as join13 } from "path";
|
|
4847
4911
|
function getArch() {
|
|
4848
4912
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
4849
4913
|
}
|
|
4850
4914
|
function isInstalled() {
|
|
4851
|
-
return
|
|
4915
|
+
return existsSync16(APP_PATH);
|
|
4852
4916
|
}
|
|
4853
4917
|
function isRunning() {
|
|
4854
4918
|
try {
|
|
@@ -6182,6 +6246,7 @@ var TOP_LEVEL = [
|
|
|
6182
6246
|
"init",
|
|
6183
6247
|
"estimate",
|
|
6184
6248
|
"fleet",
|
|
6249
|
+
"merge-db",
|
|
6185
6250
|
"todos",
|
|
6186
6251
|
"serve",
|
|
6187
6252
|
"mcp",
|
|
@@ -6511,7 +6576,7 @@ function registerFleetCommands(program) {
|
|
|
6511
6576
|
const db = openDatabase();
|
|
6512
6577
|
const period = parsePeriod(opts.period, "today");
|
|
6513
6578
|
const summary = querySummary(db, period, undefined, true);
|
|
6514
|
-
const machines = listMachines(db);
|
|
6579
|
+
const machines = listMachines(db, period);
|
|
6515
6580
|
const registry = listMachineRegistry(db);
|
|
6516
6581
|
if (opts.json) {
|
|
6517
6582
|
console.log(JSON.stringify({ period, summary, machines, registry }, null, 2));
|
|
@@ -6535,6 +6600,285 @@ function registerFleetCommands(program) {
|
|
|
6535
6600
|
init_agents();
|
|
6536
6601
|
init_sync_all();
|
|
6537
6602
|
init_cloud_sync();
|
|
6603
|
+
|
|
6604
|
+
// src/lib/peer-sync.ts
|
|
6605
|
+
init_database();
|
|
6606
|
+
init_package_metadata();
|
|
6607
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
6608
|
+
import { existsSync as existsSync12 } from "fs";
|
|
6609
|
+
var GENERIC_PEER_TABLES = [
|
|
6610
|
+
"usage_snapshots",
|
|
6611
|
+
"subscriptions",
|
|
6612
|
+
"billing_daily",
|
|
6613
|
+
"savings_daily",
|
|
6614
|
+
"budgets",
|
|
6615
|
+
"goals",
|
|
6616
|
+
"model_pricing",
|
|
6617
|
+
"machines"
|
|
6618
|
+
];
|
|
6619
|
+
function quoteIdent(identifier) {
|
|
6620
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
6621
|
+
}
|
|
6622
|
+
function tableExists(db, table) {
|
|
6623
|
+
const row = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table);
|
|
6624
|
+
return Boolean(row);
|
|
6625
|
+
}
|
|
6626
|
+
function tableColumns(db, table) {
|
|
6627
|
+
if (!tableExists(db, table))
|
|
6628
|
+
return [];
|
|
6629
|
+
return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
6630
|
+
}
|
|
6631
|
+
function commonColumns(source, target, table) {
|
|
6632
|
+
const sourceCols = new Set(tableColumns(source, table).map((c) => c.name));
|
|
6633
|
+
return tableColumns(target, table).map((c) => c.name).filter((c) => sourceCols.has(c));
|
|
6634
|
+
}
|
|
6635
|
+
function primaryKeyColumns(db, table) {
|
|
6636
|
+
return tableColumns(db, table).filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
6637
|
+
}
|
|
6638
|
+
function selectRows(source, table, columns) {
|
|
6639
|
+
if (columns.length === 0)
|
|
6640
|
+
return [];
|
|
6641
|
+
const select = columns.map(quoteIdent).join(", ");
|
|
6642
|
+
return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
|
|
6643
|
+
}
|
|
6644
|
+
function rowByKey(target, table, keyColumns, row) {
|
|
6645
|
+
if (keyColumns.length === 0)
|
|
6646
|
+
return null;
|
|
6647
|
+
if (keyColumns.some((c) => row[c] == null))
|
|
6648
|
+
return null;
|
|
6649
|
+
const where = keyColumns.map((c) => `${quoteIdent(c)} = ?`).join(" AND ");
|
|
6650
|
+
return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
|
|
6651
|
+
}
|
|
6652
|
+
function hasId(target, table, id) {
|
|
6653
|
+
return target.prepare(`SELECT id, machine_id FROM ${quoteIdent(table)} WHERE id = ?`).get(id);
|
|
6654
|
+
}
|
|
6655
|
+
function shouldReplace(source, existing) {
|
|
6656
|
+
if (!existing)
|
|
6657
|
+
return true;
|
|
6658
|
+
const sourceUpdated = source["updated_at"];
|
|
6659
|
+
const existingUpdated = existing["updated_at"];
|
|
6660
|
+
if (typeof sourceUpdated === "string" && typeof existingUpdated === "string" && existingUpdated !== "") {
|
|
6661
|
+
return sourceUpdated >= existingUpdated;
|
|
6662
|
+
}
|
|
6663
|
+
return true;
|
|
6664
|
+
}
|
|
6665
|
+
function normalizeRow(row, columns, sourceMachine, now) {
|
|
6666
|
+
const next = { ...row };
|
|
6667
|
+
if (columns.includes("machine_id") && (!next["machine_id"] || next["machine_id"] === "")) {
|
|
6668
|
+
next["machine_id"] = sourceMachine;
|
|
6669
|
+
}
|
|
6670
|
+
if (columns.includes("updated_at") && (!next["updated_at"] || next["updated_at"] === "")) {
|
|
6671
|
+
next["updated_at"] = next["timestamp"] ?? next["started_at"] ?? next["created_at"] ?? now;
|
|
6672
|
+
}
|
|
6673
|
+
if (columns.includes("synced_at") && next["synced_at"] == null)
|
|
6674
|
+
next["synced_at"] = "";
|
|
6675
|
+
if (columns.includes("attribution_tag") && next["attribution_tag"] == null)
|
|
6676
|
+
next["attribution_tag"] = "";
|
|
6677
|
+
return next;
|
|
6678
|
+
}
|
|
6679
|
+
function insertOrReplace(target, table, columns, row) {
|
|
6680
|
+
const colSql = columns.map(quoteIdent).join(", ");
|
|
6681
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
6682
|
+
target.prepare(`
|
|
6683
|
+
INSERT OR REPLACE INTO ${quoteIdent(table)} (${colSql})
|
|
6684
|
+
VALUES (${placeholders})
|
|
6685
|
+
`).run(...columns.map((c) => row[c] ?? null));
|
|
6686
|
+
}
|
|
6687
|
+
function collisionId(target, table, machine, originalId) {
|
|
6688
|
+
const base = `${machine || "peer"}:${originalId}`;
|
|
6689
|
+
const baseRow = hasId(target, table, base);
|
|
6690
|
+
if (!baseRow || String(baseRow["machine_id"] ?? "") === machine)
|
|
6691
|
+
return base;
|
|
6692
|
+
for (let i = 2;; i++) {
|
|
6693
|
+
const candidate = `${base}:${i}`;
|
|
6694
|
+
const row = hasId(target, table, candidate);
|
|
6695
|
+
if (!row || String(row["machine_id"] ?? "") === machine)
|
|
6696
|
+
return candidate;
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
function mergeIdentityTable(target, source, table, sourceMachine, now, sessionIdMap) {
|
|
6700
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
6701
|
+
const columns = commonColumns(source, target, table);
|
|
6702
|
+
const rows = selectRows(source, table, columns);
|
|
6703
|
+
const idMap = new Map;
|
|
6704
|
+
for (const raw of rows) {
|
|
6705
|
+
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
6706
|
+
const originalId = String(row["id"] ?? "");
|
|
6707
|
+
if (!originalId) {
|
|
6708
|
+
stats.skipped++;
|
|
6709
|
+
continue;
|
|
6710
|
+
}
|
|
6711
|
+
const machine = String(row["machine_id"] ?? "");
|
|
6712
|
+
const directExisting = hasId(target, table, originalId);
|
|
6713
|
+
if (directExisting && String(directExisting["machine_id"] ?? "") !== machine) {
|
|
6714
|
+
row["id"] = collisionId(target, table, machine, originalId);
|
|
6715
|
+
stats.collisions++;
|
|
6716
|
+
}
|
|
6717
|
+
if (table === "requests" && sessionIdMap) {
|
|
6718
|
+
const originalSessionId = String(row["session_id"] ?? "");
|
|
6719
|
+
row["session_id"] = sessionIdMap.get(originalSessionId) ?? originalSessionId;
|
|
6720
|
+
}
|
|
6721
|
+
const existing = hasId(target, table, String(row["id"]));
|
|
6722
|
+
idMap.set(originalId, String(row["id"]));
|
|
6723
|
+
if (existing && !shouldReplace(row, existing)) {
|
|
6724
|
+
stats.skipped++;
|
|
6725
|
+
continue;
|
|
6726
|
+
}
|
|
6727
|
+
insertOrReplace(target, table, columns, row);
|
|
6728
|
+
if (existing)
|
|
6729
|
+
stats.updated++;
|
|
6730
|
+
else
|
|
6731
|
+
stats.inserted++;
|
|
6732
|
+
}
|
|
6733
|
+
return { stats, idMap };
|
|
6734
|
+
}
|
|
6735
|
+
function mergeProjects(target, source) {
|
|
6736
|
+
const table = "projects";
|
|
6737
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
6738
|
+
const columns = commonColumns(source, target, table);
|
|
6739
|
+
const rows = selectRows(source, table, columns);
|
|
6740
|
+
for (const raw of rows) {
|
|
6741
|
+
const row = { ...raw };
|
|
6742
|
+
const path = String(row["path"] ?? "");
|
|
6743
|
+
const id = String(row["id"] ?? "");
|
|
6744
|
+
if (!path || !id) {
|
|
6745
|
+
stats.skipped++;
|
|
6746
|
+
continue;
|
|
6747
|
+
}
|
|
6748
|
+
const existingByPath = target.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
6749
|
+
if (existingByPath) {
|
|
6750
|
+
row["id"] = existingByPath["id"] ?? id;
|
|
6751
|
+
insertOrReplace(target, table, columns, row);
|
|
6752
|
+
stats.updated++;
|
|
6753
|
+
continue;
|
|
6754
|
+
}
|
|
6755
|
+
const existingById = target.prepare(`SELECT * FROM projects WHERE id = ?`).get(id);
|
|
6756
|
+
if (existingById && String(existingById["path"] ?? "") !== path) {
|
|
6757
|
+
row["id"] = `peer:${id}`;
|
|
6758
|
+
stats.collisions++;
|
|
6759
|
+
while (target.prepare(`SELECT id FROM projects WHERE id = ?`).get(row["id"])) {
|
|
6760
|
+
row["id"] = `peer:${String(row["id"])}`;
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
insertOrReplace(target, table, columns, row);
|
|
6764
|
+
stats.inserted++;
|
|
6765
|
+
}
|
|
6766
|
+
return stats;
|
|
6767
|
+
}
|
|
6768
|
+
function mergeGenericTable(target, source, table, sourceMachine, now) {
|
|
6769
|
+
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
6770
|
+
const columns = commonColumns(source, target, table);
|
|
6771
|
+
const keyColumns = primaryKeyColumns(target, table).filter((c) => columns.includes(c));
|
|
6772
|
+
const rows = selectRows(source, table, columns);
|
|
6773
|
+
for (const raw of rows) {
|
|
6774
|
+
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
6775
|
+
const existing = rowByKey(target, table, keyColumns, row);
|
|
6776
|
+
if (existing && !shouldReplace(row, existing)) {
|
|
6777
|
+
stats.skipped++;
|
|
6778
|
+
continue;
|
|
6779
|
+
}
|
|
6780
|
+
insertOrReplace(target, table, columns, row);
|
|
6781
|
+
if (existing)
|
|
6782
|
+
stats.updated++;
|
|
6783
|
+
else
|
|
6784
|
+
stats.inserted++;
|
|
6785
|
+
}
|
|
6786
|
+
return stats;
|
|
6787
|
+
}
|
|
6788
|
+
function detectSourceMachine(source, fallback) {
|
|
6789
|
+
if (fallback && fallback.trim())
|
|
6790
|
+
return fallback.trim();
|
|
6791
|
+
const counts = new Map;
|
|
6792
|
+
for (const table of ["sessions", "requests", "usage_snapshots"]) {
|
|
6793
|
+
if (!tableExists(source, table))
|
|
6794
|
+
continue;
|
|
6795
|
+
const rows = source.prepare(`
|
|
6796
|
+
SELECT machine_id, COUNT(*) as cnt
|
|
6797
|
+
FROM ${quoteIdent(table)}
|
|
6798
|
+
WHERE machine_id != '' AND machine_id IS NOT NULL
|
|
6799
|
+
GROUP BY machine_id
|
|
6800
|
+
`).all();
|
|
6801
|
+
for (const row of rows) {
|
|
6802
|
+
counts.set(row.machine_id, (counts.get(row.machine_id) ?? 0) + row.cnt);
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
let best = "";
|
|
6806
|
+
let bestCount = -1;
|
|
6807
|
+
for (const [machine, count] of counts.entries()) {
|
|
6808
|
+
if (count > bestCount) {
|
|
6809
|
+
best = machine;
|
|
6810
|
+
bestCount = count;
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
return best || "peer";
|
|
6814
|
+
}
|
|
6815
|
+
function ensureMachineRegistry(target, machine, now) {
|
|
6816
|
+
if (!machine)
|
|
6817
|
+
return;
|
|
6818
|
+
target.prepare(`
|
|
6819
|
+
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
6820
|
+
VALUES (?, ?, ?, NULL, ?, ?, ?)
|
|
6821
|
+
ON CONFLICT(machine_id) DO UPDATE SET
|
|
6822
|
+
hostname = COALESCE(NULLIF(machines.hostname, ''), excluded.hostname),
|
|
6823
|
+
last_seen_at = CASE
|
|
6824
|
+
WHEN machines.last_seen_at IS NULL OR machines.last_seen_at < excluded.last_seen_at THEN excluded.last_seen_at
|
|
6825
|
+
ELSE machines.last_seen_at
|
|
6826
|
+
END,
|
|
6827
|
+
last_pull_at = excluded.last_pull_at,
|
|
6828
|
+
economy_version = excluded.economy_version,
|
|
6829
|
+
updated_at = excluded.updated_at
|
|
6830
|
+
`).run(machine, machine, now, now, packageMetadata.version, now);
|
|
6831
|
+
}
|
|
6832
|
+
function openSourceDatabase(path) {
|
|
6833
|
+
try {
|
|
6834
|
+
return new BunDatabase2(path, { readonly: true });
|
|
6835
|
+
} catch {
|
|
6836
|
+
return new BunDatabase2(path);
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
6840
|
+
if (!existsSync12(sourcePath))
|
|
6841
|
+
throw new Error(`source database does not exist: ${sourcePath}`);
|
|
6842
|
+
const source = openSourceDatabase(sourcePath);
|
|
6843
|
+
const now = opts.now ?? new Date().toISOString();
|
|
6844
|
+
const sourceMachine = detectSourceMachine(source, opts.sourceMachine);
|
|
6845
|
+
const tables = [];
|
|
6846
|
+
try {
|
|
6847
|
+
target.exec("PRAGMA foreign_keys = OFF");
|
|
6848
|
+
target.exec("BEGIN IMMEDIATE");
|
|
6849
|
+
try {
|
|
6850
|
+
tables.push(mergeProjects(target, source));
|
|
6851
|
+
const sessionMerge = mergeIdentityTable(target, source, "sessions", sourceMachine, now);
|
|
6852
|
+
tables.push(sessionMerge.stats);
|
|
6853
|
+
tables.push(mergeIdentityTable(target, source, "requests", sourceMachine, now, sessionMerge.idMap).stats);
|
|
6854
|
+
for (const table of GENERIC_PEER_TABLES) {
|
|
6855
|
+
tables.push(mergeGenericTable(target, source, table, sourceMachine, now));
|
|
6856
|
+
}
|
|
6857
|
+
ensureMachineRegistry(target, sourceMachine, now);
|
|
6858
|
+
target.exec("COMMIT");
|
|
6859
|
+
} catch (err) {
|
|
6860
|
+
target.exec("ROLLBACK");
|
|
6861
|
+
throw err;
|
|
6862
|
+
} finally {
|
|
6863
|
+
target.exec("PRAGMA foreign_keys = ON");
|
|
6864
|
+
}
|
|
6865
|
+
} finally {
|
|
6866
|
+
source.close();
|
|
6867
|
+
}
|
|
6868
|
+
const deduped = dedupeRequests(target);
|
|
6869
|
+
const rowsWritten = tables.reduce((sum, table) => sum + table.inserted + table.updated, 0);
|
|
6870
|
+
const collisions = tables.reduce((sum, table) => sum + table.collisions, 0);
|
|
6871
|
+
return {
|
|
6872
|
+
source_path: sourcePath,
|
|
6873
|
+
source_machine: sourceMachine,
|
|
6874
|
+
rows_written: rowsWritten,
|
|
6875
|
+
collisions,
|
|
6876
|
+
deduped,
|
|
6877
|
+
tables: tables.filter((t) => t.inserted || t.updated || t.skipped || t.collisions)
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
// src/cli/index.ts
|
|
6538
6882
|
init_database();
|
|
6539
6883
|
init_database();
|
|
6540
6884
|
init_billing();
|
|
@@ -7361,6 +7705,21 @@ program.command("machines").description("List all machines that have synced data
|
|
|
7361
7705
|
${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
|
|
7362
7706
|
console.log();
|
|
7363
7707
|
});
|
|
7708
|
+
program.command("merge-db <source-db>").description("Merge another Economy SQLite database into this machine").option("--source-machine <id>", "Machine id to use for source rows that do not have one").option("--json", "Output JSON").action((sourceDb, opts) => {
|
|
7709
|
+
const db = openDatabase();
|
|
7710
|
+
const result = mergePeerDatabase(db, sourceDb, { sourceMachine: opts.sourceMachine });
|
|
7711
|
+
if (opts.json) {
|
|
7712
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7713
|
+
return;
|
|
7714
|
+
}
|
|
7715
|
+
console.log();
|
|
7716
|
+
console.log(chalk7.bold.cyan(` Merged Economy DB \u2014 ${result.source_machine}`));
|
|
7717
|
+
console.log(` Rows written: ${fmtCount(result.rows_written)} \xB7 collisions remapped: ${fmtCount(result.collisions)} \xB7 deduped: ${fmtCount(result.deduped)}`);
|
|
7718
|
+
for (const table of result.tables) {
|
|
7719
|
+
console.log(` ${chalk7.white(table.table.padEnd(16))}` + ` inserted ${fmtCount(table.inserted).padStart(6)}` + ` updated ${fmtCount(table.updated).padStart(6)}` + ` skipped ${fmtCount(table.skipped).padStart(6)}` + ` collisions ${fmtCount(table.collisions).padStart(3)}`);
|
|
7720
|
+
}
|
|
7721
|
+
console.log();
|
|
7722
|
+
});
|
|
7364
7723
|
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
7365
7724
|
await autoSync();
|
|
7366
7725
|
const db = openDatabase();
|