@hasna/economy 0.3.5 → 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 +3 -3
- 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/mcp/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 queryProjectBreakdownSince(db, since) {
|
|
1396
1564
|
return db.prepare(`
|
|
1397
1565
|
SELECT project_path, project_name,
|
|
@@ -1563,9 +1731,9 @@ function deleteProject(db, path) {
|
|
|
1563
1731
|
function upsertBudget(db, budget) {
|
|
1564
1732
|
db.prepare(`
|
|
1565
1733
|
INSERT OR REPLACE INTO budgets
|
|
1566
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1567
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1568
|
-
`).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);
|
|
1734
|
+
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1735
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1736
|
+
`).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);
|
|
1569
1737
|
}
|
|
1570
1738
|
function listBudgets(db) {
|
|
1571
1739
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
@@ -1587,6 +1755,10 @@ function getBudgetStatuses(db) {
|
|
|
1587
1755
|
spendQuery += ` AND agent = ?`;
|
|
1588
1756
|
params.push(b.agent);
|
|
1589
1757
|
}
|
|
1758
|
+
if (b.cost_center_id) {
|
|
1759
|
+
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1760
|
+
params.push(b.cost_center_id);
|
|
1761
|
+
}
|
|
1590
1762
|
const row = db.prepare(spendQuery).get(...params);
|
|
1591
1763
|
const spend = row.spend;
|
|
1592
1764
|
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
@@ -3137,32 +3309,219 @@ function statSyncSafe(path) {
|
|
|
3137
3309
|
}
|
|
3138
3310
|
}
|
|
3139
3311
|
|
|
3312
|
+
// src/ingest/loops.ts
|
|
3313
|
+
init_database();
|
|
3314
|
+
init_pricing();
|
|
3315
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3316
|
+
import { homedir as homedir8 } from "os";
|
|
3317
|
+
import { join as join8 } from "path";
|
|
3318
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
3319
|
+
var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
|
|
3320
|
+
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
3321
|
+
function loopsDbPath() {
|
|
3322
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
|
|
3323
|
+
}
|
|
3324
|
+
function loopsModel() {
|
|
3325
|
+
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
3326
|
+
}
|
|
3327
|
+
function openLoopsDb(dbPath, verbose) {
|
|
3328
|
+
let lastError;
|
|
3329
|
+
for (const readonly of [true, false]) {
|
|
3330
|
+
let loopsDb = null;
|
|
3331
|
+
try {
|
|
3332
|
+
loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
3333
|
+
loopsDb.prepare("PRAGMA schema_version").get();
|
|
3334
|
+
return loopsDb;
|
|
3335
|
+
} catch (error) {
|
|
3336
|
+
lastError = error;
|
|
3337
|
+
loopsDb?.close();
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
if (verbose) {
|
|
3341
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
3342
|
+
console.log("OpenLoops DB unreadable:", dbPath, message);
|
|
3343
|
+
}
|
|
3344
|
+
return null;
|
|
3345
|
+
}
|
|
3346
|
+
function slug(value) {
|
|
3347
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
3348
|
+
}
|
|
3349
|
+
function parseTarget(raw) {
|
|
3350
|
+
if (!raw)
|
|
3351
|
+
return {};
|
|
3352
|
+
try {
|
|
3353
|
+
const parsed = JSON.parse(raw);
|
|
3354
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3355
|
+
} catch {
|
|
3356
|
+
return {};
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
function stringValue(value) {
|
|
3360
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3361
|
+
}
|
|
3362
|
+
function accountFields(row) {
|
|
3363
|
+
const target = parseTarget(row.target_json);
|
|
3364
|
+
const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
|
|
3365
|
+
const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
|
|
3366
|
+
if (!tool || !profile) {
|
|
3367
|
+
return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
|
|
3368
|
+
}
|
|
3369
|
+
return {
|
|
3370
|
+
account_key: `${tool}:${profile}`,
|
|
3371
|
+
account_tool: tool,
|
|
3372
|
+
account_name: profile,
|
|
3373
|
+
account_source: "loops"
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
function readLoopGoalRuns(loopsDb) {
|
|
3377
|
+
return loopsDb.prepare(`
|
|
3378
|
+
SELECT
|
|
3379
|
+
gr.id as goal_run_id,
|
|
3380
|
+
gr.goal_id as goal_id,
|
|
3381
|
+
COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
|
|
3382
|
+
COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
|
|
3383
|
+
COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
|
|
3384
|
+
l.labels_json as labels_json,
|
|
3385
|
+
l.target_json as target_json,
|
|
3386
|
+
g.objective as objective,
|
|
3387
|
+
gr.phase as phase,
|
|
3388
|
+
gr.status as status,
|
|
3389
|
+
COALESCE(gr.tokens_used, 0) as tokens_used,
|
|
3390
|
+
gr.created_at as created_at,
|
|
3391
|
+
gr.updated_at as updated_at,
|
|
3392
|
+
lr.started_at as loop_run_started_at,
|
|
3393
|
+
wsr.account_profile as account_profile,
|
|
3394
|
+
wsr.account_tool as account_tool
|
|
3395
|
+
FROM goal_runs gr
|
|
3396
|
+
LEFT JOIN goals g ON g.id = gr.goal_id
|
|
3397
|
+
LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
|
|
3398
|
+
LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
|
|
3399
|
+
LEFT JOIN (
|
|
3400
|
+
SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
|
|
3401
|
+
FROM workflow_step_runs
|
|
3402
|
+
WHERE goal_run_id IS NOT NULL
|
|
3403
|
+
GROUP BY goal_run_id
|
|
3404
|
+
) wsr ON wsr.goal_run_id = gr.id
|
|
3405
|
+
WHERE COALESCE(gr.tokens_used, 0) > 0
|
|
3406
|
+
ORDER BY gr.created_at ASC, gr.id ASC
|
|
3407
|
+
`).all();
|
|
3408
|
+
}
|
|
3409
|
+
async function ingestLoops(db, verbose = false) {
|
|
3410
|
+
const dbPath = loopsDbPath();
|
|
3411
|
+
if (!existsSync8(dbPath)) {
|
|
3412
|
+
if (verbose)
|
|
3413
|
+
console.log("OpenLoops DB not found:", dbPath);
|
|
3414
|
+
return { sessions: 0, requests: 0 };
|
|
3415
|
+
}
|
|
3416
|
+
const model = loopsModel();
|
|
3417
|
+
const machineId = getMachineId();
|
|
3418
|
+
let loopsDb = null;
|
|
3419
|
+
let requests = 0;
|
|
3420
|
+
const sessions = new Set;
|
|
3421
|
+
try {
|
|
3422
|
+
loopsDb = openLoopsDb(dbPath, verbose);
|
|
3423
|
+
if (!loopsDb)
|
|
3424
|
+
return { sessions: 0, requests: 0 };
|
|
3425
|
+
for (const row of readLoopGoalRuns(loopsDb)) {
|
|
3426
|
+
const loopName = row.loop_name || "unknown-loop";
|
|
3427
|
+
const costCenterId = `loop:${slug(loopName)}`;
|
|
3428
|
+
const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
|
|
3429
|
+
const requestId = `loops-${row.goal_run_id}`;
|
|
3430
|
+
const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
|
|
3431
|
+
const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
|
|
3432
|
+
if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
|
|
3433
|
+
continue;
|
|
3434
|
+
upsertCostCenter(db, {
|
|
3435
|
+
id: costCenterId,
|
|
3436
|
+
kind: "loop",
|
|
3437
|
+
name: loopName,
|
|
3438
|
+
repo_path: null,
|
|
3439
|
+
labels_json: row.labels_json ?? "{}",
|
|
3440
|
+
created_at: row.created_at ?? timestamp
|
|
3441
|
+
});
|
|
3442
|
+
const account = accountFields(row);
|
|
3443
|
+
upsertSession(db, {
|
|
3444
|
+
id: sessionId,
|
|
3445
|
+
agent: "loop",
|
|
3446
|
+
project_path: "",
|
|
3447
|
+
project_name: loopName,
|
|
3448
|
+
started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
|
|
3449
|
+
ended_at: row.updated_at ?? null,
|
|
3450
|
+
total_cost_usd: 0,
|
|
3451
|
+
total_tokens: 0,
|
|
3452
|
+
request_count: 0,
|
|
3453
|
+
machine_id: machineId,
|
|
3454
|
+
cost_center_id: costCenterId,
|
|
3455
|
+
attribution_tag: "loops-orchestration",
|
|
3456
|
+
...account
|
|
3457
|
+
});
|
|
3458
|
+
const inputTokens = row.tokens_used;
|
|
3459
|
+
const outputTokens = 0;
|
|
3460
|
+
upsertRequest(db, {
|
|
3461
|
+
id: requestId,
|
|
3462
|
+
agent: "loop",
|
|
3463
|
+
session_id: sessionId,
|
|
3464
|
+
model,
|
|
3465
|
+
input_tokens: inputTokens,
|
|
3466
|
+
output_tokens: outputTokens,
|
|
3467
|
+
cache_read_tokens: 0,
|
|
3468
|
+
cache_create_tokens: 0,
|
|
3469
|
+
cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
|
|
3470
|
+
cost_basis: "estimated",
|
|
3471
|
+
duration_ms: 0,
|
|
3472
|
+
timestamp,
|
|
3473
|
+
source_request_id: requestId,
|
|
3474
|
+
machine_id: machineId,
|
|
3475
|
+
cost_center_id: costCenterId,
|
|
3476
|
+
attribution_tag: "loops-orchestration",
|
|
3477
|
+
...account
|
|
3478
|
+
});
|
|
3479
|
+
sessions.add(sessionId);
|
|
3480
|
+
requests++;
|
|
3481
|
+
setIngestState(db, "loops", row.goal_run_id, stateValue);
|
|
3482
|
+
if (verbose)
|
|
3483
|
+
console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
|
|
3484
|
+
}
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
if (verbose) {
|
|
3487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3488
|
+
console.log("OpenLoops ingest skipped:", message);
|
|
3489
|
+
}
|
|
3490
|
+
return { sessions: 0, requests: 0 };
|
|
3491
|
+
} finally {
|
|
3492
|
+
loopsDb?.close();
|
|
3493
|
+
}
|
|
3494
|
+
for (const sessionId of sessions)
|
|
3495
|
+
rollupSession(db, sessionId);
|
|
3496
|
+
return { sessions: sessions.size, requests };
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3140
3499
|
// src/ingest/claude-quota.ts
|
|
3141
3500
|
init_database();
|
|
3142
|
-
import { existsSync as
|
|
3501
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
3143
3502
|
|
|
3144
3503
|
// src/lib/paths.ts
|
|
3145
|
-
import { homedir as
|
|
3146
|
-
import { join as
|
|
3504
|
+
import { homedir as homedir9 } from "os";
|
|
3505
|
+
import { join as join9 } from "path";
|
|
3147
3506
|
function getHomeDir() {
|
|
3148
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
3507
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
3149
3508
|
}
|
|
3150
3509
|
function agentPaths() {
|
|
3151
3510
|
const home = getHomeDir();
|
|
3152
3511
|
return {
|
|
3153
|
-
claudeProjects:
|
|
3154
|
-
claudeCredentials:
|
|
3155
|
-
takumiProjects:
|
|
3156
|
-
codexDir:
|
|
3157
|
-
codexDb:
|
|
3158
|
-
codexAuth:
|
|
3159
|
-
codexConfig:
|
|
3160
|
-
geminiTmp:
|
|
3161
|
-
geminiHistory:
|
|
3162
|
-
opencodeMessages:
|
|
3163
|
-
piSessions:
|
|
3164
|
-
hermesDir:
|
|
3165
|
-
hermesDb:
|
|
3512
|
+
claudeProjects: join9(home, ".claude", "projects"),
|
|
3513
|
+
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
3514
|
+
takumiProjects: join9(home, ".takumi", "projects"),
|
|
3515
|
+
codexDir: join9(home, ".codex"),
|
|
3516
|
+
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
3517
|
+
codexAuth: join9(home, ".codex", "auth.json"),
|
|
3518
|
+
codexConfig: join9(home, ".codex", "config.toml"),
|
|
3519
|
+
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
3520
|
+
geminiHistory: join9(home, ".gemini", "history"),
|
|
3521
|
+
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
3522
|
+
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
3523
|
+
hermesDir: join9(home, ".hermes"),
|
|
3524
|
+
hermesDb: join9(home, ".hermes", "state.db")
|
|
3166
3525
|
};
|
|
3167
3526
|
}
|
|
3168
3527
|
|
|
@@ -3174,7 +3533,7 @@ function readClaudeToken() {
|
|
|
3174
3533
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
3175
3534
|
if (fromEnv)
|
|
3176
3535
|
return { token: fromEnv };
|
|
3177
|
-
if (!
|
|
3536
|
+
if (!existsSync9(CREDENTIALS_PATH))
|
|
3178
3537
|
return null;
|
|
3179
3538
|
try {
|
|
3180
3539
|
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
@@ -3313,14 +3672,14 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
3313
3672
|
|
|
3314
3673
|
// src/ingest/codex-quota.ts
|
|
3315
3674
|
init_database();
|
|
3316
|
-
import { existsSync as
|
|
3675
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
3317
3676
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
3318
3677
|
function readCodexAuth() {
|
|
3319
3678
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
3320
3679
|
if (fromEnv)
|
|
3321
3680
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
3322
3681
|
const authPath = agentPaths().codexAuth;
|
|
3323
|
-
if (!
|
|
3682
|
+
if (!existsSync10(authPath))
|
|
3324
3683
|
return null;
|
|
3325
3684
|
try {
|
|
3326
3685
|
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
@@ -3453,7 +3812,7 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
3453
3812
|
// src/lib/sync-all.ts
|
|
3454
3813
|
init_database();
|
|
3455
3814
|
async function syncAll(db, opts = {}) {
|
|
3456
|
-
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
3815
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
|
|
3457
3816
|
const all = !anySpecific;
|
|
3458
3817
|
const result = { deduped: 0 };
|
|
3459
3818
|
if (all || opts.claude) {
|
|
@@ -3476,6 +3835,8 @@ async function syncAll(db, opts = {}) {
|
|
|
3476
3835
|
result.pi = await ingestPi(db, opts.verbose);
|
|
3477
3836
|
if (all || opts.hermes)
|
|
3478
3837
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
3838
|
+
if (all || opts.loops)
|
|
3839
|
+
result.loops = await ingestLoops(db, opts.verbose);
|
|
3479
3840
|
if (opts.dedupe !== false)
|
|
3480
3841
|
result.deduped = dedupeRequests(db);
|
|
3481
3842
|
return result;
|
|
@@ -4794,6 +5155,13 @@ class LocalStore {
|
|
|
4794
5155
|
async accountBreakdown(query = {}) {
|
|
4795
5156
|
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4796
5157
|
}
|
|
5158
|
+
async costCenterBreakdown(query = {}) {
|
|
5159
|
+
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
5160
|
+
kind: query.kind,
|
|
5161
|
+
machine: query.machine,
|
|
5162
|
+
since: query.since
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
4797
5165
|
async accounts(period) {
|
|
4798
5166
|
return queryAccountBreakdown(this.db(), period);
|
|
4799
5167
|
}
|
|
@@ -4983,6 +5351,14 @@ class ApiStore {
|
|
|
4983
5351
|
async accountBreakdown(query = {}) {
|
|
4984
5352
|
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
4985
5353
|
}
|
|
5354
|
+
async costCenterBreakdown(query = {}) {
|
|
5355
|
+
const rows = await cloudListItems(this.cloud, "breakdown", q({
|
|
5356
|
+
by: query.kind ?? "cost-center",
|
|
5357
|
+
period: query.period,
|
|
5358
|
+
since: query.since
|
|
5359
|
+
}));
|
|
5360
|
+
return rows.filter((row) => Boolean(row?.cost_center_id));
|
|
5361
|
+
}
|
|
4986
5362
|
async accounts(period) {
|
|
4987
5363
|
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
4988
5364
|
}
|
|
@@ -5056,6 +5432,7 @@ class ApiStore {
|
|
|
5056
5432
|
const created = await this.cloud.client.create("budgets", {
|
|
5057
5433
|
project_path: input.project_path,
|
|
5058
5434
|
...input.agent ? { agent: input.agent } : {},
|
|
5435
|
+
...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
|
|
5059
5436
|
period: input.period,
|
|
5060
5437
|
limit_usd: input.limit_usd,
|
|
5061
5438
|
alert_at_percent: input.alert_at_percent
|
|
@@ -5146,6 +5523,7 @@ function buildServer() {
|
|
|
5146
5523
|
name: MCP_NAME,
|
|
5147
5524
|
version: packageMetadata.version
|
|
5148
5525
|
});
|
|
5526
|
+
const SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
5149
5527
|
const TOOL_NAMES = [
|
|
5150
5528
|
"get_cost_summary",
|
|
5151
5529
|
"get_sessions",
|
|
@@ -5154,6 +5532,7 @@ function buildServer() {
|
|
|
5154
5532
|
"get_project_breakdown",
|
|
5155
5533
|
"get_agent_breakdown",
|
|
5156
5534
|
"get_account_breakdown",
|
|
5535
|
+
"get_cost_center_breakdown",
|
|
5157
5536
|
"get_budget_status",
|
|
5158
5537
|
"set_budget",
|
|
5159
5538
|
"remove_budget",
|
|
@@ -5186,30 +5565,31 @@ function buildServer() {
|
|
|
5186
5565
|
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
5187
5566
|
get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
|
|
5188
5567
|
get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
|
|
5189
|
-
list_machines: "
|
|
5190
|
-
get_model_breakdown: "
|
|
5191
|
-
get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
|
|
5568
|
+
list_machines: "limit(20), verbose?, json? -> machine_id, sessions, requests, cost, last_active",
|
|
5569
|
+
get_model_breakdown: "limit(20), verbose?, json? -> model, requests, tokens, cost",
|
|
5570
|
+
get_project_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> project_name, sessions, tokens, cost",
|
|
5192
5571
|
get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5193
|
-
get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5194
|
-
|
|
5195
|
-
|
|
5572
|
+
get_account_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5573
|
+
get_cost_center_breakdown: "period?(today|week|month|year|all), kind?(loop|app|repo|service|team) -> cost center, sessions, requests, tokens, cost",
|
|
5574
|
+
get_budget_status: "limit(20), verbose?, json? -> budget limits, current spend, percent_used, is_over_alert",
|
|
5575
|
+
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, cost_center_id?, alert_at_percent? -> create budget",
|
|
5196
5576
|
remove_budget: "id -> delete budget",
|
|
5197
|
-
get_pricing: "
|
|
5577
|
+
get_pricing: "limit(20), verbose?, json? -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
5198
5578
|
set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
|
|
5199
5579
|
remove_pricing: "model -> delete pricing row",
|
|
5200
5580
|
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
5201
5581
|
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
5202
|
-
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
5203
|
-
get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
|
|
5204
|
-
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
|
|
5582
|
+
get_session_detail: "session_id(prefix ok), limit(20), verbose? -> per-request breakdown with model, tokens, cost",
|
|
5583
|
+
get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> usage snapshots and all-machine summary`,
|
|
5584
|
+
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> subscription/API-equivalent savings`,
|
|
5205
5585
|
estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight cost estimate priced from the store pricing table",
|
|
5206
5586
|
list_subscriptions: "no params -> configured subscription plans and included usage",
|
|
5207
|
-
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
|
|
5587
|
+
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}), json? -> create/update subscription plan`,
|
|
5208
5588
|
remove_subscription: "id -> delete subscription plan",
|
|
5209
|
-
sync: `sources(
|
|
5589
|
+
sync: `sources(${SYNC_SOURCES.join("|")}), json? -> ingest latest cost data`,
|
|
5210
5590
|
search_tools: "query substring -> tool name list",
|
|
5211
5591
|
describe_tools: "names[] -> one-line parameter hints",
|
|
5212
|
-
get_goals: "
|
|
5592
|
+
get_goals: "limit(20), verbose?, json? -> goal progress summary",
|
|
5213
5593
|
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
5214
5594
|
remove_goal: "id -> delete goal",
|
|
5215
5595
|
register_agent: "name, session_id? -> register agent session",
|
|
@@ -5220,6 +5600,20 @@ function buildServer() {
|
|
|
5220
5600
|
};
|
|
5221
5601
|
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
5222
5602
|
const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
5603
|
+
const DEFAULT_MCP_ROW_LIMIT = 20;
|
|
5604
|
+
function clampLimit(limit, fallback = DEFAULT_MCP_ROW_LIMIT, max = 100) {
|
|
5605
|
+
if (limit == null)
|
|
5606
|
+
return fallback;
|
|
5607
|
+
if (!Number.isFinite(limit))
|
|
5608
|
+
return fallback;
|
|
5609
|
+
return Math.max(1, Math.min(max, Math.floor(limit)));
|
|
5610
|
+
}
|
|
5611
|
+
function rowLimit(limit, verbose) {
|
|
5612
|
+
return verbose ? Number.POSITIVE_INFINITY : clampLimit(limit);
|
|
5613
|
+
}
|
|
5614
|
+
function hiddenRowsHint(total, shown, noun) {
|
|
5615
|
+
return total > shown ? `... ${total - shown} more ${noun} hidden; call with limit, verbose=true, or json=true.` : undefined;
|
|
5616
|
+
}
|
|
5223
5617
|
function fmtSession(s) {
|
|
5224
5618
|
const id = String(s["id"] ?? "").slice(0, 8);
|
|
5225
5619
|
const agent = String(s["agent"] ?? "");
|
|
@@ -5228,6 +5622,26 @@ function buildServer() {
|
|
|
5228
5622
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
5229
5623
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
5230
5624
|
}
|
|
5625
|
+
function compactPrimitive(value) {
|
|
5626
|
+
if (value == null)
|
|
5627
|
+
return "none";
|
|
5628
|
+
if (typeof value === "number")
|
|
5629
|
+
return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
|
|
5630
|
+
if (typeof value === "boolean")
|
|
5631
|
+
return value ? "yes" : "no";
|
|
5632
|
+
return String(value);
|
|
5633
|
+
}
|
|
5634
|
+
function compactObject(value) {
|
|
5635
|
+
if (value == null)
|
|
5636
|
+
return "none";
|
|
5637
|
+
if (Array.isArray(value))
|
|
5638
|
+
return `${value.length} rows`;
|
|
5639
|
+
if (typeof value !== "object")
|
|
5640
|
+
return compactPrimitive(value);
|
|
5641
|
+
const entries = Object.entries(value);
|
|
5642
|
+
const primitives = entries.filter(([, entry]) => entry == null || ["string", "number", "boolean"].includes(typeof entry)).slice(0, 6).map(([key, entry]) => `${key}=${compactPrimitive(entry)}`);
|
|
5643
|
+
return primitives.length ? primitives.join(", ") : `${entries.length} fields`;
|
|
5644
|
+
}
|
|
5231
5645
|
function text(text2) {
|
|
5232
5646
|
return { content: [{ type: "text", text: text2 }] };
|
|
5233
5647
|
}
|
|
@@ -5282,22 +5696,34 @@ function buildServer() {
|
|
|
5282
5696
|
return text(lines.join(`
|
|
5283
5697
|
`));
|
|
5284
5698
|
});
|
|
5285
|
-
server.tool("get_model_breakdown", "Cost per model.
|
|
5699
|
+
server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5286
5700
|
const rows = await store.modelBreakdown();
|
|
5701
|
+
if (json)
|
|
5702
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5703
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5287
5704
|
const lines = ["model agent reqs tokens cost"];
|
|
5288
|
-
for (const row of
|
|
5705
|
+
for (const row of visibleRows) {
|
|
5289
5706
|
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5290
5707
|
}
|
|
5708
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "models");
|
|
5709
|
+
if (hint)
|
|
5710
|
+
lines.push(hint);
|
|
5291
5711
|
return text(lines.join(`
|
|
5292
5712
|
`));
|
|
5293
5713
|
});
|
|
5294
|
-
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5714
|
+
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5295
5715
|
const rows = await store.projectBreakdown({ period: period ?? "all" });
|
|
5716
|
+
if (json)
|
|
5717
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5718
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5296
5719
|
const lines = ["project sessions tokens cost"];
|
|
5297
|
-
for (const row of
|
|
5720
|
+
for (const row of visibleRows) {
|
|
5298
5721
|
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
5299
5722
|
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5300
5723
|
}
|
|
5724
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "projects");
|
|
5725
|
+
if (hint)
|
|
5726
|
+
lines.push(hint);
|
|
5301
5727
|
return text(lines.join(`
|
|
5302
5728
|
`));
|
|
5303
5729
|
});
|
|
@@ -5312,29 +5738,55 @@ function buildServer() {
|
|
|
5312
5738
|
return text(lines.join(`
|
|
5313
5739
|
`));
|
|
5314
5740
|
});
|
|
5315
|
-
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5741
|
+
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5316
5742
|
const rows = await store.accountBreakdown({ period: period ?? "all" });
|
|
5743
|
+
if (json)
|
|
5744
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5317
5745
|
if (rows.length === 0)
|
|
5318
5746
|
return text("No account-attributed sessions yet.");
|
|
5747
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5319
5748
|
const lines = ["account agent sessions requests tokens api_eq billable included"];
|
|
5320
|
-
for (const row of
|
|
5749
|
+
for (const row of visibleRows) {
|
|
5321
5750
|
const label = String(row["account_email"] || row["account_name"] || row["account_key"] || "\u2014").slice(0, 20);
|
|
5322
5751
|
lines.push(`${label.padEnd(21)}` + `${String(row["account_tool"] ?? "").slice(0, 10).padEnd(11)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
|
|
5323
5752
|
}
|
|
5753
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "accounts");
|
|
5754
|
+
if (hint)
|
|
5755
|
+
lines.push(hint);
|
|
5324
5756
|
return text(lines.join(`
|
|
5325
5757
|
`));
|
|
5326
5758
|
});
|
|
5327
|
-
server.tool("
|
|
5759
|
+
server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
|
|
5760
|
+
period: z.enum(["today", "week", "month", "year", "all"]).optional(),
|
|
5761
|
+
kind: z.enum(["loop", "app", "repo", "service", "team"]).optional()
|
|
5762
|
+
}, async ({ period, kind }) => {
|
|
5763
|
+
const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
|
|
5764
|
+
if (rows.length === 0)
|
|
5765
|
+
return text("No cost-center usage yet.");
|
|
5766
|
+
const lines = ["kind cost_center sessions requests tokens cost"];
|
|
5767
|
+
for (const row of rows) {
|
|
5768
|
+
lines.push(`${String(row["kind"]).slice(0, 8).padEnd(9)}` + `${String(row["name"] || row["cost_center_id"] || "\u2014").slice(0, 20).padEnd(21)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5769
|
+
}
|
|
5770
|
+
return text(lines.join(`
|
|
5771
|
+
`));
|
|
5772
|
+
});
|
|
5773
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5328
5774
|
const budgets = await store.listBudgets();
|
|
5775
|
+
if (json)
|
|
5776
|
+
return text(JSON.stringify(budgets, null, 2));
|
|
5329
5777
|
if (budgets.length === 0)
|
|
5330
5778
|
return text("No budgets set.");
|
|
5779
|
+
const visibleBudgets = budgets.slice(0, rowLimit(limit, verbose));
|
|
5331
5780
|
const lines = ["scope period spent limit used% status"];
|
|
5332
|
-
for (const budget of
|
|
5333
|
-
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
5781
|
+
for (const budget of visibleBudgets) {
|
|
5782
|
+
const scope = String(budget["cost_center_id"] ?? budget["project_path"] ?? "global").slice(0, 20);
|
|
5334
5783
|
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
5335
5784
|
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
5336
5785
|
lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
5337
5786
|
}
|
|
5787
|
+
const hint = hiddenRowsHint(budgets.length, visibleBudgets.length, "budgets");
|
|
5788
|
+
if (hint)
|
|
5789
|
+
lines.push(hint);
|
|
5338
5790
|
return text(lines.join(`
|
|
5339
5791
|
`));
|
|
5340
5792
|
});
|
|
@@ -5343,11 +5795,13 @@ function buildServer() {
|
|
|
5343
5795
|
limit_usd: z.number().positive(),
|
|
5344
5796
|
project_path: z.string().optional(),
|
|
5345
5797
|
agent: z.enum(AGENTS).optional(),
|
|
5798
|
+
cost_center_id: z.string().optional(),
|
|
5346
5799
|
alert_at_percent: z.number().positive().max(100).optional()
|
|
5347
|
-
}, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
|
|
5800
|
+
}, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
|
|
5348
5801
|
const id = await store.setBudget({
|
|
5349
5802
|
project_path: project_path ?? null,
|
|
5350
5803
|
agent: agent ?? null,
|
|
5804
|
+
cost_center_id: cost_center_id ?? null,
|
|
5351
5805
|
period,
|
|
5352
5806
|
limit_usd,
|
|
5353
5807
|
alert_at_percent: alert_at_percent ?? 80
|
|
@@ -5358,12 +5812,18 @@ function buildServer() {
|
|
|
5358
5812
|
await store.removeBudget(id);
|
|
5359
5813
|
return text("Budget removed.");
|
|
5360
5814
|
});
|
|
5361
|
-
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
|
|
5815
|
+
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5362
5816
|
const rows = await store.listPricing();
|
|
5817
|
+
if (json)
|
|
5818
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5819
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5363
5820
|
const lines = ["model input output cache-r cache-w cache-1h storage-h"];
|
|
5364
|
-
for (const row of
|
|
5821
|
+
for (const row of visibleRows) {
|
|
5365
5822
|
lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
|
|
5366
5823
|
}
|
|
5824
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "pricing rows");
|
|
5825
|
+
if (hint)
|
|
5826
|
+
lines.push(hint);
|
|
5367
5827
|
return text(lines.join(`
|
|
5368
5828
|
`));
|
|
5369
5829
|
});
|
|
@@ -5428,11 +5888,14 @@ function buildServer() {
|
|
|
5428
5888
|
return text(lines.join(`
|
|
5429
5889
|
`));
|
|
5430
5890
|
});
|
|
5431
|
-
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
5891
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z.string(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional() }, async ({ session_id, limit, verbose }) => {
|
|
5432
5892
|
const detail = await store.sessionDetail(session_id);
|
|
5433
5893
|
if (!detail)
|
|
5434
5894
|
return textError(`Session not found: ${session_id}`);
|
|
5435
|
-
const { session, requests } = detail;
|
|
5895
|
+
const { session, requests: allRequests } = detail;
|
|
5896
|
+
const requestLimit = clampLimit(limit, verbose ? 50 : DEFAULT_MCP_ROW_LIMIT);
|
|
5897
|
+
const requestCount = { count: allRequests.length };
|
|
5898
|
+
const requests = allRequests.slice(0, requestLimit);
|
|
5436
5899
|
const lines = [
|
|
5437
5900
|
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
5438
5901
|
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
@@ -5440,27 +5903,85 @@ function buildServer() {
|
|
|
5440
5903
|
"",
|
|
5441
5904
|
"time model input output cache-r cache-5m cache-1h cost"
|
|
5442
5905
|
];
|
|
5443
|
-
for (const request of requests
|
|
5906
|
+
for (const request of requests) {
|
|
5444
5907
|
lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
|
|
5445
5908
|
}
|
|
5909
|
+
if (requestCount.count > requests.length) {
|
|
5910
|
+
lines.push(`... ${requestCount.count - requests.length} more requests hidden; call with limit or verbose=true for more.`);
|
|
5911
|
+
}
|
|
5446
5912
|
return text(lines.join(`
|
|
5447
5913
|
`));
|
|
5448
5914
|
});
|
|
5449
|
-
server.tool("sync", `Ingest new cost data. sources:
|
|
5915
|
+
server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z.enum([...SYNC_SOURCES]).optional(), json: z.boolean().optional() }, async ({ sources, json }) => {
|
|
5450
5916
|
if (isCloudStore()) {
|
|
5451
5917
|
return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
|
|
5452
5918
|
}
|
|
5453
5919
|
const selected = sources ?? "all";
|
|
5454
5920
|
const opts = selected === "all" ? {} : { [selected]: true };
|
|
5455
5921
|
const result = await syncAll(localDb(), opts);
|
|
5456
|
-
|
|
5922
|
+
if (json)
|
|
5923
|
+
return text(JSON.stringify(result, null, 2));
|
|
5924
|
+
const lines = [
|
|
5925
|
+
`sync: ${selected}`,
|
|
5926
|
+
`deduped: ${result.deduped}`
|
|
5927
|
+
];
|
|
5928
|
+
for (const [source, value] of Object.entries(result)) {
|
|
5929
|
+
if (source === "deduped")
|
|
5930
|
+
continue;
|
|
5931
|
+
if (value == null)
|
|
5932
|
+
continue;
|
|
5933
|
+
lines.push(`${source}: ${compactObject(value)}`);
|
|
5934
|
+
}
|
|
5935
|
+
lines.push("Use json=true for the full sync result.");
|
|
5936
|
+
return text(lines.join(`
|
|
5937
|
+
`));
|
|
5457
5938
|
});
|
|
5458
|
-
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
5939
|
+
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
5459
5940
|
const p = period ?? "month";
|
|
5460
|
-
|
|
5941
|
+
const { snapshots: snaps, summary } = await store.usage(p, agent);
|
|
5942
|
+
if (json)
|
|
5943
|
+
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
5944
|
+
const snapshotLimit = clampLimit(limit);
|
|
5945
|
+
const lines = [
|
|
5946
|
+
`period: ${p}${agent ? ` agent: ${agent}` : ""}`,
|
|
5947
|
+
`fleet: ${fmtUsd(summary.total_usd)} sessions: ${summary.sessions} requests: ${summary.requests.toLocaleString()} tokens: ${fmtTok(summary.tokens)}`,
|
|
5948
|
+
`snapshots: ${snaps.length}${snaps.length > snapshotLimit ? ` (showing ${snapshotLimit})` : ""}`
|
|
5949
|
+
];
|
|
5950
|
+
if (snaps.length > 0) {
|
|
5951
|
+
lines.push("", "date agent metric value unit machine");
|
|
5952
|
+
for (const snapshot of snaps.slice(0, snapshotLimit)) {
|
|
5953
|
+
lines.push(`${snapshot.date.padEnd(12)}` + `${snapshot.agent.slice(0, 10).padEnd(11)}` + `${snapshot.metric.slice(0, 27).padEnd(28)}` + `${compactPrimitive(snapshot.value).padEnd(13)}` + `${(snapshot.unit ?? "").slice(0, 8).padEnd(9)}` + `${snapshot.machine_id ?? "\u2014"}`);
|
|
5954
|
+
}
|
|
5955
|
+
if (snaps.length > snapshotLimit)
|
|
5956
|
+
lines.push(`... ${snaps.length - snapshotLimit} more snapshots hidden; call with limit or json=true.`);
|
|
5957
|
+
}
|
|
5958
|
+
return text(lines.join(`
|
|
5959
|
+
`));
|
|
5461
5960
|
});
|
|
5462
|
-
server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
5463
|
-
|
|
5961
|
+
server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
5962
|
+
const savings = await store.savings(period ?? "month", agent);
|
|
5963
|
+
if (json)
|
|
5964
|
+
return text(JSON.stringify(savings, null, 2));
|
|
5965
|
+
const lines = [
|
|
5966
|
+
`period: ${savings.period}${agent ? ` agent: ${agent}` : ""}`,
|
|
5967
|
+
`api_equivalent: ${fmtUsd(savings.api_equivalent_usd)}`,
|
|
5968
|
+
`subscription_fee: ${fmtUsd(savings.subscription_fee_usd)}`,
|
|
5969
|
+
`included_consumed: ${fmtUsd(savings.included_consumed_usd)}`,
|
|
5970
|
+
`on_demand: ${fmtUsd(savings.on_demand_usd)}`,
|
|
5971
|
+
`saved: ${fmtUsd(savings.saved_usd)}`
|
|
5972
|
+
];
|
|
5973
|
+
const rows = Object.entries(savings.by_agent ?? {});
|
|
5974
|
+
if (rows.length > 0) {
|
|
5975
|
+
const agentLimit = clampLimit(limit);
|
|
5976
|
+
lines.push("", `by_agent: ${rows.length}${rows.length > agentLimit ? ` (showing ${agentLimit})` : ""}`, "agent api_eq sub_fee included on_demand saved");
|
|
5977
|
+
for (const [agentName, row] of rows.slice(0, agentLimit)) {
|
|
5978
|
+
lines.push(`${agentName.slice(0, 10).padEnd(11)}` + `${fmtUsd(Number(row.api_equivalent_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.subscription_fee_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.included_consumed_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.on_demand_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.saved_usd ?? 0))}`);
|
|
5979
|
+
}
|
|
5980
|
+
if (rows.length > agentLimit)
|
|
5981
|
+
lines.push(`... ${rows.length - agentLimit} more agents hidden; call with limit or json=true.`);
|
|
5982
|
+
}
|
|
5983
|
+
return text(lines.join(`
|
|
5984
|
+
`));
|
|
5464
5985
|
});
|
|
5465
5986
|
server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
|
|
5466
5987
|
const rows = await store.listSubscriptions();
|
|
@@ -5482,7 +6003,8 @@ function buildServer() {
|
|
|
5482
6003
|
included_usage_usd: z.number().nonnegative().optional(),
|
|
5483
6004
|
billing_cycle_start: z.string().optional(),
|
|
5484
6005
|
reset_policy: z.string().optional(),
|
|
5485
|
-
active: z.boolean().optional()
|
|
6006
|
+
active: z.boolean().optional(),
|
|
6007
|
+
json: z.boolean().optional()
|
|
5486
6008
|
}, async (input) => {
|
|
5487
6009
|
const row = await store.setSubscription({
|
|
5488
6010
|
id: input.id,
|
|
@@ -5495,7 +6017,14 @@ function buildServer() {
|
|
|
5495
6017
|
reset_policy: input.reset_policy ?? "monthly",
|
|
5496
6018
|
active: input.active === false ? 0 : 1
|
|
5497
6019
|
});
|
|
5498
|
-
|
|
6020
|
+
if (input.json)
|
|
6021
|
+
return text(JSON.stringify(row, null, 2));
|
|
6022
|
+
return text(`Subscription set: ${row.id}
|
|
6023
|
+
provider: ${row.provider}
|
|
6024
|
+
plan: ${row.plan}
|
|
6025
|
+
agent: ${row.agent ?? "all"}
|
|
6026
|
+
fee: ${fmtUsd(row.monthly_fee_usd)}
|
|
6027
|
+
included: ${fmtUsd(row.included_usage_usd)}`);
|
|
5499
6028
|
});
|
|
5500
6029
|
server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
|
|
5501
6030
|
await store.removeSubscription(id);
|
|
@@ -5505,17 +6034,23 @@ function buildServer() {
|
|
|
5505
6034
|
const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
|
|
5506
6035
|
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
5507
6036
|
});
|
|
5508
|
-
server.tool("get_goals", "All spending goals with current progress.
|
|
6037
|
+
server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5509
6038
|
const goals = await store.listGoals();
|
|
6039
|
+
if (json)
|
|
6040
|
+
return text(JSON.stringify(goals, null, 2));
|
|
5510
6041
|
if (goals.length === 0)
|
|
5511
6042
|
return text("No goals set.");
|
|
6043
|
+
const visibleGoals = goals.slice(0, rowLimit(limit, verbose));
|
|
5512
6044
|
const lines = ["period scope limit spent used% status"];
|
|
5513
|
-
for (const goal of
|
|
6045
|
+
for (const goal of visibleGoals) {
|
|
5514
6046
|
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
5515
6047
|
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
5516
6048
|
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
5517
6049
|
lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
5518
6050
|
}
|
|
6051
|
+
const hint = hiddenRowsHint(goals.length, visibleGoals.length, "goals");
|
|
6052
|
+
if (hint)
|
|
6053
|
+
lines.push(hint);
|
|
5519
6054
|
return text(lines.join(`
|
|
5520
6055
|
`));
|
|
5521
6056
|
});
|
|
@@ -5537,14 +6072,20 @@ function buildServer() {
|
|
|
5537
6072
|
await store.removeGoal(id);
|
|
5538
6073
|
return text("Goal removed.");
|
|
5539
6074
|
});
|
|
5540
|
-
server.tool("list_machines", "List all machines that have synced data.
|
|
6075
|
+
server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5541
6076
|
const machines = await store.machines();
|
|
6077
|
+
if (json)
|
|
6078
|
+
return text(JSON.stringify(machines, null, 2));
|
|
5542
6079
|
if (machines.length === 0)
|
|
5543
6080
|
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
6081
|
+
const visibleMachines = machines.slice(0, rowLimit(limit, verbose));
|
|
5544
6082
|
const lines = ["machine sessions requests cost last_active"];
|
|
5545
|
-
for (const m of
|
|
6083
|
+
for (const m of visibleMachines) {
|
|
5546
6084
|
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
5547
6085
|
}
|
|
6086
|
+
const hint = hiddenRowsHint(machines.length, visibleMachines.length, "machines");
|
|
6087
|
+
if (hint)
|
|
6088
|
+
lines.push(hint);
|
|
5548
6089
|
lines.push(`
|
|
5549
6090
|
current machine: ${getMachineId()}`);
|
|
5550
6091
|
return text(lines.join(`
|