@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/mcp/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, {
|
|
@@ -535,7 +591,6 @@ var init_pricing = __esm(() => {
|
|
|
535
591
|
});
|
|
536
592
|
|
|
537
593
|
// src/db/database.ts
|
|
538
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
539
594
|
import { execFileSync } from "child_process";
|
|
540
595
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
541
596
|
import { hostname, platform } from "os";
|
|
@@ -628,7 +683,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
628
683
|
mkdirSync(dir, { recursive: true });
|
|
629
684
|
}
|
|
630
685
|
const db = withSqliteBusyRetry(() => {
|
|
631
|
-
const opened = new
|
|
686
|
+
const opened = new SqliteAdapter(path);
|
|
632
687
|
try {
|
|
633
688
|
opened.exec("PRAGMA busy_timeout = 10000");
|
|
634
689
|
opened.exec("PRAGMA journal_mode = WAL");
|
|
@@ -669,6 +724,16 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
669
724
|
}
|
|
670
725
|
function initSchema(db) {
|
|
671
726
|
db.exec(`
|
|
727
|
+
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
728
|
+
id TEXT PRIMARY KEY,
|
|
729
|
+
kind TEXT NOT NULL,
|
|
730
|
+
name TEXT NOT NULL,
|
|
731
|
+
repo_path TEXT,
|
|
732
|
+
labels_json TEXT DEFAULT '{}',
|
|
733
|
+
created_at TEXT NOT NULL,
|
|
734
|
+
updated_at TEXT NOT NULL
|
|
735
|
+
);
|
|
736
|
+
|
|
672
737
|
CREATE TABLE IF NOT EXISTS requests (
|
|
673
738
|
id TEXT PRIMARY KEY,
|
|
674
739
|
agent TEXT NOT NULL,
|
|
@@ -685,6 +750,7 @@ function initSchema(db) {
|
|
|
685
750
|
timestamp TEXT NOT NULL,
|
|
686
751
|
source_request_id TEXT,
|
|
687
752
|
machine_id TEXT DEFAULT '',
|
|
753
|
+
cost_center_id TEXT,
|
|
688
754
|
account_key TEXT DEFAULT '',
|
|
689
755
|
account_tool TEXT DEFAULT '',
|
|
690
756
|
account_name TEXT DEFAULT '',
|
|
@@ -703,6 +769,7 @@ function initSchema(db) {
|
|
|
703
769
|
total_tokens INTEGER DEFAULT 0,
|
|
704
770
|
request_count INTEGER DEFAULT 0,
|
|
705
771
|
machine_id TEXT DEFAULT '',
|
|
772
|
+
cost_center_id TEXT,
|
|
706
773
|
account_key TEXT DEFAULT '',
|
|
707
774
|
account_tool TEXT DEFAULT '',
|
|
708
775
|
account_name TEXT DEFAULT '',
|
|
@@ -723,6 +790,7 @@ function initSchema(db) {
|
|
|
723
790
|
id TEXT PRIMARY KEY,
|
|
724
791
|
project_path TEXT,
|
|
725
792
|
agent TEXT,
|
|
793
|
+
cost_center_id TEXT,
|
|
726
794
|
period TEXT NOT NULL,
|
|
727
795
|
limit_usd REAL NOT NULL,
|
|
728
796
|
alert_at_percent INTEGER DEFAULT 80,
|
|
@@ -753,6 +821,7 @@ function initSchema(db) {
|
|
|
753
821
|
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent);
|
|
754
822
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
|
|
755
823
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
|
|
824
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
756
825
|
|
|
757
826
|
CREATE TABLE IF NOT EXISTS model_pricing (
|
|
758
827
|
model TEXT PRIMARY KEY,
|
|
@@ -839,6 +908,12 @@ function initSchema(db) {
|
|
|
839
908
|
`);
|
|
840
909
|
addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
|
|
841
910
|
addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
|
|
911
|
+
if (addColumnIfMissing(db, "cost_centers", "updated_at", `TEXT DEFAULT ''`)) {
|
|
912
|
+
db.exec(`UPDATE cost_centers SET updated_at = created_at WHERE updated_at = '' OR updated_at IS NULL`);
|
|
913
|
+
}
|
|
914
|
+
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
915
|
+
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
916
|
+
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
842
917
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
843
918
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
844
919
|
}
|
|
@@ -867,6 +942,10 @@ function initSchema(db) {
|
|
|
867
942
|
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
868
943
|
CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key);
|
|
869
944
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key);
|
|
945
|
+
CREATE INDEX IF NOT EXISTS idx_requests_cost_center ON requests(cost_center_id);
|
|
946
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_cost_center ON sessions(cost_center_id);
|
|
947
|
+
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
948
|
+
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
870
949
|
`);
|
|
871
950
|
}
|
|
872
951
|
function periodWhere(period) {
|
|
@@ -917,6 +996,14 @@ function requestPeriodWhere(period) {
|
|
|
917
996
|
return "1=1";
|
|
918
997
|
}
|
|
919
998
|
}
|
|
999
|
+
function upsertCostCenter(db, center) {
|
|
1000
|
+
const updatedAt = center.updated_at ?? center.created_at;
|
|
1001
|
+
db.prepare(`
|
|
1002
|
+
INSERT OR REPLACE INTO cost_centers
|
|
1003
|
+
(id, kind, name, repo_path, labels_json, created_at, updated_at)
|
|
1004
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1005
|
+
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
1006
|
+
}
|
|
920
1007
|
function upsertRequest(db, req) {
|
|
921
1008
|
const now = req.updated_at ?? new Date().toISOString();
|
|
922
1009
|
db.prepare(`
|
|
@@ -924,10 +1011,10 @@ function upsertRequest(db, req) {
|
|
|
924
1011
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
925
1012
|
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
926
1013
|
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
927
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
1014
|
+
source_request_id, machine_id, cost_center_id, attribution_tag, account_key, account_tool,
|
|
928
1015
|
account_name, account_email, account_source, updated_at, synced_at)
|
|
929
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
930
|
-
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
1016
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1017
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
931
1018
|
}
|
|
932
1019
|
function upsertSession(db, session) {
|
|
933
1020
|
const now = session.updated_at ?? new Date().toISOString();
|
|
@@ -935,9 +1022,9 @@ function upsertSession(db, session) {
|
|
|
935
1022
|
INSERT OR REPLACE INTO sessions
|
|
936
1023
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
937
1024
|
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
938
|
-
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
939
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
940
|
-
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
1025
|
+
cost_center_id, account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
1026
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1027
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "", session.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", session.cost_center_id ?? null, session.account_key ?? "", session.account_tool ?? "", session.account_name ?? "", session.account_email ?? "", session.account_source ?? "", now, session.synced_at ?? "");
|
|
941
1028
|
}
|
|
942
1029
|
function rollupSession(db, sessionId) {
|
|
943
1030
|
db.prepare(`
|
|
@@ -963,9 +1050,12 @@ function rollupSession(db, sessionId) {
|
|
|
963
1050
|
ELSE account_email END,
|
|
964
1051
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
965
1052
|
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
966
|
-
ELSE account_source END
|
|
1053
|
+
ELSE account_source END,
|
|
1054
|
+
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1055
|
+
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)
|
|
1056
|
+
ELSE cost_center_id END
|
|
967
1057
|
WHERE id = ?
|
|
968
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
1058
|
+
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
969
1059
|
}
|
|
970
1060
|
function querySessions(db, filter = {}) {
|
|
971
1061
|
const conditions = [];
|
|
@@ -1181,9 +1271,12 @@ function labelForPath(projectPath, projectName) {
|
|
|
1181
1271
|
function groupKeyForPath(projectPath, projectName) {
|
|
1182
1272
|
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1183
1273
|
}
|
|
1184
|
-
function
|
|
1185
|
-
|
|
1186
|
-
|
|
1274
|
+
function laterTimestamp(current, candidate) {
|
|
1275
|
+
if (!candidate)
|
|
1276
|
+
return current;
|
|
1277
|
+
return candidate > current ? candidate : current;
|
|
1278
|
+
}
|
|
1279
|
+
function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
|
|
1187
1280
|
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1188
1281
|
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1189
1282
|
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
@@ -1191,67 +1284,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
|
|
|
1191
1284
|
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1192
1285
|
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1193
1286
|
const sessions = db.prepare(`
|
|
1194
|
-
SELECT id, project_path, project_name
|
|
1287
|
+
SELECT id, project_path, project_name
|
|
1195
1288
|
FROM sessions
|
|
1196
1289
|
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1197
1290
|
`).all(...sessionMachineParams);
|
|
1198
1291
|
const groups = new Map;
|
|
1292
|
+
const groupKeyBySession = new Map;
|
|
1199
1293
|
for (const s of sessions) {
|
|
1200
1294
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1201
1295
|
if (!label)
|
|
1202
1296
|
continue;
|
|
1203
1297
|
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1204
|
-
const g = groups.get(key) ?? { label,
|
|
1205
|
-
g.sessionIds.push(s.id);
|
|
1298
|
+
const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
|
|
1206
1299
|
if (!g.samplePath)
|
|
1207
1300
|
g.samplePath = s.project_path;
|
|
1208
1301
|
groups.set(key, g);
|
|
1302
|
+
groupKeyBySession.set(s.id, key);
|
|
1303
|
+
}
|
|
1304
|
+
const groupFor = (sessionId) => {
|
|
1305
|
+
const key = groupKeyBySession.get(sessionId);
|
|
1306
|
+
return key === undefined ? undefined : groups.get(key);
|
|
1307
|
+
};
|
|
1308
|
+
const requestRows = db.prepare(`
|
|
1309
|
+
SELECT session_id,
|
|
1310
|
+
COUNT(*) as requests,
|
|
1311
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1312
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1313
|
+
MAX(timestamp) as last_active
|
|
1314
|
+
FROM requests
|
|
1315
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1316
|
+
GROUP BY session_id
|
|
1317
|
+
`).all(...requestScopeParams, ...requestMachineParams);
|
|
1318
|
+
for (const row of requestRows) {
|
|
1319
|
+
const g = groupFor(row.session_id);
|
|
1320
|
+
if (!g)
|
|
1321
|
+
continue;
|
|
1322
|
+
g.sessions += 1;
|
|
1323
|
+
g.requests += row.requests;
|
|
1324
|
+
g.cost_usd += row.cost_usd;
|
|
1325
|
+
g.total_tokens += row.total_tokens;
|
|
1326
|
+
g.last_active = laterTimestamp(g.last_active, row.last_active);
|
|
1327
|
+
}
|
|
1328
|
+
const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
|
|
1329
|
+
const sessionOnlyRows = db.prepare(`
|
|
1330
|
+
SELECT id,
|
|
1331
|
+
COALESCE(request_count, 0) as request_count,
|
|
1332
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
1333
|
+
COALESCE(total_cost_usd, 0) as total_cost_usd,
|
|
1334
|
+
started_at
|
|
1335
|
+
FROM sessions
|
|
1336
|
+
WHERE ${sessionWhere}${sessionOnlyMachineClause}
|
|
1337
|
+
`).all(...sessionScopeParams, ...sessionOnlyMachineParams);
|
|
1338
|
+
for (const row of sessionOnlyRows) {
|
|
1339
|
+
if (sessionsWithRequests.has(row.id))
|
|
1340
|
+
continue;
|
|
1341
|
+
const g = groupFor(row.id);
|
|
1342
|
+
if (!g)
|
|
1343
|
+
continue;
|
|
1344
|
+
g.sessions += 1;
|
|
1345
|
+
g.requests += row.request_count;
|
|
1346
|
+
g.total_tokens += row.total_tokens;
|
|
1347
|
+
g.cost_usd += row.total_cost_usd;
|
|
1348
|
+
g.last_active = laterTimestamp(g.last_active, row.started_at);
|
|
1209
1349
|
}
|
|
1210
1350
|
const result = [];
|
|
1211
1351
|
for (const g of groups.values()) {
|
|
1212
|
-
|
|
1213
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
1214
|
-
SELECT
|
|
1215
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1216
|
-
COUNT(*) as requests,
|
|
1217
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1218
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1219
|
-
MAX(timestamp) as last_active
|
|
1220
|
-
FROM requests
|
|
1221
|
-
WHERE session_id IN (${placeholders})
|
|
1222
|
-
AND ${requestWhere}
|
|
1223
|
-
${requestMachineClause}
|
|
1224
|
-
`).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
|
|
1225
|
-
const sessionOnlyStats = placeholders.length ? db.prepare(`
|
|
1226
|
-
SELECT
|
|
1227
|
-
COUNT(*) as sessions,
|
|
1228
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1229
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1230
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1231
|
-
MAX(started_at) as last_active
|
|
1232
|
-
FROM sessions
|
|
1233
|
-
WHERE id IN (${placeholders})
|
|
1234
|
-
AND ${sessionWhere}
|
|
1235
|
-
${sessionOnlyMachineClause}
|
|
1236
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1237
|
-
`).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1238
|
-
const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1239
|
-
if (totalSessions === 0)
|
|
1352
|
+
if (g.sessions === 0)
|
|
1240
1353
|
continue;
|
|
1241
|
-
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1242
1354
|
result.push({
|
|
1243
1355
|
project_path: g.samplePath,
|
|
1244
1356
|
project_name: g.label,
|
|
1245
|
-
sessions:
|
|
1246
|
-
requests:
|
|
1247
|
-
total_tokens:
|
|
1248
|
-
cost_usd:
|
|
1249
|
-
last_active:
|
|
1357
|
+
sessions: g.sessions,
|
|
1358
|
+
requests: g.requests,
|
|
1359
|
+
total_tokens: g.total_tokens,
|
|
1360
|
+
cost_usd: g.cost_usd,
|
|
1361
|
+
last_active: g.last_active
|
|
1250
1362
|
});
|
|
1251
1363
|
}
|
|
1252
1364
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1253
1365
|
return result;
|
|
1254
1366
|
}
|
|
1367
|
+
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1368
|
+
return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
|
|
1369
|
+
}
|
|
1255
1370
|
function normalizeAccountEmail(email) {
|
|
1256
1371
|
return (email ?? "").trim().toLowerCase();
|
|
1257
1372
|
}
|
|
@@ -1392,17 +1507,141 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1392
1507
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1393
1508
|
return result;
|
|
1394
1509
|
}
|
|
1395
|
-
function
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1510
|
+
function kindFromCostCenterId(id) {
|
|
1511
|
+
const prefix = id.split(":", 1)[0] ?? "";
|
|
1512
|
+
return ["loop", "app", "repo", "service", "team"].includes(prefix) ? prefix : "";
|
|
1513
|
+
}
|
|
1514
|
+
function addCostCenterBreakdownRow(groups, row, sessionOnly) {
|
|
1515
|
+
if (!row.cost_center_id)
|
|
1516
|
+
return;
|
|
1517
|
+
const derivedKind = row.kind || kindFromCostCenterId(row.cost_center_id) || "service";
|
|
1518
|
+
const group = groups.get(row.cost_center_id) ?? {
|
|
1519
|
+
cost_center_id: row.cost_center_id,
|
|
1520
|
+
kind: derivedKind,
|
|
1521
|
+
name: row.name || row.cost_center_id,
|
|
1522
|
+
repo_path: row.repo_path ?? null,
|
|
1523
|
+
labels_json: row.labels_json || "{}",
|
|
1524
|
+
sessionIds: new Set,
|
|
1525
|
+
requests: 0,
|
|
1526
|
+
total_tokens: 0,
|
|
1527
|
+
api_equivalent_usd: 0,
|
|
1528
|
+
metered_api_usd: 0,
|
|
1529
|
+
subscription_included_usd: 0,
|
|
1530
|
+
estimated_usd: 0,
|
|
1531
|
+
unknown_usd: 0,
|
|
1532
|
+
last_active: ""
|
|
1533
|
+
};
|
|
1534
|
+
group.kind = derivedKind;
|
|
1535
|
+
if (row.name)
|
|
1536
|
+
group.name = row.name;
|
|
1537
|
+
if (row.repo_path && !group.repo_path)
|
|
1538
|
+
group.repo_path = row.repo_path;
|
|
1539
|
+
if (row.labels_json && row.labels_json !== "{}")
|
|
1540
|
+
group.labels_json = row.labels_json;
|
|
1541
|
+
if (row.session_id)
|
|
1542
|
+
group.sessionIds.add(row.session_id);
|
|
1543
|
+
group.requests += row.requests;
|
|
1544
|
+
group.total_tokens += row.total_tokens;
|
|
1545
|
+
group.api_equivalent_usd += row.cost_usd;
|
|
1546
|
+
if (sessionOnly) {
|
|
1547
|
+
group.estimated_usd += row.cost_usd;
|
|
1548
|
+
} else if (row.cost_basis === "metered_api") {
|
|
1549
|
+
group.metered_api_usd += row.cost_usd;
|
|
1550
|
+
} else if (row.cost_basis === "subscription_included") {
|
|
1551
|
+
group.subscription_included_usd += row.cost_usd;
|
|
1552
|
+
} else if (row.cost_basis === "unknown") {
|
|
1553
|
+
group.unknown_usd += row.cost_usd;
|
|
1554
|
+
} else {
|
|
1555
|
+
group.estimated_usd += row.cost_usd;
|
|
1556
|
+
}
|
|
1557
|
+
if (!group.last_active || row.last_active > group.last_active)
|
|
1558
|
+
group.last_active = row.last_active;
|
|
1559
|
+
groups.set(row.cost_center_id, group);
|
|
1560
|
+
}
|
|
1561
|
+
function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
1562
|
+
const requestWhere = filter.since ? "r.timestamp >= ?" : requestPeriodWhere(period);
|
|
1563
|
+
const sessionWhere = filter.since ? "s.started_at >= ?" : sessionPeriodWhere(period);
|
|
1564
|
+
const requestMachineClause = filter.machine ? " AND r.machine_id = ?" : "";
|
|
1565
|
+
const sessionMachineClause = filter.machine ? " AND s.machine_id = ?" : "";
|
|
1566
|
+
const requestParams = [];
|
|
1567
|
+
const sessionParams = [];
|
|
1568
|
+
if (filter.since) {
|
|
1569
|
+
requestParams.push(filter.since);
|
|
1570
|
+
sessionParams.push(filter.since);
|
|
1571
|
+
}
|
|
1572
|
+
if (filter.machine) {
|
|
1573
|
+
requestParams.push(filter.machine);
|
|
1574
|
+
sessionParams.push(filter.machine);
|
|
1575
|
+
}
|
|
1576
|
+
const groups = new Map;
|
|
1577
|
+
const requestRows = db.prepare(`
|
|
1578
|
+
SELECT
|
|
1579
|
+
COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) as cost_center_id,
|
|
1580
|
+
COALESCE(cc.kind, '') as kind,
|
|
1581
|
+
COALESCE(cc.name, COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))) as name,
|
|
1582
|
+
cc.repo_path as repo_path,
|
|
1583
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1584
|
+
r.session_id as session_id,
|
|
1585
|
+
1 as requests,
|
|
1586
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1587
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1588
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1589
|
+
r.timestamp as last_active
|
|
1590
|
+
FROM requests r
|
|
1591
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1592
|
+
LEFT JOIN cost_centers cc ON cc.id = COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, ''))
|
|
1593
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1594
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) IS NOT NULL
|
|
1595
|
+
AND COALESCE(NULLIF(r.cost_center_id, ''), NULLIF(s.cost_center_id, '')) != ''
|
|
1596
|
+
`).all(...requestParams);
|
|
1597
|
+
for (const row of requestRows)
|
|
1598
|
+
addCostCenterBreakdownRow(groups, row, false);
|
|
1599
|
+
const sessionOnlyRows = db.prepare(`
|
|
1600
|
+
SELECT
|
|
1601
|
+
s.cost_center_id as cost_center_id,
|
|
1602
|
+
COALESCE(cc.kind, '') as kind,
|
|
1603
|
+
COALESCE(cc.name, s.cost_center_id) as name,
|
|
1604
|
+
cc.repo_path as repo_path,
|
|
1605
|
+
COALESCE(cc.labels_json, '{}') as labels_json,
|
|
1606
|
+
s.id as session_id,
|
|
1607
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1608
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1609
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1610
|
+
'estimated' as cost_basis,
|
|
1611
|
+
s.started_at as last_active
|
|
1612
|
+
FROM sessions s
|
|
1613
|
+
LEFT JOIN cost_centers cc ON cc.id = s.cost_center_id
|
|
1614
|
+
WHERE ${sessionWhere}${sessionMachineClause}
|
|
1615
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1616
|
+
AND s.cost_center_id IS NOT NULL
|
|
1617
|
+
AND s.cost_center_id != ''
|
|
1618
|
+
`).all(...sessionParams);
|
|
1619
|
+
for (const row of sessionOnlyRows)
|
|
1620
|
+
addCostCenterBreakdownRow(groups, row, true);
|
|
1621
|
+
const result = [...groups.values()].map((group) => ({
|
|
1622
|
+
cost_center_id: group.cost_center_id,
|
|
1623
|
+
kind: group.kind,
|
|
1624
|
+
name: group.name,
|
|
1625
|
+
repo_path: group.repo_path,
|
|
1626
|
+
labels_json: group.labels_json,
|
|
1627
|
+
sessions: group.sessionIds.size,
|
|
1628
|
+
requests: group.requests,
|
|
1629
|
+
total_tokens: group.total_tokens,
|
|
1630
|
+
api_equivalent_usd: group.api_equivalent_usd,
|
|
1631
|
+
billable_usd: group.metered_api_usd,
|
|
1632
|
+
metered_api_usd: group.metered_api_usd,
|
|
1633
|
+
subscription_included_usd: group.subscription_included_usd,
|
|
1634
|
+
estimated_usd: group.estimated_usd,
|
|
1635
|
+
unknown_usd: group.unknown_usd,
|
|
1636
|
+
cost_usd: group.api_equivalent_usd,
|
|
1637
|
+
last_active: group.last_active
|
|
1638
|
+
}));
|
|
1639
|
+
const filtered = filter.kind ? result.filter((row) => row.kind === filter.kind) : result;
|
|
1640
|
+
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1641
|
+
return filtered;
|
|
1642
|
+
}
|
|
1643
|
+
function queryProjectBreakdownSince(db, since, machine) {
|
|
1644
|
+
return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
|
|
1406
1645
|
}
|
|
1407
1646
|
function queryAgentBreakdownSince(db, since) {
|
|
1408
1647
|
return db.prepare(`
|
|
@@ -1563,9 +1802,9 @@ function deleteProject(db, path) {
|
|
|
1563
1802
|
function upsertBudget(db, budget) {
|
|
1564
1803
|
db.prepare(`
|
|
1565
1804
|
INSERT OR REPLACE INTO budgets
|
|
1566
|
-
(id, project_path, agent, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1567
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1568
|
-
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1805
|
+
(id, project_path, agent, cost_center_id, period, limit_usd, alert_at_percent, created_at, updated_at)
|
|
1806
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1807
|
+
`).run(budget.id, budget.project_path ?? null, budget.agent ?? null, budget.cost_center_id ?? null, budget.period, budget.limit_usd, budget.alert_at_percent, budget.created_at, budget.updated_at);
|
|
1569
1808
|
}
|
|
1570
1809
|
function listBudgets(db) {
|
|
1571
1810
|
return db.prepare(`SELECT * FROM budgets ORDER BY created_at DESC`).all();
|
|
@@ -1587,6 +1826,10 @@ function getBudgetStatuses(db) {
|
|
|
1587
1826
|
spendQuery += ` AND agent = ?`;
|
|
1588
1827
|
params.push(b.agent);
|
|
1589
1828
|
}
|
|
1829
|
+
if (b.cost_center_id) {
|
|
1830
|
+
spendQuery += ` AND COALESCE(NULLIF(cost_center_id, ''), (SELECT NULLIF(s.cost_center_id, '') FROM sessions s WHERE s.id = requests.session_id)) = ?`;
|
|
1831
|
+
params.push(b.cost_center_id);
|
|
1832
|
+
}
|
|
1590
1833
|
const row = db.prepare(spendQuery).get(...params);
|
|
1591
1834
|
const spend = row.spend;
|
|
1592
1835
|
const percent = b.limit_usd > 0 ? spend / b.limit_usd * 100 : 0;
|
|
@@ -1812,7 +2055,9 @@ function dedupeRequests(db) {
|
|
|
1812
2055
|
}
|
|
1813
2056
|
return removed;
|
|
1814
2057
|
}
|
|
1815
|
-
var init_database = () => {
|
|
2058
|
+
var init_database = __esm(() => {
|
|
2059
|
+
init_sqlite_adapter();
|
|
2060
|
+
});
|
|
1816
2061
|
|
|
1817
2062
|
// src/mcp/index.ts
|
|
1818
2063
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -1836,7 +2081,6 @@ var packageMetadata = getPackageMetadata();
|
|
|
1836
2081
|
// src/mcp/server.ts
|
|
1837
2082
|
init_database();
|
|
1838
2083
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1839
|
-
import { registerCloudTools } from "@hasna/cloud";
|
|
1840
2084
|
import { registerAgentTools } from "@hasna/agent-registry";
|
|
1841
2085
|
import { z } from "zod";
|
|
1842
2086
|
|
|
@@ -2002,6 +2246,7 @@ function defaultCostBasisForAgent(agent) {
|
|
|
2002
2246
|
}
|
|
2003
2247
|
|
|
2004
2248
|
// src/lib/accounts.ts
|
|
2249
|
+
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
2005
2250
|
var AGENT_ACCOUNT_TOOLS = {
|
|
2006
2251
|
claude: ["claude"],
|
|
2007
2252
|
takumi: ["takumi", "claude"],
|
|
@@ -2066,57 +2311,35 @@ function envOverride(agent, env) {
|
|
|
2066
2311
|
account_source: "override"
|
|
2067
2312
|
};
|
|
2068
2313
|
}
|
|
2069
|
-
function
|
|
2070
|
-
try {
|
|
2071
|
-
return new Set(api.listTools().map((tool) => tool.id));
|
|
2072
|
-
} catch {
|
|
2073
|
-
return new Set;
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
function profileForEnvDir(api, tool, env) {
|
|
2314
|
+
async function profileForEnvDir(store, tool, env) {
|
|
2077
2315
|
const configuredDir = env[tool.envVar];
|
|
2078
2316
|
if (!configuredDir)
|
|
2079
2317
|
return null;
|
|
2080
2318
|
const normalized = normalizeDir(configuredDir);
|
|
2081
|
-
|
|
2082
|
-
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2083
|
-
} catch {
|
|
2084
|
-
return null;
|
|
2085
|
-
}
|
|
2319
|
+
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2086
2320
|
}
|
|
2087
2321
|
async function resolveAccountForAgent(agent, env = process.env) {
|
|
2088
2322
|
const override = envOverride(agent, env);
|
|
2089
2323
|
if (override)
|
|
2090
2324
|
return override;
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
api = await import("@hasna/accounts");
|
|
2094
|
-
} catch {
|
|
2095
|
-
return null;
|
|
2096
|
-
}
|
|
2097
|
-
const toolIds = knownToolIds(api);
|
|
2325
|
+
const store = resolveStore(env);
|
|
2326
|
+
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
2098
2327
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
let tool;
|
|
2102
|
-
try {
|
|
2103
|
-
tool = api.getTool(toolId);
|
|
2104
|
-
} catch {
|
|
2328
|
+
const tool = tools.get(toolId);
|
|
2329
|
+
if (!tool)
|
|
2105
2330
|
continue;
|
|
2106
|
-
|
|
2107
|
-
const envProfile = profileForEnvDir(api, tool, env);
|
|
2331
|
+
const envProfile = await profileForEnvDir(store, tool, env);
|
|
2108
2332
|
if (envProfile)
|
|
2109
2333
|
return fromProfile(envProfile, "env");
|
|
2110
|
-
|
|
2111
|
-
|
|
2334
|
+
const appliedName = appliedProfileName(toolId);
|
|
2335
|
+
if (appliedName) {
|
|
2336
|
+
const applied = await store.findProfile(appliedName, toolId);
|
|
2112
2337
|
if (applied)
|
|
2113
2338
|
return fromProfile(applied, "applied");
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
return fromProfile(current, "current");
|
|
2119
|
-
} catch {}
|
|
2339
|
+
}
|
|
2340
|
+
const current = await store.currentProfile(toolId);
|
|
2341
|
+
if (current)
|
|
2342
|
+
return fromProfile(current, "current");
|
|
2120
2343
|
}
|
|
2121
2344
|
return null;
|
|
2122
2345
|
}
|
|
@@ -3137,32 +3360,219 @@ function statSyncSafe(path) {
|
|
|
3137
3360
|
}
|
|
3138
3361
|
}
|
|
3139
3362
|
|
|
3363
|
+
// src/ingest/loops.ts
|
|
3364
|
+
init_database();
|
|
3365
|
+
init_pricing();
|
|
3366
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3367
|
+
import { homedir as homedir8 } from "os";
|
|
3368
|
+
import { join as join8 } from "path";
|
|
3369
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
3370
|
+
var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
|
|
3371
|
+
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
3372
|
+
function loopsDbPath() {
|
|
3373
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
|
|
3374
|
+
}
|
|
3375
|
+
function loopsModel() {
|
|
3376
|
+
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
3377
|
+
}
|
|
3378
|
+
function openLoopsDb(dbPath, verbose) {
|
|
3379
|
+
let lastError;
|
|
3380
|
+
for (const readonly of [true, false]) {
|
|
3381
|
+
let loopsDb = null;
|
|
3382
|
+
try {
|
|
3383
|
+
loopsDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
3384
|
+
loopsDb.prepare("PRAGMA schema_version").get();
|
|
3385
|
+
return loopsDb;
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
lastError = error;
|
|
3388
|
+
loopsDb?.close();
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (verbose) {
|
|
3392
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
3393
|
+
console.log("OpenLoops DB unreadable:", dbPath, message);
|
|
3394
|
+
}
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
function slug(value) {
|
|
3398
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
3399
|
+
}
|
|
3400
|
+
function parseTarget(raw) {
|
|
3401
|
+
if (!raw)
|
|
3402
|
+
return {};
|
|
3403
|
+
try {
|
|
3404
|
+
const parsed = JSON.parse(raw);
|
|
3405
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3406
|
+
} catch {
|
|
3407
|
+
return {};
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
function stringValue(value) {
|
|
3411
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3412
|
+
}
|
|
3413
|
+
function accountFields(row) {
|
|
3414
|
+
const target = parseTarget(row.target_json);
|
|
3415
|
+
const tool = stringValue(row.account_tool) || stringValue(target["account_tool"]) || stringValue(target["tool"]) || stringValue(target["agent"]);
|
|
3416
|
+
const profile = stringValue(row.account_profile) || stringValue(target["account_profile"]) || stringValue(target["profile"]) || stringValue(target["account"]);
|
|
3417
|
+
if (!tool || !profile) {
|
|
3418
|
+
return { account_key: "", account_tool: tool, account_name: profile, account_source: "" };
|
|
3419
|
+
}
|
|
3420
|
+
return {
|
|
3421
|
+
account_key: `${tool}:${profile}`,
|
|
3422
|
+
account_tool: tool,
|
|
3423
|
+
account_name: profile,
|
|
3424
|
+
account_source: "loops"
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
function readLoopGoalRuns(loopsDb) {
|
|
3428
|
+
return loopsDb.prepare(`
|
|
3429
|
+
SELECT
|
|
3430
|
+
gr.id as goal_run_id,
|
|
3431
|
+
gr.goal_id as goal_id,
|
|
3432
|
+
COALESCE(gr.loop_id, g.loop_id, lr.loop_id) as loop_id,
|
|
3433
|
+
COALESCE(gr.loop_run_id, g.loop_run_id, lr.id) as loop_run_id,
|
|
3434
|
+
COALESCE(l.name, lr.loop_name, 'unknown-loop') as loop_name,
|
|
3435
|
+
l.labels_json as labels_json,
|
|
3436
|
+
l.target_json as target_json,
|
|
3437
|
+
g.objective as objective,
|
|
3438
|
+
gr.phase as phase,
|
|
3439
|
+
gr.status as status,
|
|
3440
|
+
COALESCE(gr.tokens_used, 0) as tokens_used,
|
|
3441
|
+
gr.created_at as created_at,
|
|
3442
|
+
gr.updated_at as updated_at,
|
|
3443
|
+
lr.started_at as loop_run_started_at,
|
|
3444
|
+
wsr.account_profile as account_profile,
|
|
3445
|
+
wsr.account_tool as account_tool
|
|
3446
|
+
FROM goal_runs gr
|
|
3447
|
+
LEFT JOIN goals g ON g.id = gr.goal_id
|
|
3448
|
+
LEFT JOIN loop_runs lr ON lr.id = COALESCE(gr.loop_run_id, g.loop_run_id)
|
|
3449
|
+
LEFT JOIN loops l ON l.id = COALESCE(gr.loop_id, g.loop_id, lr.loop_id)
|
|
3450
|
+
LEFT JOIN (
|
|
3451
|
+
SELECT goal_run_id, MIN(account_profile) as account_profile, MIN(account_tool) as account_tool
|
|
3452
|
+
FROM workflow_step_runs
|
|
3453
|
+
WHERE goal_run_id IS NOT NULL
|
|
3454
|
+
GROUP BY goal_run_id
|
|
3455
|
+
) wsr ON wsr.goal_run_id = gr.id
|
|
3456
|
+
WHERE COALESCE(gr.tokens_used, 0) > 0
|
|
3457
|
+
ORDER BY gr.created_at ASC, gr.id ASC
|
|
3458
|
+
`).all();
|
|
3459
|
+
}
|
|
3460
|
+
async function ingestLoops(db, verbose = false) {
|
|
3461
|
+
const dbPath = loopsDbPath();
|
|
3462
|
+
if (!existsSync8(dbPath)) {
|
|
3463
|
+
if (verbose)
|
|
3464
|
+
console.log("OpenLoops DB not found:", dbPath);
|
|
3465
|
+
return { sessions: 0, requests: 0 };
|
|
3466
|
+
}
|
|
3467
|
+
const model = loopsModel();
|
|
3468
|
+
const machineId = getMachineId();
|
|
3469
|
+
let loopsDb = null;
|
|
3470
|
+
let requests = 0;
|
|
3471
|
+
const sessions = new Set;
|
|
3472
|
+
try {
|
|
3473
|
+
loopsDb = openLoopsDb(dbPath, verbose);
|
|
3474
|
+
if (!loopsDb)
|
|
3475
|
+
return { sessions: 0, requests: 0 };
|
|
3476
|
+
for (const row of readLoopGoalRuns(loopsDb)) {
|
|
3477
|
+
const loopName = row.loop_name || "unknown-loop";
|
|
3478
|
+
const costCenterId = `loop:${slug(loopName)}`;
|
|
3479
|
+
const sessionId = `loops-${row.loop_run_id ?? row.loop_id ?? row.goal_id}`;
|
|
3480
|
+
const requestId = `loops-${row.goal_run_id}`;
|
|
3481
|
+
const timestamp = row.updated_at ?? row.created_at ?? row.loop_run_started_at ?? new Date().toISOString();
|
|
3482
|
+
const stateValue = `${LOOPS_INGEST_VERSION}:${row.updated_at ?? ""}:${row.tokens_used}:${model}:${costCenterId}`;
|
|
3483
|
+
if (getIngestState(db, "loops", row.goal_run_id) === stateValue)
|
|
3484
|
+
continue;
|
|
3485
|
+
upsertCostCenter(db, {
|
|
3486
|
+
id: costCenterId,
|
|
3487
|
+
kind: "loop",
|
|
3488
|
+
name: loopName,
|
|
3489
|
+
repo_path: null,
|
|
3490
|
+
labels_json: row.labels_json ?? "{}",
|
|
3491
|
+
created_at: row.created_at ?? timestamp
|
|
3492
|
+
});
|
|
3493
|
+
const account = accountFields(row);
|
|
3494
|
+
upsertSession(db, {
|
|
3495
|
+
id: sessionId,
|
|
3496
|
+
agent: "loop",
|
|
3497
|
+
project_path: "",
|
|
3498
|
+
project_name: loopName,
|
|
3499
|
+
started_at: row.loop_run_started_at ?? row.created_at ?? timestamp,
|
|
3500
|
+
ended_at: row.updated_at ?? null,
|
|
3501
|
+
total_cost_usd: 0,
|
|
3502
|
+
total_tokens: 0,
|
|
3503
|
+
request_count: 0,
|
|
3504
|
+
machine_id: machineId,
|
|
3505
|
+
cost_center_id: costCenterId,
|
|
3506
|
+
attribution_tag: "loops-orchestration",
|
|
3507
|
+
...account
|
|
3508
|
+
});
|
|
3509
|
+
const inputTokens = row.tokens_used;
|
|
3510
|
+
const outputTokens = 0;
|
|
3511
|
+
upsertRequest(db, {
|
|
3512
|
+
id: requestId,
|
|
3513
|
+
agent: "loop",
|
|
3514
|
+
session_id: sessionId,
|
|
3515
|
+
model,
|
|
3516
|
+
input_tokens: inputTokens,
|
|
3517
|
+
output_tokens: outputTokens,
|
|
3518
|
+
cache_read_tokens: 0,
|
|
3519
|
+
cache_create_tokens: 0,
|
|
3520
|
+
cost_usd: computeCostFromDb(db, model, inputTokens, outputTokens),
|
|
3521
|
+
cost_basis: "estimated",
|
|
3522
|
+
duration_ms: 0,
|
|
3523
|
+
timestamp,
|
|
3524
|
+
source_request_id: requestId,
|
|
3525
|
+
machine_id: machineId,
|
|
3526
|
+
cost_center_id: costCenterId,
|
|
3527
|
+
attribution_tag: "loops-orchestration",
|
|
3528
|
+
...account
|
|
3529
|
+
});
|
|
3530
|
+
sessions.add(sessionId);
|
|
3531
|
+
requests++;
|
|
3532
|
+
setIngestState(db, "loops", row.goal_run_id, stateValue);
|
|
3533
|
+
if (verbose)
|
|
3534
|
+
console.log(`OpenLoops ${loopName}: ${row.tokens_used} orchestration tokens`);
|
|
3535
|
+
}
|
|
3536
|
+
} catch (error) {
|
|
3537
|
+
if (verbose) {
|
|
3538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3539
|
+
console.log("OpenLoops ingest skipped:", message);
|
|
3540
|
+
}
|
|
3541
|
+
return { sessions: 0, requests: 0 };
|
|
3542
|
+
} finally {
|
|
3543
|
+
loopsDb?.close();
|
|
3544
|
+
}
|
|
3545
|
+
for (const sessionId of sessions)
|
|
3546
|
+
rollupSession(db, sessionId);
|
|
3547
|
+
return { sessions: sessions.size, requests };
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3140
3550
|
// src/ingest/claude-quota.ts
|
|
3141
3551
|
init_database();
|
|
3142
|
-
import { existsSync as
|
|
3552
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
3143
3553
|
|
|
3144
3554
|
// src/lib/paths.ts
|
|
3145
|
-
import { homedir as
|
|
3146
|
-
import { join as
|
|
3555
|
+
import { homedir as homedir9 } from "os";
|
|
3556
|
+
import { join as join9 } from "path";
|
|
3147
3557
|
function getHomeDir() {
|
|
3148
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
3558
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
3149
3559
|
}
|
|
3150
3560
|
function agentPaths() {
|
|
3151
3561
|
const home = getHomeDir();
|
|
3152
3562
|
return {
|
|
3153
|
-
claudeProjects:
|
|
3154
|
-
claudeCredentials:
|
|
3155
|
-
takumiProjects:
|
|
3156
|
-
codexDir:
|
|
3157
|
-
codexDb:
|
|
3158
|
-
codexAuth:
|
|
3159
|
-
codexConfig:
|
|
3160
|
-
geminiTmp:
|
|
3161
|
-
geminiHistory:
|
|
3162
|
-
opencodeMessages:
|
|
3163
|
-
piSessions:
|
|
3164
|
-
hermesDir:
|
|
3165
|
-
hermesDb:
|
|
3563
|
+
claudeProjects: join9(home, ".claude", "projects"),
|
|
3564
|
+
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
3565
|
+
takumiProjects: join9(home, ".takumi", "projects"),
|
|
3566
|
+
codexDir: join9(home, ".codex"),
|
|
3567
|
+
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
3568
|
+
codexAuth: join9(home, ".codex", "auth.json"),
|
|
3569
|
+
codexConfig: join9(home, ".codex", "config.toml"),
|
|
3570
|
+
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
3571
|
+
geminiHistory: join9(home, ".gemini", "history"),
|
|
3572
|
+
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
3573
|
+
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
3574
|
+
hermesDir: join9(home, ".hermes"),
|
|
3575
|
+
hermesDb: join9(home, ".hermes", "state.db")
|
|
3166
3576
|
};
|
|
3167
3577
|
}
|
|
3168
3578
|
|
|
@@ -3174,7 +3584,7 @@ function readClaudeToken() {
|
|
|
3174
3584
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
3175
3585
|
if (fromEnv)
|
|
3176
3586
|
return { token: fromEnv };
|
|
3177
|
-
if (!
|
|
3587
|
+
if (!existsSync9(CREDENTIALS_PATH))
|
|
3178
3588
|
return null;
|
|
3179
3589
|
try {
|
|
3180
3590
|
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
@@ -3313,14 +3723,14 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
3313
3723
|
|
|
3314
3724
|
// src/ingest/codex-quota.ts
|
|
3315
3725
|
init_database();
|
|
3316
|
-
import { existsSync as
|
|
3726
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
3317
3727
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
3318
3728
|
function readCodexAuth() {
|
|
3319
3729
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
3320
3730
|
if (fromEnv)
|
|
3321
3731
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
3322
3732
|
const authPath = agentPaths().codexAuth;
|
|
3323
|
-
if (!
|
|
3733
|
+
if (!existsSync10(authPath))
|
|
3324
3734
|
return null;
|
|
3325
3735
|
try {
|
|
3326
3736
|
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
@@ -3453,7 +3863,7 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
3453
3863
|
// src/lib/sync-all.ts
|
|
3454
3864
|
init_database();
|
|
3455
3865
|
async function syncAll(db, opts = {}) {
|
|
3456
|
-
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
3866
|
+
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes || opts.loops);
|
|
3457
3867
|
const all = !anySpecific;
|
|
3458
3868
|
const result = { deduped: 0 };
|
|
3459
3869
|
if (all || opts.claude) {
|
|
@@ -3476,6 +3886,8 @@ async function syncAll(db, opts = {}) {
|
|
|
3476
3886
|
result.pi = await ingestPi(db, opts.verbose);
|
|
3477
3887
|
if (all || opts.hermes)
|
|
3478
3888
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
3889
|
+
if (all || opts.loops)
|
|
3890
|
+
result.loops = await ingestLoops(db, opts.verbose);
|
|
3479
3891
|
if (opts.dedupe !== false)
|
|
3480
3892
|
result.deduped = dedupeRequests(db);
|
|
3481
3893
|
return result;
|
|
@@ -4789,11 +5201,18 @@ class LocalStore {
|
|
|
4789
5201
|
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4790
5202
|
}
|
|
4791
5203
|
async projectBreakdown(query = {}) {
|
|
4792
|
-
return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
5204
|
+
return query.since ? queryProjectBreakdownSince(this.db(), query.since, query.machine) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4793
5205
|
}
|
|
4794
5206
|
async accountBreakdown(query = {}) {
|
|
4795
5207
|
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4796
5208
|
}
|
|
5209
|
+
async costCenterBreakdown(query = {}) {
|
|
5210
|
+
return queryCostCenterBreakdown(this.db(), query.period ?? "all", {
|
|
5211
|
+
kind: query.kind,
|
|
5212
|
+
machine: query.machine,
|
|
5213
|
+
since: query.since
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
4797
5216
|
async accounts(period) {
|
|
4798
5217
|
return queryAccountBreakdown(this.db(), period);
|
|
4799
5218
|
}
|
|
@@ -4978,11 +5397,19 @@ class ApiStore {
|
|
|
4978
5397
|
return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
|
|
4979
5398
|
}
|
|
4980
5399
|
async projectBreakdown(query = {}) {
|
|
4981
|
-
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
|
|
5400
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since, machine: query.machine }));
|
|
4982
5401
|
}
|
|
4983
5402
|
async accountBreakdown(query = {}) {
|
|
4984
5403
|
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
4985
5404
|
}
|
|
5405
|
+
async costCenterBreakdown(query = {}) {
|
|
5406
|
+
const rows = await cloudListItems(this.cloud, "breakdown", q({
|
|
5407
|
+
by: query.kind ?? "cost-center",
|
|
5408
|
+
period: query.period,
|
|
5409
|
+
since: query.since
|
|
5410
|
+
}));
|
|
5411
|
+
return rows.filter((row) => Boolean(row?.cost_center_id));
|
|
5412
|
+
}
|
|
4986
5413
|
async accounts(period) {
|
|
4987
5414
|
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
4988
5415
|
}
|
|
@@ -5056,6 +5483,7 @@ class ApiStore {
|
|
|
5056
5483
|
const created = await this.cloud.client.create("budgets", {
|
|
5057
5484
|
project_path: input.project_path,
|
|
5058
5485
|
...input.agent ? { agent: input.agent } : {},
|
|
5486
|
+
...input.cost_center_id ? { cost_center_id: input.cost_center_id } : {},
|
|
5059
5487
|
period: input.period,
|
|
5060
5488
|
limit_usd: input.limit_usd,
|
|
5061
5489
|
alert_at_percent: input.alert_at_percent
|
|
@@ -5146,6 +5574,7 @@ function buildServer() {
|
|
|
5146
5574
|
name: MCP_NAME,
|
|
5147
5575
|
version: packageMetadata.version
|
|
5148
5576
|
});
|
|
5577
|
+
const SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
5149
5578
|
const TOOL_NAMES = [
|
|
5150
5579
|
"get_cost_summary",
|
|
5151
5580
|
"get_sessions",
|
|
@@ -5154,6 +5583,7 @@ function buildServer() {
|
|
|
5154
5583
|
"get_project_breakdown",
|
|
5155
5584
|
"get_agent_breakdown",
|
|
5156
5585
|
"get_account_breakdown",
|
|
5586
|
+
"get_cost_center_breakdown",
|
|
5157
5587
|
"get_budget_status",
|
|
5158
5588
|
"set_budget",
|
|
5159
5589
|
"remove_budget",
|
|
@@ -5186,30 +5616,31 @@ function buildServer() {
|
|
|
5186
5616
|
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
5187
5617
|
get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
|
|
5188
5618
|
get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
|
|
5189
|
-
list_machines: "
|
|
5190
|
-
get_model_breakdown: "
|
|
5191
|
-
get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
|
|
5619
|
+
list_machines: "limit(20), verbose?, json? -> machine_id, sessions, requests, cost, last_active",
|
|
5620
|
+
get_model_breakdown: "limit(20), verbose?, json? -> model, requests, tokens, cost",
|
|
5621
|
+
get_project_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> project_name, sessions, tokens, cost",
|
|
5192
5622
|
get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5193
|
-
get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5194
|
-
|
|
5195
|
-
|
|
5623
|
+
get_account_breakdown: "period?(today|week|month|year|all), limit(20), verbose?, json? -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5624
|
+
get_cost_center_breakdown: "period?(today|week|month|year|all), kind?(loop|app|repo|service|team) -> cost center, sessions, requests, tokens, cost",
|
|
5625
|
+
get_budget_status: "limit(20), verbose?, json? -> budget limits, current spend, percent_used, is_over_alert",
|
|
5626
|
+
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, cost_center_id?, alert_at_percent? -> create budget",
|
|
5196
5627
|
remove_budget: "id -> delete budget",
|
|
5197
|
-
get_pricing: "
|
|
5628
|
+
get_pricing: "limit(20), verbose?, json? -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
5198
5629
|
set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
|
|
5199
5630
|
remove_pricing: "model -> delete pricing row",
|
|
5200
5631
|
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
5201
5632
|
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
5202
|
-
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
5203
|
-
get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
|
|
5204
|
-
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
|
|
5633
|
+
get_session_detail: "session_id(prefix ok), limit(20), verbose? -> per-request breakdown with model, tokens, cost",
|
|
5634
|
+
get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> usage snapshots and all-machine summary`,
|
|
5635
|
+
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}), limit(20), json? -> subscription/API-equivalent savings`,
|
|
5205
5636
|
estimate_cost: "model, input_tokens?, output_tokens? -> pre-flight cost estimate priced from the store pricing table",
|
|
5206
5637
|
list_subscriptions: "no params -> configured subscription plans and included usage",
|
|
5207
|
-
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
|
|
5638
|
+
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}), json? -> create/update subscription plan`,
|
|
5208
5639
|
remove_subscription: "id -> delete subscription plan",
|
|
5209
|
-
sync: `sources(
|
|
5640
|
+
sync: `sources(${SYNC_SOURCES.join("|")}), json? -> ingest latest cost data`,
|
|
5210
5641
|
search_tools: "query substring -> tool name list",
|
|
5211
5642
|
describe_tools: "names[] -> one-line parameter hints",
|
|
5212
|
-
get_goals: "
|
|
5643
|
+
get_goals: "limit(20), verbose?, json? -> goal progress summary",
|
|
5213
5644
|
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
5214
5645
|
remove_goal: "id -> delete goal",
|
|
5215
5646
|
register_agent: "name, session_id? -> register agent session",
|
|
@@ -5220,6 +5651,20 @@ function buildServer() {
|
|
|
5220
5651
|
};
|
|
5221
5652
|
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
5222
5653
|
const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
5654
|
+
const DEFAULT_MCP_ROW_LIMIT = 20;
|
|
5655
|
+
function clampLimit(limit, fallback = DEFAULT_MCP_ROW_LIMIT, max = 100) {
|
|
5656
|
+
if (limit == null)
|
|
5657
|
+
return fallback;
|
|
5658
|
+
if (!Number.isFinite(limit))
|
|
5659
|
+
return fallback;
|
|
5660
|
+
return Math.max(1, Math.min(max, Math.floor(limit)));
|
|
5661
|
+
}
|
|
5662
|
+
function rowLimit(limit, verbose) {
|
|
5663
|
+
return verbose ? Number.POSITIVE_INFINITY : clampLimit(limit);
|
|
5664
|
+
}
|
|
5665
|
+
function hiddenRowsHint(total, shown, noun) {
|
|
5666
|
+
return total > shown ? `... ${total - shown} more ${noun} hidden; call with limit, verbose=true, or json=true.` : undefined;
|
|
5667
|
+
}
|
|
5223
5668
|
function fmtSession(s) {
|
|
5224
5669
|
const id = String(s["id"] ?? "").slice(0, 8);
|
|
5225
5670
|
const agent = String(s["agent"] ?? "");
|
|
@@ -5228,6 +5673,26 @@ function buildServer() {
|
|
|
5228
5673
|
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
5229
5674
|
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
5230
5675
|
}
|
|
5676
|
+
function compactPrimitive(value) {
|
|
5677
|
+
if (value == null)
|
|
5678
|
+
return "none";
|
|
5679
|
+
if (typeof value === "number")
|
|
5680
|
+
return Number.isFinite(value) ? value.toLocaleString("en-US") : String(value);
|
|
5681
|
+
if (typeof value === "boolean")
|
|
5682
|
+
return value ? "yes" : "no";
|
|
5683
|
+
return String(value);
|
|
5684
|
+
}
|
|
5685
|
+
function compactObject(value) {
|
|
5686
|
+
if (value == null)
|
|
5687
|
+
return "none";
|
|
5688
|
+
if (Array.isArray(value))
|
|
5689
|
+
return `${value.length} rows`;
|
|
5690
|
+
if (typeof value !== "object")
|
|
5691
|
+
return compactPrimitive(value);
|
|
5692
|
+
const entries = Object.entries(value);
|
|
5693
|
+
const primitives = entries.filter(([, entry]) => entry == null || ["string", "number", "boolean"].includes(typeof entry)).slice(0, 6).map(([key, entry]) => `${key}=${compactPrimitive(entry)}`);
|
|
5694
|
+
return primitives.length ? primitives.join(", ") : `${entries.length} fields`;
|
|
5695
|
+
}
|
|
5231
5696
|
function text(text2) {
|
|
5232
5697
|
return { content: [{ type: "text", text: text2 }] };
|
|
5233
5698
|
}
|
|
@@ -5282,22 +5747,34 @@ function buildServer() {
|
|
|
5282
5747
|
return text(lines.join(`
|
|
5283
5748
|
`));
|
|
5284
5749
|
});
|
|
5285
|
-
server.tool("get_model_breakdown", "Cost per model.
|
|
5750
|
+
server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5286
5751
|
const rows = await store.modelBreakdown();
|
|
5752
|
+
if (json)
|
|
5753
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5754
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5287
5755
|
const lines = ["model agent reqs tokens cost"];
|
|
5288
|
-
for (const row of
|
|
5756
|
+
for (const row of visibleRows) {
|
|
5289
5757
|
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5290
5758
|
}
|
|
5759
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "models");
|
|
5760
|
+
if (hint)
|
|
5761
|
+
lines.push(hint);
|
|
5291
5762
|
return text(lines.join(`
|
|
5292
5763
|
`));
|
|
5293
5764
|
});
|
|
5294
|
-
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5765
|
+
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5295
5766
|
const rows = await store.projectBreakdown({ period: period ?? "all" });
|
|
5767
|
+
if (json)
|
|
5768
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5769
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5296
5770
|
const lines = ["project sessions tokens cost"];
|
|
5297
|
-
for (const row of
|
|
5771
|
+
for (const row of visibleRows) {
|
|
5298
5772
|
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
5299
5773
|
lines.push(`${name.padEnd(21)}${String(row["sessions"]).padEnd(9)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5300
5774
|
}
|
|
5775
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "projects");
|
|
5776
|
+
if (hint)
|
|
5777
|
+
lines.push(hint);
|
|
5301
5778
|
return text(lines.join(`
|
|
5302
5779
|
`));
|
|
5303
5780
|
});
|
|
@@ -5312,29 +5789,55 @@ function buildServer() {
|
|
|
5312
5789
|
return text(lines.join(`
|
|
5313
5790
|
`));
|
|
5314
5791
|
});
|
|
5315
|
-
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5792
|
+
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5316
5793
|
const rows = await store.accountBreakdown({ period: period ?? "all" });
|
|
5794
|
+
if (json)
|
|
5795
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5317
5796
|
if (rows.length === 0)
|
|
5318
5797
|
return text("No account-attributed sessions yet.");
|
|
5798
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5319
5799
|
const lines = ["account agent sessions requests tokens api_eq billable included"];
|
|
5320
|
-
for (const row of
|
|
5800
|
+
for (const row of visibleRows) {
|
|
5321
5801
|
const label = String(row["account_email"] || row["account_name"] || row["account_key"] || "\u2014").slice(0, 20);
|
|
5322
5802
|
lines.push(`${label.padEnd(21)}` + `${String(row["account_tool"] ?? "").slice(0, 10).padEnd(11)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["api_equivalent_usd"] ?? row["cost_usd"])).padEnd(10)}` + `${fmtUsd(Number(row["billable_usd"] ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row["subscription_included_usd"] ?? 0))}`);
|
|
5323
5803
|
}
|
|
5804
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "accounts");
|
|
5805
|
+
if (hint)
|
|
5806
|
+
lines.push(hint);
|
|
5807
|
+
return text(lines.join(`
|
|
5808
|
+
`));
|
|
5809
|
+
});
|
|
5810
|
+
server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
|
|
5811
|
+
period: z.enum(["today", "week", "month", "year", "all"]).optional(),
|
|
5812
|
+
kind: z.enum(["loop", "app", "repo", "service", "team"]).optional()
|
|
5813
|
+
}, async ({ period, kind }) => {
|
|
5814
|
+
const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
|
|
5815
|
+
if (rows.length === 0)
|
|
5816
|
+
return text("No cost-center usage yet.");
|
|
5817
|
+
const lines = ["kind cost_center sessions requests tokens cost"];
|
|
5818
|
+
for (const row of rows) {
|
|
5819
|
+
lines.push(`${String(row["kind"]).slice(0, 8).padEnd(9)}` + `${String(row["name"] || row["cost_center_id"] || "\u2014").slice(0, 20).padEnd(21)}` + `${String(row["sessions"]).padEnd(9)}` + `${String(row["requests"]).padEnd(9)}` + `${fmtTok(Number(row["total_tokens"])).padEnd(9)}` + `${fmtUsd(Number(row["cost_usd"]))}`);
|
|
5820
|
+
}
|
|
5324
5821
|
return text(lines.join(`
|
|
5325
5822
|
`));
|
|
5326
5823
|
});
|
|
5327
|
-
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags.
|
|
5824
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5328
5825
|
const budgets = await store.listBudgets();
|
|
5826
|
+
if (json)
|
|
5827
|
+
return text(JSON.stringify(budgets, null, 2));
|
|
5329
5828
|
if (budgets.length === 0)
|
|
5330
5829
|
return text("No budgets set.");
|
|
5830
|
+
const visibleBudgets = budgets.slice(0, rowLimit(limit, verbose));
|
|
5331
5831
|
const lines = ["scope period spent limit used% status"];
|
|
5332
|
-
for (const budget of
|
|
5333
|
-
const scope = String(budget["project_path"] ?? "global").slice(0, 20);
|
|
5832
|
+
for (const budget of visibleBudgets) {
|
|
5833
|
+
const scope = String(budget["cost_center_id"] ?? budget["project_path"] ?? "global").slice(0, 20);
|
|
5334
5834
|
const pct = Number(budget["percent_used"]).toFixed(1);
|
|
5335
5835
|
const status = budget["is_over_limit"] ? "OVER" : budget["is_over_alert"] ? "ALERT" : "OK";
|
|
5336
5836
|
lines.push(`${scope.padEnd(21)}${String(budget["period"]).padEnd(9)}${fmtUsd(Number(budget["current_spend_usd"])).padEnd(11)}${fmtUsd(Number(budget["limit_usd"])).padEnd(11)}${pct}%`.padEnd(49) + ` ${status}`);
|
|
5337
5837
|
}
|
|
5838
|
+
const hint = hiddenRowsHint(budgets.length, visibleBudgets.length, "budgets");
|
|
5839
|
+
if (hint)
|
|
5840
|
+
lines.push(hint);
|
|
5338
5841
|
return text(lines.join(`
|
|
5339
5842
|
`));
|
|
5340
5843
|
});
|
|
@@ -5343,11 +5846,13 @@ function buildServer() {
|
|
|
5343
5846
|
limit_usd: z.number().positive(),
|
|
5344
5847
|
project_path: z.string().optional(),
|
|
5345
5848
|
agent: z.enum(AGENTS).optional(),
|
|
5849
|
+
cost_center_id: z.string().optional(),
|
|
5346
5850
|
alert_at_percent: z.number().positive().max(100).optional()
|
|
5347
|
-
}, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
|
|
5851
|
+
}, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
|
|
5348
5852
|
const id = await store.setBudget({
|
|
5349
5853
|
project_path: project_path ?? null,
|
|
5350
5854
|
agent: agent ?? null,
|
|
5855
|
+
cost_center_id: cost_center_id ?? null,
|
|
5351
5856
|
period,
|
|
5352
5857
|
limit_usd,
|
|
5353
5858
|
alert_at_percent: alert_at_percent ?? 80
|
|
@@ -5358,12 +5863,18 @@ function buildServer() {
|
|
|
5358
5863
|
await store.removeBudget(id);
|
|
5359
5864
|
return text("Budget removed.");
|
|
5360
5865
|
});
|
|
5361
|
-
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
|
|
5866
|
+
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5362
5867
|
const rows = await store.listPricing();
|
|
5868
|
+
if (json)
|
|
5869
|
+
return text(JSON.stringify(rows, null, 2));
|
|
5870
|
+
const visibleRows = rows.slice(0, rowLimit(limit, verbose));
|
|
5363
5871
|
const lines = ["model input output cache-r cache-w cache-1h storage-h"];
|
|
5364
|
-
for (const row of
|
|
5872
|
+
for (const row of visibleRows) {
|
|
5365
5873
|
lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
|
|
5366
5874
|
}
|
|
5875
|
+
const hint = hiddenRowsHint(rows.length, visibleRows.length, "pricing rows");
|
|
5876
|
+
if (hint)
|
|
5877
|
+
lines.push(hint);
|
|
5367
5878
|
return text(lines.join(`
|
|
5368
5879
|
`));
|
|
5369
5880
|
});
|
|
@@ -5428,11 +5939,14 @@ function buildServer() {
|
|
|
5428
5939
|
return text(lines.join(`
|
|
5429
5940
|
`));
|
|
5430
5941
|
});
|
|
5431
|
-
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
5942
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z.string(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional() }, async ({ session_id, limit, verbose }) => {
|
|
5432
5943
|
const detail = await store.sessionDetail(session_id);
|
|
5433
5944
|
if (!detail)
|
|
5434
5945
|
return textError(`Session not found: ${session_id}`);
|
|
5435
|
-
const { session, requests } = detail;
|
|
5946
|
+
const { session, requests: allRequests } = detail;
|
|
5947
|
+
const requestLimit = clampLimit(limit, verbose ? 50 : DEFAULT_MCP_ROW_LIMIT);
|
|
5948
|
+
const requestCount = { count: allRequests.length };
|
|
5949
|
+
const requests = allRequests.slice(0, requestLimit);
|
|
5436
5950
|
const lines = [
|
|
5437
5951
|
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
5438
5952
|
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
@@ -5440,27 +5954,85 @@ function buildServer() {
|
|
|
5440
5954
|
"",
|
|
5441
5955
|
"time model input output cache-r cache-5m cache-1h cost"
|
|
5442
5956
|
];
|
|
5443
|
-
for (const request of requests
|
|
5957
|
+
for (const request of requests) {
|
|
5444
5958
|
lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
|
|
5445
5959
|
}
|
|
5960
|
+
if (requestCount.count > requests.length) {
|
|
5961
|
+
lines.push(`... ${requestCount.count - requests.length} more requests hidden; call with limit or verbose=true for more.`);
|
|
5962
|
+
}
|
|
5446
5963
|
return text(lines.join(`
|
|
5447
5964
|
`));
|
|
5448
5965
|
});
|
|
5449
|
-
server.tool("sync", `Ingest new cost data. sources:
|
|
5966
|
+
server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z.enum([...SYNC_SOURCES]).optional(), json: z.boolean().optional() }, async ({ sources, json }) => {
|
|
5450
5967
|
if (isCloudStore()) {
|
|
5451
5968
|
return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
|
|
5452
5969
|
}
|
|
5453
5970
|
const selected = sources ?? "all";
|
|
5454
5971
|
const opts = selected === "all" ? {} : { [selected]: true };
|
|
5455
5972
|
const result = await syncAll(localDb(), opts);
|
|
5456
|
-
|
|
5973
|
+
if (json)
|
|
5974
|
+
return text(JSON.stringify(result, null, 2));
|
|
5975
|
+
const lines = [
|
|
5976
|
+
`sync: ${selected}`,
|
|
5977
|
+
`deduped: ${result.deduped}`
|
|
5978
|
+
];
|
|
5979
|
+
for (const [source, value] of Object.entries(result)) {
|
|
5980
|
+
if (source === "deduped")
|
|
5981
|
+
continue;
|
|
5982
|
+
if (value == null)
|
|
5983
|
+
continue;
|
|
5984
|
+
lines.push(`${source}: ${compactObject(value)}`);
|
|
5985
|
+
}
|
|
5986
|
+
lines.push("Use json=true for the full sync result.");
|
|
5987
|
+
return text(lines.join(`
|
|
5988
|
+
`));
|
|
5457
5989
|
});
|
|
5458
|
-
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
5990
|
+
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
5459
5991
|
const p = period ?? "month";
|
|
5460
|
-
|
|
5992
|
+
const { snapshots: snaps, summary } = await store.usage(p, agent);
|
|
5993
|
+
if (json)
|
|
5994
|
+
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
5995
|
+
const snapshotLimit = clampLimit(limit);
|
|
5996
|
+
const lines = [
|
|
5997
|
+
`period: ${p}${agent ? ` agent: ${agent}` : ""}`,
|
|
5998
|
+
`fleet: ${fmtUsd(summary.total_usd)} sessions: ${summary.sessions} requests: ${summary.requests.toLocaleString()} tokens: ${fmtTok(summary.tokens)}`,
|
|
5999
|
+
`snapshots: ${snaps.length}${snaps.length > snapshotLimit ? ` (showing ${snapshotLimit})` : ""}`
|
|
6000
|
+
];
|
|
6001
|
+
if (snaps.length > 0) {
|
|
6002
|
+
lines.push("", "date agent metric value unit machine");
|
|
6003
|
+
for (const snapshot of snaps.slice(0, snapshotLimit)) {
|
|
6004
|
+
lines.push(`${snapshot.date.padEnd(12)}` + `${snapshot.agent.slice(0, 10).padEnd(11)}` + `${snapshot.metric.slice(0, 27).padEnd(28)}` + `${compactPrimitive(snapshot.value).padEnd(13)}` + `${(snapshot.unit ?? "").slice(0, 8).padEnd(9)}` + `${snapshot.machine_id ?? "\u2014"}`);
|
|
6005
|
+
}
|
|
6006
|
+
if (snaps.length > snapshotLimit)
|
|
6007
|
+
lines.push(`... ${snaps.length - snapshotLimit} more snapshots hidden; call with limit or json=true.`);
|
|
6008
|
+
}
|
|
6009
|
+
return text(lines.join(`
|
|
6010
|
+
`));
|
|
5461
6011
|
});
|
|
5462
|
-
server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
5463
|
-
|
|
6012
|
+
server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
6013
|
+
const savings = await store.savings(period ?? "month", agent);
|
|
6014
|
+
if (json)
|
|
6015
|
+
return text(JSON.stringify(savings, null, 2));
|
|
6016
|
+
const lines = [
|
|
6017
|
+
`period: ${savings.period}${agent ? ` agent: ${agent}` : ""}`,
|
|
6018
|
+
`api_equivalent: ${fmtUsd(savings.api_equivalent_usd)}`,
|
|
6019
|
+
`subscription_fee: ${fmtUsd(savings.subscription_fee_usd)}`,
|
|
6020
|
+
`included_consumed: ${fmtUsd(savings.included_consumed_usd)}`,
|
|
6021
|
+
`on_demand: ${fmtUsd(savings.on_demand_usd)}`,
|
|
6022
|
+
`saved: ${fmtUsd(savings.saved_usd)}`
|
|
6023
|
+
];
|
|
6024
|
+
const rows = Object.entries(savings.by_agent ?? {});
|
|
6025
|
+
if (rows.length > 0) {
|
|
6026
|
+
const agentLimit = clampLimit(limit);
|
|
6027
|
+
lines.push("", `by_agent: ${rows.length}${rows.length > agentLimit ? ` (showing ${agentLimit})` : ""}`, "agent api_eq sub_fee included on_demand saved");
|
|
6028
|
+
for (const [agentName, row] of rows.slice(0, agentLimit)) {
|
|
6029
|
+
lines.push(`${agentName.slice(0, 10).padEnd(11)}` + `${fmtUsd(Number(row.api_equivalent_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.subscription_fee_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.included_consumed_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.on_demand_usd ?? 0)).padEnd(10)}` + `${fmtUsd(Number(row.saved_usd ?? 0))}`);
|
|
6030
|
+
}
|
|
6031
|
+
if (rows.length > agentLimit)
|
|
6032
|
+
lines.push(`... ${rows.length - agentLimit} more agents hidden; call with limit or json=true.`);
|
|
6033
|
+
}
|
|
6034
|
+
return text(lines.join(`
|
|
6035
|
+
`));
|
|
5464
6036
|
});
|
|
5465
6037
|
server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
|
|
5466
6038
|
const rows = await store.listSubscriptions();
|
|
@@ -5482,7 +6054,8 @@ function buildServer() {
|
|
|
5482
6054
|
included_usage_usd: z.number().nonnegative().optional(),
|
|
5483
6055
|
billing_cycle_start: z.string().optional(),
|
|
5484
6056
|
reset_policy: z.string().optional(),
|
|
5485
|
-
active: z.boolean().optional()
|
|
6057
|
+
active: z.boolean().optional(),
|
|
6058
|
+
json: z.boolean().optional()
|
|
5486
6059
|
}, async (input) => {
|
|
5487
6060
|
const row = await store.setSubscription({
|
|
5488
6061
|
id: input.id,
|
|
@@ -5495,7 +6068,14 @@ function buildServer() {
|
|
|
5495
6068
|
reset_policy: input.reset_policy ?? "monthly",
|
|
5496
6069
|
active: input.active === false ? 0 : 1
|
|
5497
6070
|
});
|
|
5498
|
-
|
|
6071
|
+
if (input.json)
|
|
6072
|
+
return text(JSON.stringify(row, null, 2));
|
|
6073
|
+
return text(`Subscription set: ${row.id}
|
|
6074
|
+
provider: ${row.provider}
|
|
6075
|
+
plan: ${row.plan}
|
|
6076
|
+
agent: ${row.agent ?? "all"}
|
|
6077
|
+
fee: ${fmtUsd(row.monthly_fee_usd)}
|
|
6078
|
+
included: ${fmtUsd(row.included_usage_usd)}`);
|
|
5499
6079
|
});
|
|
5500
6080
|
server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
|
|
5501
6081
|
await store.removeSubscription(id);
|
|
@@ -5505,17 +6085,23 @@ function buildServer() {
|
|
|
5505
6085
|
const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
|
|
5506
6086
|
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
5507
6087
|
});
|
|
5508
|
-
server.tool("get_goals", "All spending goals with current progress.
|
|
6088
|
+
server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5509
6089
|
const goals = await store.listGoals();
|
|
6090
|
+
if (json)
|
|
6091
|
+
return text(JSON.stringify(goals, null, 2));
|
|
5510
6092
|
if (goals.length === 0)
|
|
5511
6093
|
return text("No goals set.");
|
|
6094
|
+
const visibleGoals = goals.slice(0, rowLimit(limit, verbose));
|
|
5512
6095
|
const lines = ["period scope limit spent used% status"];
|
|
5513
|
-
for (const goal of
|
|
6096
|
+
for (const goal of visibleGoals) {
|
|
5514
6097
|
const scope = String(goal["project_path"] ?? goal["agent"] ?? "global").slice(0, 20);
|
|
5515
6098
|
const pct = Number(goal["percent_used"]).toFixed(1);
|
|
5516
6099
|
const status = goal["is_over"] ? "OVER" : goal["is_at_risk"] ? "AT RISK" : "ON TRACK";
|
|
5517
6100
|
lines.push(`${String(goal["period"]).padEnd(9)}${scope.padEnd(21)}${fmtUsd(Number(goal["limit_usd"])).padEnd(11)}${fmtUsd(Number(goal["current_spend_usd"])).padEnd(11)}${pct}% ${status}`);
|
|
5518
6101
|
}
|
|
6102
|
+
const hint = hiddenRowsHint(goals.length, visibleGoals.length, "goals");
|
|
6103
|
+
if (hint)
|
|
6104
|
+
lines.push(hint);
|
|
5519
6105
|
return text(lines.join(`
|
|
5520
6106
|
`));
|
|
5521
6107
|
});
|
|
@@ -5537,14 +6123,20 @@ function buildServer() {
|
|
|
5537
6123
|
await store.removeGoal(id);
|
|
5538
6124
|
return text("Goal removed.");
|
|
5539
6125
|
});
|
|
5540
|
-
server.tool("list_machines", "List all machines that have synced data.
|
|
6126
|
+
server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5541
6127
|
const machines = await store.machines();
|
|
6128
|
+
if (json)
|
|
6129
|
+
return text(JSON.stringify(machines, null, 2));
|
|
5542
6130
|
if (machines.length === 0)
|
|
5543
6131
|
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
6132
|
+
const visibleMachines = machines.slice(0, rowLimit(limit, verbose));
|
|
5544
6133
|
const lines = ["machine sessions requests cost last_active"];
|
|
5545
|
-
for (const m of
|
|
6134
|
+
for (const m of visibleMachines) {
|
|
5546
6135
|
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
5547
6136
|
}
|
|
6137
|
+
const hint = hiddenRowsHint(machines.length, visibleMachines.length, "machines");
|
|
6138
|
+
if (hint)
|
|
6139
|
+
lines.push(hint);
|
|
5548
6140
|
lines.push(`
|
|
5549
6141
|
current machine: ${getMachineId()}`);
|
|
5550
6142
|
return text(lines.join(`
|
|
@@ -5563,7 +6155,6 @@ current machine: ${getMachineId()}`);
|
|
|
5563
6155
|
return textError(String(error));
|
|
5564
6156
|
}
|
|
5565
6157
|
});
|
|
5566
|
-
registerCloudTools(server, MCP_NAME);
|
|
5567
6158
|
return server;
|
|
5568
6159
|
}
|
|
5569
6160
|
|