@hasna/economy 0.3.6 → 0.3.7
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/CHANGELOG.md +35 -0
- package/README.md +50 -4
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/todos.d.ts +3 -0
- package/dist/cli/commands/todos.d.ts.map +1 -1
- package/dist/cli/index.js +694 -86
- package/dist/db/database.d.ts +8 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +180 -3
- package/dist/ingest/loops.d.ts +6 -0
- package/dist/ingest/loops.d.ts.map +1 -0
- package/dist/ingest/otel.d.ts +15 -2
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +9 -1
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +2 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +613 -72
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +193 -23
- package/dist/server/index.js +435 -47
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +37 -4
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/lib/cloud-sync.d.ts +0 -35
- package/dist/lib/cloud-sync.d.ts.map +0 -1
- package/dist/lib/fleet-sync.d.ts +0 -61
- package/dist/lib/fleet-sync.d.ts.map +0 -1
- package/dist/lib/peer-sync.d.ts +0 -21
- package/dist/lib/peer-sync.d.ts.map +0 -1
- package/dist/storage.js +0 -2746
package/dist/server/index.js
CHANGED
|
@@ -669,6 +669,16 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
669
669
|
}
|
|
670
670
|
function initSchema(db) {
|
|
671
671
|
db.exec(`
|
|
672
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
673
|
+
id TEXT PRIMARY KEY,
|
|
674
|
+
kind TEXT NOT NULL,
|
|
675
|
+
name TEXT NOT NULL,
|
|
676
|
+
repo_path TEXT,
|
|
677
|
+
labels_json TEXT DEFAULT '{}',
|
|
678
|
+
created_at TEXT NOT NULL,
|
|
679
|
+
updated_at TEXT NOT NULL
|
|
680
|
+
);
|
|
681
|
+
|
|
672
682
|
CREATE TABLE IF NOT EXISTS requests (
|
|
673
683
|
id TEXT PRIMARY KEY,
|
|
674
684
|
agent TEXT NOT NULL,
|
|
@@ -685,6 +695,7 @@ function initSchema(db) {
|
|
|
685
695
|
timestamp TEXT NOT NULL,
|
|
686
696
|
source_request_id TEXT,
|
|
687
697
|
machine_id TEXT DEFAULT '',
|
|
698
|
+
cost_center_id TEXT,
|
|
688
699
|
account_key TEXT DEFAULT '',
|
|
689
700
|
account_tool TEXT DEFAULT '',
|
|
690
701
|
account_name TEXT DEFAULT '',
|
|
@@ -703,6 +714,7 @@ function initSchema(db) {
|
|
|
703
714
|
total_tokens INTEGER DEFAULT 0,
|
|
704
715
|
request_count INTEGER DEFAULT 0,
|
|
705
716
|
machine_id TEXT DEFAULT '',
|
|
717
|
+
cost_center_id TEXT,
|
|
706
718
|
account_key TEXT DEFAULT '',
|
|
707
719
|
account_tool TEXT DEFAULT '',
|
|
708
720
|
account_name TEXT DEFAULT '',
|
|
@@ -723,6 +735,7 @@ function initSchema(db) {
|
|
|
723
735
|
id TEXT PRIMARY KEY,
|
|
724
736
|
project_path TEXT,
|
|
725
737
|
agent TEXT,
|
|
738
|
+
cost_center_id TEXT,
|
|
726
739
|
period TEXT NOT NULL,
|
|
727
740
|
limit_usd REAL NOT NULL,
|
|
728
741
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -753,6 +766,7 @@ function initSchema(db) {
|
|
|
753
766
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
754
767
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
755
768
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
769
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
756
770
|
|
|
757
771
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
758
772
|
model TEXT PRIMARY KEY,
|
|
@@ -839,6 +853,12 @@ function initSchema(db) {
|
|
|
839
853
|
`);
|
|
840
854
|
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
841
855
|
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
856
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
857
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
858
|
+
}
|
|
859
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
860
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
861
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
842
862
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
843
863
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
844
864
|
}
|
|
@@ -867,6 +887,10 @@ function initSchema(db) {
|
|
|
867
887
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
868
888
|
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
869
889
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
890
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
891
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
892
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
893
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
870
894
|
`);
|
|
871
895
|
}
|
|
872
896
|
function periodWhere(period) {
|
|
@@ -917,6 +941,14 @@ function requestPeriodWhere(period) {
|
|
|
917
941
|
return "1=1";
|
|
918
942
|
}
|
|
919
943
|
}
|
|
944
|
+
function upsertCostCenter(db, center) {
|
|
945
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
946
|
+
db.prepare(`
|
|
947
|
+
INSERT OR REPLACE INTO cost_centers
|
|
948
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
949
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
950
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
951
|
+
}
|
|
920
952
|
function upsertRequest(db, req) {
|
|
921
953
|
const now = req.updated_at ?? new Date().toISOString();
|
|
922
954
|
db.prepare(`
|
|
@@ -924,10 +956,10 @@ function upsertRequest(db, req) {
|
|
|
924
956
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
925
957
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
926
958
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
927
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
959
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
928
960
|
account_name, account_email, account_source, updated_at, synced_at)
|
|
929
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
930
|
-
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
961
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
962
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
931
963
|
}
|
|
932
964
|
function upsertSession(db, session) {
|
|
933
965
|
const now = session.updated_at ?? new Date().toISOString();
|
|
@@ -935,9 +967,9 @@ function upsertSession(db, session) {
|
|
|
935
967
|
INSERT OR REPLACE INTO sessions
|
|
936
968
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
937
969
|
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
938
|
-
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
939
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
940
|
-
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
970
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
971
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
972
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
941
973
|
}
|
|
942
974
|
function rollupSession(db, sessionId) {
|
|
943
975
|
db.prepare(`
|
|
@@ -963,9 +995,12 @@ function rollupSession(db, sessionId) {
|
|
|
963
995
|
ELSE account_email END,
|
|
964
996
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
965
997
|
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
966
|
-
ELSE account_source END
|
|
998
|
+
ELSE account_source END,
|
|
999
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1000
|
+
THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
|
|
1001
|
+
ELSE cost_center_id END
|
|
967
1002
|
WHERE id = ?
|
|
968
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1003
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
969
1004
|
}
|
|
970
1005
|
function querySessions(db, filter = {}) {
|
|
971
1006
|
const conditions = [];
|
|
@@ -1392,6 +1427,139 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1392
1427
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1393
1428
|
return result;
|
|
1394
1429
|
}
|
|
1430
|
+
function kindFromCostCenterId(id) {
|
|
1431
|
+
const prefix = id.split(":", 1)[0] ?? "";
|
|
1432
|
+
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1433
|
+
}
|
|
1434
|
+
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1435
|
+
if (!row.cost_center_id)
|
|
1436
|
+
return;
|
|
1437
|
+
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1438
|
+
const group = groups.get(row.cost_center_id) ?? {
|
|
1439
|
+
cost_center_id: row.cost_center_id,
|
|
1440
|
+
kind: derivedKind,
|
|
1441
|
+
name: row.name || row.cost_center_id,
|
|
1442
|
+
repo_path: row.repo_path ?? null,
|
|
1443
|
+
labels_json: row.labels_json || "{}",
|
|
1444
|
+
sessionIds: new Set,
|
|
1445
|
+
requests: 0,
|
|
1446
|
+
total_tokens: 0,
|
|
1447
|
+
api_equivalent_usd: 0,
|
|
1448
|
+
metered_api_usd: 0,
|
|
1449
|
+
subscription_included_usd: 0,
|
|
1450
|
+
estimated_usd: 0,
|
|
1451
|
+
unknown_usd: 0,
|
|
1452
|
+
last_active: ""
|
|
1453
|
+
};
|
|
1454
|
+
group.kind = derivedKind;
|
|
1455
|
+
if (row.name)
|
|
1456
|
+
group.name = row.name;
|
|
1457
|
+
if (row.repo_path && !group.repo_path)
|
|
1458
|
+
group.repo_path = row.repo_path;
|
|
1459
|
+
if (row.labels_json && row.labels_json !== "{}")
|
|
1460
|
+
group.labels_json = row.labels_json;
|
|
1461
|
+
if (row.session_id)
|
|
1462
|
+
group.sessionIds.add(row.session_id);
|
|
1463
|
+
group.requests += row.requests;
|
|
1464
|
+
group.total_tokens += row.total_tokens;
|
|
1465
|
+
group.api_equivalent_usd += row.cost_usd;
|
|
1466
|
+
if (sessionOnly) {
|
|
1467
|
+
group.estimated_usd += row.cost_usd;
|
|
1468
|
+
} else if (row.cost_basis === "metered_api") {
|
|
1469
|
+
group.metered_api_usd += row.cost_usd;
|
|
1470
|
+
} else if (row.cost_basis === "subscription_included") {
|
|
1471
|
+
group.subscription_included_usd += row.cost_usd;
|
|
1472
|
+
} else if (row.cost_basis === "unknown") {
|
|
1473
|
+
group.unknown_usd += row.cost_usd;
|
|
1474
|
+
} else {
|
|
1475
|
+
group.estimated_usd += row.cost_usd;
|
|
1476
|
+
}
|
|
1477
|
+
if (!group.last_active || row.last_active > group.last_active)
|
|
1478
|
+
group.last_active = row.last_active;
|
|
1479
|
+
groups.set(row.cost_center_id, group);
|
|
1480
|
+
}
|
|
1481
|
+
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1482
|
+
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1483
|
+
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1484
|
+
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1485
|
+
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1486
|
+
const requestParams = [];
|
|
1487
|
+
const sessionParams = [];
|
|
1488
|
+
if (filter.since) {
|
|
1489
|
+
requestParams.push(filter.since);
|
|
1490
|
+
sessionParams.push(filter.since);
|
|
1491
|
+
}
|
|
1492
|
+
if (filter.machine) {
|
|
1493
|
+
requestParams.push(filter.machine);
|
|
1494
|
+
sessionParams.push(filter.machine);
|
|
1495
|
+
}
|
|
1496
|
+
const groups = new Map;
|
|
1497
|
+
const requestRows = db.prepare(`
|
|
1498
|
+
SELECT
|
|
1499
|
+
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1500
|
+
COALESCE(cc.kind, '') as kind,
|
|
1501
|
+
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1502
|
+
cc.repo_path as repo_path,
|
|
1503
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1504
|
+
r.session_id as session_id,
|
|
1505
|
+
1 as requests,
|
|
1506
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1507
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1508
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1509
|
+
r.timestamp as last_active
|
|
1510
|
+
FROM requests r
|
|
1511
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1512
|
+
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1513
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1514
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1515
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1516
|
+
`).all(...requestParams);
|
|
1517
|
+
for (const row of requestRows)
|
|
1518
|
+
addCostCenterBreakdownRow(groups, row, false);
|
|
1519
|
+
const sessionOnlyRows = db.prepare(`
|
|
1520
|
+
SELECT
|
|
1521
|
+
s.cost_center_id as cost_center_id,
|
|
1522
|
+
COALESCE(cc.kind, '') as kind,
|
|
1523
|
+
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1524
|
+
cc.repo_path as repo_path,
|
|
1525
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1526
|
+
s.id as session_id,
|
|
1527
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1528
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1529
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1530
|
+
'estimated' as cost_basis,
|
|
1531
|
+
s.started_at as last_active
|
|
1532
|
+
FROM sessions s
|
|
1533
|
+
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1534
|
+
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1535
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1536
|
+
AND s.cost_center_id IS NOT NULL
|
|
1537
|
+
AND s.cost_center_id != ''
|
|
1538
|
+
`).all(...sessionParams);
|
|
1539
|
+
for (const row of sessionOnlyRows)
|
|
1540
|
+
addCostCenterBreakdownRow(groups, row, true);
|
|
1541
|
+
const result = [...groups.values()].map((group) => ({
|
|
1542
|
+
cost_center_id: group.cost_center_id,
|
|
1543
|
+
kind: group.kind,
|
|
1544
|
+
name: group.name,
|
|
1545
|
+
repo_path: group.repo_path,
|
|
1546
|
+
labels_json: group.labels_json,
|
|
1547
|
+
sessions: group.sessionIds.size,
|
|
1548
|
+
requests: group.requests,
|
|
1549
|
+
total_tokens: group.total_tokens,
|
|
1550
|
+
api_equivalent_usd: group.api_equivalent_usd,
|
|
1551
|
+
billable_usd: group.metered_api_usd,
|
|
1552
|
+
metered_api_usd: group.metered_api_usd,
|
|
1553
|
+
subscription_included_usd: group.subscription_included_usd,
|
|
1554
|
+
estimated_usd: group.estimated_usd,
|
|
1555
|
+
unknown_usd: group.unknown_usd,
|
|
1556
|
+
cost_usd: group.api_equivalent_usd,
|
|
1557
|
+
last_active: group.last_active
|
|
1558
|
+
}));
|
|
1559
|
+
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1560
|
+
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1561
|
+
return filtered;
|
|
1562
|
+
}
|
|
1395
1563
|
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1396
1564
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1397
1565
|
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
@@ -1433,9 +1601,9 @@ function deleteProject(db, path) {
|
|
|
1433
1601
|
function upsertBudget(db, budget) {
|
|
1434
1602
|
db.prepare(`
|
|
1435
1603
|
INSERT OR REPLACE INTO budgets
|
|
1436
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1437
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1438
|
-
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1604
|
+
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1606
|
+
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1439
1607
|
}
|
|
1440
1608
|
function listBudgets(db) {
|
|
1441
1609
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
@@ -1457,6 +1625,10 @@ function getBudgetStatuses(db) {
|
|
|
1457
1625
|
spendQuery += ` AND agent = ?`;
|
|
1458
1626
|
params.push(b.agent);
|
|
1459
1627
|
}
|
|
1628
|
+
if (b.cost_center_id) {
|
|
1629
|
+
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1630
|
+
params.push(b.cost_center_id);
|
|
1631
|
+
}
|
|
1460
1632
|
const row = db.prepare(spendQuery).get(...params);
|
|
1461
1633
|
const spend = row.spend;
|
|
1462
1634
|
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
@@ -1952,8 +2124,8 @@ var init_database = () => {};
|
|
|
1952
2124
|
// src/db/sync-pg.ts
|
|
1953
2125
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
1954
2126
|
import { fileURLToPath } from "url";
|
|
1955
|
-
import { dirname, join as
|
|
1956
|
-
import { existsSync as
|
|
2127
|
+
import { dirname, join as join10 } from "path";
|
|
2128
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1957
2129
|
import { translateSql, translateParams } from "@hasna/cloud";
|
|
1958
2130
|
function translateSqliteDates(sql) {
|
|
1959
2131
|
let s = sql;
|
|
@@ -1979,12 +2151,12 @@ function translateSqliteDates(sql) {
|
|
|
1979
2151
|
function resolveWorkerPath() {
|
|
1980
2152
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1981
2153
|
const candidates = [
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
2154
|
+
join10(here, "pg-sync-worker.ts"),
|
|
2155
|
+
join10(here, "pg-sync-worker.js"),
|
|
2156
|
+
join10(here, "..", "db", "pg-sync-worker.js")
|
|
1985
2157
|
];
|
|
1986
2158
|
for (const candidate of candidates)
|
|
1987
|
-
if (
|
|
2159
|
+
if (existsSync11(candidate))
|
|
1988
2160
|
return candidate;
|
|
1989
2161
|
return candidates[0];
|
|
1990
2162
|
}
|
|
@@ -2080,6 +2252,15 @@ var init_sync_pg = () => {};
|
|
|
2080
2252
|
var PG_MIGRATIONS;
|
|
2081
2253
|
var init_pg_migrations = __esm(() => {
|
|
2082
2254
|
PG_MIGRATIONS = [
|
|
2255
|
+
`CREATE TABLE IF NOT EXISTS cost_centers (
|
|
2256
|
+
id TEXT PRIMARY KEY,
|
|
2257
|
+
kind TEXT NOT NULL,
|
|
2258
|
+
name TEXT NOT NULL,
|
|
2259
|
+
repo_path TEXT,
|
|
2260
|
+
labels_json TEXT DEFAULT '{}',
|
|
2261
|
+
created_at TEXT NOT NULL,
|
|
2262
|
+
updated_at TEXT NOT NULL
|
|
2263
|
+
)`,
|
|
2083
2264
|
`CREATE TABLE IF NOT EXISTS requests (
|
|
2084
2265
|
id TEXT PRIMARY KEY,
|
|
2085
2266
|
agent TEXT NOT NULL,
|
|
@@ -2096,6 +2277,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
2096
2277
|
timestamp TEXT NOT NULL,
|
|
2097
2278
|
source_request_id TEXT,
|
|
2098
2279
|
machine_id TEXT DEFAULT '',
|
|
2280
|
+
cost_center_id TEXT,
|
|
2099
2281
|
account_key TEXT DEFAULT '',
|
|
2100
2282
|
account_tool TEXT DEFAULT '',
|
|
2101
2283
|
account_name TEXT DEFAULT '',
|
|
@@ -2113,6 +2295,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
2113
2295
|
total_tokens INTEGER DEFAULT 0,
|
|
2114
2296
|
request_count INTEGER DEFAULT 0,
|
|
2115
2297
|
machine_id TEXT DEFAULT '',
|
|
2298
|
+
cost_center_id TEXT,
|
|
2116
2299
|
account_key TEXT DEFAULT '',
|
|
2117
2300
|
account_tool TEXT DEFAULT '',
|
|
2118
2301
|
account_name TEXT DEFAULT '',
|
|
@@ -2131,6 +2314,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
2131
2314
|
id TEXT PRIMARY KEY,
|
|
2132
2315
|
project_path TEXT,
|
|
2133
2316
|
agent TEXT,
|
|
2317
|
+
cost_center_id TEXT,
|
|
2134
2318
|
period TEXT NOT NULL,
|
|
2135
2319
|
limit_usd REAL NOT NULL,
|
|
2136
2320
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -2160,6 +2344,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
2160
2344
|
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
2161
2345
|
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
2162
2346
|
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
2347
|
+
`CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind)`,
|
|
2163
2348
|
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
2164
2349
|
model TEXT PRIMARY KEY,
|
|
2165
2350
|
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
@@ -2245,6 +2430,7 @@ var init_pg_migrations = __esm(() => {
|
|
|
2245
2430
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
2246
2431
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
2247
2432
|
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
2433
|
+
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
2248
2434
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
2249
2435
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
2250
2436
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
@@ -2253,10 +2439,16 @@ var init_pg_migrations = __esm(() => {
|
|
|
2253
2439
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
2254
2440
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
2255
2441
|
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
2442
|
+
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
2443
|
+
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS cost_center_id TEXT`,
|
|
2444
|
+
`ALTER TABLE cost_centers ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
2256
2445
|
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
2257
2446
|
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
2258
2447
|
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
2259
2448
|
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`,
|
|
2449
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id)`,
|
|
2450
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id)`,
|
|
2451
|
+
`CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id)`,
|
|
2260
2452
|
`ALTER TABLE requests ALTER COLUMN input_tokens TYPE BIGINT`,
|
|
2261
2453
|
`ALTER TABLE requests ALTER COLUMN output_tokens TYPE BIGINT`,
|
|
2262
2454
|
`ALTER TABLE requests ALTER COLUMN cache_read_tokens TYPE BIGINT`,
|
|
@@ -2649,15 +2841,15 @@ var init_open_projects = __esm(() => {
|
|
|
2649
2841
|
});
|
|
2650
2842
|
|
|
2651
2843
|
// src/lib/config.ts
|
|
2652
|
-
import { existsSync as
|
|
2653
|
-
import { dirname as dirname2, join as
|
|
2844
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2845
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
2654
2846
|
function getConfigPath() {
|
|
2655
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
2847
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join11(getDataDir(), "config.json");
|
|
2656
2848
|
}
|
|
2657
2849
|
function loadConfig() {
|
|
2658
2850
|
try {
|
|
2659
2851
|
const configPath = getConfigPath();
|
|
2660
|
-
if (
|
|
2852
|
+
if (existsSync12(configPath)) {
|
|
2661
2853
|
const raw = readFileSync10(configPath, "utf-8");
|
|
2662
2854
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
2663
2855
|
}
|
|
@@ -2737,6 +2929,8 @@ async function checkAndFireWebhooks(db) {
|
|
|
2737
2929
|
event: "budget_alert",
|
|
2738
2930
|
budget_id: b.id,
|
|
2739
2931
|
project: b.project_path ?? "global",
|
|
2932
|
+
cost_center_id: b.cost_center_id ?? null,
|
|
2933
|
+
scope: b.cost_center_id ?? b.project_path ?? "global",
|
|
2740
2934
|
period: b.period,
|
|
2741
2935
|
spend: b.current_spend_usd,
|
|
2742
2936
|
limit: b.limit_usd,
|
|
@@ -4095,32 +4289,219 @@ function statSyncSafe(path) {
|
|
|
4095
4289
|
}
|
|
4096
4290
|
}
|
|
4097
4291
|
|
|
4292
|
+
// src/ingest/loops.ts
|
|
4293
|
+
init_database();
|
|
4294
|
+
init_pricing();
|
|
4295
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4296
|
+
import { homedir as homedir8 } from "os";
|
|
4297
|
+
import { join as join8 } from "path";
|
|
4298
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
4299
|
+
var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
|
|
4300
|
+
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
4301
|
+
function loopsDbPath() {
|
|
4302
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
|
|
4303
|
+
}
|
|
4304
|
+
function loopsModel() {
|
|
4305
|
+
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
4306
|
+
}
|
|
4307
|
+
function openLoopsDb(dbPath, verbose) {
|
|
4308
|
+
let lastError;
|
|
4309
|
+
for (const readonly of [true, false]) {
|
|
4310
|
+
let loopsDb = null;
|
|
4311
|
+
try {
|
|
4312
|
+
loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
4313
|
+
loopsDb.prepare("PRAGMA schema_version").get();
|
|
4314
|
+
return loopsDb;
|
|
4315
|
+
} catch (error) {
|
|
4316
|
+
lastError = error;
|
|
4317
|
+
loopsDb?.close();
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
if (verbose) {
|
|
4321
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
4322
|
+
console.log("OpenLoops DB unreadable:", dbPath, message);
|
|
4323
|
+
}
|
|
4324
|
+
return null;
|
|
4325
|
+
}
|
|
4326
|
+
function slug(value) {
|
|
4327
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
4328
|
+
}
|
|
4329
|
+
function parseTarget(raw) {
|
|
4330
|
+
if (!raw)
|
|
4331
|
+
return {};
|
|
4332
|
+
try {
|
|
4333
|
+
const parsed = JSON.parse(raw);
|
|
4334
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4335
|
+
} catch {
|
|
4336
|
+
return {};
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
function stringValue(value) {
|
|
4340
|
+
return typeof value === "string" ? value.trim() : "";
|
|
4341
|
+
}
|
|
4342
|
+
function accountFields(row) {
|
|
4343
|
+
const target = parseTarget(row.target_json);
|
|
4344
|
+
const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
|
|
4345
|
+
const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
|
|
4346
|
+
if (!tool || !profile) {
|
|
4347
|
+
return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
|
|
4348
|
+
}
|
|
4349
|
+
return {
|
|
4350
|
+
account_key: `${tool}:${profile}`,
|
|
4351
|
+
account_tool: tool,
|
|
4352
|
+
account_name: profile,
|
|
4353
|
+
account_source: "loops"
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
function readLoopGoalRuns(loopsDb) {
|
|
4357
|
+
return loopsDb.prepare(`
|
|
4358
|
+
SELECT
|
|
4359
|
+
gr.id as goal_run_id,
|
|
4360
|
+
gr.goal_id as goal_id,
|
|
4361
|
+
COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
|
|
4362
|
+
COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
|
|
4363
|
+
COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
|
|
4364
|
+
l.labels_json as labels_json,
|
|
4365
|
+
l.target_json as target_json,
|
|
4366
|
+
g.objective as objective,
|
|
4367
|
+
gr.phase as phase,
|
|
4368
|
+
gr.status as status,
|
|
4369
|
+
COALESCE(gr.tokens_used, 0) as tokens_used,
|
|
4370
|
+
gr.created_at as created_at,
|
|
4371
|
+
gr.updated_at as updated_at,
|
|
4372
|
+
lr.started_at as loop_run_started_at,
|
|
4373
|
+
wsr.account_profile as account_profile,
|
|
4374
|
+
wsr.account_tool as account_tool
|
|
4375
|
+
FROM goal_runs gr
|
|
4376
|
+
LEFT JOIN goals g ON g.id = gr.goal_id
|
|
4377
|
+
LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
|
|
4378
|
+
LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
|
|
4379
|
+
LEFT JOIN (
|
|
4380
|
+
SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
|
|
4381
|
+
FROM workflow_step_runs
|
|
4382
|
+
WHERE goal_run_id IS NOT NULL
|
|
4383
|
+
GROUP BY goal_run_id
|
|
4384
|
+
) wsr ON wsr.goal_run_id = gr.id
|
|
4385
|
+
WHERE COALESCE(gr.tokens_used, 0) > 0
|
|
4386
|
+
ORDER BY gr.created_at ASC, gr.id ASC
|
|
4387
|
+
`).all();
|
|
4388
|
+
}
|
|
4389
|
+
async function ingestLoops(db, verbose = false) {
|
|
4390
|
+
const dbPath = loopsDbPath();
|
|
4391
|
+
if (!existsSync8(dbPath)) {
|
|
4392
|
+
if (verbose)
|
|
4393
|
+
console.log("OpenLoops DB not found:", dbPath);
|
|
4394
|
+
return { sessions: 0, requests: 0 };
|
|
4395
|
+
}
|
|
4396
|
+
const model = loopsModel();
|
|
4397
|
+
const machineId = getMachineId();
|
|
4398
|
+
let loopsDb = null;
|
|
4399
|
+
let requests = 0;
|
|
4400
|
+
const sessions = new Set;
|
|
4401
|
+
try {
|
|
4402
|
+
loopsDb = openLoopsDb(dbPath, verbose);
|
|
4403
|
+
if (!loopsDb)
|
|
4404
|
+
return { sessions: 0, requests: 0 };
|
|
4405
|
+
for (const row of readLoopGoalRuns(loopsDb)) {
|
|
4406
|
+
const loopName = row.loop_name || "unknown-loop";
|
|
4407
|
+
const costCenterId = `loop:${slug(loopName)}`;
|
|
4408
|
+
const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
|
|
4409
|
+
const requestId = `loops-${row.goal_run_id}`;
|
|
4410
|
+
const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
|
|
4411
|
+
const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
|
|
4412
|
+
if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
|
|
4413
|
+
continue;
|
|
4414
|
+
upsertCostCenter(db, {
|
|
4415
|
+
id: costCenterId,
|
|
4416
|
+
kind: "loop",
|
|
4417
|
+
name: loopName,
|
|
4418
|
+
repo_path: null,
|
|
4419
|
+
labels_json: row.labels_json ?? "{}",
|
|
4420
|
+
created_at: row.created_at ?? timestamp
|
|
4421
|
+
});
|
|
4422
|
+
const account = accountFields(row);
|
|
4423
|
+
upsertSession(db, {
|
|
4424
|
+
id: sessionId,
|
|
4425
|
+
agent: "loop",
|
|
4426
|
+
project_path: "",
|
|
4427
|
+
project_name: loopName,
|
|
4428
|
+
started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
|
|
4429
|
+
ended_at: row.updated_at ?? null,
|
|
4430
|
+
total_cost_usd: 0,
|
|
4431
|
+
total_tokens: 0,
|
|
4432
|
+
request_count: 0,
|
|
4433
|
+
machine_id: machineId,
|
|
4434
|
+
cost_center_id: costCenterId,
|
|
4435
|
+
attribution_tag: "loops-orchestration",
|
|
4436
|
+
...account
|
|
4437
|
+
});
|
|
4438
|
+
const inputTokens = row.tokens_used;
|
|
4439
|
+
const outputTokens = 0;
|
|
4440
|
+
upsertRequest(db, {
|
|
4441
|
+
id: requestId,
|
|
4442
|
+
agent: "loop",
|
|
4443
|
+
session_id: sessionId,
|
|
4444
|
+
model,
|
|
4445
|
+
input_tokens: inputTokens,
|
|
4446
|
+
output_tokens: outputTokens,
|
|
4447
|
+
cache_read_tokens: 0,
|
|
4448
|
+
cache_create_tokens: 0,
|
|
4449
|
+
cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
|
|
4450
|
+
cost_basis: "estimated",
|
|
4451
|
+
duration_ms: 0,
|
|
4452
|
+
timestamp,
|
|
4453
|
+
source_request_id: requestId,
|
|
4454
|
+
machine_id: machineId,
|
|
4455
|
+
cost_center_id: costCenterId,
|
|
4456
|
+
attribution_tag: "loops-orchestration",
|
|
4457
|
+
...account
|
|
4458
|
+
});
|
|
4459
|
+
sessions.add(sessionId);
|
|
4460
|
+
requests++;
|
|
4461
|
+
setIngestState(db, "loops", row.goal_run_id, stateValue);
|
|
4462
|
+
if (verbose)
|
|
4463
|
+
console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
|
|
4464
|
+
}
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
if (verbose) {
|
|
4467
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4468
|
+
console.log("OpenLoops ingest skipped:", message);
|
|
4469
|
+
}
|
|
4470
|
+
return { sessions: 0, requests: 0 };
|
|
4471
|
+
} finally {
|
|
4472
|
+
loopsDb?.close();
|
|
4473
|
+
}
|
|
4474
|
+
for (const sessionId of sessions)
|
|
4475
|
+
rollupSession(db, sessionId);
|
|
4476
|
+
return { sessions: sessions.size, requests };
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4098
4479
|
// src/ingest/claude-quota.ts
|
|
4099
4480
|
init_database();
|
|
4100
|
-
import { existsSync as
|
|
4481
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
4101
4482
|
|
|
4102
4483
|
// src/lib/paths.ts
|
|
4103
|
-
import { homedir as
|
|
4104
|
-
import { join as
|
|
4484
|
+
import { homedir as homedir9 } from "os";
|
|
4485
|
+
import { join as join9 } from "path";
|
|
4105
4486
|
function getHomeDir() {
|
|
4106
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
4487
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
4107
4488
|
}
|
|
4108
4489
|
function agentPaths() {
|
|
4109
4490
|
const home = getHomeDir();
|
|
4110
4491
|
return {
|
|
4111
|
-
claudeProjects:
|
|
4112
|
-
claudeCredentials:
|
|
4113
|
-
takumiProjects:
|
|
4114
|
-
codexDir:
|
|
4115
|
-
codexDb:
|
|
4116
|
-
codexAuth:
|
|
4117
|
-
codexConfig:
|
|
4118
|
-
geminiTmp:
|
|
4119
|
-
geminiHistory:
|
|
4120
|
-
opencodeMessages:
|
|
4121
|
-
piSessions:
|
|
4122
|
-
hermesDir:
|
|
4123
|
-
hermesDb:
|
|
4492
|
+
claudeProjects: join9(home, ".claude", "projects"),
|
|
4493
|
+
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
4494
|
+
takumiProjects: join9(home, ".takumi", "projects"),
|
|
4495
|
+
codexDir: join9(home, ".codex"),
|
|
4496
|
+
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
4497
|
+
codexAuth: join9(home, ".codex", "auth.json"),
|
|
4498
|
+
codexConfig: join9(home, ".codex", "config.toml"),
|
|
4499
|
+
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
4500
|
+
geminiHistory: join9(home, ".gemini", "history"),
|
|
4501
|
+
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
4502
|
+
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
4503
|
+
hermesDir: join9(home, ".hermes"),
|
|
4504
|
+
hermesDb: join9(home, ".hermes", "state.db")
|
|
4124
4505
|
};
|
|
4125
4506
|
}
|
|
4126
4507
|
|
|
@@ -4132,7 +4513,7 @@ function readClaudeToken() {
|
|
|
4132
4513
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
4133
4514
|
if (fromEnv)
|
|
4134
4515
|
return { token: fromEnv };
|
|
4135
|
-
if (!
|
|
4516
|
+
if (!existsSync9(CREDENTIALS_PATH))
|
|
4136
4517
|
return null;
|
|
4137
4518
|
try {
|
|
4138
4519
|
const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
|
|
@@ -4271,14 +4652,14 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
4271
4652
|
|
|
4272
4653
|
// src/ingest/codex-quota.ts
|
|
4273
4654
|
init_database();
|
|
4274
|
-
import { existsSync as
|
|
4655
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
4275
4656
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
4276
4657
|
function readCodexAuth() {
|
|
4277
4658
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
4278
4659
|
if (fromEnv)
|
|
4279
4660
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
4280
4661
|
const authPath = agentPaths().codexAuth;
|
|
4281
|
-
if (!
|
|
4662
|
+
if (!existsSync10(authPath))
|
|
4282
4663
|
return null;
|
|
4283
4664
|
try {
|
|
4284
4665
|
const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
|
|
@@ -4411,7 +4792,7 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
4411
4792
|
// src/lib/sync-all.ts
|
|
4412
4793
|
init_database();
|
|
4413
4794
|
async function syncAll(db, opts = {}) {
|
|
4414
|
-
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
4795
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
|
|
4415
4796
|
const all = !anySpecific;
|
|
4416
4797
|
const result = { deduped: 0 };
|
|
4417
4798
|
if (all || opts.claude) {
|
|
@@ -4434,6 +4815,8 @@ async function syncAll(db, opts = {}) {
|
|
|
4434
4815
|
result.pi = await ingestPi(db, opts.verbose);
|
|
4435
4816
|
if (all || opts.hermes)
|
|
4436
4817
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
4818
|
+
if (all || opts.loops)
|
|
4819
|
+
result.loops = await ingestLoops(db, opts.verbose);
|
|
4437
4820
|
if (opts.dedupe !== false)
|
|
4438
4821
|
result.deduped = dedupeRequests(db);
|
|
4439
4822
|
return result;
|
|
@@ -5277,7 +5660,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5277
5660
|
|
|
5278
5661
|
// src/server/serve.ts
|
|
5279
5662
|
import { randomUUID } from "crypto";
|
|
5280
|
-
import { existsSync as
|
|
5663
|
+
import { existsSync as existsSync13 } from "fs";
|
|
5281
5664
|
import { resolve, sep } from "path";
|
|
5282
5665
|
|
|
5283
5666
|
// src/lib/package-metadata.ts
|
|
@@ -6914,7 +7297,7 @@ var CORS = {
|
|
|
6914
7297
|
var APP = "economy";
|
|
6915
7298
|
var ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
6916
7299
|
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
6917
|
-
var SYNC_SOURCES = ["all", ...AGENTS];
|
|
7300
|
+
var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
6918
7301
|
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
6919
7302
|
function json(data, status = 200) {
|
|
6920
7303
|
return new Response(JSON.stringify(data), {
|
|
@@ -6987,13 +7370,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
6987
7370
|
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
6988
7371
|
return apiHandler(req);
|
|
6989
7372
|
}
|
|
6990
|
-
if (
|
|
7373
|
+
if (existsSync13(dashboardDir)) {
|
|
6991
7374
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
6992
|
-
if (filePath &&
|
|
7375
|
+
if (filePath && existsSync13(filePath)) {
|
|
6993
7376
|
return new Response(Bun.file(filePath));
|
|
6994
7377
|
}
|
|
6995
7378
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
6996
|
-
if (indexPath &&
|
|
7379
|
+
if (indexPath && existsSync13(indexPath)) {
|
|
6997
7380
|
return new Response(Bun.file(indexPath));
|
|
6998
7381
|
}
|
|
6999
7382
|
}
|
|
@@ -7162,6 +7545,10 @@ function createHandler(db, options = {}) {
|
|
|
7162
7545
|
return ok(queryAgentBreakdown(db, period, machine));
|
|
7163
7546
|
if (by === "account")
|
|
7164
7547
|
return ok(queryAccountBreakdown(db, period, machine));
|
|
7548
|
+
if (by === "cost-center")
|
|
7549
|
+
return ok(queryCostCenterBreakdown(db, period, { machine }));
|
|
7550
|
+
if (["loop", "app", "repo", "service", "team"].includes(by))
|
|
7551
|
+
return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
|
|
7165
7552
|
return ok(queryModelBreakdown(db));
|
|
7166
7553
|
}
|
|
7167
7554
|
if (path === "/api/budgets" && method === "GET") {
|
|
@@ -7185,6 +7572,7 @@ function createHandler(db, options = {}) {
|
|
|
7185
7572
|
id: randomUUID(),
|
|
7186
7573
|
project_path: body["project_path"] ?? null,
|
|
7187
7574
|
agent,
|
|
7575
|
+
cost_center_id: optionalString(body["cost_center_id"]) ?? null,
|
|
7188
7576
|
period: normalizeBudgetPeriod(body["period"]),
|
|
7189
7577
|
limit_usd: limitUsd,
|
|
7190
7578
|
alert_at_percent: alertAtPercent,
|