@hasna/economy 0.3.6 → 0.3.8
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 +31 -0
- package/README.md +73 -60
- package/dashboard/README.md +17 -64
- 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 +914 -199
- package/dist/db/cloud.d.ts +3 -4
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +10 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/dialect.d.ts +13 -0
- package/dist/db/dialect.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +30 -0
- package/dist/db/pg-migrate.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +36 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +309 -59
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +6 -0
- package/dist/ingest/loops.d.ts.map +1 -0
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +16 -3
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +1 -1
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +10 -2
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +9 -1
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +3 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +757 -166
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +262 -27
- package/dist/server/index.js +683 -135
- package/dist/server/serve.d.ts +1 -1
- 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/docs/README.md +12 -0
- package/docs/cli.md +88 -0
- package/docs/configuration.md +86 -0
- package/docs/ingestion.md +47 -0
- package/docs/mcp.md +65 -0
- package/docs/otel.md +53 -0
- package/docs/rest-api.md +89 -0
- package/package.json +4 -5
- 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
|
@@ -17,6 +17,62 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = import.meta.require;
|
|
19
19
|
|
|
20
|
+
// src/db/sqlite-adapter.ts
|
|
21
|
+
import { Database } from "bun:sqlite";
|
|
22
|
+
|
|
23
|
+
class SqliteAdapter {
|
|
24
|
+
db;
|
|
25
|
+
constructor(path) {
|
|
26
|
+
this.db = new Database(path, { create: true });
|
|
27
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
28
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
29
|
+
}
|
|
30
|
+
run(sql, ...params) {
|
|
31
|
+
const result = this.db.prepare(sql).run(...params);
|
|
32
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
33
|
+
}
|
|
34
|
+
get(sql, ...params) {
|
|
35
|
+
return this.db.prepare(sql).get(...params);
|
|
36
|
+
}
|
|
37
|
+
all(sql, ...params) {
|
|
38
|
+
return this.db.prepare(sql).all(...params);
|
|
39
|
+
}
|
|
40
|
+
exec(sql) {
|
|
41
|
+
this.db.exec(sql);
|
|
42
|
+
}
|
|
43
|
+
query(sql) {
|
|
44
|
+
return this.db.query(sql);
|
|
45
|
+
}
|
|
46
|
+
prepare(sql) {
|
|
47
|
+
const stmt = this.db.prepare(sql);
|
|
48
|
+
return {
|
|
49
|
+
run(...params) {
|
|
50
|
+
const r = stmt.run(...params);
|
|
51
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
52
|
+
},
|
|
53
|
+
get(...params) {
|
|
54
|
+
return stmt.get(...params);
|
|
55
|
+
},
|
|
56
|
+
all(...params) {
|
|
57
|
+
return stmt.all(...params);
|
|
58
|
+
},
|
|
59
|
+
finalize() {
|
|
60
|
+
stmt.finalize();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
this.db.close();
|
|
66
|
+
}
|
|
67
|
+
transaction(fn) {
|
|
68
|
+
return this.db.transaction(fn)();
|
|
69
|
+
}
|
|
70
|
+
get raw() {
|
|
71
|
+
return this.db;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var init_sqlite_adapter = () => {};
|
|
75
|
+
|
|
20
76
|
// src/lib/pricing.ts
|
|
21
77
|
var exports_pricing = {};
|
|
22
78
|
__export(exports_pricing, {
|
|
@@ -544,6 +600,7 @@ __export(exports_database, {
|
|
|
544
600
|
upsertProject: () => upsertProject,
|
|
545
601
|
upsertModelPricing: () => upsertModelPricing,
|
|
546
602
|
upsertGoal: () => upsertGoal,
|
|
603
|
+
upsertCostCenter: () => upsertCostCenter,
|
|
547
604
|
upsertBudget: () => upsertBudget,
|
|
548
605
|
upsertBillingDaily: () => upsertBillingDaily,
|
|
549
606
|
setIngestState: () => setIngestState,
|
|
@@ -561,6 +618,7 @@ __export(exports_database, {
|
|
|
561
618
|
queryModelBreakdown: () => queryModelBreakdown,
|
|
562
619
|
queryHourlyBreakdown: () => queryHourlyBreakdown,
|
|
563
620
|
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
621
|
+
queryCostCenterBreakdown: () => queryCostCenterBreakdown,
|
|
564
622
|
queryBillingSummary: () => queryBillingSummary,
|
|
565
623
|
queryAgentBreakdownSince: () => queryAgentBreakdownSince,
|
|
566
624
|
queryAgentBreakdown: () => queryAgentBreakdown,
|
|
@@ -583,6 +641,7 @@ __export(exports_database, {
|
|
|
583
641
|
getGoalStatuses: () => getGoalStatuses,
|
|
584
642
|
getDbPath: () => getDbPath,
|
|
585
643
|
getDataDir: () => getDataDir,
|
|
644
|
+
getCostCenter: () => getCostCenter,
|
|
586
645
|
getBudgetStatuses: () => getBudgetStatuses,
|
|
587
646
|
deleteSubscription: () => deleteSubscription,
|
|
588
647
|
deleteProject: () => deleteProject,
|
|
@@ -593,7 +652,6 @@ __export(exports_database, {
|
|
|
593
652
|
clearBillingRange: () => clearBillingRange,
|
|
594
653
|
bulkIngest: () => bulkIngest
|
|
595
654
|
});
|
|
596
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
597
655
|
import { execFileSync } from "child_process";
|
|
598
656
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
599
657
|
import { hostname, platform } from "os";
|
|
@@ -686,7 +744,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
686
744
|
mkdirSync(dir, { recursive: true });
|
|
687
745
|
}
|
|
688
746
|
const db = withSqliteBusyRetry(() => {
|
|
689
|
-
const opened = new
|
|
747
|
+
const opened = new SqliteAdapter(path);
|
|
690
748
|
try {
|
|
691
749
|
opened.exec("PRAGMA busy_timeout = 10000");
|
|
692
750
|
opened.exec("PRAGMA journal_mode = WAL");
|
|
@@ -727,6 +785,16 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
727
785
|
}
|
|
728
786
|
function initSchema(db) {
|
|
729
787
|
db.exec(`
|
|
788
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
789
|
+
id TEXT PRIMARY KEY,
|
|
790
|
+
kind TEXT NOT NULL,
|
|
791
|
+
name TEXT NOT NULL,
|
|
792
|
+
repo_path TEXT,
|
|
793
|
+
labels_json TEXT DEFAULT '{}',
|
|
794
|
+
created_at TEXT NOT NULL,
|
|
795
|
+
updated_at TEXT NOT NULL
|
|
796
|
+
);
|
|
797
|
+
|
|
730
798
|
CREATE TABLE IF NOT EXISTS requests (
|
|
731
799
|
id TEXT PRIMARY KEY,
|
|
732
800
|
agent TEXT NOT NULL,
|
|
@@ -743,6 +811,7 @@ function initSchema(db) {
|
|
|
743
811
|
timestamp TEXT NOT NULL,
|
|
744
812
|
source_request_id TEXT,
|
|
745
813
|
machine_id TEXT DEFAULT '',
|
|
814
|
+
cost_center_id TEXT,
|
|
746
815
|
account_key TEXT DEFAULT '',
|
|
747
816
|
account_tool TEXT DEFAULT '',
|
|
748
817
|
account_name TEXT DEFAULT '',
|
|
@@ -761,6 +830,7 @@ function initSchema(db) {
|
|
|
761
830
|
total_tokens INTEGER DEFAULT 0,
|
|
762
831
|
request_count INTEGER DEFAULT 0,
|
|
763
832
|
machine_id TEXT DEFAULT '',
|
|
833
|
+
cost_center_id TEXT,
|
|
764
834
|
account_key TEXT DEFAULT '',
|
|
765
835
|
account_tool TEXT DEFAULT '',
|
|
766
836
|
account_name TEXT DEFAULT '',
|
|
@@ -781,6 +851,7 @@ function initSchema(db) {
|
|
|
781
851
|
id TEXT PRIMARY KEY,
|
|
782
852
|
project_path TEXT,
|
|
783
853
|
agent TEXT,
|
|
854
|
+
cost_center_id TEXT,
|
|
784
855
|
period TEXT NOT NULL,
|
|
785
856
|
limit_usd REAL NOT NULL,
|
|
786
857
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -811,6 +882,7 @@ function initSchema(db) {
|
|
|
811
882
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
812
883
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
813
884
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
885
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
814
886
|
|
|
815
887
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
816
888
|
model TEXT PRIMARY KEY,
|
|
@@ -897,6 +969,12 @@ function initSchema(db) {
|
|
|
897
969
|
`);
|
|
898
970
|
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
899
971
|
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
972
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
973
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
974
|
+
}
|
|
975
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
976
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
977
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
900
978
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
901
979
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
902
980
|
}
|
|
@@ -925,6 +1003,10 @@ function initSchema(db) {
|
|
|
925
1003
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
926
1004
|
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
927
1005
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
1006
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
1007
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
1008
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
1009
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
928
1010
|
`);
|
|
929
1011
|
}
|
|
930
1012
|
function periodWhere(period) {
|
|
@@ -975,6 +1057,17 @@ function requestPeriodWhere(period) {
|
|
|
975
1057
|
return "1=1";
|
|
976
1058
|
}
|
|
977
1059
|
}
|
|
1060
|
+
function upsertCostCenter(db, center) {
|
|
1061
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
1062
|
+
db.prepare(`
|
|
1063
|
+
INSERT OR REPLACE INTO cost_centers
|
|
1064
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
1065
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1066
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
1067
|
+
}
|
|
1068
|
+
function getCostCenter(db, id) {
|
|
1069
|
+
return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
|
|
1070
|
+
}
|
|
978
1071
|
function upsertRequest(db, req) {
|
|
979
1072
|
const now = req.updated_at ?? new Date().toISOString();
|
|
980
1073
|
db.prepare(`
|
|
@@ -982,10 +1075,10 @@ function upsertRequest(db, req) {
|
|
|
982
1075
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
983
1076
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
984
1077
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
985
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
1078
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
986
1079
|
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 ?? "");
|
|
1080
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1081
|
+
`).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
1082
|
}
|
|
990
1083
|
function upsertSession(db, session) {
|
|
991
1084
|
const now = session.updated_at ?? new Date().toISOString();
|
|
@@ -993,9 +1086,9 @@ function upsertSession(db, session) {
|
|
|
993
1086
|
INSERT OR REPLACE INTO sessions
|
|
994
1087
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
995
1088
|
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 ?? "");
|
|
1089
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
1090
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1091
|
+
`).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
1092
|
}
|
|
1000
1093
|
function rollupSession(db, sessionId) {
|
|
1001
1094
|
db.prepare(`
|
|
@@ -1021,9 +1114,12 @@ function rollupSession(db, sessionId) {
|
|
|
1021
1114
|
ELSE account_email END,
|
|
1022
1115
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
1023
1116
|
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1024
|
-
ELSE account_source END
|
|
1117
|
+
ELSE account_source END,
|
|
1118
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1119
|
+
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)
|
|
1120
|
+
ELSE cost_center_id END
|
|
1025
1121
|
WHERE id = ?
|
|
1026
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1122
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1027
1123
|
}
|
|
1028
1124
|
function querySessions(db, filter = {}) {
|
|
1029
1125
|
const conditions = [];
|
|
@@ -1254,9 +1350,12 @@ function labelForPath(projectPath, projectName) {
|
|
|
1254
1350
|
function groupKeyForPath(projectPath, projectName) {
|
|
1255
1351
|
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1256
1352
|
}
|
|
1257
|
-
function
|
|
1258
|
-
|
|
1259
|
-
|
|
1353
|
+
function laterTimestamp(current, candidate) {
|
|
1354
|
+
if (!candidate)
|
|
1355
|
+
return current;
|
|
1356
|
+
return candidate > current ? candidate : current;
|
|
1357
|
+
}
|
|
1358
|
+
function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
|
|
1260
1359
|
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1261
1360
|
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1262
1361
|
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
@@ -1264,67 +1363,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
|
|
|
1264
1363
|
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1265
1364
|
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1266
1365
|
const sessions = db.prepare(`
|
|
1267
|
-
SELECT id, project_path, project_name
|
|
1366
|
+
SELECT id, project_path, project_name
|
|
1268
1367
|
FROM sessions
|
|
1269
1368
|
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1270
1369
|
`).all(...sessionMachineParams);
|
|
1271
1370
|
const groups = new Map;
|
|
1371
|
+
const groupKeyBySession = new Map;
|
|
1272
1372
|
for (const s of sessions) {
|
|
1273
1373
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1274
1374
|
if (!label)
|
|
1275
1375
|
continue;
|
|
1276
1376
|
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1277
|
-
const g = groups.get(key) ?? { label,
|
|
1278
|
-
g.sessionIds.push(s.id);
|
|
1377
|
+
const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
|
|
1279
1378
|
if (!g.samplePath)
|
|
1280
1379
|
g.samplePath = s.project_path;
|
|
1281
1380
|
groups.set(key, g);
|
|
1381
|
+
groupKeyBySession.set(s.id, key);
|
|
1382
|
+
}
|
|
1383
|
+
const groupFor = (sessionId) => {
|
|
1384
|
+
const key = groupKeyBySession.get(sessionId);
|
|
1385
|
+
return key === undefined ? undefined : groups.get(key);
|
|
1386
|
+
};
|
|
1387
|
+
const requestRows = db.prepare(`
|
|
1388
|
+
SELECT session_id,
|
|
1389
|
+
COUNT(*) as requests,
|
|
1390
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1391
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1392
|
+
MAX(timestamp) as last_active
|
|
1393
|
+
FROM requests
|
|
1394
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1395
|
+
GROUP BY session_id
|
|
1396
|
+
`).all(...requestScopeParams, ...requestMachineParams);
|
|
1397
|
+
for (const row of requestRows) {
|
|
1398
|
+
const g = groupFor(row.session_id);
|
|
1399
|
+
if (!g)
|
|
1400
|
+
continue;
|
|
1401
|
+
g.sessions += 1;
|
|
1402
|
+
g.requests += row.requests;
|
|
1403
|
+
g.cost_usd += row.cost_usd;
|
|
1404
|
+
g.total_tokens += row.total_tokens;
|
|
1405
|
+
g.last_active = laterTimestamp(g.last_active, row.last_active);
|
|
1406
|
+
}
|
|
1407
|
+
const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
|
|
1408
|
+
const sessionOnlyRows = db.prepare(`
|
|
1409
|
+
SELECT id,
|
|
1410
|
+
COALESCE(request_count, 0) as request_count,
|
|
1411
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
1412
|
+
COALESCE(total_cost_usd, 0) as total_cost_usd,
|
|
1413
|
+
started_at
|
|
1414
|
+
FROM sessions
|
|
1415
|
+
WHERE ${sessionWhere}${sessionOnlyMachineClause}
|
|
1416
|
+
`).all(...sessionScopeParams, ...sessionOnlyMachineParams);
|
|
1417
|
+
for (const row of sessionOnlyRows) {
|
|
1418
|
+
if (sessionsWithRequests.has(row.id))
|
|
1419
|
+
continue;
|
|
1420
|
+
const g = groupFor(row.id);
|
|
1421
|
+
if (!g)
|
|
1422
|
+
continue;
|
|
1423
|
+
g.sessions += 1;
|
|
1424
|
+
g.requests += row.request_count;
|
|
1425
|
+
g.total_tokens += row.total_tokens;
|
|
1426
|
+
g.cost_usd += row.total_cost_usd;
|
|
1427
|
+
g.last_active = laterTimestamp(g.last_active, row.started_at);
|
|
1282
1428
|
}
|
|
1283
1429
|
const result = [];
|
|
1284
1430
|
for (const g of groups.values()) {
|
|
1285
|
-
|
|
1286
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
1287
|
-
SELECT
|
|
1288
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1289
|
-
COUNT(*) as requests,
|
|
1290
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1291
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1292
|
-
MAX(timestamp) as last_active
|
|
1293
|
-
FROM requests
|
|
1294
|
-
WHERE session_id IN (${placeholders})
|
|
1295
|
-
AND ${requestWhere}
|
|
1296
|
-
${requestMachineClause}
|
|
1297
|
-
`).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
|
|
1298
|
-
const sessionOnlyStats = placeholders.length ? db.prepare(`
|
|
1299
|
-
SELECT
|
|
1300
|
-
COUNT(*) as sessions,
|
|
1301
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1302
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1303
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1304
|
-
MAX(started_at) as last_active
|
|
1305
|
-
FROM sessions
|
|
1306
|
-
WHERE id IN (${placeholders})
|
|
1307
|
-
AND ${sessionWhere}
|
|
1308
|
-
${sessionOnlyMachineClause}
|
|
1309
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1310
|
-
`).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1311
|
-
const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1312
|
-
if (totalSessions === 0)
|
|
1431
|
+
if (g.sessions === 0)
|
|
1313
1432
|
continue;
|
|
1314
|
-
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1315
1433
|
result.push({
|
|
1316
1434
|
project_path: g.samplePath,
|
|
1317
1435
|
project_name: g.label,
|
|
1318
|
-
sessions:
|
|
1319
|
-
requests:
|
|
1320
|
-
total_tokens:
|
|
1321
|
-
cost_usd:
|
|
1322
|
-
last_active:
|
|
1436
|
+
sessions: g.sessions,
|
|
1437
|
+
requests: g.requests,
|
|
1438
|
+
total_tokens: g.total_tokens,
|
|
1439
|
+
cost_usd: g.cost_usd,
|
|
1440
|
+
last_active: g.last_active
|
|
1323
1441
|
});
|
|
1324
1442
|
}
|
|
1325
1443
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1326
1444
|
return result;
|
|
1327
1445
|
}
|
|
1446
|
+
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1447
|
+
return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
|
|
1448
|
+
}
|
|
1328
1449
|
function normalizeAccountEmail(email) {
|
|
1329
1450
|
return (email ?? "").trim().toLowerCase();
|
|
1330
1451
|
}
|
|
@@ -1465,17 +1586,141 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1465
1586
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1466
1587
|
return result;
|
|
1467
1588
|
}
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1589
|
+
function kindFromCostCenterId(id) {
|
|
1590
|
+
const prefix = id.split(":", 1)[0] ?? "";
|
|
1591
|
+
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1592
|
+
}
|
|
1593
|
+
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1594
|
+
if (!row.cost_center_id)
|
|
1595
|
+
return;
|
|
1596
|
+
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1597
|
+
const group = groups.get(row.cost_center_id) ?? {
|
|
1598
|
+
cost_center_id: row.cost_center_id,
|
|
1599
|
+
kind: derivedKind,
|
|
1600
|
+
name: row.name || row.cost_center_id,
|
|
1601
|
+
repo_path: row.repo_path ?? null,
|
|
1602
|
+
labels_json: row.labels_json || "{}",
|
|
1603
|
+
sessionIds: new Set,
|
|
1604
|
+
requests: 0,
|
|
1605
|
+
total_tokens: 0,
|
|
1606
|
+
api_equivalent_usd: 0,
|
|
1607
|
+
metered_api_usd: 0,
|
|
1608
|
+
subscription_included_usd: 0,
|
|
1609
|
+
estimated_usd: 0,
|
|
1610
|
+
unknown_usd: 0,
|
|
1611
|
+
last_active: ""
|
|
1612
|
+
};
|
|
1613
|
+
group.kind = derivedKind;
|
|
1614
|
+
if (row.name)
|
|
1615
|
+
group.name = row.name;
|
|
1616
|
+
if (row.repo_path && !group.repo_path)
|
|
1617
|
+
group.repo_path = row.repo_path;
|
|
1618
|
+
if (row.labels_json && row.labels_json !== "{}")
|
|
1619
|
+
group.labels_json = row.labels_json;
|
|
1620
|
+
if (row.session_id)
|
|
1621
|
+
group.sessionIds.add(row.session_id);
|
|
1622
|
+
group.requests += row.requests;
|
|
1623
|
+
group.total_tokens += row.total_tokens;
|
|
1624
|
+
group.api_equivalent_usd += row.cost_usd;
|
|
1625
|
+
if (sessionOnly) {
|
|
1626
|
+
group.estimated_usd += row.cost_usd;
|
|
1627
|
+
} else if (row.cost_basis === "metered_api") {
|
|
1628
|
+
group.metered_api_usd += row.cost_usd;
|
|
1629
|
+
} else if (row.cost_basis === "subscription_included") {
|
|
1630
|
+
group.subscription_included_usd += row.cost_usd;
|
|
1631
|
+
} else if (row.cost_basis === "unknown") {
|
|
1632
|
+
group.unknown_usd += row.cost_usd;
|
|
1633
|
+
} else {
|
|
1634
|
+
group.estimated_usd += row.cost_usd;
|
|
1635
|
+
}
|
|
1636
|
+
if (!group.last_active || row.last_active > group.last_active)
|
|
1637
|
+
group.last_active = row.last_active;
|
|
1638
|
+
groups.set(row.cost_center_id, group);
|
|
1639
|
+
}
|
|
1640
|
+
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1641
|
+
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1642
|
+
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1643
|
+
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1644
|
+
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1645
|
+
const requestParams = [];
|
|
1646
|
+
const sessionParams = [];
|
|
1647
|
+
if (filter.since) {
|
|
1648
|
+
requestParams.push(filter.since);
|
|
1649
|
+
sessionParams.push(filter.since);
|
|
1650
|
+
}
|
|
1651
|
+
if (filter.machine) {
|
|
1652
|
+
requestParams.push(filter.machine);
|
|
1653
|
+
sessionParams.push(filter.machine);
|
|
1654
|
+
}
|
|
1655
|
+
const groups = new Map;
|
|
1656
|
+
const requestRows = db.prepare(`
|
|
1657
|
+
SELECT
|
|
1658
|
+
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1659
|
+
COALESCE(cc.kind, '') as kind,
|
|
1660
|
+
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1661
|
+
cc.repo_path as repo_path,
|
|
1662
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1663
|
+
r.session_id as session_id,
|
|
1664
|
+
1 as requests,
|
|
1665
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1666
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1667
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1668
|
+
r.timestamp as last_active
|
|
1669
|
+
FROM requests r
|
|
1670
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1671
|
+
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1672
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1673
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1674
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1675
|
+
`).all(...requestParams);
|
|
1676
|
+
for (const row of requestRows)
|
|
1677
|
+
addCostCenterBreakdownRow(groups, row, false);
|
|
1678
|
+
const sessionOnlyRows = db.prepare(`
|
|
1679
|
+
SELECT
|
|
1680
|
+
s.cost_center_id as cost_center_id,
|
|
1681
|
+
COALESCE(cc.kind, '') as kind,
|
|
1682
|
+
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1683
|
+
cc.repo_path as repo_path,
|
|
1684
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1685
|
+
s.id as session_id,
|
|
1686
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1687
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1688
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1689
|
+
'estimated' as cost_basis,
|
|
1690
|
+
s.started_at as last_active
|
|
1691
|
+
FROM sessions s
|
|
1692
|
+
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1693
|
+
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1694
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1695
|
+
AND s.cost_center_id IS NOT NULL
|
|
1696
|
+
AND s.cost_center_id != ''
|
|
1697
|
+
`).all(...sessionParams);
|
|
1698
|
+
for (const row of sessionOnlyRows)
|
|
1699
|
+
addCostCenterBreakdownRow(groups, row, true);
|
|
1700
|
+
const result = [...groups.values()].map((group) => ({
|
|
1701
|
+
cost_center_id: group.cost_center_id,
|
|
1702
|
+
kind: group.kind,
|
|
1703
|
+
name: group.name,
|
|
1704
|
+
repo_path: group.repo_path,
|
|
1705
|
+
labels_json: group.labels_json,
|
|
1706
|
+
sessions: group.sessionIds.size,
|
|
1707
|
+
requests: group.requests,
|
|
1708
|
+
total_tokens: group.total_tokens,
|
|
1709
|
+
api_equivalent_usd: group.api_equivalent_usd,
|
|
1710
|
+
billable_usd: group.metered_api_usd,
|
|
1711
|
+
metered_api_usd: group.metered_api_usd,
|
|
1712
|
+
subscription_included_usd: group.subscription_included_usd,
|
|
1713
|
+
estimated_usd: group.estimated_usd,
|
|
1714
|
+
unknown_usd: group.unknown_usd,
|
|
1715
|
+
cost_usd: group.api_equivalent_usd,
|
|
1716
|
+
last_active: group.last_active
|
|
1717
|
+
}));
|
|
1718
|
+
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1719
|
+
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1720
|
+
return filtered;
|
|
1721
|
+
}
|
|
1722
|
+
function queryProjectBreakdownSince(db, since, machine) {
|
|
1723
|
+
return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
|
|
1479
1724
|
}
|
|
1480
1725
|
function queryAgentBreakdownSince(db, since) {
|
|
1481
1726
|
return db.prepare(`
|
|
@@ -1654,9 +1899,9 @@ function deleteProject(db, path) {
|
|
|
1654
1899
|
function upsertBudget(db, budget) {
|
|
1655
1900
|
db.prepare(`
|
|
1656
1901
|
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);
|
|
1902
|
+
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1903
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1904
|
+
`).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
1905
|
}
|
|
1661
1906
|
function listBudgets(db) {
|
|
1662
1907
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
@@ -1678,6 +1923,10 @@ function getBudgetStatuses(db) {
|
|
|
1678
1923
|
spendQuery += ` AND agent = ?`;
|
|
1679
1924
|
params.push(b.agent);
|
|
1680
1925
|
}
|
|
1926
|
+
if (b.cost_center_id) {
|
|
1927
|
+
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1928
|
+
params.push(b.cost_center_id);
|
|
1929
|
+
}
|
|
1681
1930
|
const row = db.prepare(spendQuery).get(...params);
|
|
1682
1931
|
const spend = row.spend;
|
|
1683
1932
|
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
@@ -2175,7 +2424,9 @@ var numOr = (v, d = 0) => {
|
|
|
2175
2424
|
const num = Number(v);
|
|
2176
2425
|
return Number.isFinite(num) ? num : d;
|
|
2177
2426
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2178
|
-
var init_database = () => {
|
|
2427
|
+
var init_database = __esm(() => {
|
|
2428
|
+
init_sqlite_adapter();
|
|
2429
|
+
});
|
|
2179
2430
|
|
|
2180
2431
|
// src/lib/package-metadata.ts
|
|
2181
2432
|
import { readFileSync } from "fs";
|
|
@@ -3655,11 +3906,18 @@ class LocalStore {
|
|
|
3655
3906
|
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3656
3907
|
}
|
|
3657
3908
|
async projectBreakdown(query = {}) {
|
|
3658
|
-
return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3909
|
+
return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3659
3910
|
}
|
|
3660
3911
|
async accountBreakdown(query = {}) {
|
|
3661
3912
|
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3662
3913
|
}
|
|
3914
|
+
async costCenterBreakdown(query = {}) {
|
|
3915
|
+
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
3916
|
+
kind: query.kind,
|
|
3917
|
+
machine: query.machine,
|
|
3918
|
+
since: query.since
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3663
3921
|
async accounts(period) {
|
|
3664
3922
|
return queryAccountBreakdown(this.db(), period);
|
|
3665
3923
|
}
|
|
@@ -3844,11 +4102,19 @@ class ApiStore {
|
|
|
3844
4102
|
return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
|
|
3845
4103
|
}
|
|
3846
4104
|
async projectBreakdown(query = {}) {
|
|
3847
|
-
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
|
|
4105
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since, machine: query.machine }));
|
|
3848
4106
|
}
|
|
3849
4107
|
async accountBreakdown(query = {}) {
|
|
3850
4108
|
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
3851
4109
|
}
|
|
4110
|
+
async costCenterBreakdown(query = {}) {
|
|
4111
|
+
const rows = await cloudListItems(this.cloud, "breakdown", q({
|
|
4112
|
+
by: query.kind ?? "cost-center",
|
|
4113
|
+
period: query.period,
|
|
4114
|
+
since: query.since
|
|
4115
|
+
}));
|
|
4116
|
+
return rows.filter((row) => Boolean(row?.cost_center_id));
|
|
4117
|
+
}
|
|
3852
4118
|
async accounts(period) {
|
|
3853
4119
|
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
3854
4120
|
}
|
|
@@ -3922,6 +4188,7 @@ class ApiStore {
|
|
|
3922
4188
|
const created = await this.cloud.client.create("budgets", {
|
|
3923
4189
|
project_path: input.project_path,
|
|
3924
4190
|
...input.agent ? { agent: input.agent } : {},
|
|
4191
|
+
...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
|
|
3925
4192
|
period: input.period,
|
|
3926
4193
|
limit_usd: input.limit_usd,
|
|
3927
4194
|
alert_at_percent: input.alert_at_percent
|
|
@@ -4199,6 +4466,7 @@ function agentPaths() {
|
|
|
4199
4466
|
var init_paths = () => {};
|
|
4200
4467
|
|
|
4201
4468
|
// src/lib/accounts.ts
|
|
4469
|
+
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
4202
4470
|
function normalizeEmail(email) {
|
|
4203
4471
|
return (email ?? "").trim().toLowerCase();
|
|
4204
4472
|
}
|
|
@@ -4253,57 +4521,35 @@ function envOverride(agent, env) {
|
|
|
4253
4521
|
account_source: "override"
|
|
4254
4522
|
};
|
|
4255
4523
|
}
|
|
4256
|
-
function
|
|
4257
|
-
try {
|
|
4258
|
-
return new Set(api.listTools().map((tool) => tool.id));
|
|
4259
|
-
} catch {
|
|
4260
|
-
return new Set;
|
|
4261
|
-
}
|
|
4262
|
-
}
|
|
4263
|
-
function profileForEnvDir(api, tool, env) {
|
|
4524
|
+
async function profileForEnvDir(store, tool, env) {
|
|
4264
4525
|
const configuredDir = env[tool.envVar];
|
|
4265
4526
|
if (!configuredDir)
|
|
4266
4527
|
return null;
|
|
4267
4528
|
const normalized = normalizeDir(configuredDir);
|
|
4268
|
-
|
|
4269
|
-
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
4270
|
-
} catch {
|
|
4271
|
-
return null;
|
|
4272
|
-
}
|
|
4529
|
+
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
4273
4530
|
}
|
|
4274
4531
|
async function resolveAccountForAgent(agent, env = process.env) {
|
|
4275
4532
|
const override = envOverride(agent, env);
|
|
4276
4533
|
if (override)
|
|
4277
4534
|
return override;
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
api = await import("@hasna/accounts");
|
|
4281
|
-
} catch {
|
|
4282
|
-
return null;
|
|
4283
|
-
}
|
|
4284
|
-
const toolIds = knownToolIds(api);
|
|
4535
|
+
const store = resolveStore(env);
|
|
4536
|
+
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
4285
4537
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
let tool;
|
|
4289
|
-
try {
|
|
4290
|
-
tool = api.getTool(toolId);
|
|
4291
|
-
} catch {
|
|
4538
|
+
const tool = tools.get(toolId);
|
|
4539
|
+
if (!tool)
|
|
4292
4540
|
continue;
|
|
4293
|
-
|
|
4294
|
-
const envProfile = profileForEnvDir(api, tool, env);
|
|
4541
|
+
const envProfile = await profileForEnvDir(store, tool, env);
|
|
4295
4542
|
if (envProfile)
|
|
4296
4543
|
return fromProfile(envProfile, "env");
|
|
4297
|
-
|
|
4298
|
-
|
|
4544
|
+
const appliedName = appliedProfileName(toolId);
|
|
4545
|
+
if (appliedName) {
|
|
4546
|
+
const applied = await store.findProfile(appliedName, toolId);
|
|
4299
4547
|
if (applied)
|
|
4300
4548
|
return fromProfile(applied, "applied");
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
return fromProfile(current, "current");
|
|
4306
|
-
} catch {}
|
|
4549
|
+
}
|
|
4550
|
+
const current = await store.currentProfile(toolId);
|
|
4551
|
+
if (current)
|
|
4552
|
+
return fromProfile(current, "current");
|
|
4307
4553
|
}
|
|
4308
4554
|
return null;
|
|
4309
4555
|
}
|
|
@@ -5368,13 +5614,202 @@ var init_hermes = __esm(() => {
|
|
|
5368
5614
|
HERMES_DB = join11(homedir8(), ".hermes", "state.db");
|
|
5369
5615
|
});
|
|
5370
5616
|
|
|
5617
|
+
// src/ingest/loops.ts
|
|
5618
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5619
|
+
import { homedir as homedir9 } from "os";
|
|
5620
|
+
import { join as join12 } from "path";
|
|
5621
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
5622
|
+
function loopsDbPath() {
|
|
5623
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
|
|
5624
|
+
}
|
|
5625
|
+
function loopsModel() {
|
|
5626
|
+
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
5627
|
+
}
|
|
5628
|
+
function openLoopsDb(dbPath, verbose) {
|
|
5629
|
+
let lastError;
|
|
5630
|
+
for (const readonly of [true, false]) {
|
|
5631
|
+
let loopsDb = null;
|
|
5632
|
+
try {
|
|
5633
|
+
loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
5634
|
+
loopsDb.prepare("PRAGMA schema_version").get();
|
|
5635
|
+
return loopsDb;
|
|
5636
|
+
} catch (error) {
|
|
5637
|
+
lastError = error;
|
|
5638
|
+
loopsDb?.close();
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
if (verbose) {
|
|
5642
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
5643
|
+
console.log("OpenLoops DB unreadable:", dbPath, message);
|
|
5644
|
+
}
|
|
5645
|
+
return null;
|
|
5646
|
+
}
|
|
5647
|
+
function slug(value) {
|
|
5648
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
5649
|
+
}
|
|
5650
|
+
function parseTarget(raw) {
|
|
5651
|
+
if (!raw)
|
|
5652
|
+
return {};
|
|
5653
|
+
try {
|
|
5654
|
+
const parsed = JSON.parse(raw);
|
|
5655
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
5656
|
+
} catch {
|
|
5657
|
+
return {};
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
function stringValue(value) {
|
|
5661
|
+
return typeof value === "string" ? value.trim() : "";
|
|
5662
|
+
}
|
|
5663
|
+
function accountFields(row) {
|
|
5664
|
+
const target = parseTarget(row.target_json);
|
|
5665
|
+
const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
|
|
5666
|
+
const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
|
|
5667
|
+
if (!tool || !profile) {
|
|
5668
|
+
return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
|
|
5669
|
+
}
|
|
5670
|
+
return {
|
|
5671
|
+
account_key: `${tool}:${profile}`,
|
|
5672
|
+
account_tool: tool,
|
|
5673
|
+
account_name: profile,
|
|
5674
|
+
account_source: "loops"
|
|
5675
|
+
};
|
|
5676
|
+
}
|
|
5677
|
+
function readLoopGoalRuns(loopsDb) {
|
|
5678
|
+
return loopsDb.prepare(`
|
|
5679
|
+
SELECT
|
|
5680
|
+
gr.id as goal_run_id,
|
|
5681
|
+
gr.goal_id as goal_id,
|
|
5682
|
+
COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
|
|
5683
|
+
COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
|
|
5684
|
+
COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
|
|
5685
|
+
l.labels_json as labels_json,
|
|
5686
|
+
l.target_json as target_json,
|
|
5687
|
+
g.objective as objective,
|
|
5688
|
+
gr.phase as phase,
|
|
5689
|
+
gr.status as status,
|
|
5690
|
+
COALESCE(gr.tokens_used, 0) as tokens_used,
|
|
5691
|
+
gr.created_at as created_at,
|
|
5692
|
+
gr.updated_at as updated_at,
|
|
5693
|
+
lr.started_at as loop_run_started_at,
|
|
5694
|
+
wsr.account_profile as account_profile,
|
|
5695
|
+
wsr.account_tool as account_tool
|
|
5696
|
+
FROM goal_runs gr
|
|
5697
|
+
LEFT JOIN goals g ON g.id = gr.goal_id
|
|
5698
|
+
LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
|
|
5699
|
+
LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
|
|
5700
|
+
LEFT JOIN (
|
|
5701
|
+
SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
|
|
5702
|
+
FROM workflow_step_runs
|
|
5703
|
+
WHERE goal_run_id IS NOT NULL
|
|
5704
|
+
GROUP BY goal_run_id
|
|
5705
|
+
) wsr ON wsr.goal_run_id = gr.id
|
|
5706
|
+
WHERE COALESCE(gr.tokens_used, 0) > 0
|
|
5707
|
+
ORDER BY gr.created_at ASC, gr.id ASC
|
|
5708
|
+
`).all();
|
|
5709
|
+
}
|
|
5710
|
+
async function ingestLoops(db, verbose = false) {
|
|
5711
|
+
const dbPath = loopsDbPath();
|
|
5712
|
+
if (!existsSync10(dbPath)) {
|
|
5713
|
+
if (verbose)
|
|
5714
|
+
console.log("OpenLoops DB not found:", dbPath);
|
|
5715
|
+
return { sessions: 0, requests: 0 };
|
|
5716
|
+
}
|
|
5717
|
+
const model = loopsModel();
|
|
5718
|
+
const machineId = getMachineId();
|
|
5719
|
+
let loopsDb = null;
|
|
5720
|
+
let requests = 0;
|
|
5721
|
+
const sessions = new Set;
|
|
5722
|
+
try {
|
|
5723
|
+
loopsDb = openLoopsDb(dbPath, verbose);
|
|
5724
|
+
if (!loopsDb)
|
|
5725
|
+
return { sessions: 0, requests: 0 };
|
|
5726
|
+
for (const row of readLoopGoalRuns(loopsDb)) {
|
|
5727
|
+
const loopName = row.loop_name || "unknown-loop";
|
|
5728
|
+
const costCenterId = `loop:${slug(loopName)}`;
|
|
5729
|
+
const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
|
|
5730
|
+
const requestId = `loops-${row.goal_run_id}`;
|
|
5731
|
+
const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
|
|
5732
|
+
const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
|
|
5733
|
+
if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
|
|
5734
|
+
continue;
|
|
5735
|
+
upsertCostCenter(db, {
|
|
5736
|
+
id: costCenterId,
|
|
5737
|
+
kind: "loop",
|
|
5738
|
+
name: loopName,
|
|
5739
|
+
repo_path: null,
|
|
5740
|
+
labels_json: row.labels_json ?? "{}",
|
|
5741
|
+
created_at: row.created_at ?? timestamp
|
|
5742
|
+
});
|
|
5743
|
+
const account = accountFields(row);
|
|
5744
|
+
upsertSession(db, {
|
|
5745
|
+
id: sessionId,
|
|
5746
|
+
agent: "loop",
|
|
5747
|
+
project_path: "",
|
|
5748
|
+
project_name: loopName,
|
|
5749
|
+
started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
|
|
5750
|
+
ended_at: row.updated_at ?? null,
|
|
5751
|
+
total_cost_usd: 0,
|
|
5752
|
+
total_tokens: 0,
|
|
5753
|
+
request_count: 0,
|
|
5754
|
+
machine_id: machineId,
|
|
5755
|
+
cost_center_id: costCenterId,
|
|
5756
|
+
attribution_tag: "loops-orchestration",
|
|
5757
|
+
...account
|
|
5758
|
+
});
|
|
5759
|
+
const inputTokens = row.tokens_used;
|
|
5760
|
+
const outputTokens = 0;
|
|
5761
|
+
upsertRequest(db, {
|
|
5762
|
+
id: requestId,
|
|
5763
|
+
agent: "loop",
|
|
5764
|
+
session_id: sessionId,
|
|
5765
|
+
model,
|
|
5766
|
+
input_tokens: inputTokens,
|
|
5767
|
+
output_tokens: outputTokens,
|
|
5768
|
+
cache_read_tokens: 0,
|
|
5769
|
+
cache_create_tokens: 0,
|
|
5770
|
+
cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
|
|
5771
|
+
cost_basis: "estimated",
|
|
5772
|
+
duration_ms: 0,
|
|
5773
|
+
timestamp,
|
|
5774
|
+
source_request_id: requestId,
|
|
5775
|
+
machine_id: machineId,
|
|
5776
|
+
cost_center_id: costCenterId,
|
|
5777
|
+
attribution_tag: "loops-orchestration",
|
|
5778
|
+
...account
|
|
5779
|
+
});
|
|
5780
|
+
sessions.add(sessionId);
|
|
5781
|
+
requests++;
|
|
5782
|
+
setIngestState(db, "loops", row.goal_run_id, stateValue);
|
|
5783
|
+
if (verbose)
|
|
5784
|
+
console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
|
|
5785
|
+
}
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
if (verbose) {
|
|
5788
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5789
|
+
console.log("OpenLoops ingest skipped:", message);
|
|
5790
|
+
}
|
|
5791
|
+
return { sessions: 0, requests: 0 };
|
|
5792
|
+
} finally {
|
|
5793
|
+
loopsDb?.close();
|
|
5794
|
+
}
|
|
5795
|
+
for (const sessionId of sessions)
|
|
5796
|
+
rollupSession(db, sessionId);
|
|
5797
|
+
return { sessions: sessions.size, requests };
|
|
5798
|
+
}
|
|
5799
|
+
var DEFAULT_LOOPS_DB_PATH, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
5800
|
+
var init_loops = __esm(() => {
|
|
5801
|
+
init_database();
|
|
5802
|
+
init_pricing();
|
|
5803
|
+
DEFAULT_LOOPS_DB_PATH = join12(homedir9(), ".hasna", "loops", "loops.db");
|
|
5804
|
+
});
|
|
5805
|
+
|
|
5371
5806
|
// src/ingest/claude-quota.ts
|
|
5372
|
-
import { existsSync as
|
|
5807
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
5373
5808
|
function readClaudeToken() {
|
|
5374
5809
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
5375
5810
|
if (fromEnv)
|
|
5376
5811
|
return { token: fromEnv };
|
|
5377
|
-
if (!
|
|
5812
|
+
if (!existsSync11(CREDENTIALS_PATH))
|
|
5378
5813
|
return null;
|
|
5379
5814
|
try {
|
|
5380
5815
|
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
@@ -5518,13 +5953,13 @@ var init_claude_quota = __esm(() => {
|
|
|
5518
5953
|
});
|
|
5519
5954
|
|
|
5520
5955
|
// src/ingest/codex-quota.ts
|
|
5521
|
-
import { existsSync as
|
|
5956
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
5522
5957
|
function readCodexAuth() {
|
|
5523
5958
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
5524
5959
|
if (fromEnv)
|
|
5525
5960
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
5526
5961
|
const authPath = agentPaths().codexAuth;
|
|
5527
|
-
if (!
|
|
5962
|
+
if (!existsSync12(authPath))
|
|
5528
5963
|
return null;
|
|
5529
5964
|
try {
|
|
5530
5965
|
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
@@ -5662,7 +6097,7 @@ var init_codex_quota = __esm(() => {
|
|
|
5662
6097
|
|
|
5663
6098
|
// src/lib/sync-all.ts
|
|
5664
6099
|
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);
|
|
6100
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
|
|
5666
6101
|
const all = !anySpecific;
|
|
5667
6102
|
const result = { deduped: 0 };
|
|
5668
6103
|
if (all || opts.claude) {
|
|
@@ -5685,6 +6120,8 @@ async function syncAll(db, opts = {}) {
|
|
|
5685
6120
|
result.pi = await ingestPi(db, opts.verbose);
|
|
5686
6121
|
if (all || opts.hermes)
|
|
5687
6122
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
6123
|
+
if (all || opts.loops)
|
|
6124
|
+
result.loops = await ingestLoops(db, opts.verbose);
|
|
5688
6125
|
if (opts.dedupe !== false)
|
|
5689
6126
|
result.deduped = dedupeRequests(db);
|
|
5690
6127
|
return result;
|
|
@@ -5697,6 +6134,7 @@ var init_sync_all = __esm(() => {
|
|
|
5697
6134
|
init_cursor();
|
|
5698
6135
|
init_pi();
|
|
5699
6136
|
init_hermes();
|
|
6137
|
+
init_loops();
|
|
5700
6138
|
init_claude_quota();
|
|
5701
6139
|
init_codex_quota();
|
|
5702
6140
|
init_database();
|
|
@@ -5977,13 +6415,14 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
|
5977
6415
|
let imported = 0;
|
|
5978
6416
|
let skipped = 0;
|
|
5979
6417
|
for (const project of projects) {
|
|
5980
|
-
|
|
6418
|
+
const path = resolveProjectPath(project);
|
|
6419
|
+
if (!path) {
|
|
5981
6420
|
skipped++;
|
|
5982
6421
|
continue;
|
|
5983
6422
|
}
|
|
5984
6423
|
upsertProject(db, {
|
|
5985
6424
|
id: project.id,
|
|
5986
|
-
path
|
|
6425
|
+
path,
|
|
5987
6426
|
name: project.name,
|
|
5988
6427
|
description: project.description,
|
|
5989
6428
|
tags: project.tags ?? [],
|
|
@@ -5993,6 +6432,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
|
5993
6432
|
}
|
|
5994
6433
|
return { imported, skipped };
|
|
5995
6434
|
}
|
|
6435
|
+
var resolveProjectPath = (project) => project.primary_path || project.path || "";
|
|
5996
6436
|
var init_open_projects = __esm(() => {
|
|
5997
6437
|
init_database();
|
|
5998
6438
|
});
|
|
@@ -6005,15 +6445,15 @@ __export(exports_config, {
|
|
|
6005
6445
|
loadConfig: () => loadConfig2,
|
|
6006
6446
|
getConfigValue: () => getConfigValue
|
|
6007
6447
|
});
|
|
6008
|
-
import { existsSync as
|
|
6009
|
-
import { dirname as dirname2, join as
|
|
6448
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
6449
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
6010
6450
|
function getConfigPath() {
|
|
6011
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
6451
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
6012
6452
|
}
|
|
6013
6453
|
function loadConfig2() {
|
|
6014
6454
|
try {
|
|
6015
6455
|
const configPath = getConfigPath();
|
|
6016
|
-
if (
|
|
6456
|
+
if (existsSync13(configPath)) {
|
|
6017
6457
|
const raw = readFileSync11(configPath, "utf-8");
|
|
6018
6458
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
6019
6459
|
}
|
|
@@ -6023,7 +6463,7 @@ function loadConfig2() {
|
|
|
6023
6463
|
function saveConfig2(config) {
|
|
6024
6464
|
const configPath = getConfigPath();
|
|
6025
6465
|
const dir = dirname2(configPath);
|
|
6026
|
-
if (!
|
|
6466
|
+
if (!existsSync13(dir))
|
|
6027
6467
|
mkdirSync3(dir, { recursive: true });
|
|
6028
6468
|
writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
|
|
6029
6469
|
`);
|
|
@@ -6124,6 +6564,8 @@ async function checkAndFireWebhooks(db) {
|
|
|
6124
6564
|
event: "budget_alert",
|
|
6125
6565
|
budget_id: b.id,
|
|
6126
6566
|
project: b.project_path ?? "global",
|
|
6567
|
+
cost_center_id: b.cost_center_id ?? null,
|
|
6568
|
+
scope: b.cost_center_id ?? b.project_path ?? "global",
|
|
6127
6569
|
period: b.period,
|
|
6128
6570
|
spend: b.current_spend_usd,
|
|
6129
6571
|
limit: b.limit_usd,
|
|
@@ -6167,7 +6609,7 @@ var init_webhooks = __esm(() => {
|
|
|
6167
6609
|
});
|
|
6168
6610
|
|
|
6169
6611
|
// src/lib/watch-paths.ts
|
|
6170
|
-
import { existsSync as
|
|
6612
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6171
6613
|
function getWatchPaths() {
|
|
6172
6614
|
const p = agentPaths();
|
|
6173
6615
|
const candidates = [
|
|
@@ -6180,7 +6622,7 @@ function getWatchPaths() {
|
|
|
6180
6622
|
p.piSessions,
|
|
6181
6623
|
p.hermesDir
|
|
6182
6624
|
];
|
|
6183
|
-
return candidates.filter((path) =>
|
|
6625
|
+
return candidates.filter((path) => existsSync14(path));
|
|
6184
6626
|
}
|
|
6185
6627
|
var init_watch_paths = __esm(() => {
|
|
6186
6628
|
init_paths();
|
|
@@ -6355,12 +6797,48 @@ var init_watch = __esm(() => {
|
|
|
6355
6797
|
init_notification();
|
|
6356
6798
|
});
|
|
6357
6799
|
|
|
6800
|
+
// src/db/dialect.ts
|
|
6801
|
+
function translateSql(sql, dialect) {
|
|
6802
|
+
if (dialect === "sqlite")
|
|
6803
|
+
return sql;
|
|
6804
|
+
return sqliteToPostgres(sql);
|
|
6805
|
+
}
|
|
6806
|
+
function translateParams(params) {
|
|
6807
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
6808
|
+
return flat.map((p) => p === undefined ? null : p);
|
|
6809
|
+
}
|
|
6810
|
+
function sqliteToPostgres(sql) {
|
|
6811
|
+
let out = sql;
|
|
6812
|
+
let paramIdx = 0;
|
|
6813
|
+
out = out.replace(/\?/g, () => `$${++paramIdx}`);
|
|
6814
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
|
|
6815
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
|
|
6816
|
+
const n = parseInt(amount, 10);
|
|
6817
|
+
const absN = Math.abs(n);
|
|
6818
|
+
const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
|
|
6819
|
+
const plural = absN === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
|
6820
|
+
return n < 0 ? `NOW() - INTERVAL '${absN} ${plural}'` : `NOW() + INTERVAL '${absN} ${plural}'`;
|
|
6821
|
+
});
|
|
6822
|
+
out = out.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
|
|
6823
|
+
out = out.replace(/GROUP_CONCAT\s*\(/gi, "STRING_AGG(");
|
|
6824
|
+
out = out.replace(/json_extract\s*\(\s*(\w+)\s*,\s*'\$\.(\w+)'\s*\)/gi, (_match, col, key) => `${col}->>'${key}'`);
|
|
6825
|
+
out = out.replace(/\bAUTOINCREMENT\b/gi, "GENERATED ALWAYS AS IDENTITY");
|
|
6826
|
+
out = out.replace(/(?<![I])LIKE/gi, "ILIKE");
|
|
6827
|
+
out = out.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
|
|
6828
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO\s+"?(\w+)"?\s*\(([^)]+)\)\s*VALUES/gi, (_match, table2, colList) => `INSERT INTO "${table2}" (${colList}) VALUES`);
|
|
6829
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
|
|
6830
|
+
if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(out)) {
|
|
6831
|
+
out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
|
|
6832
|
+
out = out.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
|
|
6833
|
+
}
|
|
6834
|
+
return out;
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6358
6837
|
// src/db/sync-pg.ts
|
|
6359
6838
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
6360
6839
|
import { fileURLToPath } from "url";
|
|
6361
|
-
import { dirname as dirname3, join as
|
|
6362
|
-
import { existsSync as
|
|
6363
|
-
import { translateSql, translateParams } from "@hasna/cloud";
|
|
6840
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
6841
|
+
import { existsSync as existsSync15 } from "fs";
|
|
6364
6842
|
function translateSqliteDates(sql) {
|
|
6365
6843
|
let s = sql;
|
|
6366
6844
|
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
@@ -6385,12 +6863,12 @@ function translateSqliteDates(sql) {
|
|
|
6385
6863
|
function resolveWorkerPath() {
|
|
6386
6864
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
6387
6865
|
const candidates = [
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6866
|
+
join14(here, "pg-sync-worker.ts"),
|
|
6867
|
+
join14(here, "pg-sync-worker.js"),
|
|
6868
|
+
join14(here, "..", "db", "pg-sync-worker.js")
|
|
6391
6869
|
];
|
|
6392
6870
|
for (const candidate of candidates)
|
|
6393
|
-
if (
|
|
6871
|
+
if (existsSync15(candidate))
|
|
6394
6872
|
return candidate;
|
|
6395
6873
|
return candidates[0];
|
|
6396
6874
|
}
|
|
@@ -6482,11 +6960,18 @@ class SyncPgAdapter {
|
|
|
6482
6960
|
}
|
|
6483
6961
|
var init_sync_pg = () => {};
|
|
6484
6962
|
|
|
6963
|
+
// src/db/pg-migrate.ts
|
|
6964
|
+
import pg from "pg";
|
|
6965
|
+
function resolvePgSsl(dsn) {
|
|
6966
|
+
return dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
6967
|
+
}
|
|
6968
|
+
var init_pg_migrate = () => {};
|
|
6969
|
+
|
|
6485
6970
|
// src/db/pg-migrations.ts
|
|
6486
6971
|
var init_pg_migrations = () => {};
|
|
6487
6972
|
|
|
6488
6973
|
// src/db/cloud.ts
|
|
6489
|
-
import
|
|
6974
|
+
import pg2 from "pg";
|
|
6490
6975
|
function getCloudDatabaseUrl() {
|
|
6491
6976
|
return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
|
|
6492
6977
|
}
|
|
@@ -6510,8 +6995,7 @@ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
|
|
|
6510
6995
|
function createCloudPool(dsn = getCloudDatabaseUrl()) {
|
|
6511
6996
|
if (!dsn)
|
|
6512
6997
|
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
6513
|
-
const
|
|
6514
|
-
const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
6998
|
+
const pool = new pg2.Pool({ connectionString: dsn, ssl: resolvePgSsl(dsn), max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
6515
6999
|
pool.on("error", (err) => {
|
|
6516
7000
|
console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
|
|
6517
7001
|
});
|
|
@@ -6534,6 +7018,7 @@ function authClientFromPool(pool) {
|
|
|
6534
7018
|
}
|
|
6535
7019
|
var init_cloud = __esm(() => {
|
|
6536
7020
|
init_sync_pg();
|
|
7021
|
+
init_pg_migrate();
|
|
6537
7022
|
init_pg_migrations();
|
|
6538
7023
|
});
|
|
6539
7024
|
|
|
@@ -7302,6 +7787,15 @@ var init_openapi = __esm(() => {
|
|
|
7302
7787
|
type: "string"
|
|
7303
7788
|
}
|
|
7304
7789
|
},
|
|
7790
|
+
{
|
|
7791
|
+
name: "since",
|
|
7792
|
+
in: "query",
|
|
7793
|
+
description: "ISO timestamp lower bound; takes precedence over period",
|
|
7794
|
+
schema: {
|
|
7795
|
+
type: "string",
|
|
7796
|
+
format: "date-time"
|
|
7797
|
+
}
|
|
7798
|
+
},
|
|
7305
7799
|
{
|
|
7306
7800
|
name: "machine",
|
|
7307
7801
|
in: "query",
|
|
@@ -8144,7 +8638,7 @@ __export(exports_serve, {
|
|
|
8144
8638
|
createHandler: () => createHandler
|
|
8145
8639
|
});
|
|
8146
8640
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8147
|
-
import { existsSync as
|
|
8641
|
+
import { existsSync as existsSync16 } from "fs";
|
|
8148
8642
|
import { resolve, sep } from "path";
|
|
8149
8643
|
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
8150
8644
|
function openApiDocument() {
|
|
@@ -8227,13 +8721,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
8227
8721
|
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
8228
8722
|
return apiHandler(req);
|
|
8229
8723
|
}
|
|
8230
|
-
if (
|
|
8724
|
+
if (existsSync16(dashboardDir)) {
|
|
8231
8725
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
8232
|
-
if (filePath &&
|
|
8726
|
+
if (filePath && existsSync16(filePath)) {
|
|
8233
8727
|
return new Response(Bun.file(filePath));
|
|
8234
8728
|
}
|
|
8235
8729
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
8236
|
-
if (indexPath &&
|
|
8730
|
+
if (indexPath && existsSync16(indexPath)) {
|
|
8237
8731
|
return new Response(Bun.file(indexPath));
|
|
8238
8732
|
}
|
|
8239
8733
|
}
|
|
@@ -8395,13 +8889,18 @@ function createHandler(db, options = {}) {
|
|
|
8395
8889
|
if (path === "/api/breakdown" && method === "GET") {
|
|
8396
8890
|
const by = url.searchParams.get("by") ?? "model";
|
|
8397
8891
|
const period = url.searchParams.get("period") ?? "all";
|
|
8892
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
8398
8893
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
8399
8894
|
if (by === "project")
|
|
8400
|
-
return ok(queryProjectBreakdown(db, period, machine));
|
|
8895
|
+
return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
|
|
8401
8896
|
if (by === "agent")
|
|
8402
8897
|
return ok(queryAgentBreakdown(db, period, machine));
|
|
8403
8898
|
if (by === "account")
|
|
8404
8899
|
return ok(queryAccountBreakdown(db, period, machine));
|
|
8900
|
+
if (by === "cost-center")
|
|
8901
|
+
return ok(queryCostCenterBreakdown(db, period, { machine }));
|
|
8902
|
+
if (["loop", "app", "repo", "service", "team"].includes(by))
|
|
8903
|
+
return ok(queryCostCenterBreakdown(db, period, { kind: by, machine }));
|
|
8405
8904
|
return ok(queryModelBreakdown(db));
|
|
8406
8905
|
}
|
|
8407
8906
|
if (path === "/api/budgets" && method === "GET") {
|
|
@@ -8425,6 +8924,7 @@ function createHandler(db, options = {}) {
|
|
|
8425
8924
|
id: randomUUID2(),
|
|
8426
8925
|
project_path: body["project_path"] ?? null,
|
|
8427
8926
|
agent,
|
|
8927
|
+
cost_center_id: optionalString(body["cost_center_id"]) ?? null,
|
|
8428
8928
|
period: normalizeBudgetPeriod(body["period"]),
|
|
8429
8929
|
limit_usd: limitUsd,
|
|
8430
8930
|
alert_at_percent: alertAtPercent,
|
|
@@ -8766,7 +9266,7 @@ var init_serve = __esm(() => {
|
|
|
8766
9266
|
};
|
|
8767
9267
|
ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
8768
9268
|
AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
8769
|
-
SYNC_SOURCES = ["all", ...AGENTS];
|
|
9269
|
+
SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
8770
9270
|
DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
8771
9271
|
FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
8772
9272
|
});
|
|
@@ -8781,14 +9281,14 @@ __export(exports_menubar, {
|
|
|
8781
9281
|
});
|
|
8782
9282
|
import chalk6 from "chalk";
|
|
8783
9283
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
8784
|
-
import { cpSync, existsSync as
|
|
9284
|
+
import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
8785
9285
|
import { tmpdir, arch } from "os";
|
|
8786
|
-
import { join as
|
|
9286
|
+
import { join as join15 } from "path";
|
|
8787
9287
|
function getArch() {
|
|
8788
9288
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
8789
9289
|
}
|
|
8790
9290
|
function isInstalled() {
|
|
8791
|
-
return
|
|
9291
|
+
return existsSync17(APP_PATH);
|
|
8792
9292
|
}
|
|
8793
9293
|
function isRunning() {
|
|
8794
9294
|
try {
|
|
@@ -8825,8 +9325,8 @@ async function menubarInstall(opts) {
|
|
|
8825
9325
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
8826
9326
|
process.exit(1);
|
|
8827
9327
|
}
|
|
8828
|
-
const zipPath =
|
|
8829
|
-
const extractDir =
|
|
9328
|
+
const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
9329
|
+
const extractDir = join15(tmpdir(), "economy-bar-extracted");
|
|
8830
9330
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
8831
9331
|
try {
|
|
8832
9332
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
@@ -8846,7 +9346,7 @@ async function menubarInstall(opts) {
|
|
|
8846
9346
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
8847
9347
|
if (isInstalled())
|
|
8848
9348
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
8849
|
-
cpSync(
|
|
9349
|
+
cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
8850
9350
|
try {
|
|
8851
9351
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
8852
9352
|
} catch {}
|
|
@@ -9645,7 +10145,7 @@ var ROADMAP_PHASES = [
|
|
|
9645
10145
|
{
|
|
9646
10146
|
id: "phase-9",
|
|
9647
10147
|
title: "Multi-machine auto sync (4-machine fleet)",
|
|
9648
|
-
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist;
|
|
10148
|
+
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist; the (since-retired) shared sync package had incremental sync, conflict resolution, and launchd/systemd schedulers \u2014 but economy does not use them, requests/sessions lack updated_at, ingest is local-only, RDS is hardcoded, and default commands never pull before query.",
|
|
9649
10149
|
tasks: [
|
|
9650
10150
|
{
|
|
9651
10151
|
id: "9.1",
|
|
@@ -9660,7 +10160,7 @@ var ROADMAP_PHASES = [
|
|
|
9660
10160
|
},
|
|
9661
10161
|
{
|
|
9662
10162
|
id: "9.3",
|
|
9663
|
-
title: "Switch cloud sync to
|
|
10163
|
+
title: "Switch cloud sync to incrementalSyncPush/Pull with _sync_meta (replace full table scans)",
|
|
9664
10164
|
status: "done",
|
|
9665
10165
|
deps: ["9.2"]
|
|
9666
10166
|
},
|
|
@@ -9684,7 +10184,7 @@ var ROADMAP_PHASES = [
|
|
|
9684
10184
|
},
|
|
9685
10185
|
{
|
|
9686
10186
|
id: "9.7",
|
|
9687
|
-
title: "CLI economy cloud schedule install|status|remove \u2014 wrap
|
|
10187
|
+
title: "CLI economy cloud schedule install|status|remove \u2014 wrap registerSyncSchedule (launchd/systemd every 5\u201315m)",
|
|
9688
10188
|
status: "done",
|
|
9689
10189
|
deps: ["9.5"]
|
|
9690
10190
|
},
|
|
@@ -9950,6 +10450,8 @@ var ROADMAP_PHASES = [
|
|
|
9950
10450
|
]
|
|
9951
10451
|
}
|
|
9952
10452
|
];
|
|
10453
|
+
var DEFAULT_TODO_LIST_LIMIT = 20;
|
|
10454
|
+
var TODO_TITLE_WIDTH = 140;
|
|
9953
10455
|
function statusLabel(status) {
|
|
9954
10456
|
if (status === "done")
|
|
9955
10457
|
return chalk2.green("done");
|
|
@@ -9963,6 +10465,30 @@ function countByStatus(tasks) {
|
|
|
9963
10465
|
return acc;
|
|
9964
10466
|
}, { pending: 0, in_progress: 0, done: 0 });
|
|
9965
10467
|
}
|
|
10468
|
+
function truncateText(value, max) {
|
|
10469
|
+
if (value.length <= max)
|
|
10470
|
+
return value;
|
|
10471
|
+
return `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
|
|
10472
|
+
}
|
|
10473
|
+
function parseListLimit(value) {
|
|
10474
|
+
if (value == null)
|
|
10475
|
+
return;
|
|
10476
|
+
const parsed = Number(value);
|
|
10477
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10478
|
+
console.error(chalk2.red("--limit must be a positive integer"));
|
|
10479
|
+
process.exit(1);
|
|
10480
|
+
}
|
|
10481
|
+
return parsed;
|
|
10482
|
+
}
|
|
10483
|
+
function matchingTasks(phases, status) {
|
|
10484
|
+
const rows = [];
|
|
10485
|
+
for (const phase of phases) {
|
|
10486
|
+
const tasks = status ? phase.tasks.filter((task) => task.status === status) : phase.tasks;
|
|
10487
|
+
for (const task of tasks)
|
|
10488
|
+
rows.push({ phase, task });
|
|
10489
|
+
}
|
|
10490
|
+
return rows;
|
|
10491
|
+
}
|
|
9966
10492
|
function printTodosHelp() {
|
|
9967
10493
|
console.log();
|
|
9968
10494
|
console.log(chalk2.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
|
|
@@ -9972,14 +10498,16 @@ function printTodosHelp() {
|
|
|
9972
10498
|
console.log();
|
|
9973
10499
|
console.log(chalk2.bold(" Commands"));
|
|
9974
10500
|
console.log(" economy todos Overview of phases and progress");
|
|
9975
|
-
console.log(" economy todos list
|
|
10501
|
+
console.log(" economy todos list Compact task list (first 20 by default)");
|
|
10502
|
+
console.log(" economy todos list --verbose Full task list with dependencies");
|
|
10503
|
+
console.log(" economy todos list --json Machine-readable roadmap payload");
|
|
9976
10504
|
console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
|
|
9977
10505
|
console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
|
|
9978
10506
|
console.log();
|
|
9979
|
-
console.log(chalk2.bold(" Multi-machine
|
|
9980
|
-
console.log("
|
|
9981
|
-
console.log("
|
|
9982
|
-
console.log("
|
|
10507
|
+
console.log(chalk2.bold(" Multi-machine storage"));
|
|
10508
|
+
console.log(" local on-box SQLite; economy sync ingests this machine's agent data");
|
|
10509
|
+
console.log(" cloud CLI/MCP reads and writes the shared /v1 API directly with URL + key");
|
|
10510
|
+
console.log(" fleet machine_id keeps shared summaries and per-machine filters distinct");
|
|
9983
10511
|
console.log();
|
|
9984
10512
|
console.log(chalk2.bold(" Agents covered"));
|
|
9985
10513
|
console.log(" claude Claude Code telemetry + Pro/Max quota (partial)");
|
|
@@ -10014,7 +10542,8 @@ function printTodosOverview() {
|
|
|
10014
10542
|
console.log(chalk2.dim(` ${phase.summary}`));
|
|
10015
10543
|
}
|
|
10016
10544
|
console.log();
|
|
10017
|
-
console.log(chalk2.dim(" Run: economy todos list"));
|
|
10545
|
+
console.log(chalk2.dim(" Run: economy todos list --limit 20"));
|
|
10546
|
+
console.log(chalk2.dim(" Run: economy todos list --verbose"));
|
|
10018
10547
|
console.log(chalk2.dim(" Run: economy todos --help"));
|
|
10019
10548
|
console.log();
|
|
10020
10549
|
}
|
|
@@ -10025,21 +10554,55 @@ function printTodosList(opts) {
|
|
|
10025
10554
|
console.log(chalk2.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
|
|
10026
10555
|
return;
|
|
10027
10556
|
}
|
|
10557
|
+
const allMatches = matchingTasks(phases, opts.status);
|
|
10558
|
+
const limit = opts.limit ?? (opts.verbose ? Number.POSITIVE_INFINITY : DEFAULT_TODO_LIST_LIMIT);
|
|
10559
|
+
const visible = allMatches.slice(0, limit);
|
|
10560
|
+
if (opts.json) {
|
|
10561
|
+
const filteredPhases = phases.map((phase) => ({
|
|
10562
|
+
...phase,
|
|
10563
|
+
tasks: opts.status ? phase.tasks.filter((task) => task.status === opts.status) : phase.tasks
|
|
10564
|
+
})).filter((phase) => phase.tasks.length > 0);
|
|
10565
|
+
console.log(JSON.stringify({
|
|
10566
|
+
version: ROADMAP_VERSION,
|
|
10567
|
+
goal: ROADMAP_GOAL,
|
|
10568
|
+
total: allMatches.length,
|
|
10569
|
+
phases: filteredPhases
|
|
10570
|
+
}, null, 2));
|
|
10571
|
+
return;
|
|
10572
|
+
}
|
|
10573
|
+
console.log();
|
|
10574
|
+
console.log(chalk2.bold.cyan(" Economy Roadmap Tasks"));
|
|
10575
|
+
console.log(chalk2.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
|
|
10576
|
+
if (!opts.verbose && visible.length < allMatches.length) {
|
|
10577
|
+
console.log(chalk2.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
|
|
10578
|
+
}
|
|
10028
10579
|
console.log();
|
|
10580
|
+
const visibleByPhase = new Map;
|
|
10581
|
+
for (const { phase, task } of visible) {
|
|
10582
|
+
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10583
|
+
tasks.push(task);
|
|
10584
|
+
visibleByPhase.set(phase.id, tasks);
|
|
10585
|
+
}
|
|
10029
10586
|
for (const phase of phases) {
|
|
10030
|
-
const tasks =
|
|
10587
|
+
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10031
10588
|
if (tasks.length === 0)
|
|
10032
10589
|
continue;
|
|
10033
10590
|
console.log(chalk2.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
|
|
10034
|
-
|
|
10591
|
+
if (opts.verbose)
|
|
10592
|
+
console.log(chalk2.dim(` ${phase.summary}`));
|
|
10035
10593
|
console.log();
|
|
10036
10594
|
for (const task of tasks) {
|
|
10037
10595
|
const deps = task.deps?.length ? chalk2.dim(` deps: ${task.deps.join(", ")}`) : "";
|
|
10038
|
-
|
|
10596
|
+
const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
|
|
10597
|
+
console.log(` ${chalk2.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
|
|
10039
10598
|
${" ".repeat(22)}${deps}` : ""}`);
|
|
10040
10599
|
}
|
|
10041
10600
|
console.log();
|
|
10042
10601
|
}
|
|
10602
|
+
if (visible.length < allMatches.length) {
|
|
10603
|
+
console.log(chalk2.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
|
|
10604
|
+
console.log();
|
|
10605
|
+
}
|
|
10043
10606
|
}
|
|
10044
10607
|
function printTodoShow(taskId) {
|
|
10045
10608
|
for (const phase of ROADMAP_PHASES) {
|
|
@@ -10069,13 +10632,19 @@ function registerTodosCommand(program) {
|
|
|
10069
10632
|
}
|
|
10070
10633
|
printTodosOverview();
|
|
10071
10634
|
});
|
|
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) => {
|
|
10635
|
+
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
10636
|
const status = opts.status;
|
|
10074
10637
|
if (status && !["pending", "in_progress", "done"].includes(status)) {
|
|
10075
10638
|
console.error(chalk2.red("--status must be one of: pending, in_progress, done"));
|
|
10076
10639
|
process.exit(1);
|
|
10077
10640
|
}
|
|
10078
|
-
printTodosList({
|
|
10641
|
+
printTodosList({
|
|
10642
|
+
phase: opts.phase,
|
|
10643
|
+
status,
|
|
10644
|
+
limit: parseListLimit(opts.limit),
|
|
10645
|
+
verbose: Boolean(opts.verbose),
|
|
10646
|
+
json: Boolean(opts.json)
|
|
10647
|
+
});
|
|
10079
10648
|
});
|
|
10080
10649
|
todosCmd.command("show <task-id>").description("Show a single task (e.g. 4.2)").action((taskId) => {
|
|
10081
10650
|
printTodoShow(taskId);
|
|
@@ -10259,8 +10828,18 @@ function parsePeriod(value, fallback = "month") {
|
|
|
10259
10828
|
}
|
|
10260
10829
|
return p;
|
|
10261
10830
|
}
|
|
10831
|
+
function parseLimit(value, fallback, verbose) {
|
|
10832
|
+
if (verbose && value == null)
|
|
10833
|
+
return Number.POSITIVE_INFINITY;
|
|
10834
|
+
const parsed = Number(value ?? String(fallback));
|
|
10835
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10836
|
+
console.error(chalk4.red("--limit must be a positive integer"));
|
|
10837
|
+
process.exit(1);
|
|
10838
|
+
}
|
|
10839
|
+
return parsed;
|
|
10840
|
+
}
|
|
10262
10841
|
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) => {
|
|
10842
|
+
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
10843
|
const period = parsePeriod(periodArg, "month");
|
|
10265
10844
|
const agent = parseAgent(opts.agent, "--agent");
|
|
10266
10845
|
const { snapshots: snaps, summary } = await getStore().usage(period, agent);
|
|
@@ -10274,16 +10853,26 @@ function registerExtendedCommands(program) {
|
|
|
10274
10853
|
if (snaps.length === 0) {
|
|
10275
10854
|
console.log(chalk4.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
|
|
10276
10855
|
} else {
|
|
10277
|
-
|
|
10856
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10857
|
+
const visibleSnaps = snaps.slice(0, limit);
|
|
10858
|
+
if (visibleSnaps.length < snaps.length) {
|
|
10859
|
+
console.log(chalk4.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
|
|
10860
|
+
console.log();
|
|
10861
|
+
}
|
|
10862
|
+
for (const s of visibleSnaps) {
|
|
10278
10863
|
console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
|
|
10279
10864
|
}
|
|
10865
|
+
if (visibleSnaps.length < snaps.length) {
|
|
10866
|
+
console.log(chalk4.dim(`
|
|
10867
|
+
... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10868
|
+
}
|
|
10280
10869
|
}
|
|
10281
10870
|
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
10282
10871
|
console.log(`
|
|
10283
10872
|
${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
|
|
10284
10873
|
console.log();
|
|
10285
10874
|
});
|
|
10286
|
-
program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>",
|
|
10875
|
+
program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
10287
10876
|
const period = parsePeriod(periodArg, "month");
|
|
10288
10877
|
const agent = parseAgent(opts.agent, "--agent");
|
|
10289
10878
|
const savings = await getStore().savings(period, agent);
|
|
@@ -10302,7 +10891,7 @@ function registerExtendedCommands(program) {
|
|
|
10302
10891
|
console.log();
|
|
10303
10892
|
});
|
|
10304
10893
|
const subsCmd = program.command("subscriptions").description("Manage subscription plans");
|
|
10305
|
-
subsCmd.command("set").description("Set a subscription plan").requiredOption("--provider <name>", "Provider name").requiredOption("--plan <name>", "Plan name").option("--agent <agent>",
|
|
10894
|
+
subsCmd.command("set").description("Set a subscription plan").requiredOption("--provider <name>", "Provider name").requiredOption("--plan <name>", "Plan name").option("--agent <agent>", `Agent scope (${AGENTS.join("|")})`).option("--fee <usd>", "Monthly fee USD", "0").option("--included <usd>", "Included usage USD", "0").action(async (opts) => {
|
|
10306
10895
|
const agent = parseAgent(opts.agent, "--agent") ?? null;
|
|
10307
10896
|
await getStore().setSubscription({
|
|
10308
10897
|
agent,
|
|
@@ -10316,7 +10905,7 @@ function registerExtendedCommands(program) {
|
|
|
10316
10905
|
});
|
|
10317
10906
|
console.log(chalk4.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
|
|
10318
10907
|
});
|
|
10319
|
-
subsCmd.command("list").description("List subscription plans").option("--json", "Output JSON").action(async (opts) => {
|
|
10908
|
+
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
10909
|
const rows = await getStore().listSubscriptions();
|
|
10321
10910
|
if (opts.json) {
|
|
10322
10911
|
console.log(JSON.stringify(rows, null, 2));
|
|
@@ -10326,10 +10915,20 @@ function registerExtendedCommands(program) {
|
|
|
10326
10915
|
console.log(chalk4.yellow("No subscriptions configured."));
|
|
10327
10916
|
return;
|
|
10328
10917
|
}
|
|
10918
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10919
|
+
const visibleRows = rows.slice(0, limit);
|
|
10329
10920
|
console.log();
|
|
10330
|
-
|
|
10921
|
+
if (visibleRows.length < rows.length) {
|
|
10922
|
+
console.log(chalk4.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
|
|
10923
|
+
console.log();
|
|
10924
|
+
}
|
|
10925
|
+
for (const r of visibleRows) {
|
|
10331
10926
|
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
10927
|
}
|
|
10928
|
+
if (visibleRows.length < rows.length) {
|
|
10929
|
+
console.log(chalk4.dim(`
|
|
10930
|
+
... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10931
|
+
}
|
|
10333
10932
|
console.log();
|
|
10334
10933
|
});
|
|
10335
10934
|
subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
|
|
@@ -10453,20 +11052,29 @@ function registerExtendedCommands(program) {
|
|
|
10453
11052
|
}
|
|
10454
11053
|
}
|
|
10455
11054
|
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) => {
|
|
11055
|
+
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
11056
|
const period = parsePeriod(opts.period, "today");
|
|
10458
11057
|
const { summary, machines, registry } = await getStore().fleet(period);
|
|
10459
11058
|
if (opts.json) {
|
|
10460
11059
|
console.log(JSON.stringify({ period, summary, machines, registry }, null, 2));
|
|
10461
11060
|
return;
|
|
10462
11061
|
}
|
|
11062
|
+
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
11063
|
+
const visibleMachines = machines.slice(0, limit);
|
|
10463
11064
|
console.log();
|
|
10464
11065
|
console.log(chalk4.bold.cyan(` Fleet \u2014 ${period}`));
|
|
10465
11066
|
console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
|
|
11067
|
+
if (visibleMachines.length < machines.length) {
|
|
11068
|
+
console.log(chalk4.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
11069
|
+
}
|
|
10466
11070
|
console.log();
|
|
10467
|
-
for (const m of
|
|
11071
|
+
for (const m of visibleMachines) {
|
|
10468
11072
|
console.log(` ${chalk4.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
|
|
10469
11073
|
}
|
|
11074
|
+
if (visibleMachines.length < machines.length) {
|
|
11075
|
+
console.log(chalk4.dim(`
|
|
11076
|
+
... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
|
|
11077
|
+
}
|
|
10470
11078
|
console.log();
|
|
10471
11079
|
});
|
|
10472
11080
|
program.command("completion <shell>").description("Print shell completion script (bash, zsh, or fish)").action((shell) => {
|
|
@@ -10614,7 +11222,7 @@ init_store();
|
|
|
10614
11222
|
init_transport();
|
|
10615
11223
|
import { execSync } from "child_process";
|
|
10616
11224
|
var program = new Command;
|
|
10617
|
-
program.name("economy").description("AI coding cost tracker \u2014 Claude, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
|
|
11225
|
+
program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
|
|
10618
11226
|
async function autoSync(opts = {}) {
|
|
10619
11227
|
if (isCloudStore())
|
|
10620
11228
|
return;
|
|
@@ -10721,6 +11329,16 @@ function parseOptionalCliAgent(value) {
|
|
|
10721
11329
|
fail(e instanceof Error ? e.message : String(e));
|
|
10722
11330
|
}
|
|
10723
11331
|
}
|
|
11332
|
+
function resolveRowLimit(value, verbose, fallback) {
|
|
11333
|
+
if (verbose && value == null)
|
|
11334
|
+
return Number.POSITIVE_INFINITY;
|
|
11335
|
+
return parsePositiveCliInteger(value ?? String(fallback), "--limit");
|
|
11336
|
+
}
|
|
11337
|
+
function printHiddenRowsHint(total, shown, detail) {
|
|
11338
|
+
if (total <= shown)
|
|
11339
|
+
return;
|
|
11340
|
+
console.log(chalk7.dim(` ... ${total - shown} more rows hidden. ${detail}`));
|
|
11341
|
+
}
|
|
10724
11342
|
function requireCliChoice(value, option, allowed) {
|
|
10725
11343
|
const selected = value ?? allowed[0];
|
|
10726
11344
|
if (allowed.includes(selected))
|
|
@@ -10820,14 +11438,14 @@ program.action(async () => {
|
|
|
10820
11438
|
}
|
|
10821
11439
|
console.log();
|
|
10822
11440
|
});
|
|
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) => {
|
|
11441
|
+
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
11442
|
if (isCloudStore()) {
|
|
10825
11443
|
console.log(chalk7.yellow("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)"));
|
|
10826
11444
|
return;
|
|
10827
11445
|
}
|
|
10828
11446
|
const db = openDatabase();
|
|
10829
11447
|
ensurePricingSeeded(db);
|
|
10830
|
-
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes;
|
|
11448
|
+
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops;
|
|
10831
11449
|
if (!anySpecific || opts.verbose) {
|
|
10832
11450
|
try {
|
|
10833
11451
|
const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
|
|
@@ -10840,7 +11458,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
10840
11458
|
}
|
|
10841
11459
|
}
|
|
10842
11460
|
if (opts.force) {
|
|
10843
|
-
const ingestSources = [...AGENTS, "codex-codewith"];
|
|
11461
|
+
const ingestSources = [...AGENTS, "codex-codewith", "loops"];
|
|
10844
11462
|
db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
|
|
10845
11463
|
if (opts.verbose)
|
|
10846
11464
|
console.log(chalk7.dim("Cleared ingest cache"));
|
|
@@ -10855,6 +11473,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
10855
11473
|
cursor: opts.cursor,
|
|
10856
11474
|
pi: opts.pi,
|
|
10857
11475
|
hermes: opts.hermes,
|
|
11476
|
+
loops: opts.loops,
|
|
10858
11477
|
verbose: opts.verbose
|
|
10859
11478
|
});
|
|
10860
11479
|
console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
|
|
@@ -10917,7 +11536,7 @@ program.command("month").description("Cost summary for this month").action(async
|
|
|
10917
11536
|
await autoSync();
|
|
10918
11537
|
await printSummary("This Month", "month");
|
|
10919
11538
|
});
|
|
10920
|
-
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>",
|
|
11539
|
+
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--project <path>", "Filter by project path").option("--account <query>", "Filter by account key, name, or email").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
10921
11540
|
const limit = parsePositiveCliInteger(opts.limit ?? "20", "--limit");
|
|
10922
11541
|
const agent = parseOptionalCliAgent(opts.agent);
|
|
10923
11542
|
await autoSync();
|
|
@@ -10964,7 +11583,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
10964
11583
|
]));
|
|
10965
11584
|
console.log();
|
|
10966
11585
|
});
|
|
10967
|
-
program.command("top").description("Most expensive sessions").option("-n <n>", "Number of sessions", "10").option("--agent <agent>",
|
|
11586
|
+
program.command("top").description("Most expensive sessions").option("-n <n>", "Number of sessions", "10").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").action(async (opts) => {
|
|
10968
11587
|
const count = parsePositiveCliInteger(opts.n ?? "10", "-n");
|
|
10969
11588
|
const agent = parseOptionalCliAgent(opts.agent);
|
|
10970
11589
|
const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
@@ -10984,22 +11603,36 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
10984
11603
|
]));
|
|
10985
11604
|
console.log();
|
|
10986
11605
|
});
|
|
10987
|
-
program.command("breakdown").description("Cost breakdown by model, agent, project, or
|
|
11606
|
+
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
11607
|
const store = getStore();
|
|
11608
|
+
const by = requireCliChoice(opts.by, "--by", ["model", "agent", "project", "account", "cost-center", "loop", "app", "repo"]);
|
|
10989
11609
|
const since = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
11610
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11611
|
+
const costCenterKinds = new Set(["loop", "app", "repo"]);
|
|
10990
11612
|
console.log();
|
|
10991
|
-
if (
|
|
11613
|
+
if (by === "project") {
|
|
10992
11614
|
const rows = await store.projectBreakdown({ since });
|
|
10993
|
-
|
|
11615
|
+
if (opts.json) {
|
|
11616
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11617
|
+
return;
|
|
11618
|
+
}
|
|
11619
|
+
const visibleRows = rows.slice(0, limit);
|
|
11620
|
+
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
10994
11621
|
chalk7.white(r.project_name || chalk7.dim("unknown")),
|
|
10995
11622
|
String(r.sessions),
|
|
10996
11623
|
String(r.requests),
|
|
10997
11624
|
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
10998
11625
|
fmt4(r.cost_usd)
|
|
10999
11626
|
]));
|
|
11000
|
-
|
|
11627
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11628
|
+
} else if (by === "agent") {
|
|
11001
11629
|
const rows = await store.agentBreakdown({ since });
|
|
11002
|
-
|
|
11630
|
+
if (opts.json) {
|
|
11631
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11632
|
+
return;
|
|
11633
|
+
}
|
|
11634
|
+
const visibleRows = rows.slice(0, limit);
|
|
11635
|
+
printTable(["Agent", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], visibleRows.map((r) => [
|
|
11003
11636
|
fmtAgent(r.agent),
|
|
11004
11637
|
String(r.sessions),
|
|
11005
11638
|
String(r.requests),
|
|
@@ -11008,22 +11641,57 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11008
11641
|
fmt4(r.billable_usd),
|
|
11009
11642
|
fmt4(r.subscription_included_usd)
|
|
11010
11643
|
]));
|
|
11011
|
-
|
|
11012
|
-
|
|
11644
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11645
|
+
} else if (by === "account") {
|
|
11646
|
+
const rows = await store.accountBreakdown({ since });
|
|
11647
|
+
if (opts.json) {
|
|
11648
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11649
|
+
return;
|
|
11650
|
+
}
|
|
11651
|
+
const visibleRows = rows.slice(0, limit);
|
|
11652
|
+
printAccountBreakdown(visibleRows);
|
|
11653
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11654
|
+
} else if (by === "cost-center" || costCenterKinds.has(by)) {
|
|
11655
|
+
const kind = costCenterKinds.has(by) ? by : undefined;
|
|
11656
|
+
const rows = await store.costCenterBreakdown({ since, kind });
|
|
11657
|
+
if (opts.json) {
|
|
11658
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11659
|
+
return;
|
|
11660
|
+
}
|
|
11661
|
+
if (rows.length === 0) {
|
|
11662
|
+
console.log(chalk7.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
|
|
11663
|
+
} else {
|
|
11664
|
+
const visibleRows = rows.slice(0, limit);
|
|
11665
|
+
printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11666
|
+
chalk7.white(r.kind),
|
|
11667
|
+
chalk7.white(r.name),
|
|
11668
|
+
String(r.sessions),
|
|
11669
|
+
String(r.requests),
|
|
11670
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
11671
|
+
fmt4(r.cost_usd)
|
|
11672
|
+
]));
|
|
11673
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11674
|
+
}
|
|
11013
11675
|
} else {
|
|
11014
11676
|
const rows = await store.modelBreakdown({ since });
|
|
11015
|
-
|
|
11677
|
+
if (opts.json) {
|
|
11678
|
+
console.log(JSON.stringify({ by, since: since ?? null, total: rows.length, rows }, null, 2));
|
|
11679
|
+
return;
|
|
11680
|
+
}
|
|
11681
|
+
const visibleRows = rows.slice(0, limit);
|
|
11682
|
+
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11016
11683
|
chalk7.white(r.model),
|
|
11017
11684
|
fmtAgent(r.agent),
|
|
11018
11685
|
String(r.requests),
|
|
11019
11686
|
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
11020
11687
|
fmt4(r.cost_usd)
|
|
11021
11688
|
]));
|
|
11689
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11022
11690
|
}
|
|
11023
11691
|
console.log();
|
|
11024
11692
|
});
|
|
11025
11693
|
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) => {
|
|
11694
|
+
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
11695
|
const period = requireCliChoice(periodArg, "period", ACCOUNT_PERIODS);
|
|
11028
11696
|
const rows = await getStore().accounts(period);
|
|
11029
11697
|
if (opts.json) {
|
|
@@ -11036,11 +11704,17 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
11036
11704
|
}
|
|
11037
11705
|
console.log();
|
|
11038
11706
|
console.log(chalk7.bold.cyan(` Accounts \u2014 ${period}`));
|
|
11707
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11708
|
+
const visibleRows = rows.slice(0, limit);
|
|
11709
|
+
if (visibleRows.length < rows.length) {
|
|
11710
|
+
console.log(chalk7.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
|
|
11711
|
+
}
|
|
11039
11712
|
console.log();
|
|
11040
|
-
printAccountBreakdown(
|
|
11713
|
+
printAccountBreakdown(visibleRows);
|
|
11714
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
11041
11715
|
console.log();
|
|
11042
11716
|
});
|
|
11043
|
-
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>",
|
|
11717
|
+
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 (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
|
|
11044
11718
|
const { watchCosts: watchCosts2 } = await Promise.resolve().then(() => (init_watch(), exports_watch));
|
|
11045
11719
|
await watchCosts2({
|
|
11046
11720
|
interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
|
|
@@ -11050,7 +11724,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
|
|
|
11050
11724
|
});
|
|
11051
11725
|
});
|
|
11052
11726
|
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>",
|
|
11727
|
+
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 (${AGENTS.join("|")})`).action(async (opts) => {
|
|
11054
11728
|
const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
|
|
11055
11729
|
const alertAtPercent = parsePositiveCliNumber(opts.alert ?? "80", "--alert");
|
|
11056
11730
|
if (alertAtPercent > 100)
|
|
@@ -11060,25 +11734,32 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
11060
11734
|
await getStore().setBudget({
|
|
11061
11735
|
project_path: opts.project ?? null,
|
|
11062
11736
|
agent,
|
|
11737
|
+
cost_center_id: opts.costCenter ?? null,
|
|
11063
11738
|
period,
|
|
11064
11739
|
limit_usd: limitUsd,
|
|
11065
11740
|
alert_at_percent: alertAtPercent
|
|
11066
11741
|
});
|
|
11067
|
-
console.log(chalk7.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11742
|
+
console.log(chalk7.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11068
11743
|
});
|
|
11069
|
-
budgetCmd.command("list").description("List all budgets").action(async () => {
|
|
11744
|
+
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
11745
|
const statuses = await getStore().listBudgets();
|
|
11071
11746
|
if (statuses.length === 0) {
|
|
11072
11747
|
console.log(chalk7.yellow("No budgets set."));
|
|
11073
11748
|
return;
|
|
11074
11749
|
}
|
|
11750
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11751
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11075
11752
|
console.log();
|
|
11076
|
-
|
|
11753
|
+
if (visibleStatuses.length < statuses.length) {
|
|
11754
|
+
console.log(chalk7.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
|
|
11755
|
+
console.log();
|
|
11756
|
+
}
|
|
11757
|
+
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
|
|
11077
11758
|
const pct = b.percent_used.toFixed(1);
|
|
11078
11759
|
const status = b.is_over_limit ? chalk7.red("OVER") : b.is_over_alert ? chalk7.yellow("ALERT") : chalk7.green("OK");
|
|
11079
11760
|
const pctColor = b.is_over_limit ? chalk7.red(pct + "%") : b.is_over_alert ? chalk7.yellow(pct + "%") : chalk7.green(pct + "%");
|
|
11080
11761
|
return [
|
|
11081
|
-
chalk7.white(b.project_path ?? "global"),
|
|
11762
|
+
chalk7.white(b.cost_center_id ?? b.project_path ?? "global"),
|
|
11082
11763
|
b.period,
|
|
11083
11764
|
fmt4(b.limit_usd),
|
|
11084
11765
|
fmt4(b.current_spend_usd),
|
|
@@ -11086,6 +11767,7 @@ budgetCmd.command("list").description("List all budgets").action(async () => {
|
|
|
11086
11767
|
status
|
|
11087
11768
|
];
|
|
11088
11769
|
}));
|
|
11770
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11089
11771
|
console.log();
|
|
11090
11772
|
});
|
|
11091
11773
|
budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
|
|
@@ -11099,20 +11781,27 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
|
|
|
11099
11781
|
await getStore().addProject(path, name);
|
|
11100
11782
|
console.log(chalk7.green(`\u2713 Project added: ${path}`));
|
|
11101
11783
|
});
|
|
11102
|
-
projectCmd.command("list").description("List all projects with costs").action(async () => {
|
|
11784
|
+
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
11785
|
const projects = await getStore().listProjects();
|
|
11104
11786
|
if (projects.length === 0) {
|
|
11105
11787
|
console.log(chalk7.yellow("No projects tracked yet."));
|
|
11106
11788
|
return;
|
|
11107
11789
|
}
|
|
11790
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11791
|
+
const visibleProjects = projects.slice(0, limit);
|
|
11108
11792
|
console.log();
|
|
11109
|
-
|
|
11793
|
+
if (visibleProjects.length < projects.length) {
|
|
11794
|
+
console.log(chalk7.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
|
|
11795
|
+
console.log();
|
|
11796
|
+
}
|
|
11797
|
+
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
|
|
11110
11798
|
chalk7.white(p.project_name || chalk7.dim("unknown")),
|
|
11111
11799
|
chalk7.dim(p.project_path.substring(0, 40)),
|
|
11112
11800
|
String(p.sessions),
|
|
11113
11801
|
fmt4(p.cost_usd),
|
|
11114
11802
|
chalk7.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
11115
11803
|
]));
|
|
11804
|
+
printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
|
|
11116
11805
|
console.log();
|
|
11117
11806
|
});
|
|
11118
11807
|
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
|
|
@@ -11217,10 +11906,16 @@ configCmd.action(async () => {
|
|
|
11217
11906
|
console.log();
|
|
11218
11907
|
});
|
|
11219
11908
|
var pricingCmd = program.command("pricing").description("Manage model pricing rates");
|
|
11220
|
-
pricingCmd.command("list").description("List all model prices").action(async () => {
|
|
11909
|
+
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
11910
|
const rows = await getStore().listPricing();
|
|
11911
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 50);
|
|
11912
|
+
const visibleRows = rows.slice(0, limit);
|
|
11222
11913
|
console.log();
|
|
11223
|
-
|
|
11914
|
+
if (visibleRows.length < rows.length) {
|
|
11915
|
+
console.log(chalk7.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
|
|
11916
|
+
console.log();
|
|
11917
|
+
}
|
|
11918
|
+
printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
|
|
11224
11919
|
chalk7.white(r.model),
|
|
11225
11920
|
fmt4(r.input_per_1m),
|
|
11226
11921
|
fmt4(r.output_per_1m),
|
|
@@ -11229,6 +11924,7 @@ pricingCmd.command("list").description("List all model prices").action(async ()
|
|
|
11229
11924
|
fmt4(r.cache_storage_per_1m_hour ?? 0),
|
|
11230
11925
|
chalk7.dim(fmt4(r.output_per_1m / 1000))
|
|
11231
11926
|
]));
|
|
11927
|
+
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
|
|
11232
11928
|
console.log();
|
|
11233
11929
|
});
|
|
11234
11930
|
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 +12019,8 @@ Gemini (~/.gemini/settings.json):`));
|
|
|
11323
12019
|
}
|
|
11324
12020
|
console.log();
|
|
11325
12021
|
});
|
|
11326
|
-
program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
|
|
12022
|
+
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) => {
|
|
12023
|
+
const requestLimit = parsePositiveCliInteger(opts.limit ?? (opts.verbose ? "50" : "20"), "--limit");
|
|
11327
12024
|
await autoSync();
|
|
11328
12025
|
const detail = await getStore().sessionDetail(id);
|
|
11329
12026
|
if (!detail) {
|
|
@@ -11344,10 +12041,11 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11344
12041
|
["Requests", fmtCount(session["request_count"])]
|
|
11345
12042
|
]);
|
|
11346
12043
|
if (requests.length > 0) {
|
|
12044
|
+
const visibleRequests = requests.slice(0, requestLimit);
|
|
11347
12045
|
console.log(chalk7.dim(`
|
|
11348
|
-
Requests (${requests.length}):
|
|
12046
|
+
Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
|
|
11349
12047
|
`));
|
|
11350
|
-
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"],
|
|
12048
|
+
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
|
|
11351
12049
|
chalk7.dim(String(r["timestamp"]).substring(11, 19)),
|
|
11352
12050
|
chalk7.white(String(r["model"]).substring(0, 22)),
|
|
11353
12051
|
fmtTokens2(r["input_tokens"]),
|
|
@@ -11356,12 +12054,13 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11356
12054
|
fmtTokens2(r["cache_create_tokens"]),
|
|
11357
12055
|
fmt4(r["cost_usd"])
|
|
11358
12056
|
]));
|
|
11359
|
-
if (requests.length >
|
|
11360
|
-
console.log(chalk7.dim(` ...
|
|
12057
|
+
if (requests.length > visibleRequests.length) {
|
|
12058
|
+
console.log(chalk7.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
|
|
12059
|
+
}
|
|
11361
12060
|
}
|
|
11362
12061
|
console.log();
|
|
11363
12062
|
});
|
|
11364
|
-
program.command("machines").description("List all machines that have synced data").action(async () => {
|
|
12063
|
+
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
12064
|
await autoSync();
|
|
11366
12065
|
const machines = await getStore().machines();
|
|
11367
12066
|
const current = getMachineId();
|
|
@@ -11369,16 +12068,22 @@ program.command("machines").description("List all machines that have synced data
|
|
|
11369
12068
|
console.log(chalk7.yellow(`No machine data yet. Current machine: ${current}`));
|
|
11370
12069
|
return;
|
|
11371
12070
|
}
|
|
12071
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12072
|
+
const visibleMachines = machines.slice(0, limit);
|
|
11372
12073
|
console.log();
|
|
11373
12074
|
console.log(chalk7.bold.cyan(" Machines"));
|
|
12075
|
+
if (visibleMachines.length < machines.length) {
|
|
12076
|
+
console.log(chalk7.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
12077
|
+
}
|
|
11374
12078
|
console.log();
|
|
11375
|
-
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"],
|
|
12079
|
+
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
|
|
11376
12080
|
m.machine_id === current ? chalk7.green(`${m.machine_id} (this)`) : chalk7.white(m.machine_id),
|
|
11377
12081
|
fmtCount(m.sessions),
|
|
11378
12082
|
fmtCount(m.requests),
|
|
11379
12083
|
fmt4(m.total_cost_usd),
|
|
11380
12084
|
chalk7.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
11381
12085
|
]));
|
|
12086
|
+
printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
|
|
11382
12087
|
console.log(`
|
|
11383
12088
|
${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
|
|
11384
12089
|
console.log();
|
|
@@ -11528,7 +12233,7 @@ menubarCmd.command("stop").description("Quit Economy Bar").action(async () => {
|
|
|
11528
12233
|
menubarStop2();
|
|
11529
12234
|
});
|
|
11530
12235
|
var goalCmd = program.command("goal").description("Manage spending goals");
|
|
11531
|
-
goalCmd.command("set").description("Set a spending goal").option("--period <period>", "Period: day|week|month|year", "month").option("--limit <usd>", "Goal limit in USD").option("--project <path>", "Scope to project path").option("--agent <agent>",
|
|
12236
|
+
goalCmd.command("set").description("Set a spending goal").option("--period <period>", "Period: day|week|month|year", "month").option("--limit <usd>", "Goal limit in USD").option("--project <path>", "Scope to project path").option("--agent <agent>", `Scope to agent (${AGENTS.join("|")})`).action(async (opts) => {
|
|
11532
12237
|
const limitUsd = parsePositiveCliNumber(opts.limit, "--limit");
|
|
11533
12238
|
const period = requireCliChoice(opts.period, "--period", ["day", "week", "month", "year"]);
|
|
11534
12239
|
const agent = parseOptionalCliAgent(opts.agent) ?? null;
|
|
@@ -11540,14 +12245,20 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
|
|
|
11540
12245
|
});
|
|
11541
12246
|
console.log(chalk7.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
|
|
11542
12247
|
});
|
|
11543
|
-
goalCmd.command("list").description("List all goals with progress").action(async () => {
|
|
12248
|
+
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
12249
|
const statuses = await getStore().listGoals();
|
|
11545
12250
|
if (statuses.length === 0) {
|
|
11546
12251
|
console.log(chalk7.yellow("No goals set."));
|
|
11547
12252
|
return;
|
|
11548
12253
|
}
|
|
12254
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12255
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11549
12256
|
console.log();
|
|
11550
|
-
|
|
12257
|
+
if (visibleStatuses.length < statuses.length) {
|
|
12258
|
+
console.log(chalk7.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
|
|
12259
|
+
console.log();
|
|
12260
|
+
}
|
|
12261
|
+
printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
|
|
11551
12262
|
const pct = g.percent_used.toFixed(1);
|
|
11552
12263
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
11553
12264
|
const status = g.is_over ? chalk7.red("OVER") : g.is_at_risk ? chalk7.yellow("AT RISK") : chalk7.green("ON TRACK");
|
|
@@ -11561,20 +12272,23 @@ goalCmd.command("list").description("List all goals with progress").action(async
|
|
|
11561
12272
|
status
|
|
11562
12273
|
];
|
|
11563
12274
|
}));
|
|
12275
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11564
12276
|
console.log();
|
|
11565
12277
|
});
|
|
11566
12278
|
goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
|
|
11567
12279
|
await getStore().removeGoal(id);
|
|
11568
12280
|
console.log(chalk7.green(`\u2713 Goal removed`));
|
|
11569
12281
|
});
|
|
11570
|
-
goalCmd.command("status").description("Quick goal progress summary").action(async () => {
|
|
12282
|
+
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
12283
|
const statuses = await getStore().listGoals();
|
|
11572
12284
|
if (statuses.length === 0) {
|
|
11573
12285
|
console.log(chalk7.yellow("No goals set."));
|
|
11574
12286
|
return;
|
|
11575
12287
|
}
|
|
12288
|
+
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12289
|
+
const visibleStatuses = statuses.slice(0, limit);
|
|
11576
12290
|
console.log();
|
|
11577
|
-
for (const g of
|
|
12291
|
+
for (const g of visibleStatuses) {
|
|
11578
12292
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
11579
12293
|
const pct = Math.min(g.percent_used, 100);
|
|
11580
12294
|
const barFilled = Math.round(pct / 10);
|
|
@@ -11584,6 +12298,7 @@ goalCmd.command("status").description("Quick goal progress summary").action(asyn
|
|
|
11584
12298
|
const label = `${g.period} (${scope})`.padEnd(20);
|
|
11585
12299
|
console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
|
|
11586
12300
|
}
|
|
12301
|
+
printHiddenRowsHint(statuses.length, visibleStatuses.length, "Use --limit <n> or --verbose.");
|
|
11587
12302
|
console.log();
|
|
11588
12303
|
});
|
|
11589
12304
|
program.command("remove <type> <id>").alias("rm").description("Remove a record. Type: budget | project | goal | pricing").action(async (type, id) => {
|