@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/cli/index.js
CHANGED
|
@@ -544,6 +544,7 @@ __export(exports_database, {
|
|
|
544
544
|
upsertProject: () => upsertProject,
|
|
545
545
|
upsertModelPricing: () => upsertModelPricing,
|
|
546
546
|
upsertGoal: () => upsertGoal,
|
|
547
|
+
upsertCostCenter: () => upsertCostCenter,
|
|
547
548
|
upsertBudget: () => upsertBudget,
|
|
548
549
|
upsertBillingDaily: () => upsertBillingDaily,
|
|
549
550
|
setIngestState: () => setIngestState,
|
|
@@ -561,6 +562,7 @@ __export(exports_database, {
|
|
|
561
562
|
queryModelBreakdown: () => queryModelBreakdown,
|
|
562
563
|
queryHourlyBreakdown: () => queryHourlyBreakdown,
|
|
563
564
|
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
565
|
+
queryCostCenterBreakdown: () => queryCostCenterBreakdown,
|
|
564
566
|
queryBillingSummary: () => queryBillingSummary,
|
|
565
567
|
queryAgentBreakdownSince: () => queryAgentBreakdownSince,
|
|
566
568
|
queryAgentBreakdown: () => queryAgentBreakdown,
|
|
@@ -583,6 +585,7 @@ __export(exports_database, {
|
|
|
583
585
|
getGoalStatuses: () => getGoalStatuses,
|
|
584
586
|
getDbPath: () => getDbPath,
|
|
585
587
|
getDataDir: () => getDataDir,
|
|
588
|
+
getCostCenter: () => getCostCenter,
|
|
586
589
|
getBudgetStatuses: () => getBudgetStatuses,
|
|
587
590
|
deleteSubscription: () => deleteSubscription,
|
|
588
591
|
deleteProject: () => deleteProject,
|
|
@@ -727,6 +730,16 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
727
730
|
}
|
|
728
731
|
function initSchema(db) {
|
|
729
732
|
db.exec(`
|
|
733
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
734
|
+
id TEXT PRIMARY KEY,
|
|
735
|
+
kind TEXT NOT NULL,
|
|
736
|
+
name TEXT NOT NULL,
|
|
737
|
+
repo_path TEXT,
|
|
738
|
+
labels_json TEXT DEFAULT '{}',
|
|
739
|
+
created_at TEXT NOT NULL,
|
|
740
|
+
updated_at TEXT NOT NULL
|
|
741
|
+
);
|
|
742
|
+
|
|
730
743
|
CREATE TABLE IF NOT EXISTS requests (
|
|
731
744
|
id TEXT PRIMARY KEY,
|
|
732
745
|
agent TEXT NOT NULL,
|
|
@@ -743,6 +756,7 @@ function initSchema(db) {
|
|
|
743
756
|
timestamp TEXT NOT NULL,
|
|
744
757
|
source_request_id TEXT,
|
|
745
758
|
machine_id TEXT DEFAULT '',
|
|
759
|
+
cost_center_id TEXT,
|
|
746
760
|
account_key TEXT DEFAULT '',
|
|
747
761
|
account_tool TEXT DEFAULT '',
|
|
748
762
|
account_name TEXT DEFAULT '',
|
|
@@ -761,6 +775,7 @@ function initSchema(db) {
|
|
|
761
775
|
total_tokens INTEGER DEFAULT 0,
|
|
762
776
|
request_count INTEGER DEFAULT 0,
|
|
763
777
|
machine_id TEXT DEFAULT '',
|
|
778
|
+
cost_center_id TEXT,
|
|
764
779
|
account_key TEXT DEFAULT '',
|
|
765
780
|
account_tool TEXT DEFAULT '',
|
|
766
781
|
account_name TEXT DEFAULT '',
|
|
@@ -781,6 +796,7 @@ function initSchema(db) {
|
|
|
781
796
|
id TEXT PRIMARY KEY,
|
|
782
797
|
project_path TEXT,
|
|
783
798
|
agent TEXT,
|
|
799
|
+
cost_center_id TEXT,
|
|
784
800
|
period TEXT NOT NULL,
|
|
785
801
|
limit_usd REAL NOT NULL,
|
|
786
802
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -811,6 +827,7 @@ function initSchema(db) {
|
|
|
811
827
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
812
828
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
813
829
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
830
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
814
831
|
|
|
815
832
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
816
833
|
model TEXT PRIMARY KEY,
|
|
@@ -897,6 +914,12 @@ function initSchema(db) {
|
|
|
897
914
|
`);
|
|
898
915
|
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
899
916
|
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
917
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
918
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
919
|
+
}
|
|
920
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
921
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
922
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
900
923
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
901
924
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
902
925
|
}
|
|
@@ -925,6 +948,10 @@ function initSchema(db) {
|
|
|
925
948
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
926
949
|
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
927
950
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
951
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
952
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
953
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
954
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
928
955
|
`);
|
|
929
956
|
}
|
|
930
957
|
function periodWhere(period) {
|
|
@@ -975,6 +1002,17 @@ function requestPeriodWhere(period) {
|
|
|
975
1002
|
return "1=1";
|
|
976
1003
|
}
|
|
977
1004
|
}
|
|
1005
|
+
function upsertCostCenter(db, center) {
|
|
1006
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
1007
|
+
db.prepare(`
|
|
1008
|
+
INSERT OR REPLACE INTO cost_centers
|
|
1009
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
1010
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1011
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
1012
|
+
}
|
|
1013
|
+
function getCostCenter(db, id) {
|
|
1014
|
+
return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
|
|
1015
|
+
}
|
|
978
1016
|
function upsertRequest(db, req) {
|
|
979
1017
|
const now = req.updated_at ?? new Date().toISOString();
|
|
980
1018
|
db.prepare(`
|
|
@@ -982,10 +1020,10 @@ function upsertRequest(db, req) {
|
|
|
982
1020
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
983
1021
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
984
1022
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
985
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
1023
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
986
1024
|
account_name, account_email, account_source, updated_at, synced_at)
|
|
987
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
988
|
-
`).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 ?? "");
|
|
1025
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1026
|
+
`).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 ?? "");
|
|
989
1027
|
}
|
|
990
1028
|
function upsertSession(db, session) {
|
|
991
1029
|
const now = session.updated_at ?? new Date().toISOString();
|
|
@@ -993,9 +1031,9 @@ function upsertSession(db, session) {
|
|
|
993
1031
|
INSERT OR REPLACE INTO sessions
|
|
994
1032
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
995
1033
|
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
996
|
-
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
997
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
998
|
-
`).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 ?? "");
|
|
1034
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
1035
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1036
|
+
`).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 ?? "");
|
|
999
1037
|
}
|
|
1000
1038
|
function rollupSession(db, sessionId) {
|
|
1001
1039
|
db.prepare(`
|
|
@@ -1021,9 +1059,12 @@ function rollupSession(db, sessionId) {
|
|
|
1021
1059
|
ELSE account_email END,
|
|
1022
1060
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
1023
1061
|
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1024
|
-
ELSE account_source END
|
|
1062
|
+
ELSE account_source END,
|
|
1063
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1064
|
+
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)
|
|
1065
|
+
ELSE cost_center_id END
|
|
1025
1066
|
WHERE id = ?
|
|
1026
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1067
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1027
1068
|
}
|
|
1028
1069
|
function querySessions(db, filter = {}) {
|
|
1029
1070
|
const conditions = [];
|
|
@@ -1465,6 +1506,139 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1465
1506
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1466
1507
|
return result;
|
|
1467
1508
|
}
|
|
1509
|
+
function kindFromCostCenterId(id) {
|
|
1510
|
+
const prefix = id.split(":", 1)[0] ?? "";
|
|
1511
|
+
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1512
|
+
}
|
|
1513
|
+
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1514
|
+
if (!row.cost_center_id)
|
|
1515
|
+
return;
|
|
1516
|
+
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1517
|
+
const group = groups.get(row.cost_center_id) ?? {
|
|
1518
|
+
cost_center_id: row.cost_center_id,
|
|
1519
|
+
kind: derivedKind,
|
|
1520
|
+
name: row.name || row.cost_center_id,
|
|
1521
|
+
repo_path: row.repo_path ?? null,
|
|
1522
|
+
labels_json: row.labels_json || "{}",
|
|
1523
|
+
sessionIds: new Set,
|
|
1524
|
+
requests: 0,
|
|
1525
|
+
total_tokens: 0,
|
|
1526
|
+
api_equivalent_usd: 0,
|
|
1527
|
+
metered_api_usd: 0,
|
|
1528
|
+
subscription_included_usd: 0,
|
|
1529
|
+
estimated_usd: 0,
|
|
1530
|
+
unknown_usd: 0,
|
|
1531
|
+
last_active: ""
|
|
1532
|
+
};
|
|
1533
|
+
group.kind = derivedKind;
|
|
1534
|
+
if (row.name)
|
|
1535
|
+
group.name = row.name;
|
|
1536
|
+
if (row.repo_path && !group.repo_path)
|
|
1537
|
+
group.repo_path = row.repo_path;
|
|
1538
|
+
if (row.labels_json && row.labels_json !== "{}")
|
|
1539
|
+
group.labels_json = row.labels_json;
|
|
1540
|
+
if (row.session_id)
|
|
1541
|
+
group.sessionIds.add(row.session_id);
|
|
1542
|
+
group.requests += row.requests;
|
|
1543
|
+
group.total_tokens += row.total_tokens;
|
|
1544
|
+
group.api_equivalent_usd += row.cost_usd;
|
|
1545
|
+
if (sessionOnly) {
|
|
1546
|
+
group.estimated_usd += row.cost_usd;
|
|
1547
|
+
} else if (row.cost_basis === "metered_api") {
|
|
1548
|
+
group.metered_api_usd += row.cost_usd;
|
|
1549
|
+
} else if (row.cost_basis === "subscription_included") {
|
|
1550
|
+
group.subscription_included_usd += row.cost_usd;
|
|
1551
|
+
} else if (row.cost_basis === "unknown") {
|
|
1552
|
+
group.unknown_usd += row.cost_usd;
|
|
1553
|
+
} else {
|
|
1554
|
+
group.estimated_usd += row.cost_usd;
|
|
1555
|
+
}
|
|
1556
|
+
if (!group.last_active || row.last_active > group.last_active)
|
|
1557
|
+
group.last_active = row.last_active;
|
|
1558
|
+
groups.set(row.cost_center_id, group);
|
|
1559
|
+
}
|
|
1560
|
+
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1561
|
+
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1562
|
+
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1563
|
+
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1564
|
+
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1565
|
+
const requestParams = [];
|
|
1566
|
+
const sessionParams = [];
|
|
1567
|
+
if (filter.since) {
|
|
1568
|
+
requestParams.push(filter.since);
|
|
1569
|
+
sessionParams.push(filter.since);
|
|
1570
|
+
}
|
|
1571
|
+
if (filter.machine) {
|
|
1572
|
+
requestParams.push(filter.machine);
|
|
1573
|
+
sessionParams.push(filter.machine);
|
|
1574
|
+
}
|
|
1575
|
+
const groups = new Map;
|
|
1576
|
+
const requestRows = db.prepare(`
|
|
1577
|
+
SELECT
|
|
1578
|
+
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1579
|
+
COALESCE(cc.kind, '') as kind,
|
|
1580
|
+
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1581
|
+
cc.repo_path as repo_path,
|
|
1582
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1583
|
+
r.session_id as session_id,
|
|
1584
|
+
1 as requests,
|
|
1585
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1586
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1587
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1588
|
+
r.timestamp as last_active
|
|
1589
|
+
FROM requests r
|
|
1590
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1591
|
+
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1592
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1593
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1594
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1595
|
+
`).all(...requestParams);
|
|
1596
|
+
for (const row of requestRows)
|
|
1597
|
+
addCostCenterBreakdownRow(groups, row, false);
|
|
1598
|
+
const sessionOnlyRows = db.prepare(`
|
|
1599
|
+
SELECT
|
|
1600
|
+
s.cost_center_id as cost_center_id,
|
|
1601
|
+
COALESCE(cc.kind, '') as kind,
|
|
1602
|
+
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1603
|
+
cc.repo_path as repo_path,
|
|
1604
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1605
|
+
s.id as session_id,
|
|
1606
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1607
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1608
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1609
|
+
'estimated' as cost_basis,
|
|
1610
|
+
s.started_at as last_active
|
|
1611
|
+
FROM sessions s
|
|
1612
|
+
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1613
|
+
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1614
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1615
|
+
AND s.cost_center_id IS NOT NULL
|
|
1616
|
+
AND s.cost_center_id != ''
|
|
1617
|
+
`).all(...sessionParams);
|
|
1618
|
+
for (const row of sessionOnlyRows)
|
|
1619
|
+
addCostCenterBreakdownRow(groups, row, true);
|
|
1620
|
+
const result = [...groups.values()].map((group) => ({
|
|
1621
|
+
cost_center_id: group.cost_center_id,
|
|
1622
|
+
kind: group.kind,
|
|
1623
|
+
name: group.name,
|
|
1624
|
+
repo_path: group.repo_path,
|
|
1625
|
+
labels_json: group.labels_json,
|
|
1626
|
+
sessions: group.sessionIds.size,
|
|
1627
|
+
requests: group.requests,
|
|
1628
|
+
total_tokens: group.total_tokens,
|
|
1629
|
+
api_equivalent_usd: group.api_equivalent_usd,
|
|
1630
|
+
billable_usd: group.metered_api_usd,
|
|
1631
|
+
metered_api_usd: group.metered_api_usd,
|
|
1632
|
+
subscription_included_usd: group.subscription_included_usd,
|
|
1633
|
+
estimated_usd: group.estimated_usd,
|
|
1634
|
+
unknown_usd: group.unknown_usd,
|
|
1635
|
+
cost_usd: group.api_equivalent_usd,
|
|
1636
|
+
last_active: group.last_active
|
|
1637
|
+
}));
|
|
1638
|
+
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1639
|
+
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1640
|
+
return filtered;
|
|
1641
|
+
}
|
|
1468
1642
|
function queryProjectBreakdownSince(db, since) {
|
|
1469
1643
|
return db.prepare(`
|
|
1470
1644
|
SELECT project_path, project_name,
|
|
@@ -1654,9 +1828,9 @@ function deleteProject(db, path) {
|
|
|
1654
1828
|
function upsertBudget(db, budget) {
|
|
1655
1829
|
db.prepare(`
|
|
1656
1830
|
INSERT OR REPLACE INTO budgets
|
|
1657
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1658
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1659
|
-
`).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);
|
|
1831
|
+
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1832
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1833
|
+
`).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);
|
|
1660
1834
|
}
|
|
1661
1835
|
function listBudgets(db) {
|
|
1662
1836
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
@@ -1678,6 +1852,10 @@ function getBudgetStatuses(db) {
|
|
|
1678
1852
|
spendQuery += ` AND agent = ?`;
|
|
1679
1853
|
params.push(b.agent);
|
|
1680
1854
|
}
|
|
1855
|
+
if (b.cost_center_id) {
|
|
1856
|
+
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1857
|
+
params.push(b.cost_center_id);
|
|
1858
|
+
}
|
|
1681
1859
|
const row = db.prepare(spendQuery).get(...params);
|
|
1682
1860
|
const spend = row.spend;
|
|
1683
1861
|
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
@@ -3660,6 +3838,13 @@ class LocalStore {
|
|
|
3660
3838
|
async accountBreakdown(query = {}) {
|
|
3661
3839
|
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3662
3840
|
}
|
|
3841
|
+
async costCenterBreakdown(query = {}) {
|
|
3842
|
+
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
3843
|
+
kind: query.kind,
|
|
3844
|
+
machine: query.machine,
|
|
3845
|
+
since: query.since
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3663
3848
|
async accounts(period) {
|
|
3664
3849
|
return queryAccountBreakdown(this.db(), period);
|
|
3665
3850
|
}
|
|
@@ -3849,6 +4034,14 @@ class ApiStore {
|
|
|
3849
4034
|
async accountBreakdown(query = {}) {
|
|
3850
4035
|
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
3851
4036
|
}
|
|
4037
|
+
async costCenterBreakdown(query = {}) {
|
|
4038
|
+
const rows = await cloudListItems(this.cloud, "breakdown", q({
|
|
4039
|
+
by: query.kind ?? "cost-center",
|
|
4040
|
+
period: query.period,
|
|
4041
|
+
since: query.since
|
|
4042
|
+
}));
|
|
4043
|
+
return rows.filter((row) => Boolean(row?.cost_center_id));
|
|
4044
|
+
}
|
|
3852
4045
|
async accounts(period) {
|
|
3853
4046
|
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
3854
4047
|
}
|
|
@@ -3922,6 +4115,7 @@ class ApiStore {
|
|
|
3922
4115
|
const created = await this.cloud.client.create("budgets", {
|
|
3923
4116
|
project_path: input.project_path,
|
|
3924
4117
|
...input.agent ? { agent: input.agent } : {},
|
|
4118
|
+
...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
|
|
3925
4119
|
period: input.period,
|
|
3926
4120
|
limit_usd: input.limit_usd,
|
|
3927
4121
|
alert_at_percent: input.alert_at_percent
|
|
@@ -5368,13 +5562,202 @@ var init_hermes = __esm(() => {
|
|
|
5368
5562
|
HERMES_DB = join11(homedir8(), ".hermes", "state.db");
|
|
5369
5563
|
});
|
|
5370
5564
|
|
|
5565
|
+
// src/ingest/loops.ts
|
|
5566
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5567
|
+
import { homedir as homedir9 } from "os";
|
|
5568
|
+
import { join as join12 } from "path";
|
|
5569
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
5570
|
+
function loopsDbPath() {
|
|
5571
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
|
|
5572
|
+
}
|
|
5573
|
+
function loopsModel() {
|
|
5574
|
+
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
5575
|
+
}
|
|
5576
|
+
function openLoopsDb(dbPath, verbose) {
|
|
5577
|
+
let lastError;
|
|
5578
|
+
for (const readonly of [true, false]) {
|
|
5579
|
+
let loopsDb = null;
|
|
5580
|
+
try {
|
|
5581
|
+
loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
5582
|
+
loopsDb.prepare("PRAGMA schema_version").get();
|
|
5583
|
+
return loopsDb;
|
|
5584
|
+
} catch (error) {
|
|
5585
|
+
lastError = error;
|
|
5586
|
+
loopsDb?.close();
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
if (verbose) {
|
|
5590
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5591
|
+
console.log("OpenLoops DB unreadable:", dbPath, message);
|
|
5592
|
+
}
|
|
5593
|
+
return null;
|
|
5594
|
+
}
|
|
5595
|
+
function slug(value) {
|
|
5596
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
5597
|
+
}
|
|
5598
|
+
function parseTarget(raw) {
|
|
5599
|
+
if (!raw)
|
|
5600
|
+
return {};
|
|
5601
|
+
try {
|
|
5602
|
+
const parsed = JSON.parse(raw);
|
|
5603
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5604
|
+
} catch {
|
|
5605
|
+
return {};
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
function stringValue(value) {
|
|
5609
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5610
|
+
}
|
|
5611
|
+
function accountFields(row) {
|
|
5612
|
+
const target = parseTarget(row.target_json);
|
|
5613
|
+
const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
|
|
5614
|
+
const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
|
|
5615
|
+
if (!tool || !profile) {
|
|
5616
|
+
return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
|
|
5617
|
+
}
|
|
5618
|
+
return {
|
|
5619
|
+
account_key: `${tool}:${profile}`,
|
|
5620
|
+
account_tool: tool,
|
|
5621
|
+
account_name: profile,
|
|
5622
|
+
account_source: "loops"
|
|
5623
|
+
};
|
|
5624
|
+
}
|
|
5625
|
+
function readLoopGoalRuns(loopsDb) {
|
|
5626
|
+
return loopsDb.prepare(`
|
|
5627
|
+
SELECT
|
|
5628
|
+
gr.id as goal_run_id,
|
|
5629
|
+
gr.goal_id as goal_id,
|
|
5630
|
+
COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
|
|
5631
|
+
COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
|
|
5632
|
+
COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
|
|
5633
|
+
l.labels_json as labels_json,
|
|
5634
|
+
l.target_json as target_json,
|
|
5635
|
+
g.objective as objective,
|
|
5636
|
+
gr.phase as phase,
|
|
5637
|
+
gr.status as status,
|
|
5638
|
+
COALESCE(gr.tokens_used, 0) as tokens_used,
|
|
5639
|
+
gr.created_at as created_at,
|
|
5640
|
+
gr.updated_at as updated_at,
|
|
5641
|
+
lr.started_at as loop_run_started_at,
|
|
5642
|
+
wsr.account_profile as account_profile,
|
|
5643
|
+
wsr.account_tool as account_tool
|
|
5644
|
+
FROM goal_runs gr
|
|
5645
|
+
LEFT JOIN goals g ON g.id = gr.goal_id
|
|
5646
|
+
LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
|
|
5647
|
+
LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
|
|
5648
|
+
LEFT JOIN (
|
|
5649
|
+
SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
|
|
5650
|
+
FROM workflow_step_runs
|
|
5651
|
+
WHERE goal_run_id IS NOT NULL
|
|
5652
|
+
GROUP BY goal_run_id
|
|
5653
|
+
) wsr ON wsr.goal_run_id = gr.id
|
|
5654
|
+
WHERE COALESCE(gr.tokens_used, 0) > 0
|
|
5655
|
+
ORDER BY gr.created_at ASC, gr.id ASC
|
|
5656
|
+
`).all();
|
|
5657
|
+
}
|
|
5658
|
+
async function ingestLoops(db, verbose = false) {
|
|
5659
|
+
const dbPath = loopsDbPath();
|
|
5660
|
+
if (!existsSync10(dbPath)) {
|
|
5661
|
+
if (verbose)
|
|
5662
|
+
console.log("OpenLoops DB not found:", dbPath);
|
|
5663
|
+
return { sessions: 0, requests: 0 };
|
|
5664
|
+
}
|
|
5665
|
+
const model = loopsModel();
|
|
5666
|
+
const machineId = getMachineId();
|
|
5667
|
+
let loopsDb = null;
|
|
5668
|
+
let requests = 0;
|
|
5669
|
+
const sessions = new Set;
|
|
5670
|
+
try {
|
|
5671
|
+
loopsDb = openLoopsDb(dbPath, verbose);
|
|
5672
|
+
if (!loopsDb)
|
|
5673
|
+
return { sessions: 0, requests: 0 };
|
|
5674
|
+
for (const row of readLoopGoalRuns(loopsDb)) {
|
|
5675
|
+
const loopName = row.loop_name || "unknown-loop";
|
|
5676
|
+
const costCenterId = `loop:${slug(loopName)}`;
|
|
5677
|
+
const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
|
|
5678
|
+
const requestId = `loops-${row.goal_run_id}`;
|
|
5679
|
+
const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
|
|
5680
|
+
const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
|
|
5681
|
+
if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
|
|
5682
|
+
continue;
|
|
5683
|
+
upsertCostCenter(db, {
|
|
5684
|
+
id: costCenterId,
|
|
5685
|
+
kind: "loop",
|
|
5686
|
+
name: loopName,
|
|
5687
|
+
repo_path: null,
|
|
5688
|
+
labels_json: row.labels_json ?? "{}",
|
|
5689
|
+
created_at: row.created_at ?? timestamp
|
|
5690
|
+
});
|
|
5691
|
+
const account = accountFields(row);
|
|
5692
|
+
upsertSession(db, {
|
|
5693
|
+
id: sessionId,
|
|
5694
|
+
agent: "loop",
|
|
5695
|
+
project_path: "",
|
|
5696
|
+
project_name: loopName,
|
|
5697
|
+
started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
|
|
5698
|
+
ended_at: row.updated_at ?? null,
|
|
5699
|
+
total_cost_usd: 0,
|
|
5700
|
+
total_tokens: 0,
|
|
5701
|
+
request_count: 0,
|
|
5702
|
+
machine_id: machineId,
|
|
5703
|
+
cost_center_id: costCenterId,
|
|
5704
|
+
attribution_tag: "loops-orchestration",
|
|
5705
|
+
...account
|
|
5706
|
+
});
|
|
5707
|
+
const inputTokens = row.tokens_used;
|
|
5708
|
+
const outputTokens = 0;
|
|
5709
|
+
upsertRequest(db, {
|
|
5710
|
+
id: requestId,
|
|
5711
|
+
agent: "loop",
|
|
5712
|
+
session_id: sessionId,
|
|
5713
|
+
model,
|
|
5714
|
+
input_tokens: inputTokens,
|
|
5715
|
+
output_tokens: outputTokens,
|
|
5716
|
+
cache_read_tokens: 0,
|
|
5717
|
+
cache_create_tokens: 0,
|
|
5718
|
+
cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
|
|
5719
|
+
cost_basis: "estimated",
|
|
5720
|
+
duration_ms: 0,
|
|
5721
|
+
timestamp,
|
|
5722
|
+
source_request_id: requestId,
|
|
5723
|
+
machine_id: machineId,
|
|
5724
|
+
cost_center_id: costCenterId,
|
|
5725
|
+
attribution_tag: "loops-orchestration",
|
|
5726
|
+
...account
|
|
5727
|
+
});
|
|
5728
|
+
sessions.add(sessionId);
|
|
5729
|
+
requests++;
|
|
5730
|
+
setIngestState(db, "loops", row.goal_run_id, stateValue);
|
|
5731
|
+
if (verbose)
|
|
5732
|
+
console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
|
|
5733
|
+
}
|
|
5734
|
+
} catch (error) {
|
|
5735
|
+
if (verbose) {
|
|
5736
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5737
|
+
console.log("OpenLoops ingest skipped:", message);
|
|
5738
|
+
}
|
|
5739
|
+
return { sessions: 0, requests: 0 };
|
|
5740
|
+
} finally {
|
|
5741
|
+
loopsDb?.close();
|
|
5742
|
+
}
|
|
5743
|
+
for (const sessionId of sessions)
|
|
5744
|
+
rollupSession(db, sessionId);
|
|
5745
|
+
return { sessions: sessions.size, requests };
|
|
5746
|
+
}
|
|
5747
|
+
var DEFAULT_LOOPS_DB_PATH, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
5748
|
+
var init_loops = __esm(() => {
|
|
5749
|
+
init_database();
|
|
5750
|
+
init_pricing();
|
|
5751
|
+
DEFAULT_LOOPS_DB_PATH = join12(homedir9(), ".hasna", "loops", "loops.db");
|
|
5752
|
+
});
|
|
5753
|
+
|
|
5371
5754
|
// src/ingest/claude-quota.ts
|
|
5372
|
-
import { existsSync as
|
|
5755
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
5373
5756
|
function readClaudeToken() {
|
|
5374
5757
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
5375
5758
|
if (fromEnv)
|
|
5376
5759
|
return { token: fromEnv };
|
|
5377
|
-
if (!
|
|
5760
|
+
if (!existsSync11(CREDENTIALS_PATH))
|
|
5378
5761
|
return null;
|
|
5379
5762
|
try {
|
|
5380
5763
|
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
@@ -5518,13 +5901,13 @@ var init_claude_quota = __esm(() => {
|
|
|
5518
5901
|
});
|
|
5519
5902
|
|
|
5520
5903
|
// src/ingest/codex-quota.ts
|
|
5521
|
-
import { existsSync as
|
|
5904
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
5522
5905
|
function readCodexAuth() {
|
|
5523
5906
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
5524
5907
|
if (fromEnv)
|
|
5525
5908
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
5526
5909
|
const authPath = agentPaths().codexAuth;
|
|
5527
|
-
if (!
|
|
5910
|
+
if (!existsSync12(authPath))
|
|
5528
5911
|
return null;
|
|
5529
5912
|
try {
|
|
5530
5913
|
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
@@ -5662,7 +6045,7 @@ var init_codex_quota = __esm(() => {
|
|
|
5662
6045
|
|
|
5663
6046
|
// src/lib/sync-all.ts
|
|
5664
6047
|
async function syncAll(db, opts = {}) {
|
|
5665
|
-
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
6048
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
|
|
5666
6049
|
const all = !anySpecific;
|
|
5667
6050
|
const result = { deduped: 0 };
|
|
5668
6051
|
if (all || opts.claude) {
|
|
@@ -5685,6 +6068,8 @@ async function syncAll(db, opts = {}) {
|
|
|
5685
6068
|
result.pi = await ingestPi(db, opts.verbose);
|
|
5686
6069
|
if (all || opts.hermes)
|
|
5687
6070
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
6071
|
+
if (all || opts.loops)
|
|
6072
|
+
result.loops = await ingestLoops(db, opts.verbose);
|
|
5688
6073
|
if (opts.dedupe !== false)
|
|
5689
6074
|
result.deduped = dedupeRequests(db);
|
|
5690
6075
|
return result;
|
|
@@ -5697,6 +6082,7 @@ var init_sync_all = __esm(() => {
|
|
|
5697
6082
|
init_cursor();
|
|
5698
6083
|
init_pi();
|
|
5699
6084
|
init_hermes();
|
|
6085
|
+
init_loops();
|
|
5700
6086
|
init_claude_quota();
|
|
5701
6087
|
init_codex_quota();
|
|
5702
6088
|
init_database();
|
|
@@ -6005,15 +6391,15 @@ __export(exports_config, {
|
|
|
6005
6391
|
loadConfig: () => loadConfig2,
|
|
6006
6392
|
getConfigValue: () => getConfigValue
|
|
6007
6393
|
});
|
|
6008
|
-
import { existsSync as
|
|
6009
|
-
import { dirname as dirname2, join as
|
|
6394
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
6395
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
6010
6396
|
function getConfigPath() {
|
|
6011
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
6397
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
6012
6398
|
}
|
|
6013
6399
|
function loadConfig2() {
|
|
6014
6400
|
try {
|
|
6015
6401
|
const configPath = getConfigPath();
|
|
6016
|
-
if (
|
|
6402
|
+
if (existsSync13(configPath)) {
|
|
6017
6403
|
const raw = readFileSync11(configPath, "utf-8");
|
|
6018
6404
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
6019
6405
|
}
|
|
@@ -6023,7 +6409,7 @@ function loadConfig2() {
|
|
|
6023
6409
|
function saveConfig2(config) {
|
|
6024
6410
|
const configPath = getConfigPath();
|
|
6025
6411
|
const dir = dirname2(configPath);
|
|
6026
|
-
if (!
|
|
6412
|
+
if (!existsSync13(dir))
|
|
6027
6413
|
mkdirSync3(dir, { recursive: true });
|
|
6028
6414
|
writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
|
|
6029
6415
|
`);
|
|
@@ -6124,6 +6510,8 @@ async function checkAndFireWebhooks(db) {
|
|
|
6124
6510
|
event: "budget_alert",
|
|
6125
6511
|
budget_id: b.id,
|
|
6126
6512
|
project: b.project_path ?? "global",
|
|
6513
|
+
cost_center_id: b.cost_center_id ?? null,
|
|
6514
|
+
scope: b.cost_center_id ?? b.project_path ?? "global",
|
|
6127
6515
|
period: b.period,
|
|
6128
6516
|
spend: b.current_spend_usd,
|
|
6129
6517
|
limit: b.limit_usd,
|
|
@@ -6167,7 +6555,7 @@ var init_webhooks = __esm(() => {
|
|
|
6167
6555
|
});
|
|
6168
6556
|
|
|
6169
6557
|
// src/lib/watch-paths.ts
|
|
6170
|
-
import { existsSync as
|
|
6558
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6171
6559
|
function getWatchPaths() {
|
|
6172
6560
|
const p = agentPaths();
|
|
6173
6561
|
const candidates = [
|
|
@@ -6180,7 +6568,7 @@ function getWatchPaths() {
|
|
|
6180
6568
|
p.piSessions,
|
|
6181
6569
|
p.hermesDir
|
|
6182
6570
|
];
|
|
6183
|
-
return candidates.filter((path) =>
|
|
6571
|
+
return candidates.filter((path) => existsSync14(path));
|
|
6184
6572
|
}
|
|
6185
6573
|
var init_watch_paths = __esm(() => {
|
|
6186
6574
|
init_paths();
|
|
@@ -6358,8 +6746,8 @@ var init_watch = __esm(() => {
|
|
|
6358
6746
|
// src/db/sync-pg.ts
|
|
6359
6747
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
6360
6748
|
import { fileURLToPath } from "url";
|
|
6361
|
-
import { dirname as dirname3, join as
|
|
6362
|
-
import { existsSync as
|
|
6749
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
6750
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6363
6751
|
import { translateSql, translateParams } from "@hasna/cloud";
|
|
6364
6752
|
function translateSqliteDates(sql) {
|
|
6365
6753
|
let s = sql;
|
|
@@ -6385,12 +6773,12 @@ function translateSqliteDates(sql) {
|
|
|
6385
6773
|
function resolveWorkerPath() {
|
|
6386
6774
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
6387
6775
|
const candidates = [
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6776
|
+
join14(here, "pg-sync-worker.ts"),
|
|
6777
|
+
join14(here, "pg-sync-worker.js"),
|
|
6778
|
+
join14(here, "..", "db", "pg-sync-worker.js")
|
|
6391
6779
|
];
|
|
6392
6780
|
for (const candidate of candidates)
|
|
6393
|
-
if (
|
|
6781
|
+
if (existsSync15(candidate))
|
|
6394
6782
|
return candidate;
|
|
6395
6783
|
return candidates[0];
|
|
6396
6784
|
}
|
|
@@ -8144,7 +8532,7 @@ __export(exports_serve, {
|
|
|
8144
8532
|
createHandler: () => createHandler
|
|
8145
8533
|
});
|
|
8146
8534
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8147
|
-
import { existsSync as
|
|
8535
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8148
8536
|
import { resolve, sep } from "path";
|
|
8149
8537
|
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
8150
8538
|
function openApiDocument() {
|
|
@@ -8227,13 +8615,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
8227
8615
|
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
8228
8616
|
return apiHandler(req);
|
|
8229
8617
|
}
|
|
8230
|
-
if (
|
|
8618
|
+
if (existsSync16(dashboardDir)) {
|
|
8231
8619
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
8232
|
-
if (filePath &&
|
|
8620
|
+
if (filePath && existsSync16(filePath)) {
|
|
8233
8621
|
return new Response(Bun.file(filePath));
|
|
8234
8622
|
}
|
|
8235
8623
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
8236
|
-
if (indexPath &&
|
|
8624
|
+
if (indexPath && existsSync16(indexPath)) {
|
|
8237
8625
|
return new Response(Bun.file(indexPath));
|
|
8238
8626
|
}
|
|
8239
8627
|
}
|
|
@@ -8402,6 +8790,10 @@ function createHandler(db, options = {}) {
|
|
|
8402
8790
|
return ok(queryAgentBreakdown(db, period, machine));
|
|
8403
8791
|
if (by === "account")
|
|
8404
8792
|
return ok(queryAccountBreakdown(db, period, machine));
|
|
8793
|
+
if (by === "cost-center")
|
|
8794
|
+
return ok(queryCostCenterBreakdown(db, period, { machine }));
|
|
8795
|
+
if (["loop", "app", "repo", "service", "team"].includes(by))
|
|
8796
|
+
return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
|
|
8405
8797
|
return ok(queryModelBreakdown(db));
|
|
8406
8798
|
}
|
|
8407
8799
|
if (path === "/api/budgets" && method === "GET") {
|
|
@@ -8425,6 +8817,7 @@ function createHandler(db, options = {}) {
|
|
|
8425
8817
|
id: randomUUID2(),
|
|
8426
8818
|
project_path: body["project_path"] ?? null,
|
|
8427
8819
|
agent,
|
|
8820
|
+
cost_center_id: optionalString(body["cost_center_id"]) ?? null,
|
|
8428
8821
|
period: normalizeBudgetPeriod(body["period"]),
|
|
8429
8822
|
limit_usd: limitUsd,
|
|
8430
8823
|
alert_at_percent: alertAtPercent,
|
|
@@ -8766,7 +9159,7 @@ var init_serve = __esm(() => {
|
|
|
8766
9159
|
};
|
|
8767
9160
|
ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
8768
9161
|
AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
8769
|
-
SYNC_SOURCES = ["all", ...AGENTS];
|
|
9162
|
+
SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
8770
9163
|
DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
8771
9164
|
FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
8772
9165
|
});
|
|
@@ -8781,14 +9174,14 @@ __export(exports_menubar, {
|
|
|
8781
9174
|
});
|
|
8782
9175
|
import chalk6 from "chalk";
|
|
8783
9176
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
8784
|
-
import { cpSync, existsSync as
|
|
9177
|
+
import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
8785
9178
|
import { tmpdir, arch } from "os";
|
|
8786
|
-
import { join as
|
|
9179
|
+
import { join as join15 } from "path";
|
|
8787
9180
|
function getArch() {
|
|
8788
9181
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
8789
9182
|
}
|
|
8790
9183
|
function isInstalled() {
|
|
8791
|
-
return
|
|
9184
|
+
return existsSync17(APP_PATH);
|
|
8792
9185
|
}
|
|
8793
9186
|
function isRunning() {
|
|
8794
9187
|
try {
|
|
@@ -8825,8 +9218,8 @@ async function menubarInstall(opts) {
|
|
|
8825
9218
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
8826
9219
|
process.exit(1);
|
|
8827
9220
|
}
|
|
8828
|
-
const zipPath =
|
|
8829
|
-
const extractDir =
|
|
9221
|
+
const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
9222
|
+
const extractDir = join15(tmpdir(), "economy-bar-extracted");
|
|
8830
9223
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
8831
9224
|
try {
|
|
8832
9225
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
@@ -8846,7 +9239,7 @@ async function menubarInstall(opts) {
|
|
|
8846
9239
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
8847
9240
|
if (isInstalled())
|
|
8848
9241
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
8849
|
-
cpSync(
|
|
9242
|
+
cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
8850
9243
|
try {
|
|
8851
9244
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
8852
9245
|
} catch {}
|
|
@@ -9950,6 +10343,8 @@ var ROADMAP_PHASES = [
|
|
|
9950
10343
|
]
|
|
9951
10344
|
}
|
|
9952
10345
|
];
|
|
10346
|
+
var DEFAULT_TODO_LIST_LIMIT = 20;
|
|
10347
|
+
var TODO_TITLE_WIDTH = 140;
|
|
9953
10348
|
function statusLabel(status) {
|
|
9954
10349
|
if (status === "done")
|
|
9955
10350
|
return chalk2.green("done");
|
|
@@ -9963,6 +10358,30 @@ function countByStatus(tasks) {
|
|
|
9963
10358
|
return acc;
|
|
9964
10359
|
}, { pending: 0, in_progress: 0, done: 0 });
|
|
9965
10360
|
}
|
|
10361
|
+
function truncateText(value, max) {
|
|
10362
|
+
if (value.length <= max)
|
|
10363
|
+
return value;
|
|
10364
|
+
return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
|
|
10365
|
+
}
|
|
10366
|
+
function parseListLimit(value) {
|
|
10367
|
+
if (value == null)
|
|
10368
|
+
return;
|
|
10369
|
+
const parsed = Number(value);
|
|
10370
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10371
|
+
console.error(chalk2.red("--limit must be a positive integer"));
|
|
10372
|
+
process.exit(1);
|
|
10373
|
+
}
|
|
10374
|
+
return parsed;
|
|
10375
|
+
}
|
|
10376
|
+
function matchingTasks(phases, status) {
|
|
10377
|
+
const rows = [];
|
|
10378
|
+
for (const phase of phases) {
|
|
10379
|
+
const tasks = status ? phase.tasks.filter((task) => task.status === status) : phase.tasks;
|
|
10380
|
+
for (const task of tasks)
|
|
10381
|
+
rows.push({ phase, task });
|
|
10382
|
+
}
|
|
10383
|
+
return rows;
|
|
10384
|
+
}
|
|
9966
10385
|
function printTodosHelp() {
|
|
9967
10386
|
console.log();
|
|
9968
10387
|
console.log(chalk2.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
|
|
@@ -9972,7 +10391,9 @@ function printTodosHelp() {
|
|
|
9972
10391
|
console.log();
|
|
9973
10392
|
console.log(chalk2.bold(" Commands"));
|
|
9974
10393
|
console.log(" economy todos Overview of phases and progress");
|
|
9975
|
-
console.log(" economy todos list
|
|
10394
|
+
console.log(" economy todos list Compact task list (first 20 by default)");
|
|
10395
|
+
console.log(" economy todos list --verbose Full task list with dependencies");
|
|
10396
|
+
console.log(" economy todos list --json Machine-readable roadmap payload");
|
|
9976
10397
|
console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
|
|
9977
10398
|
console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
|
|
9978
10399
|
console.log();
|
|
@@ -10014,7 +10435,8 @@ function printTodosOverview() {
|
|
|
10014
10435
|
console.log(chalk2.dim(` ${phase.summary}`));
|
|
10015
10436
|
}
|
|
10016
10437
|
console.log();
|
|
10017
|
-
console.log(chalk2.dim(" Run: economy todos list"));
|
|
10438
|
+
console.log(chalk2.dim(" Run: economy todos list --limit 20"));
|
|
10439
|
+
console.log(chalk2.dim(" Run: economy todos list --verbose"));
|
|
10018
10440
|
console.log(chalk2.dim(" Run: economy todos --help"));
|
|
10019
10441
|
console.log();
|
|
10020
10442
|
}
|
|
@@ -10025,21 +10447,55 @@ function printTodosList(opts) {
|
|
|
10025
10447
|
console.log(chalk2.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
|
|
10026
10448
|
return;
|
|
10027
10449
|
}
|
|
10450
|
+
const allMatches = matchingTasks(phases, opts.status);
|
|
10451
|
+
const limit = opts.limit ?? (opts.verbose ? Number.POSITIVE_INFINITY : DEFAULT_TODO_LIST_LIMIT);
|
|
10452
|
+
const visible = allMatches.slice(0, limit);
|
|
10453
|
+
if (opts.json) {
|
|
10454
|
+
const filteredPhases = phases.map((phase) => ({
|
|
10455
|
+
...phase,
|
|
10456
|
+
tasks: opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks
|
|
10457
|
+
})).filter((phase) => phase.tasks.length > 0);
|
|
10458
|
+
console.log(JSON.stringify({
|
|
10459
|
+
version: ROADMAP_VERSION,
|
|
10460
|
+
goal: ROADMAP_GOAL,
|
|
10461
|
+
total: allMatches.length,
|
|
10462
|
+
phases: filteredPhases
|
|
10463
|
+
}, null, 2));
|
|
10464
|
+
return;
|
|
10465
|
+
}
|
|
10028
10466
|
console.log();
|
|
10467
|
+
console.log(chalk2.bold.cyan(" Economy Roadmap Tasks"));
|
|
10468
|
+
console.log(chalk2.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
|
|
10469
|
+
if (!opts.verbose && visible.length < allMatches.length) {
|
|
10470
|
+
console.log(chalk2.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
|
|
10471
|
+
}
|
|
10472
|
+
console.log();
|
|
10473
|
+
const visibleByPhase = new Map;
|
|
10474
|
+
for (const { phase, task } of visible) {
|
|
10475
|
+
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10476
|
+
tasks.push(task);
|
|
10477
|
+
visibleByPhase.set(phase.id, tasks);
|
|
10478
|
+
}
|
|
10029
10479
|
for (const phase of phases) {
|
|
10030
|
-
const tasks =
|
|
10480
|
+
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10031
10481
|
if (tasks.length === 0)
|
|
10032
10482
|
continue;
|
|
10033
10483
|
console.log(chalk2.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
|
|
10034
|
-
|
|
10484
|
+
if (opts.verbose)
|
|
10485
|
+
console.log(chalk2.dim(` ${phase.summary}`));
|
|
10035
10486
|
console.log();
|
|
10036
10487
|
for (const task of tasks) {
|
|
10037
10488
|
const deps = task.deps?.length ? chalk2.dim(` deps: ${task.deps.join(", ")}`) : "";
|
|
10038
|
-
|
|
10489
|
+
const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
|
|
10490
|
+
console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
|
|
10039
10491
|
${" ".repeat(22)}${deps}` : ""}`);
|
|
10040
10492
|
}
|
|
10041
10493
|
console.log();
|
|
10042
10494
|
}
|
|
10495
|
+
if (visible.length < allMatches.length) {
|
|
10496
|
+
console.log(chalk2.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
|
|
10497
|
+
console.log();
|
|
10498
|
+
}
|
|
10043
10499
|
}
|
|
10044
10500
|
function printTodoShow(taskId) {
|
|
10045
10501
|
for (const phase of ROADMAP_PHASES) {
|
|
@@ -10069,13 +10525,19 @@ function registerTodosCommand(program) {
|
|
|
10069
10525
|
}
|
|
10070
10526
|
printTodosOverview();
|
|
10071
10527
|
});
|
|
10072
|
-
todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").action((opts) => {
|
|
10528
|
+
todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").option("--limit <n>", "Maximum tasks to print (default: 20)").option("--verbose", "Print all matching tasks with phase summaries and dependencies").option("--json", "Output the full filtered roadmap as JSON").action((opts) => {
|
|
10073
10529
|
const status = opts.status;
|
|
10074
10530
|
if (status && !["pending", "in_progress", "done"].includes(status)) {
|
|
10075
10531
|
console.error(chalk2.red("--status must be one of: pending, in_progress, done"));
|
|
10076
10532
|
process.exit(1);
|
|
10077
10533
|
}
|
|
10078
|
-
printTodosList({
|
|
10534
|
+
printTodosList({
|
|
10535
|
+
phase: opts.phase,
|
|
10536
|
+
status,
|
|
10537
|
+
limit: parseListLimit(opts.limit),
|
|
10538
|
+
verbose: Boolean(opts.verbose),
|
|
10539
|
+
json: Boolean(opts.json)
|
|
10540
|
+
});
|
|
10079
10541
|
});
|
|
10080
10542
|
todosCmd.command("show <task-id>").description("Show a single task (e.g. 4.2)").action((taskId) => {
|
|
10081
10543
|
printTodoShow(taskId);
|
|
@@ -10259,8 +10721,18 @@ function parsePeriod(value, fallback = "month") {
|
|
|
10259
10721
|
}
|
|
10260
10722
|
return p;
|
|
10261
10723
|
}
|
|
10724
|
+
function parseLimit(value, fallback, verbose) {
|
|
10725
|
+
if (verbose && value == null)
|
|
10726
|
+
return Number.POSITIVE_INFINITY;
|
|
10727
|
+
const parsed = Number(value ?? String(fallback));
|
|
10728
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10729
|
+
console.error(chalk4.red("--limit must be a positive integer"));
|
|
10730
|
+
process.exit(1);
|
|
10731
|
+
}
|
|
10732
|
+
return parsed;
|
|
10733
|
+
}
|
|
10262
10734
|
function registerExtendedCommands(program) {
|
|
10263
|
-
program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
10735
|
+
program.command("usage [period]").description("Show subscription usage quotas and consumption metrics").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--limit <n>", "Maximum snapshot rows to print (default: 20)").option("--verbose", "Show all snapshot rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
10264
10736
|
const period = parsePeriod(periodArg, "month");
|
|
10265
10737
|
const agent = parseAgent(opts.agent, "--agent");
|
|
10266
10738
|
const { snapshots: snaps, summary } = await getStore().usage(period, agent);
|
|
@@ -10274,9 +10746,19 @@ function registerExtendedCommands(program) {
|
|
|
10274
10746
|
if (snaps.length === 0) {
|
|
10275
10747
|
console.log(chalk4.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
|
|
10276
10748
|
} else {
|
|
10277
|
-
|
|
10749
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10750
|
+
const visibleSnaps = snaps.slice(0, limit);
|
|
10751
|
+
if (visibleSnaps.length < snaps.length) {
|
|
10752
|
+
console.log(chalk4.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
|
|
10753
|
+
console.log();
|
|
10754
|
+
}
|
|
10755
|
+
for (const s of visibleSnaps) {
|
|
10278
10756
|
console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
|
|
10279
10757
|
}
|
|
10758
|
+
if (visibleSnaps.length < snaps.length) {
|
|
10759
|
+
console.log(chalk4.dim(`
|
|
10760
|
+
... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10761
|
+
}
|
|
10280
10762
|
}
|
|
10281
10763
|
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
10282
10764
|
console.log(`
|
|
@@ -10316,7 +10798,7 @@ function registerExtendedCommands(program) {
|
|
|
10316
10798
|
});
|
|
10317
10799
|
console.log(chalk4.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
|
|
10318
10800
|
});
|
|
10319
|
-
subsCmd.command("list").description("List subscription plans").option("--json", "Output JSON").action(async (opts) => {
|
|
10801
|
+
subsCmd.command("list").description("List subscription plans").option("--limit <n>", "Maximum subscription rows to print (default: 20)").option("--verbose", "Show all subscription rows").option("--json", "Output JSON").action(async (opts) => {
|
|
10320
10802
|
const rows = await getStore().listSubscriptions();
|
|
10321
10803
|
if (opts.json) {
|
|
10322
10804
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -10326,10 +10808,20 @@ function registerExtendedCommands(program) {
|
|
|
10326
10808
|
console.log(chalk4.yellow("No subscriptions configured."));
|
|
10327
10809
|
return;
|
|
10328
10810
|
}
|
|
10811
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10812
|
+
const visibleRows = rows.slice(0, limit);
|
|
10329
10813
|
console.log();
|
|
10330
|
-
|
|
10814
|
+
if (visibleRows.length < rows.length) {
|
|
10815
|
+
console.log(chalk4.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
|
|
10816
|
+
console.log();
|
|
10817
|
+
}
|
|
10818
|
+
for (const r of visibleRows) {
|
|
10331
10819
|
console.log(` ${chalk4.white(r.provider)} / ${r.plan} ${fmt2(r.monthly_fee_usd)}/mo included ${fmt2(r.included_usage_usd)} ${r.agent ?? "all agents"}`);
|
|
10332
10820
|
}
|
|
10821
|
+
if (visibleRows.length < rows.length) {
|
|
10822
|
+
console.log(chalk4.dim(`
|
|
10823
|
+
... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10824
|
+
}
|
|
10333
10825
|
console.log();
|
|
10334
10826
|
});
|
|
10335
10827
|
subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
|
|
@@ -10453,20 +10945,29 @@ function registerExtendedCommands(program) {
|
|
|
10453
10945
|
}
|
|
10454
10946
|
}
|
|
10455
10947
|
function registerFleetCommands(program) {
|
|
10456
|
-
program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--json", "Output JSON").action(async (opts) => {
|
|
10948
|
+
program.command("fleet").description("Fleet-wide summaries across all machines").option("--period <p>", "Period", "today").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").option("--json", "Output JSON").action(async (opts) => {
|
|
10457
10949
|
const period = parsePeriod(opts.period, "today");
|
|
10458
10950
|
const { summary, machines, registry } = await getStore().fleet(period);
|
|
10459
10951
|
if (opts.json) {
|
|
10460
10952
|
console.log(JSON.stringify({ period, summary, machines, registry }, null, 2));
|
|
10461
10953
|
return;
|
|
10462
10954
|
}
|
|
10955
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10956
|
+
const visibleMachines = machines.slice(0, limit);
|
|
10463
10957
|
console.log();
|
|
10464
10958
|
console.log(chalk4.bold.cyan(` Fleet \u2014 ${period}`));
|
|
10465
10959
|
console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
|
|
10960
|
+
if (visibleMachines.length < machines.length) {
|
|
10961
|
+
console.log(chalk4.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
10962
|
+
}
|
|
10466
10963
|
console.log();
|
|
10467
|
-
for (const m of
|
|
10964
|
+
for (const m of visibleMachines) {
|
|
10468
10965
|
console.log(` ${chalk4.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
|
|
10469
10966
|
}
|
|
10967
|
+
if (visibleMachines.length < machines.length) {
|
|
10968
|
+
console.log(chalk4.dim(`
|
|
10969
|
+
... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10970
|
+
}
|
|
10470
10971
|
console.log();
|
|
10471
10972
|
});
|
|
10472
10973
|
program.command("completion <shell>").description("Print shell completion script (bash, zsh, or fish)").action((shell) => {
|
|
@@ -10721,6 +11222,16 @@ function parseOptionalCliAgent(value) {
|
|
|
10721
11222
|
fail(e instanceof Error ? e.message : String(e));
|
|
10722
11223
|
}
|
|
10723
11224
|
}
|
|
11225
|
+
function resolveRowLimit(value, verbose, fallback) {
|
|
11226
|
+
if (verbose && value == null)
|
|
11227
|
+
return Number.POSITIVE_INFINITY;
|
|
11228
|
+
return parsePositiveCliInteger(value ?? String(fallback), "--limit");
|
|
11229
|
+
}
|
|
11230
|
+
function printHiddenRowsHint(total, shown, detail) {
|
|
11231
|
+
if (total <= shown)
|
|
11232
|
+
return;
|
|
11233
|
+
console.log(chalk7.dim(` ... ${total - shown} more rows hidden. ${detail}`));
|
|
11234
|
+
}
|
|
10724
11235
|
function requireCliChoice(value, option, allowed) {
|
|
10725
11236
|
const selected = value ?? allowed[0];
|
|
10726
11237
|
if (allowed.includes(selected))
|
|
@@ -10820,14 +11331,14 @@ program.action(async () => {
|
|
|
10820
11331
|
}
|
|
10821
11332
|
console.log();
|
|
10822
11333
|
});
|
|
10823
|
-
program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
|
|
11334
|
+
program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("--loops", "Only ingest OpenLoops orchestration/judge token usage").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
|
|
10824
11335
|
if (isCloudStore()) {
|
|
10825
11336
|
console.log(chalk7.yellow("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)"));
|
|
10826
11337
|
return;
|
|
10827
11338
|
}
|
|
10828
11339
|
const db = openDatabase();
|
|
10829
11340
|
ensurePricingSeeded(db);
|
|
10830
|
-
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes;
|
|
11341
|
+
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops;
|
|
10831
11342
|
if (!anySpecific || opts.verbose) {
|
|
10832
11343
|
try {
|
|
10833
11344
|
const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
|
|
@@ -10840,7 +11351,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
10840
11351
|
}
|
|
10841
11352
|
}
|
|
10842
11353
|
if (opts.force) {
|
|
10843
|
-
const ingestSources = [...AGENTS, "codex-codewith"];
|
|
11354
|
+
const ingestSources = [...AGENTS, "codex-codewith", "loops"];
|
|
10844
11355
|
db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
|
|
10845
11356
|
if (opts.verbose)
|
|
10846
11357
|
console.log(chalk7.dim("Cleared ingest cache"));
|
|
@@ -10855,6 +11366,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
10855
11366
|
cursor: opts.cursor,
|
|
10856
11367
|
pi: opts.pi,
|
|
10857
11368
|
hermes: opts.hermes,
|
|
11369
|
+
loops: opts.loops,
|
|
10858
11370
|
verbose: opts.verbose
|
|
10859
11371
|
});
|
|
10860
11372
|
console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
|
|
@@ -10984,22 +11496,36 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
10984
11496
|
]));
|
|
10985
11497
|
console.log();
|
|
10986
11498
|
});
|
|
10987
|
-
program.command("breakdown").description("Cost breakdown by model, agent, project, or
|
|
11499
|
+
program.command("breakdown").description("Cost breakdown by model, agent, project, account, or cost center").option("--by <dimension>", "Dimension: model|agent|project|account|cost-center|loop|app|repo", "model").option("--since <date>", "Filter since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--limit <n>", "Maximum breakdown rows to print (default: 20)").option("--verbose", "Show all breakdown rows").option("--json", "Output JSON").action(async (opts) => {
|
|
10988
11500
|
const store = getStore();
|
|
11501
|
+
const by = requireCliChoice(opts.by, "--by", ["model", "agent", "project", "account", "cost-center", "loop", "app", "repo"]);
|
|
10989
11502
|
const since = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
11503
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11504
|
+
const costCenterKinds = new Set(["loop", "app", "repo"]);
|
|
10990
11505
|
console.log();
|
|
10991
|
-
if (
|
|
11506
|
+
if (by === "project") {
|
|
10992
11507
|
const rows = await store.projectBreakdown({ since });
|
|
10993
|
-
|
|
11508
|
+
if (opts.json) {
|
|
11509
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11510
|
+
return;
|
|
11511
|
+
}
|
|
11512
|
+
const visibleRows = rows.slice(0, limit);
|
|
11513
|
+
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
10994
11514
|
chalk7.white(r.project_name || chalk7.dim("unknown")),
|
|
10995
11515
|
String(r.sessions),
|
|
10996
11516
|
String(r.requests),
|
|
10997
11517
|
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
10998
11518
|
fmt4(r.cost_usd)
|
|
10999
11519
|
]));
|
|
11000
|
-
|
|
11520
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11521
|
+
} else if (by === "agent") {
|
|
11001
11522
|
const rows = await store.agentBreakdown({ since });
|
|
11002
|
-
|
|
11523
|
+
if (opts.json) {
|
|
11524
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11525
|
+
return;
|
|
11526
|
+
}
|
|
11527
|
+
const visibleRows = rows.slice(0, limit);
|
|
11528
|
+
printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], visibleRows.map((r) => [
|
|
11003
11529
|
fmtAgent(r.agent),
|
|
11004
11530
|
String(r.sessions),
|
|
11005
11531
|
String(r.requests),
|
|
@@ -11008,22 +11534,57 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11008
11534
|
fmt4(r.billable_usd),
|
|
11009
11535
|
fmt4(r.subscription_included_usd)
|
|
11010
11536
|
]));
|
|
11011
|
-
|
|
11012
|
-
|
|
11537
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11538
|
+
} else if (by === "account") {
|
|
11539
|
+
const rows = await store.accountBreakdown({ since });
|
|
11540
|
+
if (opts.json) {
|
|
11541
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11542
|
+
return;
|
|
11543
|
+
}
|
|
11544
|
+
const visibleRows = rows.slice(0, limit);
|
|
11545
|
+
printAccountBreakdown(visibleRows);
|
|
11546
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11547
|
+
} else if (by === "cost-center" || costCenterKinds.has(by)) {
|
|
11548
|
+
const kind = costCenterKinds.has(by) ? by : undefined;
|
|
11549
|
+
const rows = await store.costCenterBreakdown({ since, kind });
|
|
11550
|
+
if (opts.json) {
|
|
11551
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11552
|
+
return;
|
|
11553
|
+
}
|
|
11554
|
+
if (rows.length === 0) {
|
|
11555
|
+
console.log(chalk7.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
|
|
11556
|
+
} else {
|
|
11557
|
+
const visibleRows = rows.slice(0, limit);
|
|
11558
|
+
printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11559
|
+
chalk7.white(r.kind),
|
|
11560
|
+
chalk7.white(r.name),
|
|
11561
|
+
String(r.sessions),
|
|
11562
|
+
String(r.requests),
|
|
11563
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
11564
|
+
fmt4(r.cost_usd)
|
|
11565
|
+
]));
|
|
11566
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11567
|
+
}
|
|
11013
11568
|
} else {
|
|
11014
11569
|
const rows = await store.modelBreakdown({ since });
|
|
11015
|
-
|
|
11570
|
+
if (opts.json) {
|
|
11571
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11572
|
+
return;
|
|
11573
|
+
}
|
|
11574
|
+
const visibleRows = rows.slice(0, limit);
|
|
11575
|
+
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11016
11576
|
chalk7.white(r.model),
|
|
11017
11577
|
fmtAgent(r.agent),
|
|
11018
11578
|
String(r.requests),
|
|
11019
11579
|
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
11020
11580
|
fmt4(r.cost_usd)
|
|
11021
11581
|
]));
|
|
11582
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11022
11583
|
}
|
|
11023
11584
|
console.log();
|
|
11024
11585
|
});
|
|
11025
11586
|
var ACCOUNT_PERIODS = ["today", "week", "month", "year", "all"];
|
|
11026
|
-
program.command("accounts [period]").description("List account usage by email address and coding agent").option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
11587
|
+
program.command("accounts [period]").description("List account usage by email address and coding agent").option("--limit <n>", "Maximum account rows to print (default: 20)").option("--verbose", "Show all account rows").option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
11027
11588
|
const period = requireCliChoice(periodArg, "period", ACCOUNT_PERIODS);
|
|
11028
11589
|
const rows = await getStore().accounts(period);
|
|
11029
11590
|
if (opts.json) {
|
|
@@ -11036,8 +11597,14 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
11036
11597
|
}
|
|
11037
11598
|
console.log();
|
|
11038
11599
|
console.log(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
|
|
11600
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11601
|
+
const visibleRows = rows.slice(0, limit);
|
|
11602
|
+
if (visibleRows.length < rows.length) {
|
|
11603
|
+
console.log(chalk7.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
|
|
11604
|
+
}
|
|
11039
11605
|
console.log();
|
|
11040
|
-
printAccountBreakdown(
|
|
11606
|
+
printAccountBreakdown(visibleRows);
|
|
11607
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11041
11608
|
console.log();
|
|
11042
11609
|
});
|
|
11043
11610
|
program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", "Filter by agent").option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
|
|
@@ -11050,7 +11617,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
|
|
|
11050
11617
|
});
|
|
11051
11618
|
});
|
|
11052
11619
|
var budgetCmd = program.command("budget").description("Manage spending budgets");
|
|
11053
|
-
budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|takumi|codex|gemini)").action(async (opts) => {
|
|
11620
|
+
budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--cost-center <id>", "Cost center id (for example loop:fleet-evaluator)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|takumi|codex|gemini)").action(async (opts) => {
|
|
11054
11621
|
const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
|
|
11055
11622
|
const alertAtPercent = parsePositiveCliNumber(opts.alert ?? "80", "--alert");
|
|
11056
11623
|
if (alertAtPercent > 100)
|
|
@@ -11060,25 +11627,32 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
11060
11627
|
await getStore().setBudget({
|
|
11061
11628
|
project_path: opts.project ?? null,
|
|
11062
11629
|
agent,
|
|
11630
|
+
cost_center_id: opts.costCenter ?? null,
|
|
11063
11631
|
period,
|
|
11064
11632
|
limit_usd: limitUsd,
|
|
11065
11633
|
alert_at_percent: alertAtPercent
|
|
11066
11634
|
});
|
|
11067
|
-
console.log(chalk7.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11635
|
+
console.log(chalk7.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11068
11636
|
});
|
|
11069
|
-
budgetCmd.command("list").description("List all budgets").action(async () => {
|
|
11637
|
+
budgetCmd.command("list").description("List all budgets").option("--limit <n>", "Maximum budget rows to print (default: 20)").option("--verbose", "Show all budget rows").action(async (opts) => {
|
|
11070
11638
|
const statuses = await getStore().listBudgets();
|
|
11071
11639
|
if (statuses.length === 0) {
|
|
11072
11640
|
console.log(chalk7.yellow("No budgets set."));
|
|
11073
11641
|
return;
|
|
11074
11642
|
}
|
|
11643
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11644
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11075
11645
|
console.log();
|
|
11076
|
-
|
|
11646
|
+
if (visibleStatuses.length < statuses.length) {
|
|
11647
|
+
console.log(chalk7.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
|
|
11648
|
+
console.log();
|
|
11649
|
+
}
|
|
11650
|
+
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
|
|
11077
11651
|
const pct = b.percent_used.toFixed(1);
|
|
11078
11652
|
const status = b.is_over_limit ? chalk7.red("OVER") : b.is_over_alert ? chalk7.yellow("ALERT") : chalk7.green("OK");
|
|
11079
11653
|
const pctColor = b.is_over_limit ? chalk7.red(pct + "%") : b.is_over_alert ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
|
|
11080
11654
|
return [
|
|
11081
|
-
chalk7.white(b.project_path ?? "global"),
|
|
11655
|
+
chalk7.white(b.cost_center_id ?? b.project_path ?? "global"),
|
|
11082
11656
|
b.period,
|
|
11083
11657
|
fmt4(b.limit_usd),
|
|
11084
11658
|
fmt4(b.current_spend_usd),
|
|
@@ -11086,6 +11660,7 @@ budgetCmd.command("list").description("List all budgets").action(async () => {
|
|
|
11086
11660
|
status
|
|
11087
11661
|
];
|
|
11088
11662
|
}));
|
|
11663
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11089
11664
|
console.log();
|
|
11090
11665
|
});
|
|
11091
11666
|
budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
|
|
@@ -11099,20 +11674,27 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
|
|
|
11099
11674
|
await getStore().addProject(path, name);
|
|
11100
11675
|
console.log(chalk7.green(`\u2713 Project added: ${path}`));
|
|
11101
11676
|
});
|
|
11102
|
-
projectCmd.command("list").description("List all projects with costs").action(async () => {
|
|
11677
|
+
projectCmd.command("list").description("List all projects with costs").option("--limit <n>", "Maximum project rows to print (default: 20)").option("--verbose", "Show all project rows").action(async (opts) => {
|
|
11103
11678
|
const projects = await getStore().listProjects();
|
|
11104
11679
|
if (projects.length === 0) {
|
|
11105
11680
|
console.log(chalk7.yellow("No projects tracked yet."));
|
|
11106
11681
|
return;
|
|
11107
11682
|
}
|
|
11683
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11684
|
+
const visibleProjects = projects.slice(0, limit);
|
|
11108
11685
|
console.log();
|
|
11109
|
-
|
|
11686
|
+
if (visibleProjects.length < projects.length) {
|
|
11687
|
+
console.log(chalk7.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
|
|
11688
|
+
console.log();
|
|
11689
|
+
}
|
|
11690
|
+
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
|
|
11110
11691
|
chalk7.white(p.project_name || chalk7.dim("unknown")),
|
|
11111
11692
|
chalk7.dim(p.project_path.substring(0, 40)),
|
|
11112
11693
|
String(p.sessions),
|
|
11113
11694
|
fmt4(p.cost_usd),
|
|
11114
11695
|
chalk7.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
11115
11696
|
]));
|
|
11697
|
+
printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
|
|
11116
11698
|
console.log();
|
|
11117
11699
|
});
|
|
11118
11700
|
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
|
|
@@ -11217,10 +11799,16 @@ configCmd.action(async () => {
|
|
|
11217
11799
|
console.log();
|
|
11218
11800
|
});
|
|
11219
11801
|
var pricingCmd = program.command("pricing").description("Manage model pricing rates");
|
|
11220
|
-
pricingCmd.command("list").description("List all model prices").action(async () => {
|
|
11802
|
+
pricingCmd.command("list").description("List all model prices").option("--limit <n>", "Maximum pricing rows to print (default: 50)").option("--verbose", "Show all pricing rows").action(async (opts) => {
|
|
11221
11803
|
const rows = await getStore().listPricing();
|
|
11804
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 50);
|
|
11805
|
+
const visibleRows = rows.slice(0, limit);
|
|
11222
11806
|
console.log();
|
|
11223
|
-
|
|
11807
|
+
if (visibleRows.length < rows.length) {
|
|
11808
|
+
console.log(chalk7.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
|
|
11809
|
+
console.log();
|
|
11810
|
+
}
|
|
11811
|
+
printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
|
|
11224
11812
|
chalk7.white(r.model),
|
|
11225
11813
|
fmt4(r.input_per_1m),
|
|
11226
11814
|
fmt4(r.output_per_1m),
|
|
@@ -11229,6 +11817,7 @@ pricingCmd.command("list").description("List all model prices").action(async ()
|
|
|
11229
11817
|
fmt4(r.cache_storage_per_1m_hour ?? 0),
|
|
11230
11818
|
chalk7.dim(fmt4(r.output_per_1m / 1000))
|
|
11231
11819
|
]));
|
|
11820
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
|
|
11232
11821
|
console.log();
|
|
11233
11822
|
});
|
|
11234
11823
|
pricingCmd.command("set <model>").description("Set pricing for a model").option("--input <usd>", "Input price per 1M tokens").option("--output <usd>", "Output price per 1M tokens").option("--cache-read <usd>", "Cache read price per 1M tokens", "0").option("--cache-write <usd>", "5-minute cache write price per 1M tokens", "0").option("--cache-write-1h <usd>", "1-hour cache write price per 1M tokens", "0").option("--cache-storage <usd>", "Context cache storage price per 1M token-hours", "0").action(async (model, opts) => {
|
|
@@ -11323,7 +11912,8 @@ Gemini (~/.gemini/settings.json):`));
|
|
|
11323
11912
|
}
|
|
11324
11913
|
console.log();
|
|
11325
11914
|
});
|
|
11326
|
-
program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
|
|
11915
|
+
program.command("session <id>").description("Show detailed breakdown of a single session").option("--limit <n>", "Number of request rows to show (default: 20)").option("--verbose", "Show up to 50 request rows").action(async (id, opts) => {
|
|
11916
|
+
const requestLimit = parsePositiveCliInteger(opts.limit ?? (opts.verbose ? "50" : "20"), "--limit");
|
|
11327
11917
|
await autoSync();
|
|
11328
11918
|
const detail = await getStore().sessionDetail(id);
|
|
11329
11919
|
if (!detail) {
|
|
@@ -11344,10 +11934,11 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11344
11934
|
["Requests", fmtCount(session["request_count"])]
|
|
11345
11935
|
]);
|
|
11346
11936
|
if (requests.length > 0) {
|
|
11937
|
+
const visibleRequests = requests.slice(0, requestLimit);
|
|
11347
11938
|
console.log(chalk7.dim(`
|
|
11348
|
-
Requests (${requests.length}):
|
|
11939
|
+
Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
|
|
11349
11940
|
`));
|
|
11350
|
-
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"],
|
|
11941
|
+
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
|
|
11351
11942
|
chalk7.dim(String(r["timestamp"]).substring(11, 19)),
|
|
11352
11943
|
chalk7.white(String(r["model"]).substring(0, 22)),
|
|
11353
11944
|
fmtTokens2(r["input_tokens"]),
|
|
@@ -11356,12 +11947,13 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11356
11947
|
fmtTokens2(r["cache_create_tokens"]),
|
|
11357
11948
|
fmt4(r["cost_usd"])
|
|
11358
11949
|
]));
|
|
11359
|
-
if (requests.length >
|
|
11360
|
-
console.log(chalk7.dim(` ...
|
|
11950
|
+
if (requests.length > visibleRequests.length) {
|
|
11951
|
+
console.log(chalk7.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
|
|
11952
|
+
}
|
|
11361
11953
|
}
|
|
11362
11954
|
console.log();
|
|
11363
11955
|
});
|
|
11364
|
-
program.command("machines").description("List all machines that have synced data").action(async () => {
|
|
11956
|
+
program.command("machines").description("List all machines that have synced data").option("--limit <n>", "Maximum machine rows to print (default: 20)").option("--verbose", "Show all machine rows").action(async (opts) => {
|
|
11365
11957
|
await autoSync();
|
|
11366
11958
|
const machines = await getStore().machines();
|
|
11367
11959
|
const current = getMachineId();
|
|
@@ -11369,16 +11961,22 @@ program.command("machines").description("List all machines that have synced data
|
|
|
11369
11961
|
console.log(chalk7.yellow(`No machine data yet. Current machine: ${current}`));
|
|
11370
11962
|
return;
|
|
11371
11963
|
}
|
|
11964
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11965
|
+
const visibleMachines = machines.slice(0, limit);
|
|
11372
11966
|
console.log();
|
|
11373
11967
|
console.log(chalk7.bold.cyan(" Machines"));
|
|
11968
|
+
if (visibleMachines.length < machines.length) {
|
|
11969
|
+
console.log(chalk7.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
11970
|
+
}
|
|
11374
11971
|
console.log();
|
|
11375
|
-
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"],
|
|
11972
|
+
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
|
|
11376
11973
|
m.machine_id === current ? chalk7.green(`${m.machine_id} (this)`) : chalk7.white(m.machine_id),
|
|
11377
11974
|
fmtCount(m.sessions),
|
|
11378
11975
|
fmtCount(m.requests),
|
|
11379
11976
|
fmt4(m.total_cost_usd),
|
|
11380
11977
|
chalk7.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
11381
11978
|
]));
|
|
11979
|
+
printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
|
|
11382
11980
|
console.log(`
|
|
11383
11981
|
${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
|
|
11384
11982
|
console.log();
|
|
@@ -11540,14 +12138,20 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
|
|
|
11540
12138
|
});
|
|
11541
12139
|
console.log(chalk7.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
|
|
11542
12140
|
});
|
|
11543
|
-
goalCmd.command("list").description("List all goals with progress").action(async () => {
|
|
12141
|
+
goalCmd.command("list").description("List all goals with progress").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
|
|
11544
12142
|
const statuses = await getStore().listGoals();
|
|
11545
12143
|
if (statuses.length === 0) {
|
|
11546
12144
|
console.log(chalk7.yellow("No goals set."));
|
|
11547
12145
|
return;
|
|
11548
12146
|
}
|
|
12147
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12148
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11549
12149
|
console.log();
|
|
11550
|
-
|
|
12150
|
+
if (visibleStatuses.length < statuses.length) {
|
|
12151
|
+
console.log(chalk7.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
|
|
12152
|
+
console.log();
|
|
12153
|
+
}
|
|
12154
|
+
printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
|
|
11551
12155
|
const pct = g.percent_used.toFixed(1);
|
|
11552
12156
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
11553
12157
|
const status = g.is_over ? chalk7.red("OVER") : g.is_at_risk ? chalk7.yellow("AT RISK") : chalk7.green("ON TRACK");
|
|
@@ -11561,20 +12165,23 @@ goalCmd.command("list").description("List all goals with progress").action(async
|
|
|
11561
12165
|
status
|
|
11562
12166
|
];
|
|
11563
12167
|
}));
|
|
12168
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11564
12169
|
console.log();
|
|
11565
12170
|
});
|
|
11566
12171
|
goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
|
|
11567
12172
|
await getStore().removeGoal(id);
|
|
11568
12173
|
console.log(chalk7.green(`\u2713 Goal removed`));
|
|
11569
12174
|
});
|
|
11570
|
-
goalCmd.command("status").description("Quick goal progress summary").action(async () => {
|
|
12175
|
+
goalCmd.command("status").description("Quick goal progress summary").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
|
|
11571
12176
|
const statuses = await getStore().listGoals();
|
|
11572
12177
|
if (statuses.length === 0) {
|
|
11573
12178
|
console.log(chalk7.yellow("No goals set."));
|
|
11574
12179
|
return;
|
|
11575
12180
|
}
|
|
12181
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12182
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11576
12183
|
console.log();
|
|
11577
|
-
for (const g of
|
|
12184
|
+
for (const g of visibleStatuses) {
|
|
11578
12185
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
11579
12186
|
const pct = Math.min(g.percent_used, 100);
|
|
11580
12187
|
const barFilled = Math.round(pct / 10);
|
|
@@ -11584,6 +12191,7 @@ goalCmd.command("status").description("Quick goal progress summary").action(asyn
|
|
|
11584
12191
|
const label = `${g.period} (${scope})`.padEnd(20);
|
|
11585
12192
|
console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
|
|
11586
12193
|
}
|
|
12194
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11587
12195
|
console.log();
|
|
11588
12196
|
});
|
|
11589
12197
|
program.command("remove <type> <id>").alias("rm").description("Remove a record. Type: budget | project | goal | pricing").action(async (type, id) => {
|