@hasna/economy 0.2.42 → 0.3.0
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 +8 -0
- package/dashboard/README.md +73 -0
- package/dist/cli/commands/brief.d.ts +4 -69
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +21 -5
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +8253 -6105
- package/dist/db/cloud.d.ts +28 -0
- package/dist/db/cloud.d.ts.map +1 -0
- package/dist/db/database.d.ts +18 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-sync-worker.d.ts +2 -0
- package/dist/db/pg-sync-worker.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +23 -0
- package/dist/db/sync-pg.d.ts.map +1 -0
- package/dist/index.d.ts +1 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1855 -1613
- package/dist/lib/analytics.d.ts +68 -0
- package/dist/lib/analytics.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +72 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/cloud-storage.d.ts +48 -0
- package/dist/lib/cloud-storage.d.ts.map +1 -0
- package/dist/lib/contracts-client/index.d.ts +3 -0
- package/dist/lib/contracts-client/index.d.ts.map +1 -0
- package/dist/lib/contracts-client/mode.d.ts +20 -0
- package/dist/lib/contracts-client/mode.d.ts.map +1 -0
- package/dist/lib/contracts-client/storage.d.ts +83 -0
- package/dist/lib/contracts-client/storage.d.ts.map +1 -0
- package/dist/lib/contracts-client/transport.d.ts +148 -0
- package/dist/lib/contracts-client/transport.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +3 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +283 -0
- package/dist/lib/store/index.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +0 -2
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +1934 -536
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/otel/index.js +21 -267
- package/dist/server/index.js +3145 -321
- package/dist/server/pg-sync-worker.js +31 -0
- package/dist/server/serve.d.ts +31 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -4
- package/dashboard/dist/assets/charts-BtCbslx4.js +0 -36
- package/dashboard/dist/assets/index-BDmtt3RI.css +0 -1
- package/dashboard/dist/assets/index-BFwRLqg7.js +0 -6
- package/dashboard/dist/assets/vendor-Cz8jb743.js +0 -53
- package/dashboard/dist/index.html +0 -16
- package/dashboard/dist/logo.jpg +0 -0
- package/dashboard/dist/vite.svg +0 -1
package/dist/index.js
CHANGED
|
@@ -14,14 +14,15 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
-
var __require = import.meta.require;
|
|
18
17
|
|
|
19
18
|
// src/lib/pricing.ts
|
|
20
19
|
var exports_pricing = {};
|
|
21
20
|
__export(exports_pricing, {
|
|
22
21
|
normalizeModelName: () => normalizeModelName,
|
|
22
|
+
getPricingFromRows: () => getPricingFromRows,
|
|
23
23
|
getPricingFromDb: () => getPricingFromDb,
|
|
24
24
|
getPricing: () => getPricing,
|
|
25
|
+
estimateCostFromRows: () => estimateCostFromRows,
|
|
25
26
|
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
26
27
|
computeCostFromDb: () => computeCostFromDb,
|
|
27
28
|
computeCost: () => computeCost,
|
|
@@ -212,6 +213,25 @@ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens
|
|
|
212
213
|
return 0;
|
|
213
214
|
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
214
215
|
}
|
|
216
|
+
function getPricingFromRows(rows, model) {
|
|
217
|
+
if (isFreeModel(model))
|
|
218
|
+
return FREE_PRICING;
|
|
219
|
+
for (const key of modelLookupKeys(model)) {
|
|
220
|
+
const row = rows.find((r) => r.model === key);
|
|
221
|
+
if (row)
|
|
222
|
+
return modelPricingFromDbRow(row);
|
|
223
|
+
}
|
|
224
|
+
const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
|
|
225
|
+
if (!match)
|
|
226
|
+
return null;
|
|
227
|
+
return modelPricingFromDbRow(match);
|
|
228
|
+
}
|
|
229
|
+
function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
230
|
+
const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
|
|
231
|
+
if (!pricing)
|
|
232
|
+
return 0;
|
|
233
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
234
|
+
}
|
|
215
235
|
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
216
236
|
if (isFreeModel(model))
|
|
217
237
|
return 0;
|
|
@@ -514,41 +534,9 @@ var init_pricing = __esm(() => {
|
|
|
514
534
|
|
|
515
535
|
// src/db/database.ts
|
|
516
536
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
517
|
-
import { execFileSync } from "child_process";
|
|
518
537
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
519
|
-
import { hostname, platform } from "os";
|
|
520
538
|
import { homedir } from "os";
|
|
521
539
|
import { join } from "path";
|
|
522
|
-
function normalizeMachineId(value) {
|
|
523
|
-
const id = value?.trim().toLowerCase().split(".")[0];
|
|
524
|
-
return id && id.length > 0 ? id : null;
|
|
525
|
-
}
|
|
526
|
-
function macHostMachineId() {
|
|
527
|
-
if (platform() !== "darwin")
|
|
528
|
-
return null;
|
|
529
|
-
for (const key of ["LocalHostName", "ComputerName", "HostName"]) {
|
|
530
|
-
try {
|
|
531
|
-
const value = execFileSync("/usr/sbin/scutil", ["--get", key], {
|
|
532
|
-
encoding: "utf8",
|
|
533
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
534
|
-
timeout: 1000
|
|
535
|
-
});
|
|
536
|
-
const id = normalizeMachineId(value);
|
|
537
|
-
if (id && id !== "mac" && id !== "localhost")
|
|
538
|
-
return id;
|
|
539
|
-
} catch {}
|
|
540
|
-
}
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
function getMachineId() {
|
|
544
|
-
const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
|
|
545
|
-
if (envMachine)
|
|
546
|
-
return envMachine;
|
|
547
|
-
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
548
|
-
if (hostMachine === "mac" || hostMachine === "localhost")
|
|
549
|
-
return macHostMachineId() ?? hostMachine;
|
|
550
|
-
return hostMachine;
|
|
551
|
-
}
|
|
552
540
|
function getDataDir() {
|
|
553
541
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
554
542
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -895,56 +883,6 @@ function requestPeriodWhere(period) {
|
|
|
895
883
|
return "1=1";
|
|
896
884
|
}
|
|
897
885
|
}
|
|
898
|
-
function upsertRequest(db, req) {
|
|
899
|
-
const now = req.updated_at ?? new Date().toISOString();
|
|
900
|
-
db.prepare(`
|
|
901
|
-
INSERT OR REPLACE INTO requests
|
|
902
|
-
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
903
|
-
cache_read_tokens, cache_create_tokens, cache_create_5m_tokens,
|
|
904
|
-
cache_create_1h_tokens, cost_usd, cost_basis, duration_ms, timestamp,
|
|
905
|
-
source_request_id, machine_id, attribution_tag, account_key, account_tool,
|
|
906
|
-
account_name, account_email, account_source, updated_at, synced_at)
|
|
907
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
908
|
-
`).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 ?? "");
|
|
909
|
-
}
|
|
910
|
-
function upsertSession(db, session) {
|
|
911
|
-
const now = session.updated_at ?? new Date().toISOString();
|
|
912
|
-
db.prepare(`
|
|
913
|
-
INSERT OR REPLACE INTO sessions
|
|
914
|
-
(id, agent, project_path, project_name, started_at, ended_at,
|
|
915
|
-
total_cost_usd, total_tokens, request_count, machine_id, attribution_tag,
|
|
916
|
-
account_key, account_tool, account_name, account_email, account_source, updated_at, synced_at)
|
|
917
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
918
|
-
`).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 ?? "");
|
|
919
|
-
}
|
|
920
|
-
function rollupSession(db, sessionId) {
|
|
921
|
-
db.prepare(`
|
|
922
|
-
UPDATE sessions SET
|
|
923
|
-
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
|
|
924
|
-
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
|
|
925
|
-
request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
|
|
926
|
-
ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
|
|
927
|
-
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
928
|
-
THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
|
|
929
|
-
ELSE started_at END,
|
|
930
|
-
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
931
|
-
THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
932
|
-
ELSE account_key END,
|
|
933
|
-
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
934
|
-
THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
935
|
-
ELSE account_tool END,
|
|
936
|
-
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
937
|
-
THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
938
|
-
ELSE account_name END,
|
|
939
|
-
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
940
|
-
THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
941
|
-
ELSE account_email END,
|
|
942
|
-
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
943
|
-
THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
944
|
-
ELSE account_source END
|
|
945
|
-
WHERE id = ?
|
|
946
|
-
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
947
|
-
}
|
|
948
886
|
function querySessions(db, filter = {}) {
|
|
949
887
|
const conditions = [];
|
|
950
888
|
const params = [];
|
|
@@ -1033,21 +971,6 @@ function querySummary(db, period, machine, allMachines = false, agent) {
|
|
|
1033
971
|
period
|
|
1034
972
|
};
|
|
1035
973
|
}
|
|
1036
|
-
function queryZeroCostTokenizedModels(db, limit = 10) {
|
|
1037
|
-
return db.prepare(`
|
|
1038
|
-
SELECT agent,
|
|
1039
|
-
model,
|
|
1040
|
-
COUNT(*) as requests,
|
|
1041
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1042
|
-
MAX(timestamp) as last_seen
|
|
1043
|
-
FROM requests
|
|
1044
|
-
WHERE cost_usd = 0
|
|
1045
|
-
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
|
|
1046
|
-
GROUP BY agent, model
|
|
1047
|
-
ORDER BY requests DESC, total_tokens DESC
|
|
1048
|
-
LIMIT ?
|
|
1049
|
-
`).all(limit);
|
|
1050
|
-
}
|
|
1051
974
|
function queryModelBreakdown(db) {
|
|
1052
975
|
return db.prepare(`
|
|
1053
976
|
SELECT model, agent,
|
|
@@ -1385,6 +1308,148 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1385
1308
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1386
1309
|
return result;
|
|
1387
1310
|
}
|
|
1311
|
+
function queryProjectBreakdownSince(db, since) {
|
|
1312
|
+
return db.prepare(`
|
|
1313
|
+
SELECT project_path, project_name,
|
|
1314
|
+
COUNT(*) as sessions,
|
|
1315
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1316
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1317
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1318
|
+
MAX(started_at) as last_active
|
|
1319
|
+
FROM sessions WHERE started_at >= ?
|
|
1320
|
+
GROUP BY project_path ORDER BY cost_usd DESC
|
|
1321
|
+
`).all(since);
|
|
1322
|
+
}
|
|
1323
|
+
function queryAgentBreakdownSince(db, since) {
|
|
1324
|
+
return db.prepare(`
|
|
1325
|
+
SELECT agent,
|
|
1326
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1327
|
+
COUNT(*) as requests,
|
|
1328
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1329
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1330
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1331
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1332
|
+
MAX(timestamp) as last_active
|
|
1333
|
+
FROM requests
|
|
1334
|
+
WHERE timestamp >= ?
|
|
1335
|
+
GROUP BY agent
|
|
1336
|
+
ORDER BY api_equivalent_usd DESC
|
|
1337
|
+
`).all(since);
|
|
1338
|
+
}
|
|
1339
|
+
function queryModelBreakdownSince(db, since) {
|
|
1340
|
+
return db.prepare(`
|
|
1341
|
+
SELECT model, agent,
|
|
1342
|
+
COUNT(*) as requests,
|
|
1343
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1344
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1345
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1346
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1347
|
+
FROM requests WHERE timestamp >= ?
|
|
1348
|
+
GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1349
|
+
`).all(since);
|
|
1350
|
+
}
|
|
1351
|
+
function queryAccountBreakdownSince(db, since) {
|
|
1352
|
+
return db.prepare(`
|
|
1353
|
+
WITH request_rows AS (
|
|
1354
|
+
SELECT
|
|
1355
|
+
r.session_id as session_id,
|
|
1356
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1357
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
|
|
1358
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
|
|
1359
|
+
LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
|
|
1360
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1361
|
+
1 as requests,
|
|
1362
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1363
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1364
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1365
|
+
r.timestamp as last_active
|
|
1366
|
+
FROM requests r
|
|
1367
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1368
|
+
WHERE r.timestamp >= ?
|
|
1369
|
+
AND (
|
|
1370
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1371
|
+
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1372
|
+
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1373
|
+
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1374
|
+
)
|
|
1375
|
+
),
|
|
1376
|
+
session_only_rows AS (
|
|
1377
|
+
SELECT
|
|
1378
|
+
s.id as session_id,
|
|
1379
|
+
COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1380
|
+
s.account_key as raw_account_key,
|
|
1381
|
+
s.account_name as raw_account_name,
|
|
1382
|
+
LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
|
|
1383
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1384
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1385
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1386
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1387
|
+
'estimated' as cost_basis,
|
|
1388
|
+
s.started_at as last_active
|
|
1389
|
+
FROM sessions s
|
|
1390
|
+
WHERE s.started_at >= ?
|
|
1391
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1392
|
+
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1393
|
+
),
|
|
1394
|
+
normalized AS (
|
|
1395
|
+
SELECT
|
|
1396
|
+
CASE
|
|
1397
|
+
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1398
|
+
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1399
|
+
ELSE raw_account_key
|
|
1400
|
+
END as account_key,
|
|
1401
|
+
account_agent as account_tool,
|
|
1402
|
+
raw_account_name as account_name,
|
|
1403
|
+
raw_account_email as account_email,
|
|
1404
|
+
account_source,
|
|
1405
|
+
session_id,
|
|
1406
|
+
requests,
|
|
1407
|
+
total_tokens,
|
|
1408
|
+
cost_usd,
|
|
1409
|
+
cost_basis,
|
|
1410
|
+
last_active
|
|
1411
|
+
FROM request_rows
|
|
1412
|
+
UNION ALL
|
|
1413
|
+
SELECT
|
|
1414
|
+
CASE
|
|
1415
|
+
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1416
|
+
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1417
|
+
ELSE raw_account_key
|
|
1418
|
+
END as account_key,
|
|
1419
|
+
account_agent as account_tool,
|
|
1420
|
+
raw_account_name as account_name,
|
|
1421
|
+
raw_account_email as account_email,
|
|
1422
|
+
account_source,
|
|
1423
|
+
session_id,
|
|
1424
|
+
requests,
|
|
1425
|
+
total_tokens,
|
|
1426
|
+
cost_usd,
|
|
1427
|
+
cost_basis,
|
|
1428
|
+
last_active
|
|
1429
|
+
FROM session_only_rows
|
|
1430
|
+
)
|
|
1431
|
+
SELECT account_key,
|
|
1432
|
+
account_tool,
|
|
1433
|
+
COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
|
|
1434
|
+
NULLIF(account_email, '') as account_email,
|
|
1435
|
+
COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
|
|
1436
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1437
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
1438
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1439
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1440
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1441
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1442
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1443
|
+
COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1444
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1445
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1446
|
+
MAX(last_active) as last_active
|
|
1447
|
+
FROM normalized
|
|
1448
|
+
WHERE account_key != ''
|
|
1449
|
+
GROUP BY account_key, account_tool, account_email
|
|
1450
|
+
ORDER BY api_equivalent_usd DESC
|
|
1451
|
+
`).all(since, since);
|
|
1452
|
+
}
|
|
1388
1453
|
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1389
1454
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1390
1455
|
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
@@ -1396,21 +1461,6 @@ function queryDailyBreakdown(db, days = 30, machine) {
|
|
|
1396
1461
|
ORDER BY date ASC
|
|
1397
1462
|
`).all(...params);
|
|
1398
1463
|
}
|
|
1399
|
-
function queryHourlyBreakdown(db, machine, hours) {
|
|
1400
|
-
const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
|
|
1401
|
-
const params = hours == null ? [] : [`-${hours} hours`];
|
|
1402
|
-
if (machine) {
|
|
1403
|
-
clauses.push("machine_id = ?");
|
|
1404
|
-
params.push(machine);
|
|
1405
|
-
}
|
|
1406
|
-
return db.prepare(`
|
|
1407
|
-
SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1408
|
-
FROM requests
|
|
1409
|
-
WHERE ${clauses.join(" AND ")}
|
|
1410
|
-
GROUP BY STRFTIME('%H', timestamp), agent
|
|
1411
|
-
ORDER BY hour ASC
|
|
1412
|
-
`).all(...params);
|
|
1413
|
-
}
|
|
1414
1464
|
function upsertProject(db, project) {
|
|
1415
1465
|
db.prepare(`
|
|
1416
1466
|
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
@@ -1423,9 +1473,6 @@ function getProject(db, path) {
|
|
|
1423
1473
|
return null;
|
|
1424
1474
|
return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
|
|
1425
1475
|
}
|
|
1426
|
-
function listProjects(db) {
|
|
1427
|
-
return db.prepare(`SELECT * FROM projects ORDER BY created_at DESC`).all().map((row) => ({ ...row, tags: JSON.parse(row["tags"] ?? "[]") }));
|
|
1428
|
-
}
|
|
1429
1476
|
function deleteProject(db, path) {
|
|
1430
1477
|
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
1431
1478
|
}
|
|
@@ -1508,25 +1555,9 @@ function getGoalStatuses(db) {
|
|
|
1508
1555
|
};
|
|
1509
1556
|
});
|
|
1510
1557
|
}
|
|
1511
|
-
function getIngestState(db, source, key) {
|
|
1512
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1513
|
-
return row?.value ?? null;
|
|
1514
|
-
}
|
|
1515
|
-
function setIngestState(db, source, key, value) {
|
|
1516
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1517
|
-
}
|
|
1518
1558
|
function queryRequestsSince(db, since) {
|
|
1519
1559
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
1520
1560
|
}
|
|
1521
|
-
function upsertBillingDaily(db, row) {
|
|
1522
|
-
db.prepare(`
|
|
1523
|
-
INSERT OR REPLACE INTO billing_daily (date, provider, description, cost_usd, updated_at)
|
|
1524
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1525
|
-
`).run(row.date, row.provider, row.description, row.cost_usd, row.updated_at);
|
|
1526
|
-
}
|
|
1527
|
-
function clearBillingRange(db, provider, fromDate, toDate) {
|
|
1528
|
-
db.prepare(`DELETE FROM billing_daily WHERE provider = ? AND date >= ? AND date <= ?`).run(provider, fromDate, toDate);
|
|
1529
|
-
}
|
|
1530
1561
|
function queryBillingSummary(db, period) {
|
|
1531
1562
|
const where = period === "today" ? `date = DATE('now')` : period === "yesterday" ? `date = DATE('now', '-1 day')` : period === "week" ? `date >= DATE('now', 'weekday 0', '-7 days')` : period === "month" ? `date >= DATE('now', 'start of month')` : period === "year" ? `date >= DATE('now', 'start of year')` : "1=1";
|
|
1532
1563
|
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
@@ -1538,6 +1569,13 @@ function queryBillingSummary(db, period) {
|
|
|
1538
1569
|
}
|
|
1539
1570
|
return { total_usd: total, by_provider };
|
|
1540
1571
|
}
|
|
1572
|
+
function getSessionDetail(db, id) {
|
|
1573
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
|
|
1574
|
+
if (!session)
|
|
1575
|
+
return null;
|
|
1576
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1577
|
+
return { session, requests };
|
|
1578
|
+
}
|
|
1541
1579
|
function listMachines(db, period = "all") {
|
|
1542
1580
|
const rWhere = requestPeriodWhere(period);
|
|
1543
1581
|
const sWhere = sessionPeriodWhere(period);
|
|
@@ -1630,14 +1668,6 @@ function listSubscriptions(db) {
|
|
|
1630
1668
|
function deleteSubscription(db, id) {
|
|
1631
1669
|
db.prepare(`DELETE FROM subscriptions WHERE id = ?`).run(id);
|
|
1632
1670
|
}
|
|
1633
|
-
function upsertUsageSnapshot(db, snap) {
|
|
1634
|
-
const now = snap.updated_at ?? new Date().toISOString();
|
|
1635
|
-
const id = snap.id ?? `${snap.agent}-${snap.date}-${snap.metric}-${snap.machine_id}`;
|
|
1636
|
-
db.prepare(`
|
|
1637
|
-
INSERT OR REPLACE INTO usage_snapshots (id, agent, date, metric, value, unit, machine_id, updated_at)
|
|
1638
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1639
|
-
`).run(id, snap.agent, snap.date, snap.metric, snap.value, snap.unit, snap.machine_id, now);
|
|
1640
|
-
}
|
|
1641
1671
|
function queryUsageSnapshots(db, opts = {}) {
|
|
1642
1672
|
const conditions = [];
|
|
1643
1673
|
const params = [];
|
|
@@ -1659,27 +1689,6 @@ function queryUsageSnapshots(db, opts = {}) {
|
|
|
1659
1689
|
function listMachineRegistry(db) {
|
|
1660
1690
|
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1661
1691
|
}
|
|
1662
|
-
function dedupeRequests(db) {
|
|
1663
|
-
const dupes = db.prepare(`
|
|
1664
|
-
SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
|
|
1665
|
-
FROM requests
|
|
1666
|
-
WHERE source_request_id != '' AND source_request_id IS NOT NULL
|
|
1667
|
-
GROUP BY source_request_id, agent, COALESCE(machine_id, '')
|
|
1668
|
-
HAVING cnt > 1
|
|
1669
|
-
`).all();
|
|
1670
|
-
let removed = 0;
|
|
1671
|
-
for (const row of dupes) {
|
|
1672
|
-
const result = db.prepare(`
|
|
1673
|
-
DELETE FROM requests
|
|
1674
|
-
WHERE source_request_id = ?
|
|
1675
|
-
AND agent = ?
|
|
1676
|
-
AND COALESCE(machine_id, '') = ?
|
|
1677
|
-
AND id != ?
|
|
1678
|
-
`).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
|
|
1679
|
-
removed += result.changes;
|
|
1680
|
-
}
|
|
1681
|
-
return removed;
|
|
1682
|
-
}
|
|
1683
1692
|
var init_database = () => {};
|
|
1684
1693
|
|
|
1685
1694
|
// src/lib/agents.ts
|
|
@@ -1702,1542 +1711,1775 @@ var COST_BASIS = [
|
|
|
1702
1711
|
function isAgent(value) {
|
|
1703
1712
|
return AGENTS.includes(value);
|
|
1704
1713
|
}
|
|
1705
|
-
// src/index.ts
|
|
1714
|
+
// src/lib/store/index.ts
|
|
1706
1715
|
init_database();
|
|
1707
1716
|
init_pricing();
|
|
1717
|
+
import { randomUUID } from "crypto";
|
|
1708
1718
|
|
|
1709
|
-
// src/lib/
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
if (!hasCostData(s))
|
|
1725
|
-
continue;
|
|
1726
|
-
examples.push({
|
|
1727
|
-
messages: [
|
|
1728
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1729
|
-
{ role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
|
|
1730
|
-
{
|
|
1731
|
-
role: "assistant",
|
|
1732
|
-
content: `${period === "all" ? "Total" : period.charAt(0).toUpperCase() + period.slice(1)} AI spending: $${s.total_usd.toFixed(4)} across ${s.sessions} session(s), ${s.requests} request(s), ${s.tokens.toLocaleString()} tokens.`
|
|
1733
|
-
}
|
|
1734
|
-
]
|
|
1735
|
-
});
|
|
1736
|
-
} catch {}
|
|
1737
|
-
}
|
|
1738
|
-
const sessions = querySessions(db, {
|
|
1739
|
-
limit: Math.min(Math.floor(limit / 4), 50),
|
|
1740
|
-
since: options.since?.toISOString().substring(0, 10)
|
|
1741
|
-
});
|
|
1742
|
-
for (const s of sessions) {
|
|
1743
|
-
examples.push({
|
|
1744
|
-
messages: [
|
|
1745
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1746
|
-
{
|
|
1747
|
-
role: "user",
|
|
1748
|
-
content: `How much did the session "${s.id.substring(0, 12)}" cost?`
|
|
1749
|
-
},
|
|
1750
|
-
{
|
|
1751
|
-
role: "assistant",
|
|
1752
|
-
content: `Session ${s.id.substring(0, 12)} (${s.agent}, project: ${s.project_name || "unknown"}): $${s.total_cost_usd.toFixed(4)}, ${s.total_tokens.toLocaleString()} tokens, ${s.request_count} requests. Started: ${s.started_at.substring(0, 16)}.`
|
|
1753
|
-
}
|
|
1754
|
-
]
|
|
1755
|
-
});
|
|
1756
|
-
examples.push({
|
|
1757
|
-
messages: [
|
|
1758
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1759
|
-
{
|
|
1760
|
-
role: "user",
|
|
1761
|
-
content: `What was the token usage for session ${s.id.substring(0, 12)}?`
|
|
1762
|
-
},
|
|
1763
|
-
{
|
|
1764
|
-
role: "assistant",
|
|
1765
|
-
content: `Session ${s.id.substring(0, 12)} used ${s.total_tokens.toLocaleString()} tokens across ${s.request_count} requests on project "${s.project_name || "unknown"}" (${s.agent}).`
|
|
1766
|
-
}
|
|
1767
|
-
]
|
|
1768
|
-
});
|
|
1769
|
-
if (examples.length >= limit)
|
|
1770
|
-
break;
|
|
1771
|
-
}
|
|
1772
|
-
const modelBreakdown = queryModelBreakdown(db);
|
|
1773
|
-
if (modelBreakdown.length > 0) {
|
|
1774
|
-
const topModels = modelBreakdown.slice(0, 5);
|
|
1775
|
-
examples.push({
|
|
1776
|
-
messages: [
|
|
1777
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1778
|
-
{ role: "user", content: "Which AI models have I spent the most on?" },
|
|
1779
|
-
{
|
|
1780
|
-
role: "assistant",
|
|
1781
|
-
content: `Model cost breakdown (top ${topModels.length}):
|
|
1782
|
-
${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
|
|
1783
|
-
`)}`
|
|
1784
|
-
}
|
|
1785
|
-
]
|
|
1786
|
-
});
|
|
1787
|
-
for (const m of topModels) {
|
|
1788
|
-
examples.push({
|
|
1789
|
-
messages: [
|
|
1790
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1791
|
-
{ role: "user", content: `How much have I spent on ${m.model}?` },
|
|
1792
|
-
{
|
|
1793
|
-
role: "assistant",
|
|
1794
|
-
content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
|
|
1795
|
-
}
|
|
1796
|
-
]
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
const projectBreakdown = queryProjectBreakdown(db);
|
|
1801
|
-
if (projectBreakdown.length > 0) {
|
|
1802
|
-
const topProjects = projectBreakdown.slice(0, 5);
|
|
1803
|
-
examples.push({
|
|
1804
|
-
messages: [
|
|
1805
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1806
|
-
{ role: "user", content: "Which projects are costing the most?" },
|
|
1807
|
-
{
|
|
1808
|
-
role: "assistant",
|
|
1809
|
-
content: `Project cost breakdown (top ${topProjects.length}):
|
|
1810
|
-
${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
|
|
1811
|
-
`)}`
|
|
1812
|
-
}
|
|
1813
|
-
]
|
|
1814
|
-
});
|
|
1815
|
-
for (const p of topProjects.slice(0, 3)) {
|
|
1816
|
-
examples.push({
|
|
1817
|
-
messages: [
|
|
1818
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1819
|
-
{ role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
|
|
1820
|
-
{
|
|
1821
|
-
role: "assistant",
|
|
1822
|
-
content: `Project "${p.project_name}": $${p.cost_usd.toFixed(4)} across ${p.sessions} session(s) and ${p.requests.toLocaleString()} requests. Last active: ${p.last_active?.substring(0, 10) ?? "unknown"}.`
|
|
1823
|
-
}
|
|
1824
|
-
]
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
try {
|
|
1829
|
-
const budgets = getBudgetStatuses(db);
|
|
1830
|
-
if (budgets.length > 0) {
|
|
1831
|
-
examples.push({
|
|
1832
|
-
messages: [
|
|
1833
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1834
|
-
{ role: "user", content: "How am I tracking against my AI spending budgets?" },
|
|
1835
|
-
{
|
|
1836
|
-
role: "assistant",
|
|
1837
|
-
content: `Budget status:
|
|
1838
|
-
${budgets.map((b) => `- ${b.project_path ?? "global"} (${b.period}): $${b.current_spend_usd.toFixed(4)} / $${b.limit_usd.toFixed(2)} (${b.percent_used.toFixed(1)}%) \u2014 ${b.is_over_limit ? "OVER LIMIT" : b.is_over_alert ? "ALERT" : "OK"}`).join(`
|
|
1839
|
-
`)}`
|
|
1840
|
-
}
|
|
1841
|
-
]
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
} catch {}
|
|
1845
|
-
try {
|
|
1846
|
-
const goals = getGoalStatuses(db);
|
|
1847
|
-
if (goals.length > 0) {
|
|
1848
|
-
examples.push({
|
|
1849
|
-
messages: [
|
|
1850
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1851
|
-
{ role: "user", content: "Am I on track with my AI cost reduction goals?" },
|
|
1852
|
-
{
|
|
1853
|
-
role: "assistant",
|
|
1854
|
-
content: `Goal progress:
|
|
1855
|
-
${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}): $${g.current_spend_usd.toFixed(4)} / $${g.limit_usd.toFixed(2)} (${g.percent_used.toFixed(1)}%) \u2014 ${g.is_over ? "OVER" : g.is_at_risk ? "AT RISK" : "ON TRACK"}`).join(`
|
|
1856
|
-
`)}`
|
|
1857
|
-
}
|
|
1858
|
-
]
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
} catch {}
|
|
1862
|
-
const weekSummary = querySummary(db, "week");
|
|
1863
|
-
const monthSummary = querySummary(db, "month");
|
|
1864
|
-
if (weekSummary.total_usd > 0) {
|
|
1865
|
-
const projectedMonthly = weekSummary.total_usd / 7 * 30;
|
|
1866
|
-
examples.push({
|
|
1867
|
-
messages: [
|
|
1868
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1869
|
-
{ role: "user", content: "Based on my usage, what will I spend this month?" },
|
|
1870
|
-
{
|
|
1871
|
-
role: "assistant",
|
|
1872
|
-
content: `Based on your last 7 days ($${weekSummary.total_usd.toFixed(4)}), projected monthly spend is approximately $${projectedMonthly.toFixed(2)}. Current month-to-date: $${monthSummary.total_usd.toFixed(4)}.`
|
|
1873
|
-
}
|
|
1874
|
-
]
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
if (modelBreakdown.length > 1) {
|
|
1878
|
-
const expensive = modelBreakdown[0];
|
|
1879
|
-
const cheaper = modelBreakdown[modelBreakdown.length - 1];
|
|
1880
|
-
examples.push({
|
|
1881
|
-
messages: [
|
|
1882
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1883
|
-
{ role: "user", content: "How can I reduce my AI API costs?" },
|
|
1884
|
-
{
|
|
1885
|
-
role: "assistant",
|
|
1886
|
-
content: `Your most expensive model is ${expensive.model} at $${expensive.cost_usd.toFixed(4)}. Consider switching some workloads to ${cheaper.model} ($${cheaper.cost_usd.toFixed(4)}) for cost savings. Cache frequently repeated prompts to reduce cache-miss costs.`
|
|
1887
|
-
}
|
|
1888
|
-
]
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
} catch {}
|
|
1892
|
-
const finalExamples = examples.slice(0, limit);
|
|
1893
|
-
return { source: "economy", examples: finalExamples, count: finalExamples.length };
|
|
1894
|
-
};
|
|
1895
|
-
// src/lib/model-config.ts
|
|
1896
|
-
init_database();
|
|
1897
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1898
|
-
import { dirname, join as join2 } from "path";
|
|
1899
|
-
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
1900
|
-
function getModelConfigPath() {
|
|
1901
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
1902
|
-
}
|
|
1903
|
-
function loadConfig() {
|
|
1904
|
-
try {
|
|
1905
|
-
const configPath = getModelConfigPath();
|
|
1906
|
-
if (existsSync2(configPath)) {
|
|
1907
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1908
|
-
}
|
|
1909
|
-
} catch {}
|
|
1910
|
-
return {};
|
|
1911
|
-
}
|
|
1912
|
-
function saveConfig(config) {
|
|
1913
|
-
const configPath = getModelConfigPath();
|
|
1914
|
-
const dir = dirname(configPath);
|
|
1915
|
-
if (!existsSync2(dir))
|
|
1916
|
-
mkdirSync2(dir, { recursive: true });
|
|
1917
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
1918
|
-
`);
|
|
1919
|
-
}
|
|
1920
|
-
function getActiveModel() {
|
|
1921
|
-
return loadConfig().activeModel ?? DEFAULT_MODEL;
|
|
1922
|
-
}
|
|
1923
|
-
function setActiveModel(id) {
|
|
1924
|
-
const config = loadConfig();
|
|
1925
|
-
config.activeModel = id;
|
|
1926
|
-
saveConfig(config);
|
|
1927
|
-
}
|
|
1928
|
-
function clearActiveModel() {
|
|
1929
|
-
const config = loadConfig();
|
|
1930
|
-
delete config.activeModel;
|
|
1931
|
-
saveConfig(config);
|
|
1932
|
-
}
|
|
1933
|
-
// src/lib/open-projects.ts
|
|
1934
|
-
init_database();
|
|
1935
|
-
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
1936
|
-
let listProjects2 = listActiveProjects;
|
|
1937
|
-
if (!listProjects2) {
|
|
1938
|
-
const projectsApi = await import("@hasna/projects");
|
|
1939
|
-
listProjects2 = projectsApi.listProjects;
|
|
1940
|
-
}
|
|
1941
|
-
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
1942
|
-
let imported = 0;
|
|
1943
|
-
let skipped = 0;
|
|
1944
|
-
for (const project of projects) {
|
|
1945
|
-
if (!project.path) {
|
|
1946
|
-
skipped++;
|
|
1947
|
-
continue;
|
|
1948
|
-
}
|
|
1949
|
-
upsertProject(db, {
|
|
1950
|
-
id: project.id,
|
|
1951
|
-
path: project.path,
|
|
1952
|
-
name: project.name,
|
|
1953
|
-
description: project.description,
|
|
1954
|
-
tags: project.tags ?? [],
|
|
1955
|
-
created_at: project.created_at
|
|
1956
|
-
});
|
|
1957
|
-
imported++;
|
|
1719
|
+
// src/lib/savings.ts
|
|
1720
|
+
function periodWhere2(period, column) {
|
|
1721
|
+
switch (period) {
|
|
1722
|
+
case "today":
|
|
1723
|
+
return `DATE(${column}) = DATE('now')`;
|
|
1724
|
+
case "yesterday":
|
|
1725
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1726
|
+
case "week":
|
|
1727
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1728
|
+
case "month":
|
|
1729
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
1730
|
+
case "year":
|
|
1731
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
1732
|
+
case "all":
|
|
1733
|
+
return "1=1";
|
|
1958
1734
|
}
|
|
1959
|
-
return { imported, skipped };
|
|
1960
|
-
}
|
|
1961
|
-
// src/lib/peer-sync.ts
|
|
1962
|
-
init_database();
|
|
1963
|
-
import { Database as BunDatabase } from "bun:sqlite";
|
|
1964
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1965
|
-
|
|
1966
|
-
// src/lib/package-metadata.ts
|
|
1967
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
1968
|
-
var cachedMetadata = null;
|
|
1969
|
-
function getPackageMetadata() {
|
|
1970
|
-
if (cachedMetadata)
|
|
1971
|
-
return cachedMetadata;
|
|
1972
|
-
const raw = readFileSync2(new URL("../../package.json", import.meta.url), "utf8");
|
|
1973
|
-
const parsed = JSON.parse(raw);
|
|
1974
|
-
cachedMetadata = {
|
|
1975
|
-
name: parsed.name ?? "@hasna/economy",
|
|
1976
|
-
version: parsed.version ?? "0.0.0"
|
|
1977
|
-
};
|
|
1978
|
-
return cachedMetadata;
|
|
1979
|
-
}
|
|
1980
|
-
var packageMetadata = getPackageMetadata();
|
|
1981
|
-
|
|
1982
|
-
// src/lib/peer-sync.ts
|
|
1983
|
-
var GENERIC_PEER_TABLES = [
|
|
1984
|
-
"usage_snapshots",
|
|
1985
|
-
"subscriptions",
|
|
1986
|
-
"billing_daily",
|
|
1987
|
-
"savings_daily",
|
|
1988
|
-
"budgets",
|
|
1989
|
-
"goals",
|
|
1990
|
-
"model_pricing",
|
|
1991
|
-
"machines"
|
|
1992
|
-
];
|
|
1993
|
-
function quoteIdent(identifier) {
|
|
1994
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1995
|
-
}
|
|
1996
|
-
function tableExists(db, table) {
|
|
1997
|
-
const row = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table);
|
|
1998
|
-
return Boolean(row);
|
|
1999
1735
|
}
|
|
2000
|
-
function
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
|
|
1736
|
+
function prorateMonthlyFee(monthlyFee, period) {
|
|
1737
|
+
const now = new Date;
|
|
1738
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1739
|
+
switch (period) {
|
|
1740
|
+
case "today":
|
|
1741
|
+
case "yesterday":
|
|
1742
|
+
return monthlyFee / daysInMonth;
|
|
1743
|
+
case "week":
|
|
1744
|
+
return monthlyFee / daysInMonth * 7;
|
|
1745
|
+
case "month":
|
|
1746
|
+
return monthlyFee;
|
|
1747
|
+
case "year":
|
|
1748
|
+
return monthlyFee * 12;
|
|
1749
|
+
case "all":
|
|
1750
|
+
return monthlyFee;
|
|
1751
|
+
}
|
|
2017
1752
|
}
|
|
2018
|
-
function
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (keyColumns.some((c) => row[c] == null))
|
|
2022
|
-
return null;
|
|
2023
|
-
const where = keyColumns.map((c) => `${quoteIdent(c)} = ?`).join(" AND ");
|
|
2024
|
-
return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
|
|
1753
|
+
function proratedIncludedConsumed(includedUsage, includedCap, period) {
|
|
1754
|
+
const cap = prorateMonthlyFee(includedCap, period);
|
|
1755
|
+
return cap > 0 ? Math.min(includedUsage, cap) : includedUsage;
|
|
2025
1756
|
}
|
|
2026
|
-
function
|
|
2027
|
-
return
|
|
1757
|
+
function computeSavedUsd(apiEquivalent, onDemand, subscriptionFee) {
|
|
1758
|
+
return Math.max(0, apiEquivalent - onDemand - subscriptionFee);
|
|
2028
1759
|
}
|
|
2029
|
-
function
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
const
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
const columns = commonColumns(source, target, table);
|
|
2076
|
-
const rows = selectRows(source, table, columns);
|
|
2077
|
-
const idMap = new Map;
|
|
2078
|
-
for (const raw of rows) {
|
|
2079
|
-
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
2080
|
-
const originalId = String(row["id"] ?? "");
|
|
2081
|
-
if (!originalId) {
|
|
2082
|
-
stats.skipped++;
|
|
2083
|
-
continue;
|
|
2084
|
-
}
|
|
2085
|
-
const machine = String(row["machine_id"] ?? "");
|
|
2086
|
-
const directExisting = hasId(target, table, originalId);
|
|
2087
|
-
if (directExisting && String(directExisting["machine_id"] ?? "") !== machine) {
|
|
2088
|
-
row["id"] = collisionId(target, table, machine, originalId);
|
|
2089
|
-
stats.collisions++;
|
|
2090
|
-
}
|
|
2091
|
-
if (table === "requests" && sessionIdMap) {
|
|
2092
|
-
const originalSessionId = String(row["session_id"] ?? "");
|
|
2093
|
-
row["session_id"] = sessionIdMap.get(originalSessionId) ?? originalSessionId;
|
|
2094
|
-
}
|
|
2095
|
-
const existing = hasId(target, table, String(row["id"]));
|
|
2096
|
-
idMap.set(originalId, String(row["id"]));
|
|
2097
|
-
if (existing && !shouldReplace(row, existing)) {
|
|
2098
|
-
stats.skipped++;
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
|
-
insertOrReplace(target, table, columns, row);
|
|
2102
|
-
if (existing)
|
|
2103
|
-
stats.updated++;
|
|
2104
|
-
else
|
|
2105
|
-
stats.inserted++;
|
|
2106
|
-
}
|
|
2107
|
-
return { stats, idMap };
|
|
2108
|
-
}
|
|
2109
|
-
function mergeProjects(target, source) {
|
|
2110
|
-
const table = "projects";
|
|
2111
|
-
const stats = { table, inserted: 0, updated: 0, skipped: 0, collisions: 0 };
|
|
2112
|
-
const columns = commonColumns(source, target, table);
|
|
2113
|
-
const rows = selectRows(source, table, columns);
|
|
2114
|
-
for (const raw of rows) {
|
|
2115
|
-
const row = { ...raw };
|
|
2116
|
-
const path = String(row["path"] ?? "");
|
|
2117
|
-
const id = String(row["id"] ?? "");
|
|
2118
|
-
if (!path || !id) {
|
|
2119
|
-
stats.skipped++;
|
|
2120
|
-
continue;
|
|
2121
|
-
}
|
|
2122
|
-
const existingByPath = target.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
2123
|
-
if (existingByPath) {
|
|
2124
|
-
row["id"] = existingByPath["id"] ?? id;
|
|
2125
|
-
insertOrReplace(target, table, columns, row);
|
|
2126
|
-
stats.updated++;
|
|
2127
|
-
continue;
|
|
2128
|
-
}
|
|
2129
|
-
const existingById = target.prepare(`SELECT * FROM projects WHERE id = ?`).get(id);
|
|
2130
|
-
if (existingById && String(existingById["path"] ?? "") !== path) {
|
|
2131
|
-
row["id"] = `peer:${id}`;
|
|
2132
|
-
stats.collisions++;
|
|
2133
|
-
while (target.prepare(`SELECT id FROM projects WHERE id = ?`).get(row["id"])) {
|
|
2134
|
-
row["id"] = `peer:${String(row["id"])}`;
|
|
2135
|
-
}
|
|
1760
|
+
function querySavingsSummary(db, period, agent) {
|
|
1761
|
+
const where = periodWhere2(period, "timestamp");
|
|
1762
|
+
const agentClause = agent ? " AND agent = ?" : "";
|
|
1763
|
+
const params = agent ? [agent] : [];
|
|
1764
|
+
const apiRow = db.prepare(`
|
|
1765
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1766
|
+
FROM requests
|
|
1767
|
+
WHERE ${where}${agentClause}
|
|
1768
|
+
AND COALESCE(cost_basis, 'estimated') IN ('metered_api', 'estimated', 'unknown')
|
|
1769
|
+
`).get(...params);
|
|
1770
|
+
const includedRow = db.prepare(`
|
|
1771
|
+
SELECT COALESCE(SUM(cost_usd), 0) as total
|
|
1772
|
+
FROM requests
|
|
1773
|
+
WHERE ${where}${agentClause}
|
|
1774
|
+
AND cost_basis = 'subscription_included'
|
|
1775
|
+
`).get(...params);
|
|
1776
|
+
const subWhere = periodWhere2(period, "date");
|
|
1777
|
+
const onDemandRow = db.prepare(`
|
|
1778
|
+
SELECT COALESCE(SUM(value), 0) as total
|
|
1779
|
+
FROM usage_snapshots
|
|
1780
|
+
WHERE ${subWhere}${agent ? " AND agent = ?" : ""}
|
|
1781
|
+
AND metric = 'on_demand_usd'
|
|
1782
|
+
`).get(...params);
|
|
1783
|
+
const subs = db.prepare(`
|
|
1784
|
+
SELECT
|
|
1785
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1786
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1787
|
+
FROM subscriptions
|
|
1788
|
+
WHERE active = 1${agent ? " AND (agent = ? OR agent IS NULL)" : ""}
|
|
1789
|
+
`).get(...agent ? [agent] : []);
|
|
1790
|
+
const subscriptionFee = prorateMonthlyFee(subs.fee, period);
|
|
1791
|
+
const apiEquivalent = apiRow.total + includedRow.total;
|
|
1792
|
+
const includedConsumed = proratedIncludedConsumed(includedRow.total, subs.included, period);
|
|
1793
|
+
const onDemand = onDemandRow.total;
|
|
1794
|
+
const saved = computeSavedUsd(apiEquivalent, onDemand, subscriptionFee);
|
|
1795
|
+
const byAgent = {};
|
|
1796
|
+
if (!agent) {
|
|
1797
|
+
const onDemandByAgent = new Map;
|
|
1798
|
+
for (const row of db.prepare(`
|
|
1799
|
+
SELECT agent, COALESCE(SUM(value), 0) as total
|
|
1800
|
+
FROM usage_snapshots
|
|
1801
|
+
WHERE ${subWhere}
|
|
1802
|
+
AND metric = 'on_demand_usd'
|
|
1803
|
+
GROUP BY agent
|
|
1804
|
+
`).all()) {
|
|
1805
|
+
onDemandByAgent.set(row.agent, row.total);
|
|
2136
1806
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
for (const raw of rows) {
|
|
2148
|
-
const row = normalizeRow(raw, columns, sourceMachine, now);
|
|
2149
|
-
const existing = rowByKey(target, table, keyColumns, row);
|
|
2150
|
-
if (existing && !shouldReplace(row, existing)) {
|
|
2151
|
-
stats.skipped++;
|
|
2152
|
-
continue;
|
|
1807
|
+
const subscriptionByAgent = new Map;
|
|
1808
|
+
for (const row of db.prepare(`
|
|
1809
|
+
SELECT agent,
|
|
1810
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1811
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1812
|
+
FROM subscriptions
|
|
1813
|
+
WHERE active = 1 AND agent IS NOT NULL
|
|
1814
|
+
GROUP BY agent
|
|
1815
|
+
`).all()) {
|
|
1816
|
+
subscriptionByAgent.set(row.agent, row);
|
|
2153
1817
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
if (!tableExists(source, table))
|
|
2168
|
-
continue;
|
|
2169
|
-
const rows = source.prepare(`
|
|
2170
|
-
SELECT machine_id, COUNT(*) as cnt
|
|
2171
|
-
FROM ${quoteIdent(table)}
|
|
2172
|
-
WHERE machine_id != '' AND machine_id IS NOT NULL
|
|
2173
|
-
GROUP BY machine_id
|
|
1818
|
+
const globalSubs = db.prepare(`
|
|
1819
|
+
SELECT
|
|
1820
|
+
COALESCE(SUM(monthly_fee_usd), 0) as fee,
|
|
1821
|
+
COALESCE(SUM(included_usage_usd), 0) as included
|
|
1822
|
+
FROM subscriptions
|
|
1823
|
+
WHERE active = 1 AND agent IS NULL
|
|
1824
|
+
`).get();
|
|
1825
|
+
const rows = db.prepare(`
|
|
1826
|
+
SELECT agent,
|
|
1827
|
+
COALESCE(SUM(cost_usd), 0) as api_eq,
|
|
1828
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
|
|
1829
|
+
FROM requests WHERE ${where}
|
|
1830
|
+
GROUP BY agent
|
|
2174
1831
|
`).all();
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
hostname = COALESCE(NULLIF(machines.hostname, ''), excluded.hostname),
|
|
2197
|
-
last_seen_at = CASE
|
|
2198
|
-
WHEN machines.last_seen_at IS NULL OR machines.last_seen_at < excluded.last_seen_at THEN excluded.last_seen_at
|
|
2199
|
-
ELSE machines.last_seen_at
|
|
2200
|
-
END,
|
|
2201
|
-
last_pull_at = excluded.last_pull_at,
|
|
2202
|
-
economy_version = excluded.economy_version,
|
|
2203
|
-
updated_at = excluded.updated_at
|
|
2204
|
-
`).run(machine, machine, now, now, packageMetadata.version, now);
|
|
2205
|
-
}
|
|
2206
|
-
function openSourceDatabase(path) {
|
|
2207
|
-
try {
|
|
2208
|
-
return new BunDatabase(path, { readonly: true });
|
|
2209
|
-
} catch {
|
|
2210
|
-
return new BunDatabase(path);
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
2214
|
-
if (!existsSync3(sourcePath))
|
|
2215
|
-
throw new Error(`source database does not exist: ${sourcePath}`);
|
|
2216
|
-
const source = openSourceDatabase(sourcePath);
|
|
2217
|
-
const now = opts.now ?? new Date().toISOString();
|
|
2218
|
-
const sourceMachine = detectSourceMachine(source, opts.sourceMachine);
|
|
2219
|
-
const tables = [];
|
|
2220
|
-
try {
|
|
2221
|
-
target.exec("PRAGMA foreign_keys = OFF");
|
|
2222
|
-
target.exec("BEGIN IMMEDIATE");
|
|
2223
|
-
try {
|
|
2224
|
-
tables.push(mergeProjects(target, source));
|
|
2225
|
-
const sessionMerge = mergeIdentityTable(target, source, "sessions", sourceMachine, now);
|
|
2226
|
-
tables.push(sessionMerge.stats);
|
|
2227
|
-
tables.push(mergeIdentityTable(target, source, "requests", sourceMachine, now, sessionMerge.idMap).stats);
|
|
2228
|
-
for (const table of GENERIC_PEER_TABLES) {
|
|
2229
|
-
tables.push(mergeGenericTable(target, source, table, sourceMachine, now));
|
|
2230
|
-
}
|
|
2231
|
-
ensureMachineRegistry(target, sourceMachine, now);
|
|
2232
|
-
target.exec("COMMIT");
|
|
2233
|
-
} catch (err) {
|
|
2234
|
-
target.exec("ROLLBACK");
|
|
2235
|
-
throw err;
|
|
2236
|
-
} finally {
|
|
2237
|
-
target.exec("PRAGMA foreign_keys = ON");
|
|
1832
|
+
const totalAgentApiEq = rows.reduce((sum, row) => sum + row.api_eq, 0);
|
|
1833
|
+
for (const row of db.prepare(`
|
|
1834
|
+
SELECT agent,
|
|
1835
|
+
COALESCE(SUM(cost_usd), 0) as api_eq,
|
|
1836
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as included
|
|
1837
|
+
FROM requests WHERE ${where}
|
|
1838
|
+
GROUP BY agent
|
|
1839
|
+
`).all()) {
|
|
1840
|
+
const agentSubs = subscriptionByAgent.get(row.agent) ?? { fee: 0, included: 0 };
|
|
1841
|
+
const globalShare = totalAgentApiEq > 0 ? row.api_eq / totalAgentApiEq : 0;
|
|
1842
|
+
const agentFee = prorateMonthlyFee(agentSubs.fee + globalSubs.fee * globalShare, period);
|
|
1843
|
+
const agentIncludedCap = agentSubs.included + globalSubs.included * globalShare;
|
|
1844
|
+
const agentIncludedConsumed = proratedIncludedConsumed(row.included, agentIncludedCap, period);
|
|
1845
|
+
const agentOnDemand = onDemandByAgent.get(row.agent) ?? 0;
|
|
1846
|
+
byAgent[row.agent] = {
|
|
1847
|
+
api_equivalent_usd: row.api_eq,
|
|
1848
|
+
subscription_fee_usd: agentFee,
|
|
1849
|
+
included_consumed_usd: agentIncludedConsumed,
|
|
1850
|
+
on_demand_usd: agentOnDemand,
|
|
1851
|
+
saved_usd: computeSavedUsd(row.api_eq, agentOnDemand, agentFee)
|
|
1852
|
+
};
|
|
2238
1853
|
}
|
|
2239
|
-
} finally {
|
|
2240
|
-
source.close();
|
|
2241
1854
|
}
|
|
2242
|
-
const deduped = dedupeRequests(target);
|
|
2243
|
-
const rowsWritten = tables.reduce((sum, table) => sum + table.inserted + table.updated, 0);
|
|
2244
|
-
const collisions = tables.reduce((sum, table) => sum + table.collisions, 0);
|
|
2245
1855
|
return {
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
1856
|
+
period,
|
|
1857
|
+
api_equivalent_usd: apiEquivalent,
|
|
1858
|
+
subscription_fee_usd: subscriptionFee,
|
|
1859
|
+
included_consumed_usd: includedConsumed,
|
|
1860
|
+
on_demand_usd: onDemand,
|
|
1861
|
+
saved_usd: saved,
|
|
1862
|
+
by_agent: byAgent
|
|
2252
1863
|
};
|
|
2253
1864
|
}
|
|
2254
|
-
|
|
1865
|
+
|
|
1866
|
+
// src/lib/billing-diff.ts
|
|
2255
1867
|
init_database();
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
const
|
|
2265
|
-
const
|
|
2266
|
-
const
|
|
2267
|
-
const
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
const
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
generatedAt,
|
|
2287
|
-
timeoutMs,
|
|
2288
|
-
runner,
|
|
2289
|
-
dryRun,
|
|
2290
|
-
remoteSync,
|
|
2291
|
-
cacheDir,
|
|
2292
|
-
remoteSnapshotDir
|
|
2293
|
-
}));
|
|
1868
|
+
var PROVIDER_TO_AGENT = {
|
|
1869
|
+
anthropic: "claude",
|
|
1870
|
+
openai: "codex",
|
|
1871
|
+
gemini: "gemini",
|
|
1872
|
+
google: "gemini"
|
|
1873
|
+
};
|
|
1874
|
+
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
1875
|
+
const estimated = querySummary(db, period, undefined, true);
|
|
1876
|
+
const actual = queryBillingSummary(db, period);
|
|
1877
|
+
const delta = estimated.total_usd - actual.total_usd;
|
|
1878
|
+
const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
|
|
1879
|
+
const agentRows = db.prepare(`
|
|
1880
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
1881
|
+
FROM requests
|
|
1882
|
+
WHERE ${periodWhere3(period, "timestamp")}
|
|
1883
|
+
GROUP BY agent
|
|
1884
|
+
`).all();
|
|
1885
|
+
const by_agent = agentRows.map((row) => {
|
|
1886
|
+
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
1887
|
+
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
1888
|
+
const rowDelta = row.estimated_usd - actualUsd;
|
|
1889
|
+
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
1890
|
+
return {
|
|
1891
|
+
agent: row.agent,
|
|
1892
|
+
estimated_usd: row.estimated_usd,
|
|
1893
|
+
actual_usd: actualUsd,
|
|
1894
|
+
delta_usd: rowDelta,
|
|
1895
|
+
delta_pct: rowPct
|
|
1896
|
+
};
|
|
1897
|
+
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
2294
1898
|
return {
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
|
|
2305
|
-
}
|
|
1899
|
+
period,
|
|
1900
|
+
estimated_usd: estimated.total_usd,
|
|
1901
|
+
actual_usd: actual.total_usd,
|
|
1902
|
+
delta_usd: delta,
|
|
1903
|
+
delta_pct: deltaPct,
|
|
1904
|
+
threshold_pct: thresholdPct,
|
|
1905
|
+
is_alert: Math.abs(deltaPct) > thresholdPct,
|
|
1906
|
+
by_agent,
|
|
1907
|
+
by_provider: actual.by_provider
|
|
2306
1908
|
};
|
|
2307
1909
|
}
|
|
2308
|
-
function
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
status: "failed",
|
|
2323
|
-
remote_snapshot_path: remoteSnapshotPath
|
|
2324
|
-
};
|
|
2325
|
-
if (context.remoteSync) {
|
|
2326
|
-
result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
|
|
2327
|
-
if (result.remote_sync.code !== 0) {
|
|
2328
|
-
result.error = "remote economy sync failed";
|
|
2329
|
-
return result;
|
|
2330
|
-
}
|
|
1910
|
+
function periodWhere3(period, column) {
|
|
1911
|
+
switch (period) {
|
|
1912
|
+
case "today":
|
|
1913
|
+
return `DATE(${column}) = DATE('now')`;
|
|
1914
|
+
case "yesterday":
|
|
1915
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
1916
|
+
case "week":
|
|
1917
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
1918
|
+
case "month":
|
|
1919
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
1920
|
+
case "year":
|
|
1921
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
1922
|
+
case "all":
|
|
1923
|
+
return "1=1";
|
|
2331
1924
|
}
|
|
2332
|
-
result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
|
|
2333
|
-
if (result.remote_snapshot.code !== 0) {
|
|
2334
|
-
result.error = "remote snapshot failed";
|
|
2335
|
-
return result;
|
|
2336
|
-
}
|
|
2337
|
-
const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
|
|
2338
|
-
if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
|
|
2339
|
-
result.error = `remote snapshot path refused: ${emittedPath}`;
|
|
2340
|
-
return result;
|
|
2341
|
-
}
|
|
2342
|
-
result.remote_snapshot_path = emittedPath;
|
|
2343
|
-
result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
|
|
2344
|
-
context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
|
|
2345
|
-
if (result.pull.code !== 0) {
|
|
2346
|
-
result.error = "snapshot pull failed; no cached DB merged";
|
|
2347
|
-
rmSync(tmpLocal, { force: true });
|
|
2348
|
-
return result;
|
|
2349
|
-
}
|
|
2350
|
-
const integrity = verifySqliteIntegrity(tmpLocal);
|
|
2351
|
-
result.local_integrity = integrity;
|
|
2352
|
-
if (!integrity.ok) {
|
|
2353
|
-
result.error = "pulled snapshot failed local integrity check";
|
|
2354
|
-
rmSync(tmpLocal, { force: true });
|
|
2355
|
-
return result;
|
|
2356
|
-
}
|
|
2357
|
-
renameSync(tmpLocal, verifiedLocal);
|
|
2358
|
-
const db = openDatabase();
|
|
2359
|
-
result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
|
|
2360
|
-
result.status = "merged";
|
|
2361
|
-
return result;
|
|
2362
1925
|
}
|
|
2363
|
-
function remoteSnapshotCommand(remoteDir, filename) {
|
|
2364
|
-
const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
|
|
2365
|
-
const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
2366
|
-
return [
|
|
2367
|
-
"set -euo pipefail",
|
|
2368
|
-
'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
|
|
2369
|
-
`snapshot_dir=${dirExpr}`,
|
|
2370
|
-
`out="$snapshot_dir/${safeFilename}"`,
|
|
2371
|
-
'test -f "$db"',
|
|
2372
|
-
'mkdir -p "$snapshot_dir"',
|
|
2373
|
-
'rm -f "$out"',
|
|
2374
|
-
`sqlite3 "$db" "VACUUM INTO '$out'"`,
|
|
2375
|
-
'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
|
|
2376
|
-
'printf "%s\\n" "$out"'
|
|
2377
|
-
].join("; ");
|
|
2378
|
-
}
|
|
2379
|
-
function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
|
|
2380
|
-
if (!path || path.includes("\x00") || path.includes(`
|
|
2381
|
-
`) || path.includes("\r"))
|
|
2382
|
-
return false;
|
|
2383
|
-
if (!path.endsWith(".db"))
|
|
2384
|
-
return false;
|
|
2385
|
-
if (path.includes(".."))
|
|
2386
|
-
return false;
|
|
2387
|
-
const prefix = remoteSnapshotDir.replace(/\/$/, "");
|
|
2388
|
-
return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
|
|
2389
|
-
}
|
|
2390
|
-
function verifySqliteIntegrity(path) {
|
|
2391
|
-
if (!existsSync4(path))
|
|
2392
|
-
return { ok: false, detail: "file does not exist" };
|
|
2393
|
-
let db;
|
|
2394
|
-
try {
|
|
2395
|
-
db = new Database2(path, { readonly: true });
|
|
2396
|
-
const row = db.query("PRAGMA integrity_check").get();
|
|
2397
|
-
const detail = Object.values(row ?? {})[0] ?? "";
|
|
2398
|
-
return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
|
|
2399
|
-
} catch (error) {
|
|
2400
|
-
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
2401
|
-
} finally {
|
|
2402
|
-
db?.close();
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
function writeFleetSyncReport(reportDir, result) {
|
|
2406
|
-
mkdirSync3(reportDir, { recursive: true });
|
|
2407
|
-
const path = join3(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
|
|
2408
|
-
writeFileSync2(path, `${JSON.stringify(result, null, 2)}
|
|
2409
|
-
`);
|
|
2410
|
-
return path;
|
|
2411
|
-
}
|
|
2412
|
-
function spawnCommand(command, args, options) {
|
|
2413
|
-
const result = spawnSync(command, args, {
|
|
2414
|
-
encoding: "utf8",
|
|
2415
|
-
timeout: options.timeoutMs,
|
|
2416
|
-
maxBuffer: 1024 * 1024,
|
|
2417
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2418
|
-
});
|
|
2419
|
-
return {
|
|
2420
|
-
code: result.status,
|
|
2421
|
-
stdout: result.stdout ?? "",
|
|
2422
|
-
stderr: result.stderr ?? "",
|
|
2423
|
-
...result.error ? { error: result.error.message } : {}
|
|
2424
|
-
};
|
|
2425
|
-
}
|
|
2426
|
-
function shellQuote(value) {
|
|
2427
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
2428
|
-
}
|
|
2429
|
-
function normalizePositiveInteger(value, fallback) {
|
|
2430
|
-
if (value == null || !Number.isFinite(value) || value < 1)
|
|
2431
|
-
return fallback;
|
|
2432
|
-
return Math.floor(value);
|
|
2433
|
-
}
|
|
2434
|
-
// src/ingest/claude.ts
|
|
2435
|
-
init_database();
|
|
2436
|
-
init_pricing();
|
|
2437
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
2438
|
-
import { homedir as homedir3 } from "os";
|
|
2439
|
-
import { join as join4, basename } from "path";
|
|
2440
1926
|
|
|
2441
|
-
// src/lib/
|
|
2442
|
-
function
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
1927
|
+
// src/lib/periods.ts
|
|
1928
|
+
function ymd(date) {
|
|
1929
|
+
return date.toISOString().substring(0, 10);
|
|
1930
|
+
}
|
|
1931
|
+
function usageSnapshotFilterForPeriod(period) {
|
|
1932
|
+
const now = new Date;
|
|
1933
|
+
switch (period) {
|
|
1934
|
+
case "today":
|
|
1935
|
+
return { date: ymd(now) };
|
|
1936
|
+
case "yesterday": {
|
|
1937
|
+
const yesterday = new Date(now);
|
|
1938
|
+
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
1939
|
+
return { date: ymd(yesterday) };
|
|
1940
|
+
}
|
|
1941
|
+
case "week": {
|
|
1942
|
+
const weekAgo = new Date(now);
|
|
1943
|
+
weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
|
|
1944
|
+
return { since: ymd(weekAgo) };
|
|
1945
|
+
}
|
|
1946
|
+
case "month":
|
|
1947
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
|
|
1948
|
+
case "year":
|
|
1949
|
+
return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
|
|
1950
|
+
case "all":
|
|
1951
|
+
return {};
|
|
1952
|
+
}
|
|
2448
1953
|
}
|
|
2449
1954
|
|
|
2450
|
-
// src/lib/
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
1955
|
+
// src/lib/brief.ts
|
|
1956
|
+
init_database();
|
|
1957
|
+
var ZERO_REQUEST_TOTALS = {
|
|
1958
|
+
sessions: 0,
|
|
1959
|
+
request_sessions: 0,
|
|
1960
|
+
requests: 0,
|
|
1961
|
+
input_tokens: 0,
|
|
1962
|
+
output_tokens: 0,
|
|
1963
|
+
cache_read_tokens: 0,
|
|
1964
|
+
cache_create_tokens: 0,
|
|
1965
|
+
cache_create_5m_tokens: 0,
|
|
1966
|
+
cache_create_1h_tokens: 0,
|
|
1967
|
+
total_tokens: 0,
|
|
1968
|
+
cost_usd: 0,
|
|
1969
|
+
last_active: null
|
|
2460
1970
|
};
|
|
2461
|
-
|
|
2462
|
-
|
|
1971
|
+
var ZERO_SESSION_TOTALS = {
|
|
1972
|
+
sessions: 0,
|
|
1973
|
+
requests: 0,
|
|
1974
|
+
total_tokens: 0,
|
|
1975
|
+
cost_usd: 0,
|
|
1976
|
+
last_active: null
|
|
1977
|
+
};
|
|
1978
|
+
function formatAge(timestamp, now) {
|
|
1979
|
+
if (!timestamp)
|
|
1980
|
+
return "never";
|
|
1981
|
+
const t = new Date(timestamp).getTime();
|
|
1982
|
+
if (!Number.isFinite(t))
|
|
1983
|
+
return "unknown";
|
|
1984
|
+
const ageMs = Math.max(0, now.getTime() - t);
|
|
1985
|
+
const minutes = Math.floor(ageMs / 60000);
|
|
1986
|
+
if (minutes < 1)
|
|
1987
|
+
return "<1m";
|
|
1988
|
+
if (minutes < 60)
|
|
1989
|
+
return `${minutes}m`;
|
|
1990
|
+
const hours = Math.floor(minutes / 60);
|
|
1991
|
+
if (hours < 48)
|
|
1992
|
+
return `${hours}h`;
|
|
1993
|
+
return `${Math.floor(hours / 24)}d`;
|
|
1994
|
+
}
|
|
1995
|
+
function machineFilter(machine) {
|
|
1996
|
+
if (!machine || machine === "all")
|
|
1997
|
+
return;
|
|
1998
|
+
return machine;
|
|
2463
1999
|
}
|
|
2464
|
-
function
|
|
2465
|
-
|
|
2466
|
-
|
|
2000
|
+
function requestPeriodWhere2(period) {
|
|
2001
|
+
if (period === "today")
|
|
2002
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
2003
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
2467
2004
|
}
|
|
2468
|
-
function
|
|
2469
|
-
|
|
2005
|
+
function sessionPeriodWhere2(period) {
|
|
2006
|
+
if (period === "today")
|
|
2007
|
+
return `DATE(started_at) = DATE('now')`;
|
|
2008
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
2470
2009
|
}
|
|
2471
|
-
function
|
|
2010
|
+
function baseRequestTotals(row) {
|
|
2472
2011
|
return {
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2012
|
+
...ZERO_REQUEST_TOTALS,
|
|
2013
|
+
...row,
|
|
2014
|
+
sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
2015
|
+
request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
2016
|
+
requests: Number(row?.requests ?? 0),
|
|
2017
|
+
input_tokens: Number(row?.input_tokens ?? 0),
|
|
2018
|
+
output_tokens: Number(row?.output_tokens ?? 0),
|
|
2019
|
+
cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
|
|
2020
|
+
cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
|
|
2021
|
+
cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
|
|
2022
|
+
cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
|
|
2023
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
2024
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
2025
|
+
last_active: row?.last_active ?? null
|
|
2478
2026
|
};
|
|
2479
2027
|
}
|
|
2480
|
-
function
|
|
2481
|
-
const value = raw.trim();
|
|
2482
|
-
if (!value)
|
|
2483
|
-
return null;
|
|
2484
|
-
const candidateTool = AGENT_ACCOUNT_TOOLS[agent][0] ?? agent;
|
|
2485
|
-
const [tool, name] = value.includes(":") ? value.split(":", 2) : [candidateTool, value];
|
|
2486
|
-
if (!tool || !name)
|
|
2487
|
-
return null;
|
|
2488
|
-
const email = name.includes("@") ? normalizeEmail(name) : undefined;
|
|
2028
|
+
function baseSessionTotals(row) {
|
|
2489
2029
|
return {
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2030
|
+
sessions: Number(row?.sessions ?? 0),
|
|
2031
|
+
requests: Number(row?.requests ?? 0),
|
|
2032
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
2033
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
2034
|
+
last_active: row?.last_active ?? null
|
|
2495
2035
|
};
|
|
2496
2036
|
}
|
|
2497
|
-
function
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
return fromOverride(raw, agent);
|
|
2502
|
-
const tool = env[`ECONOMY_${agentPrefix}_ACCOUNT_TOOL`] ?? env["ECONOMY_ACCOUNT_TOOL"];
|
|
2503
|
-
const name = env[`ECONOMY_${agentPrefix}_ACCOUNT_NAME`] ?? env["ECONOMY_ACCOUNT_NAME"];
|
|
2504
|
-
if (!tool || !name)
|
|
2505
|
-
return null;
|
|
2506
|
-
const email = normalizeEmail(env[`ECONOMY_${agentPrefix}_ACCOUNT_EMAIL`] ?? env["ECONOMY_ACCOUNT_EMAIL"]);
|
|
2037
|
+
function latestTimestamp(...values) {
|
|
2038
|
+
return values.filter(Boolean).sort().at(-1) ?? null;
|
|
2039
|
+
}
|
|
2040
|
+
function combineTotals(requests, sessions) {
|
|
2507
2041
|
return {
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2042
|
+
sessions: requests.request_sessions + sessions.sessions,
|
|
2043
|
+
requests: requests.requests + sessions.requests,
|
|
2044
|
+
input_tokens: requests.input_tokens,
|
|
2045
|
+
output_tokens: requests.output_tokens,
|
|
2046
|
+
cache_read_tokens: requests.cache_read_tokens,
|
|
2047
|
+
cache_create_tokens: requests.cache_create_tokens,
|
|
2048
|
+
cache_create_5m_tokens: requests.cache_create_5m_tokens,
|
|
2049
|
+
cache_create_1h_tokens: requests.cache_create_1h_tokens,
|
|
2050
|
+
total_tokens: requests.total_tokens + sessions.total_tokens,
|
|
2051
|
+
cost_usd: requests.cost_usd + sessions.cost_usd,
|
|
2052
|
+
last_active: latestTimestamp(requests.last_active, sessions.last_active)
|
|
2513
2053
|
};
|
|
2514
2054
|
}
|
|
2515
|
-
function
|
|
2516
|
-
|
|
2517
|
-
return new Set(api.listTools().map((tool) => tool.id));
|
|
2518
|
-
} catch {
|
|
2519
|
-
return new Set;
|
|
2520
|
-
}
|
|
2055
|
+
function emptyTotals() {
|
|
2056
|
+
return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
|
|
2521
2057
|
}
|
|
2522
|
-
function
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2529
|
-
} catch {
|
|
2530
|
-
return null;
|
|
2058
|
+
function requestTotalsSql(where, machine) {
|
|
2059
|
+
const params = [];
|
|
2060
|
+
let machineClause = "";
|
|
2061
|
+
if (machine) {
|
|
2062
|
+
machineClause = " AND machine_id = ?";
|
|
2063
|
+
params.push(machine);
|
|
2531
2064
|
}
|
|
2065
|
+
return {
|
|
2066
|
+
sql: `
|
|
2067
|
+
SELECT
|
|
2068
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
2069
|
+
COUNT(*) as requests,
|
|
2070
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
2071
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
2072
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
2073
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
2074
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
2075
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
2076
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2077
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2078
|
+
MAX(timestamp) as last_active
|
|
2079
|
+
FROM requests
|
|
2080
|
+
WHERE ${where}${machineClause}
|
|
2081
|
+
`,
|
|
2082
|
+
params
|
|
2083
|
+
};
|
|
2532
2084
|
}
|
|
2533
|
-
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
api = await import("@hasna/accounts");
|
|
2540
|
-
} catch {
|
|
2541
|
-
return null;
|
|
2542
|
-
}
|
|
2543
|
-
const toolIds = knownToolIds(api);
|
|
2544
|
-
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2545
|
-
if (!toolIds.has(toolId))
|
|
2546
|
-
continue;
|
|
2547
|
-
let tool;
|
|
2548
|
-
try {
|
|
2549
|
-
tool = api.getTool(toolId);
|
|
2550
|
-
} catch {
|
|
2551
|
-
continue;
|
|
2552
|
-
}
|
|
2553
|
-
const envProfile = profileForEnvDir(api, tool, env);
|
|
2554
|
-
if (envProfile)
|
|
2555
|
-
return fromProfile(envProfile, "env");
|
|
2556
|
-
try {
|
|
2557
|
-
const applied = api.appliedProfile(toolId);
|
|
2558
|
-
if (applied)
|
|
2559
|
-
return fromProfile(applied, "applied");
|
|
2560
|
-
} catch {}
|
|
2561
|
-
try {
|
|
2562
|
-
const current = api.currentProfile(toolId);
|
|
2563
|
-
if (current)
|
|
2564
|
-
return fromProfile(current, "current");
|
|
2565
|
-
} catch {}
|
|
2085
|
+
function sessionOnlyTotalsSql(where, machine) {
|
|
2086
|
+
const params = [];
|
|
2087
|
+
let machineClause = "";
|
|
2088
|
+
if (machine) {
|
|
2089
|
+
machineClause = " AND machine_id = ?";
|
|
2090
|
+
params.push(machine);
|
|
2566
2091
|
}
|
|
2567
|
-
return
|
|
2092
|
+
return {
|
|
2093
|
+
sql: `
|
|
2094
|
+
SELECT
|
|
2095
|
+
COUNT(*) as sessions,
|
|
2096
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
2097
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2098
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2099
|
+
MAX(started_at) as last_active
|
|
2100
|
+
FROM sessions
|
|
2101
|
+
WHERE ${where}${machineClause}
|
|
2102
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
2103
|
+
`,
|
|
2104
|
+
params
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
function queryRequestTotals(db, where, params, machine) {
|
|
2108
|
+
const q = requestTotalsSql(where, machine);
|
|
2109
|
+
return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
2110
|
+
}
|
|
2111
|
+
function querySessionOnlyTotals(db, where, params, machine) {
|
|
2112
|
+
const q = sessionOnlyTotalsSql(where, machine);
|
|
2113
|
+
return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
2568
2114
|
}
|
|
2569
|
-
function
|
|
2570
|
-
|
|
2571
|
-
|
|
2115
|
+
function queryPeriodTotals(db, period, machine) {
|
|
2116
|
+
const summary = querySummary(db, period, machine);
|
|
2117
|
+
const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
|
|
2118
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
|
|
2119
|
+
const totals = combineTotals(requests, sessions);
|
|
2572
2120
|
return {
|
|
2573
|
-
...
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
account_source: account.account_source
|
|
2121
|
+
...totals,
|
|
2122
|
+
sessions: summary.sessions,
|
|
2123
|
+
requests: summary.requests,
|
|
2124
|
+
total_tokens: summary.tokens,
|
|
2125
|
+
cost_usd: summary.total_usd
|
|
2579
2126
|
};
|
|
2580
2127
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
return
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
walk(join4(dir, entry.name));
|
|
2598
|
-
else if (entry.name.endsWith(".jsonl"))
|
|
2599
|
-
files.push(join4(dir, entry.name));
|
|
2600
|
-
}
|
|
2601
|
-
} catch {}
|
|
2602
|
-
}
|
|
2603
|
-
walk(projectDir);
|
|
2604
|
-
return files;
|
|
2605
|
-
}
|
|
2606
|
-
async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
|
|
2607
|
-
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
2608
|
-
}
|
|
2609
|
-
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
2610
|
-
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
2611
|
-
}
|
|
2612
|
-
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
2613
|
-
if (!existsSync5(projectsDir)) {
|
|
2614
|
-
if (verbose)
|
|
2615
|
-
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
2616
|
-
return { files: 0, requests: 0, sessions: 0 };
|
|
2617
|
-
}
|
|
2618
|
-
const machineId = getMachineId();
|
|
2619
|
-
let totalFiles = 0;
|
|
2620
|
-
let totalRequests = 0;
|
|
2621
|
-
const touchedSessions = new Set;
|
|
2622
|
-
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
2623
|
-
const account = await resolveAccountForAgent(agentName);
|
|
2624
|
-
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
2625
|
-
for (const projectDirEntry of projectDirs) {
|
|
2626
|
-
const projectDirPath = join4(projectsDir, projectDirEntry.name);
|
|
2627
|
-
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
2628
|
-
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
2629
|
-
for (const filePath of jsonlFiles) {
|
|
2630
|
-
const stateKey = filePath.replace(projectsDir, "");
|
|
2631
|
-
let fileMtime = "0";
|
|
2632
|
-
try {
|
|
2633
|
-
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
2634
|
-
} catch {
|
|
2635
|
-
continue;
|
|
2636
|
-
}
|
|
2637
|
-
const processed = getIngestState(db, agentName, stateKey);
|
|
2638
|
-
if (processed === fileMtime)
|
|
2639
|
-
continue;
|
|
2640
|
-
let lines;
|
|
2641
|
-
try {
|
|
2642
|
-
lines = readFileSync3(filePath, "utf-8").split(`
|
|
2643
|
-
`).filter((l) => l.trim());
|
|
2644
|
-
} catch {
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
const fileBasename = basename(filePath, ".jsonl");
|
|
2648
|
-
const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
|
|
2649
|
-
let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
|
|
2650
|
-
let sessionCwd = projectPath;
|
|
2651
|
-
for (const line of lines) {
|
|
2652
|
-
let entry;
|
|
2653
|
-
try {
|
|
2654
|
-
entry = JSON.parse(line);
|
|
2655
|
-
} catch {
|
|
2656
|
-
continue;
|
|
2657
|
-
}
|
|
2658
|
-
if (entry.sessionId)
|
|
2659
|
-
sessionId = entry.sessionId;
|
|
2660
|
-
if (entry.cwd)
|
|
2661
|
-
sessionCwd = entry.cwd;
|
|
2662
|
-
if (entry.message?.role !== "assistant")
|
|
2663
|
-
continue;
|
|
2664
|
-
const usage = entry.message.usage;
|
|
2665
|
-
if (!usage)
|
|
2666
|
-
continue;
|
|
2667
|
-
const model = entry.message.model;
|
|
2668
|
-
if (!model)
|
|
2669
|
-
continue;
|
|
2670
|
-
const inputTokens = usage.input_tokens ?? 0;
|
|
2671
|
-
const outputTokens = usage.output_tokens ?? 0;
|
|
2672
|
-
const cacheWrite5mTokens = usage.cache_creation?.ephemeral_5m_input_tokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
2673
|
-
const cacheWrite1hTokens = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
2674
|
-
const cacheWriteTokens = cacheWrite5mTokens + cacheWrite1hTokens;
|
|
2675
|
-
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
2676
|
-
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
2677
|
-
if (inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens === 0)
|
|
2678
|
-
continue;
|
|
2679
|
-
let costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens);
|
|
2680
|
-
costUsd = applyClaudeModifiers(costUsd, model, usage, entry);
|
|
2681
|
-
const serverToolUse = usage.server_tool_use;
|
|
2682
|
-
if (serverToolUse?.web_search_requests) {
|
|
2683
|
-
costUsd += serverToolUse.web_search_requests * 0.01;
|
|
2684
|
-
}
|
|
2685
|
-
const sourceRequestId = entry.requestId ?? entry.request_id ?? entry.message.id ?? entry.uuid ?? `${sessionId}-${timestamp}`;
|
|
2686
|
-
const reqId = `${agentName}-${sourceRequestId}`;
|
|
2687
|
-
upsertRequest(db, withAccount({
|
|
2688
|
-
id: reqId,
|
|
2689
|
-
agent: agentName,
|
|
2690
|
-
session_id: sessionId,
|
|
2691
|
-
model,
|
|
2692
|
-
input_tokens: inputTokens,
|
|
2693
|
-
output_tokens: outputTokens,
|
|
2694
|
-
cache_read_tokens: cacheReadTokens,
|
|
2695
|
-
cache_create_tokens: cacheWriteTokens,
|
|
2696
|
-
cache_create_5m_tokens: cacheWrite5mTokens,
|
|
2697
|
-
cache_create_1h_tokens: cacheWrite1hTokens,
|
|
2698
|
-
cost_usd: costUsd,
|
|
2699
|
-
cost_basis: defaultCostBasisForAgent(agentName),
|
|
2700
|
-
duration_ms: 0,
|
|
2701
|
-
timestamp,
|
|
2702
|
-
source_request_id: sourceRequestId,
|
|
2703
|
-
machine_id: machineId
|
|
2704
|
-
}, account));
|
|
2705
|
-
if (!touchedSessions.has(sessionId)) {
|
|
2706
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
2707
|
-
if (!existing) {
|
|
2708
|
-
const effectiveCwd = sessionCwd || projectPath;
|
|
2709
|
-
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
2710
|
-
const session = {
|
|
2711
|
-
id: sessionId,
|
|
2712
|
-
agent: agentName,
|
|
2713
|
-
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
2714
|
-
project_name: detectedProject ? detectedProject.name : "",
|
|
2715
|
-
started_at: timestamp,
|
|
2716
|
-
ended_at: null,
|
|
2717
|
-
total_cost_usd: 0,
|
|
2718
|
-
total_tokens: 0,
|
|
2719
|
-
request_count: 0,
|
|
2720
|
-
machine_id: machineId
|
|
2721
|
-
};
|
|
2722
|
-
upsertSession(db, withAccount(session, account));
|
|
2723
|
-
}
|
|
2724
|
-
touchedSessions.add(sessionId);
|
|
2725
|
-
}
|
|
2726
|
-
totalRequests++;
|
|
2727
|
-
}
|
|
2728
|
-
setIngestState(db, agentName, stateKey, fileMtime);
|
|
2729
|
-
totalFiles++;
|
|
2730
|
-
}
|
|
2128
|
+
function querySinceTotals(db, since, machine) {
|
|
2129
|
+
const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
|
|
2130
|
+
const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
|
|
2131
|
+
return combineTotals(requests, sessions);
|
|
2132
|
+
}
|
|
2133
|
+
function parseSinceDate(input, now) {
|
|
2134
|
+
const value = input.trim();
|
|
2135
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
|
|
2136
|
+
if (match) {
|
|
2137
|
+
const amount = Number(match[1]);
|
|
2138
|
+
const unit = match[2].toLowerCase();
|
|
2139
|
+
const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
|
|
2140
|
+
return {
|
|
2141
|
+
timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
|
|
2142
|
+
label: value
|
|
2143
|
+
};
|
|
2731
2144
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2145
|
+
const parsed = new Date(value);
|
|
2146
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
2147
|
+
throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
|
|
2734
2148
|
}
|
|
2735
|
-
return {
|
|
2149
|
+
return { timestamp: parsed.toISOString(), label: value };
|
|
2736
2150
|
}
|
|
2737
|
-
function
|
|
2738
|
-
|
|
2739
|
-
const
|
|
2740
|
-
|
|
2741
|
-
|
|
2151
|
+
function queryMachineSinceRows(db, since, machine) {
|
|
2152
|
+
const knownMachines = new Set;
|
|
2153
|
+
for (const m of listMachines(db, "all")) {
|
|
2154
|
+
if (m.machine_id)
|
|
2155
|
+
knownMachines.add(m.machine_id);
|
|
2156
|
+
}
|
|
2157
|
+
for (const m of listMachineRegistry(db)) {
|
|
2158
|
+
if (m.machine_id)
|
|
2159
|
+
knownMachines.add(m.machine_id);
|
|
2742
2160
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2161
|
+
if (machine)
|
|
2162
|
+
knownMachines.add(machine);
|
|
2163
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
2164
|
+
const requestRows = db.prepare(`
|
|
2165
|
+
SELECT
|
|
2166
|
+
machine_id,
|
|
2167
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
2168
|
+
COUNT(*) as requests,
|
|
2169
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
2170
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
2171
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
2172
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
2173
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
2174
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
2175
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
2176
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2177
|
+
MAX(timestamp) as last_active
|
|
2178
|
+
FROM requests
|
|
2179
|
+
WHERE timestamp >= ?
|
|
2180
|
+
AND machine_id != ''
|
|
2181
|
+
${requestMachineClause}
|
|
2182
|
+
GROUP BY machine_id
|
|
2183
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
2184
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
2185
|
+
const sessionRows = db.prepare(`
|
|
2186
|
+
SELECT
|
|
2187
|
+
machine_id,
|
|
2188
|
+
COUNT(*) as sessions,
|
|
2189
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
2190
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2191
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
2192
|
+
MAX(started_at) as last_active
|
|
2193
|
+
FROM sessions
|
|
2194
|
+
WHERE started_at >= ?
|
|
2195
|
+
AND machine_id != ''
|
|
2196
|
+
${sessionMachineClause}
|
|
2197
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
2198
|
+
GROUP BY machine_id
|
|
2199
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
2200
|
+
const groups = new Map;
|
|
2201
|
+
for (const row of requestRows) {
|
|
2202
|
+
knownMachines.add(row.machine_id);
|
|
2203
|
+
groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
|
|
2204
|
+
}
|
|
2205
|
+
for (const row of sessionRows) {
|
|
2206
|
+
knownMachines.add(row.machine_id);
|
|
2207
|
+
const current = groups.get(row.machine_id) ?? emptyTotals();
|
|
2208
|
+
const sessions = baseSessionTotals(row);
|
|
2209
|
+
groups.set(row.machine_id, {
|
|
2210
|
+
...current,
|
|
2211
|
+
sessions: current.sessions + sessions.sessions,
|
|
2212
|
+
requests: current.requests + sessions.requests,
|
|
2213
|
+
total_tokens: current.total_tokens + sessions.total_tokens,
|
|
2214
|
+
cost_usd: current.cost_usd + sessions.cost_usd,
|
|
2215
|
+
last_active: latestTimestamp(current.last_active, sessions.last_active)
|
|
2216
|
+
});
|
|
2746
2217
|
}
|
|
2747
|
-
|
|
2218
|
+
const lastData = queryMachineLastData(db, machine);
|
|
2219
|
+
const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
|
|
2220
|
+
const totals = groups.get(machineId) ?? emptyTotals();
|
|
2221
|
+
const lastDataAt = lastData.get(machineId) ?? totals.last_active;
|
|
2222
|
+
return {
|
|
2223
|
+
machine_id: machineId,
|
|
2224
|
+
...totals,
|
|
2225
|
+
last_data_at: lastDataAt,
|
|
2226
|
+
last_data_age: ""
|
|
2227
|
+
};
|
|
2228
|
+
});
|
|
2229
|
+
return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
|
|
2748
2230
|
}
|
|
2749
|
-
function
|
|
2750
|
-
const
|
|
2751
|
-
const
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2231
|
+
function queryMachineLastData(db, machine) {
|
|
2232
|
+
const filter = machine ? " AND machine_id = ?" : "";
|
|
2233
|
+
const requestRows = db.prepare(`
|
|
2234
|
+
SELECT machine_id, MAX(timestamp) as last_at
|
|
2235
|
+
FROM requests
|
|
2236
|
+
WHERE machine_id != ''${filter}
|
|
2237
|
+
GROUP BY machine_id
|
|
2238
|
+
`).all(...machine ? [machine] : []);
|
|
2239
|
+
const sessionRows = db.prepare(`
|
|
2240
|
+
SELECT machine_id, MAX(started_at) as last_at
|
|
2241
|
+
FROM sessions
|
|
2242
|
+
WHERE machine_id != ''${filter}
|
|
2243
|
+
GROUP BY machine_id
|
|
2244
|
+
`).all(...machine ? [machine] : []);
|
|
2245
|
+
const result = new Map;
|
|
2246
|
+
for (const row of [...requestRows, ...sessionRows]) {
|
|
2247
|
+
result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
|
|
2248
|
+
}
|
|
2249
|
+
return result;
|
|
2757
2250
|
}
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2251
|
+
function queryAgentSinceRows(db, since, machine) {
|
|
2252
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
2253
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
2254
|
+
const rows = db.prepare(`
|
|
2255
|
+
WITH request_rows AS (
|
|
2256
|
+
SELECT
|
|
2257
|
+
agent,
|
|
2258
|
+
session_id,
|
|
2259
|
+
1 as requests,
|
|
2260
|
+
input_tokens,
|
|
2261
|
+
output_tokens,
|
|
2262
|
+
cache_read_tokens,
|
|
2263
|
+
cache_create_tokens,
|
|
2264
|
+
COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
|
|
2265
|
+
COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
|
|
2266
|
+
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
2267
|
+
cost_usd,
|
|
2268
|
+
timestamp as last_active
|
|
2269
|
+
FROM requests
|
|
2270
|
+
WHERE timestamp >= ?${requestMachineClause}
|
|
2271
|
+
),
|
|
2272
|
+
session_only_rows AS (
|
|
2273
|
+
SELECT
|
|
2274
|
+
agent,
|
|
2275
|
+
id as session_id,
|
|
2276
|
+
COALESCE(request_count, 0) as requests,
|
|
2277
|
+
0 as input_tokens,
|
|
2278
|
+
0 as output_tokens,
|
|
2279
|
+
0 as cache_read_tokens,
|
|
2280
|
+
0 as cache_create_tokens,
|
|
2281
|
+
0 as cache_create_5m_tokens,
|
|
2282
|
+
0 as cache_create_1h_tokens,
|
|
2283
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
2284
|
+
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
2285
|
+
started_at as last_active
|
|
2286
|
+
FROM sessions
|
|
2287
|
+
WHERE started_at >= ?${sessionMachineClause}
|
|
2288
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
2289
|
+
),
|
|
2290
|
+
combined AS (
|
|
2291
|
+
SELECT * FROM request_rows
|
|
2292
|
+
UNION ALL
|
|
2293
|
+
SELECT * FROM session_only_rows
|
|
2294
|
+
)
|
|
2295
|
+
SELECT
|
|
2296
|
+
agent,
|
|
2297
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
2298
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
2299
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
2300
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
2301
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
2302
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
2303
|
+
COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
|
|
2304
|
+
COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
|
|
2305
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
2306
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
2307
|
+
MAX(last_active) as last_active
|
|
2308
|
+
FROM combined
|
|
2309
|
+
GROUP BY agent
|
|
2310
|
+
ORDER BY cost_usd DESC
|
|
2311
|
+
`).all(...machine ? [since, machine, since, machine] : [since, since]);
|
|
2312
|
+
if (rows.length > 0)
|
|
2313
|
+
return rows;
|
|
2314
|
+
return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
|
|
2315
|
+
}
|
|
2316
|
+
function agentBreakdownToBriefRow(row) {
|
|
2317
|
+
return {
|
|
2318
|
+
agent: row.agent,
|
|
2319
|
+
sessions: row.sessions,
|
|
2320
|
+
requests: row.requests,
|
|
2321
|
+
input_tokens: 0,
|
|
2322
|
+
output_tokens: 0,
|
|
2323
|
+
cache_read_tokens: 0,
|
|
2324
|
+
cache_create_tokens: 0,
|
|
2325
|
+
cache_create_5m_tokens: 0,
|
|
2326
|
+
cache_create_1h_tokens: 0,
|
|
2327
|
+
total_tokens: row.total_tokens,
|
|
2328
|
+
cost_usd: row.cost_usd,
|
|
2329
|
+
last_active: row.last_active
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function normalizeAccountEmail2(email) {
|
|
2333
|
+
return email.trim().toLowerCase();
|
|
2334
|
+
}
|
|
2335
|
+
function accountKey(row) {
|
|
2336
|
+
const agent = row.agent || row.account_tool || "unknown";
|
|
2337
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
2338
|
+
if (email)
|
|
2339
|
+
return `${agent}:${email}`;
|
|
2340
|
+
if (row.account_name)
|
|
2341
|
+
return `${agent}:${row.account_name}`;
|
|
2342
|
+
return row.account_key || `${agent}:unknown`;
|
|
2343
|
+
}
|
|
2344
|
+
function addAccountRow(groups, row) {
|
|
2345
|
+
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
2346
|
+
return;
|
|
2347
|
+
const key = accountKey(row);
|
|
2348
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
2349
|
+
const current = groups.get(key) ?? {
|
|
2350
|
+
account_key: key,
|
|
2351
|
+
account_tool: row.agent || row.account_tool || "unknown",
|
|
2352
|
+
account_name: row.account_name || email || row.account_key || "unknown",
|
|
2353
|
+
account_email: email || null,
|
|
2354
|
+
account_source: row.account_source || "unknown",
|
|
2355
|
+
sessions: 0,
|
|
2356
|
+
requests: 0,
|
|
2357
|
+
input_tokens: 0,
|
|
2358
|
+
output_tokens: 0,
|
|
2359
|
+
cache_read_tokens: 0,
|
|
2360
|
+
cache_create_tokens: 0,
|
|
2361
|
+
cache_create_5m_tokens: 0,
|
|
2362
|
+
cache_create_1h_tokens: 0,
|
|
2363
|
+
total_tokens: 0,
|
|
2364
|
+
cost_usd: 0,
|
|
2365
|
+
last_active: null
|
|
2366
|
+
};
|
|
2367
|
+
current.sessions += Number(row.sessions ?? 0);
|
|
2368
|
+
if (!current.account_email && email)
|
|
2369
|
+
current.account_email = email;
|
|
2370
|
+
if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
|
|
2371
|
+
current.account_name = row.account_name || email;
|
|
2372
|
+
}
|
|
2373
|
+
if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
2374
|
+
current.account_source = row.account_source;
|
|
2375
|
+
}
|
|
2376
|
+
current.requests += Number(row.requests ?? 0);
|
|
2377
|
+
current.input_tokens += Number(row.input_tokens ?? 0);
|
|
2378
|
+
current.output_tokens += Number(row.output_tokens ?? 0);
|
|
2379
|
+
current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
|
|
2380
|
+
current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
|
|
2381
|
+
current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
|
|
2382
|
+
current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
|
|
2383
|
+
current.total_tokens += Number(row.total_tokens ?? 0);
|
|
2384
|
+
current.cost_usd += Number(row.cost_usd ?? 0);
|
|
2385
|
+
current.last_active = latestTimestamp(current.last_active, row.last_active);
|
|
2386
|
+
groups.set(key, current);
|
|
2387
|
+
}
|
|
2388
|
+
function queryAccountSinceRows(db, since, machine) {
|
|
2389
|
+
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
2390
|
+
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
2391
|
+
const requestRows = db.prepare(`
|
|
2392
|
+
SELECT
|
|
2393
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
2394
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
2395
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
2396
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
2397
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
2398
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
2399
|
+
COUNT(DISTINCT r.session_id) as sessions,
|
|
2400
|
+
COUNT(*) as requests,
|
|
2401
|
+
COALESCE(SUM(r.input_tokens), 0) as input_tokens,
|
|
2402
|
+
COALESCE(SUM(r.output_tokens), 0) as output_tokens,
|
|
2403
|
+
COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
|
|
2404
|
+
COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
|
|
2405
|
+
COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
|
|
2406
|
+
COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
2407
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
2408
|
+
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
2409
|
+
MAX(r.timestamp) as last_active
|
|
2410
|
+
FROM requests r
|
|
2411
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
2412
|
+
WHERE r.timestamp >= ?${requestMachineClause}
|
|
2413
|
+
GROUP BY
|
|
2414
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
2415
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
|
|
2416
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
|
|
2417
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
|
|
2418
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
|
|
2419
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
|
|
2420
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
2421
|
+
const sessionRows = db.prepare(`
|
|
2422
|
+
SELECT
|
|
2423
|
+
s.agent as agent,
|
|
2424
|
+
s.account_key as account_key,
|
|
2425
|
+
s.account_tool as account_tool,
|
|
2426
|
+
s.account_name as account_name,
|
|
2427
|
+
COALESCE(s.account_email, '') as account_email,
|
|
2428
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
2429
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
2430
|
+
COALESCE(SUM(s.request_count), 0) as requests,
|
|
2431
|
+
0 as input_tokens,
|
|
2432
|
+
0 as output_tokens,
|
|
2433
|
+
0 as cache_read_tokens,
|
|
2434
|
+
0 as cache_create_tokens,
|
|
2435
|
+
0 as cache_create_5m_tokens,
|
|
2436
|
+
0 as cache_create_1h_tokens,
|
|
2437
|
+
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
2438
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
2439
|
+
MAX(s.started_at) as last_active
|
|
2440
|
+
FROM sessions s
|
|
2441
|
+
WHERE s.started_at >= ?${sessionMachineClause}
|
|
2442
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
|
|
2443
|
+
GROUP BY
|
|
2444
|
+
s.agent,
|
|
2445
|
+
s.account_key,
|
|
2446
|
+
s.account_tool,
|
|
2447
|
+
s.account_name,
|
|
2448
|
+
COALESCE(s.account_email, ''),
|
|
2449
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown')
|
|
2450
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
2451
|
+
const groups = new Map;
|
|
2452
|
+
for (const row of requestRows)
|
|
2453
|
+
addAccountRow(groups, row);
|
|
2454
|
+
for (const row of sessionRows)
|
|
2455
|
+
addAccountRow(groups, row);
|
|
2456
|
+
const rows = [...groups.values()];
|
|
2457
|
+
rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
|
|
2458
|
+
if (rows.length > 0)
|
|
2459
|
+
return rows;
|
|
2460
|
+
return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
|
|
2461
|
+
}
|
|
2462
|
+
function accountBreakdownToBriefRow(row) {
|
|
2463
|
+
return {
|
|
2464
|
+
account_key: row.account_key,
|
|
2465
|
+
account_tool: row.account_tool,
|
|
2466
|
+
account_name: row.account_name,
|
|
2467
|
+
account_email: row.account_email,
|
|
2468
|
+
account_source: row.account_source,
|
|
2469
|
+
sessions: row.sessions,
|
|
2470
|
+
requests: row.requests,
|
|
2471
|
+
input_tokens: 0,
|
|
2472
|
+
output_tokens: 0,
|
|
2473
|
+
cache_read_tokens: 0,
|
|
2474
|
+
cache_create_tokens: 0,
|
|
2475
|
+
cache_create_5m_tokens: 0,
|
|
2476
|
+
cache_create_1h_tokens: 0,
|
|
2477
|
+
total_tokens: row.total_tokens,
|
|
2478
|
+
cost_usd: row.cost_usd,
|
|
2479
|
+
last_active: row.last_active
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
function queryFreshness(db, now, machine, localSync) {
|
|
2483
|
+
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
2484
|
+
const requestRows = db.prepare(`
|
|
2485
|
+
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
2486
|
+
FROM requests
|
|
2487
|
+
WHERE machine_id != ''${requestFilter}
|
|
2488
|
+
GROUP BY machine_id
|
|
2489
|
+
`).all(...machine ? [machine] : []);
|
|
2490
|
+
const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
|
|
2491
|
+
const machines = new Map;
|
|
2492
|
+
for (const row of requestRows) {
|
|
2493
|
+
machines.set(row.machine_id, {
|
|
2494
|
+
machine_id: row.machine_id,
|
|
2495
|
+
max_request_at: row.max_request_at,
|
|
2496
|
+
request_age: formatAge(row.max_request_at, now),
|
|
2497
|
+
last_merge_sync_at: null,
|
|
2498
|
+
merge_sync_age: "never"
|
|
2799
2499
|
});
|
|
2800
2500
|
}
|
|
2801
|
-
const
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2501
|
+
for (const row of registryRows) {
|
|
2502
|
+
const current = machines.get(row.machine_id) ?? {
|
|
2503
|
+
machine_id: row.machine_id,
|
|
2504
|
+
max_request_at: null,
|
|
2505
|
+
request_age: "never",
|
|
2506
|
+
last_merge_sync_at: null,
|
|
2507
|
+
merge_sync_age: "never"
|
|
2508
|
+
};
|
|
2509
|
+
const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
|
|
2510
|
+
current.last_merge_sync_at = syncAt;
|
|
2511
|
+
current.merge_sync_age = formatAge(syncAt, now);
|
|
2512
|
+
machines.set(row.machine_id, current);
|
|
2513
|
+
}
|
|
2514
|
+
if (localSync && (!machine || machine === localSync.machineId)) {
|
|
2515
|
+
const current = machines.get(localSync.machineId) ?? {
|
|
2516
|
+
machine_id: localSync.machineId,
|
|
2517
|
+
max_request_at: null,
|
|
2518
|
+
request_age: "never",
|
|
2519
|
+
last_merge_sync_at: null,
|
|
2520
|
+
merge_sync_age: "never"
|
|
2521
|
+
};
|
|
2522
|
+
current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
|
|
2523
|
+
current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
|
|
2524
|
+
machines.set(localSync.machineId, current);
|
|
2525
|
+
}
|
|
2526
|
+
const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
|
|
2527
|
+
return {
|
|
2528
|
+
machines: rows,
|
|
2529
|
+
max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
|
|
2530
|
+
merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
|
|
2531
|
+
};
|
|
2808
2532
|
}
|
|
2809
|
-
function
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}
|
|
2831
|
-
function openCodexDb(dbPath, verbose) {
|
|
2832
|
-
let lastError;
|
|
2833
|
-
for (const readonly of [true, false]) {
|
|
2834
|
-
let codexDb = null;
|
|
2835
|
-
try {
|
|
2836
|
-
codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
2837
|
-
codexDb.prepare("PRAGMA schema_version").get();
|
|
2838
|
-
return codexDb;
|
|
2839
|
-
} catch (error) {
|
|
2840
|
-
lastError = error;
|
|
2841
|
-
codexDb?.close();
|
|
2533
|
+
function buildBrief(db, opts = {}) {
|
|
2534
|
+
const now = opts.now ?? new Date;
|
|
2535
|
+
const machine = machineFilter(opts.machine);
|
|
2536
|
+
const sinceInput = opts.since ?? "24h";
|
|
2537
|
+
const since = parseSinceDate(sinceInput, now);
|
|
2538
|
+
const summaries = [
|
|
2539
|
+
{
|
|
2540
|
+
label: "Today",
|
|
2541
|
+
period: "today",
|
|
2542
|
+
...queryPeriodTotals(db, "today", machine)
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
label: "Week",
|
|
2546
|
+
period: "week",
|
|
2547
|
+
...queryPeriodTotals(db, "week", machine)
|
|
2548
|
+
},
|
|
2549
|
+
{
|
|
2550
|
+
label: `Since ${since.label}`,
|
|
2551
|
+
period: "since",
|
|
2552
|
+
since: since.timestamp,
|
|
2553
|
+
...querySinceTotals(db, since.timestamp, machine)
|
|
2842
2554
|
}
|
|
2555
|
+
];
|
|
2556
|
+
const machines = queryMachineSinceRows(db, since.timestamp, machine);
|
|
2557
|
+
for (const row of machines) {
|
|
2558
|
+
row.last_data_age = formatAge(row.last_data_at, now);
|
|
2843
2559
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2560
|
+
return {
|
|
2561
|
+
generated_at: now.toISOString(),
|
|
2562
|
+
machine: machine ?? "all",
|
|
2563
|
+
since: {
|
|
2564
|
+
input: sinceInput,
|
|
2565
|
+
timestamp: since.timestamp,
|
|
2566
|
+
label: since.label
|
|
2567
|
+
},
|
|
2568
|
+
summaries,
|
|
2569
|
+
machines,
|
|
2570
|
+
agents: queryAgentSinceRows(db, since.timestamp, machine),
|
|
2571
|
+
accounts: queryAccountSinceRows(db, since.timestamp, machine),
|
|
2572
|
+
freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// src/lib/analytics.ts
|
|
2577
|
+
function queryProjectDetail(db, nameOrPath) {
|
|
2578
|
+
const like = `%${nameOrPath}%`;
|
|
2579
|
+
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
2580
|
+
FROM sessions
|
|
2581
|
+
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
2582
|
+
ORDER BY started_at DESC`).all(like, like);
|
|
2583
|
+
if (sessions.length === 0)
|
|
2584
|
+
return null;
|
|
2585
|
+
const first = sessions[0];
|
|
2586
|
+
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
2587
|
+
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
2588
|
+
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
2589
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
2590
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
2591
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
2592
|
+
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
2593
|
+
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
2594
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
2595
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
2596
|
+
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
2597
|
+
const top_sessions = [...sessions].sort((a, b) => (b.total_cost_usd ?? 0) - (a.total_cost_usd ?? 0)).slice(0, 5).map((s) => ({ id: s.id, total_cost_usd: s.total_cost_usd ?? 0, started_at: s.started_at }));
|
|
2598
|
+
return {
|
|
2599
|
+
project_name: first.project_name || nameOrPath,
|
|
2600
|
+
project_path: first.project_path || "",
|
|
2601
|
+
total_cost_usd: totalCost,
|
|
2602
|
+
total_tokens: totalTokens,
|
|
2603
|
+
sessions: sessions.length,
|
|
2604
|
+
daily,
|
|
2605
|
+
models,
|
|
2606
|
+
top_sessions
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
function exportDateWhere(column, period) {
|
|
2610
|
+
switch (period) {
|
|
2611
|
+
case "today":
|
|
2612
|
+
return `DATE(${column}) = DATE('now')`;
|
|
2613
|
+
case "week":
|
|
2614
|
+
return `${column} >= DATE('now', '-7 days')`;
|
|
2615
|
+
case "all":
|
|
2616
|
+
return "1=1";
|
|
2617
|
+
default:
|
|
2618
|
+
return `${column} >= DATE('now', '-30 days')`;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function queryExportRows(db, type, period) {
|
|
2622
|
+
if (type === "requests") {
|
|
2623
|
+
const where2 = exportDateWhere("timestamp", period);
|
|
2624
|
+
return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
2625
|
+
}
|
|
2626
|
+
const where = exportDateWhere("started_at", period);
|
|
2627
|
+
return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
2628
|
+
}
|
|
2629
|
+
function queryRangeStats(db, from, to) {
|
|
2630
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
2631
|
+
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
2632
|
+
FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
2633
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
2634
|
+
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
2635
|
+
}
|
|
2636
|
+
function queryForecast(db, now = new Date) {
|
|
2637
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
2638
|
+
const dayOfMonth = now.getDate();
|
|
2639
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
2640
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
2641
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
2642
|
+
const projected = dailyAvg * daysInMonth;
|
|
2643
|
+
const sevenDaysAgo = new Date(now);
|
|
2644
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
2645
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
2646
|
+
const last7DailyAvg = last7.cost / 7;
|
|
2647
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
2648
|
+
FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
2649
|
+
return {
|
|
2650
|
+
day_of_month: dayOfMonth,
|
|
2651
|
+
days_in_month: daysInMonth,
|
|
2652
|
+
spent_so_far_usd: monthSoFar.cost,
|
|
2653
|
+
daily_avg_usd: dailyAvg,
|
|
2654
|
+
projected_usd: projected,
|
|
2655
|
+
last7_daily_avg_usd: last7DailyAvg,
|
|
2656
|
+
last7_projected_usd: last7DailyAvg * daysInMonth,
|
|
2657
|
+
cheapest_day: dailyCosts[0] ?? null,
|
|
2658
|
+
most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function queryModelEfficiency(db) {
|
|
2662
|
+
return db.prepare(`SELECT model,
|
|
2663
|
+
COALESCE(SUM(input_tokens),0) as input,
|
|
2664
|
+
COALESCE(SUM(output_tokens),0) as output,
|
|
2665
|
+
COALESCE(SUM(cache_read_tokens),0) as cache_read,
|
|
2666
|
+
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
2667
|
+
COUNT(*) as requests,
|
|
2668
|
+
COALESCE(SUM(cost_usd),0) as cost
|
|
2669
|
+
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/lib/contracts-client/mode.ts
|
|
2673
|
+
var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
|
|
2674
|
+
function normalizeStorageMode(value) {
|
|
2675
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
2676
|
+
if (normalized === "local")
|
|
2677
|
+
return { mode: "local", deprecatedAlias: null };
|
|
2678
|
+
if (normalized === "cloud")
|
|
2679
|
+
return { mode: "cloud", deprecatedAlias: null };
|
|
2680
|
+
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
2681
|
+
return { mode: "cloud", deprecatedAlias: normalized };
|
|
2682
|
+
}
|
|
2683
|
+
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
2684
|
+
}
|
|
2685
|
+
function envToken(name) {
|
|
2686
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/lib/contracts-client/transport.ts
|
|
2690
|
+
function defaultCloudBaseUrl(name) {
|
|
2691
|
+
return `https://${name}.hasna.xyz`;
|
|
2692
|
+
}
|
|
2693
|
+
function clientTransportEnvKeys(name) {
|
|
2694
|
+
const token = envToken(name);
|
|
2695
|
+
return {
|
|
2696
|
+
modeKeys: [
|
|
2697
|
+
`HASNA_${token}_STORAGE_MODE`,
|
|
2698
|
+
`HASNA_${token}_MODE`,
|
|
2699
|
+
`${token}_STORAGE_MODE`,
|
|
2700
|
+
`${token}_MODE`
|
|
2701
|
+
],
|
|
2702
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
2703
|
+
apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function firstEnv(env, keys) {
|
|
2707
|
+
for (const key of keys) {
|
|
2708
|
+
const value = env[key]?.trim();
|
|
2709
|
+
if (value)
|
|
2710
|
+
return { key, value };
|
|
2847
2711
|
}
|
|
2848
2712
|
return null;
|
|
2849
2713
|
}
|
|
2850
|
-
function
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
let
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
const
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2714
|
+
function toV1BaseUrl(apiUrl) {
|
|
2715
|
+
const url = new URL(apiUrl);
|
|
2716
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2717
|
+
throw new Error("API URL must use http or https.");
|
|
2718
|
+
}
|
|
2719
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
2720
|
+
if (path.endsWith("/v1"))
|
|
2721
|
+
path = path.slice(0, -"/v1".length);
|
|
2722
|
+
url.pathname = `${path}/v1`;
|
|
2723
|
+
url.search = "";
|
|
2724
|
+
url.hash = "";
|
|
2725
|
+
return url.toString().replace(/\/+$/, "");
|
|
2726
|
+
}
|
|
2727
|
+
function resolveClientTransport(name, env = process.env) {
|
|
2728
|
+
const keys = clientTransportEnvKeys(name);
|
|
2729
|
+
const modeHit = firstEnv(env, keys.modeKeys);
|
|
2730
|
+
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
2731
|
+
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
2732
|
+
let mode = "local";
|
|
2733
|
+
let deprecatedAlias = null;
|
|
2734
|
+
let modeSource = "default";
|
|
2735
|
+
const warnings = [];
|
|
2736
|
+
if (modeHit) {
|
|
2737
|
+
const normalized = normalizeStorageMode(modeHit.value);
|
|
2738
|
+
mode = normalized.mode;
|
|
2739
|
+
deprecatedAlias = normalized.deprecatedAlias;
|
|
2740
|
+
modeSource = modeHit.key;
|
|
2741
|
+
if (deprecatedAlias) {
|
|
2742
|
+
warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
|
|
2878
2743
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
if (tokenTotal(usage) <= 0)
|
|
2883
|
-
continue;
|
|
2884
|
-
const key = JSON.stringify(usage);
|
|
2885
|
-
if (!fallbackUsages.has(key))
|
|
2886
|
-
fallbackUsages.set(key, usage);
|
|
2887
|
-
fallbackTimestamp = entryTimestamp ?? fallbackTimestamp;
|
|
2888
|
-
}
|
|
2889
|
-
if (aggregate)
|
|
2890
|
-
return [aggregate];
|
|
2891
|
-
if (fallbackUsages.size === 0)
|
|
2892
|
-
return [];
|
|
2893
|
-
return [{ usage: sumTokenUsages([...fallbackUsages.values()]), timestamp: fallbackTimestamp }];
|
|
2894
|
-
}
|
|
2895
|
-
function tokenTotal(usage) {
|
|
2896
|
-
return usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2897
|
-
}
|
|
2898
|
-
function sumTokenUsages(usages) {
|
|
2899
|
-
const result = {
|
|
2900
|
-
input_tokens: 0,
|
|
2901
|
-
cached_input_tokens: 0,
|
|
2902
|
-
output_tokens: 0,
|
|
2903
|
-
reasoning_output_tokens: 0,
|
|
2904
|
-
total_tokens: 0
|
|
2905
|
-
};
|
|
2906
|
-
for (const usage of usages) {
|
|
2907
|
-
result.input_tokens = (result.input_tokens ?? 0) + (usage.input_tokens ?? 0);
|
|
2908
|
-
result.cached_input_tokens = (result.cached_input_tokens ?? 0) + (usage.cached_input_tokens ?? 0);
|
|
2909
|
-
result.output_tokens = (result.output_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
2910
|
-
result.reasoning_output_tokens = (result.reasoning_output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0);
|
|
2911
|
-
result.total_tokens = (result.total_tokens ?? 0) + tokenTotal(usage);
|
|
2744
|
+
} else if (urlHit && keyHit) {
|
|
2745
|
+
mode = "cloud";
|
|
2746
|
+
modeSource = `${urlHit.key}+${keyHit.key}`;
|
|
2912
2747
|
}
|
|
2913
|
-
|
|
2748
|
+
if (mode === "local") {
|
|
2749
|
+
return {
|
|
2750
|
+
transport: "local",
|
|
2751
|
+
mode,
|
|
2752
|
+
deprecatedAlias,
|
|
2753
|
+
modeSource,
|
|
2754
|
+
baseUrl: null,
|
|
2755
|
+
apiUrlSource: null,
|
|
2756
|
+
apiKeyPresent: Boolean(keyHit),
|
|
2757
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
2758
|
+
misconfigured: false,
|
|
2759
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
if (!keyHit) {
|
|
2763
|
+
warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
|
|
2764
|
+
return {
|
|
2765
|
+
transport: "local",
|
|
2766
|
+
mode,
|
|
2767
|
+
deprecatedAlias,
|
|
2768
|
+
modeSource,
|
|
2769
|
+
baseUrl: null,
|
|
2770
|
+
apiUrlSource: null,
|
|
2771
|
+
apiKeyPresent: false,
|
|
2772
|
+
apiKeySource: null,
|
|
2773
|
+
misconfigured: true,
|
|
2774
|
+
warning: warnings.join(" ")
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
|
|
2778
|
+
const apiUrlSource = urlHit ? urlHit.key : "default";
|
|
2779
|
+
let baseUrl;
|
|
2780
|
+
try {
|
|
2781
|
+
baseUrl = toV1BaseUrl(rawUrl);
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2784
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
2785
|
+
return {
|
|
2786
|
+
transport: "local",
|
|
2787
|
+
mode,
|
|
2788
|
+
deprecatedAlias,
|
|
2789
|
+
modeSource,
|
|
2790
|
+
baseUrl: null,
|
|
2791
|
+
apiUrlSource: null,
|
|
2792
|
+
apiKeyPresent: true,
|
|
2793
|
+
apiKeySource: keyHit.key,
|
|
2794
|
+
misconfigured: true,
|
|
2795
|
+
warning: warnings.join(" ")
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
transport: "cloud-http",
|
|
2800
|
+
mode,
|
|
2801
|
+
deprecatedAlias,
|
|
2802
|
+
modeSource,
|
|
2803
|
+
baseUrl,
|
|
2804
|
+
apiUrlSource,
|
|
2805
|
+
apiKeyPresent: true,
|
|
2806
|
+
apiKeySource: keyHit.key,
|
|
2807
|
+
misconfigured: false,
|
|
2808
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
2809
|
+
};
|
|
2914
2810
|
}
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2811
|
+
|
|
2812
|
+
class HasnaHttpError extends Error {
|
|
2813
|
+
status;
|
|
2814
|
+
method;
|
|
2815
|
+
path;
|
|
2816
|
+
body;
|
|
2817
|
+
constructor(method, path, status, body) {
|
|
2818
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
|
|
2819
|
+
this.name = "HasnaHttpError";
|
|
2820
|
+
this.status = status;
|
|
2821
|
+
this.method = method;
|
|
2822
|
+
this.path = path;
|
|
2823
|
+
this.body = body;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
2827
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
2828
|
+
function appendQuery(path, query) {
|
|
2829
|
+
if (!query)
|
|
2830
|
+
return path;
|
|
2831
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
2832
|
+
if (!(query instanceof URLSearchParams)) {
|
|
2833
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2834
|
+
if (value === null || value === undefined)
|
|
2835
|
+
continue;
|
|
2836
|
+
if (Array.isArray(value)) {
|
|
2837
|
+
for (const v of value)
|
|
2838
|
+
params.append(key, String(v));
|
|
2839
|
+
} else {
|
|
2840
|
+
params.append(key, String(value));
|
|
2841
|
+
}
|
|
2923
2842
|
}
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2843
|
+
}
|
|
2844
|
+
const qs = params.toString();
|
|
2845
|
+
if (!qs)
|
|
2846
|
+
return path;
|
|
2847
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
2848
|
+
}
|
|
2849
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2850
|
+
function createHasnaHttpTransport(options) {
|
|
2851
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
2852
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
2853
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
2854
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
2855
|
+
const defaultRetry = options.retry;
|
|
2856
|
+
function resolveRetry(callRetry) {
|
|
2857
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
2858
|
+
if (chosen === false)
|
|
2859
|
+
return null;
|
|
2860
|
+
const r = chosen ?? {};
|
|
2861
|
+
return {
|
|
2862
|
+
retries: r.retries ?? 2,
|
|
2863
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
2864
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
2865
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
async function once(method, rel, url, body, opts) {
|
|
2869
|
+
const headers = {
|
|
2870
|
+
"x-api-key": options.apiKey,
|
|
2871
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
2872
|
+
Accept: "application/json",
|
|
2873
|
+
...options.headers ?? {},
|
|
2874
|
+
...opts.headers ?? {}
|
|
2875
|
+
};
|
|
2876
|
+
if (opts.idempotencyKey)
|
|
2877
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
2878
|
+
const init = { method, headers };
|
|
2879
|
+
if (body !== undefined) {
|
|
2880
|
+
headers["Content-Type"] = "application/json";
|
|
2881
|
+
init.body = JSON.stringify(body);
|
|
2936
2882
|
}
|
|
2937
|
-
|
|
2883
|
+
const controller = new AbortController;
|
|
2884
|
+
const onAbort = () => controller.abort();
|
|
2885
|
+
if (opts.signal) {
|
|
2886
|
+
if (opts.signal.aborted)
|
|
2887
|
+
controller.abort();
|
|
2888
|
+
else
|
|
2889
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
2890
|
+
}
|
|
2891
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
2892
|
+
init.signal = controller.signal;
|
|
2893
|
+
let response;
|
|
2938
2894
|
try {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
const stateValue = `${CODEX_INGEST_VERSION}:${thread.updated_at}:${thread.tokens_used}:${model}`;
|
|
2946
|
-
const processed = getIngestState(db, source.stateSource, thread.id);
|
|
2947
|
-
if (processed === stateValue)
|
|
2948
|
-
continue;
|
|
2949
|
-
const projectPath = thread.cwd ?? "";
|
|
2950
|
-
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
2951
|
-
const sessionId = `${source.sessionPrefix}-${thread.id}`;
|
|
2952
|
-
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
2953
|
-
const endedAt = thread.updated_at ? new Date(thread.updated_at * 1000).toISOString() : null;
|
|
2954
|
-
upsertSession(db, withAccount({
|
|
2955
|
-
id: sessionId,
|
|
2956
|
-
agent: "codex",
|
|
2957
|
-
project_path: projectPath,
|
|
2958
|
-
project_name: projectName,
|
|
2959
|
-
started_at: startedAt,
|
|
2960
|
-
ended_at: endedAt,
|
|
2961
|
-
total_cost_usd: 0,
|
|
2962
|
-
total_tokens: 0,
|
|
2963
|
-
request_count: 0,
|
|
2964
|
-
machine_id: machineId
|
|
2965
|
-
}, account));
|
|
2966
|
-
const events = readTokenEvents(thread.rollout_path);
|
|
2967
|
-
const tokenEvents = events.length > 0 ? events : fallbackEvents(thread.tokens_used);
|
|
2968
|
-
const ingestedTokens = tokenEvents.reduce((sum, event) => sum + tokenTotal(event.usage), 0);
|
|
2969
|
-
db.prepare(`DELETE FROM requests WHERE session_id = ?`).run(sessionId);
|
|
2970
|
-
tokenEvents.forEach((event, index) => {
|
|
2971
|
-
const usage = event.usage;
|
|
2972
|
-
const inputTotal = usage.input_tokens ?? 0;
|
|
2973
|
-
const cacheReadTokens = usage.cached_input_tokens ?? 0;
|
|
2974
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
2975
|
-
const outputTokens = usage.output_tokens ?? Math.max((usage.total_tokens ?? 0) - inputTotal, 0);
|
|
2976
|
-
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
2977
|
-
const timestamp = event.timestamp ?? (thread.created_at ? new Date(thread.created_at * 1000 + index).toISOString() : new Date().toISOString());
|
|
2978
|
-
const requestId = `${sessionId}-${index}`;
|
|
2979
|
-
upsertRequest(db, withAccount({
|
|
2980
|
-
id: requestId,
|
|
2981
|
-
agent: "codex",
|
|
2982
|
-
session_id: sessionId,
|
|
2983
|
-
model,
|
|
2984
|
-
input_tokens: inputTokens,
|
|
2985
|
-
output_tokens: outputTokens,
|
|
2986
|
-
cache_read_tokens: cacheReadTokens,
|
|
2987
|
-
cache_create_tokens: 0,
|
|
2988
|
-
cost_usd: costUsd,
|
|
2989
|
-
cost_basis: defaultCostBasisForAgent("codex"),
|
|
2990
|
-
duration_ms: 0,
|
|
2991
|
-
timestamp,
|
|
2992
|
-
source_request_id: requestId,
|
|
2993
|
-
machine_id: machineId
|
|
2994
|
-
}, account));
|
|
2995
|
-
requests++;
|
|
2996
|
-
});
|
|
2997
|
-
rollupSession(db, sessionId);
|
|
2998
|
-
setIngestState(db, source.stateSource, thread.id, stateValue);
|
|
2999
|
-
ingested++;
|
|
3000
|
-
if (verbose)
|
|
3001
|
-
console.log(`${source.label} session ${thread.id}: ${ingestedTokens} tokens on ${model}`);
|
|
3002
|
-
}
|
|
2895
|
+
response = await fetchImpl(url, init);
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2898
|
+
if (opts.signal?.aborted)
|
|
2899
|
+
return { ok: false, retryable: false, error: err };
|
|
2900
|
+
return { ok: false, retryable: true, error: err };
|
|
3003
2901
|
} finally {
|
|
3004
|
-
|
|
2902
|
+
clearTimeout(timer);
|
|
2903
|
+
if (opts.signal)
|
|
2904
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
2905
|
+
}
|
|
2906
|
+
const text = await response.text();
|
|
2907
|
+
let parsed = undefined;
|
|
2908
|
+
if (text.length > 0) {
|
|
2909
|
+
try {
|
|
2910
|
+
parsed = JSON.parse(text);
|
|
2911
|
+
} catch {
|
|
2912
|
+
parsed = text;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
if (!response.ok) {
|
|
2916
|
+
const retry = resolveRetry(opts.retry);
|
|
2917
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
2918
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
3005
2919
|
}
|
|
2920
|
+
return { ok: true, value: parsed };
|
|
2921
|
+
}
|
|
2922
|
+
async function request(method, path, body, opts = {}) {
|
|
2923
|
+
const upper = method.toUpperCase();
|
|
2924
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
2925
|
+
const url = `${base}${rel}`;
|
|
2926
|
+
const retry = resolveRetry(opts.retry);
|
|
2927
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
2928
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
2929
|
+
let last = null;
|
|
2930
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2931
|
+
const result = await once(upper, rel, url, body, opts);
|
|
2932
|
+
if (result.ok)
|
|
2933
|
+
return result.value;
|
|
2934
|
+
last = result;
|
|
2935
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
2936
|
+
if (!canRetry)
|
|
2937
|
+
break;
|
|
2938
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
2939
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
2940
|
+
await sleep(backoff + jitter);
|
|
2941
|
+
}
|
|
2942
|
+
throw last.error;
|
|
3006
2943
|
}
|
|
3007
|
-
return {
|
|
2944
|
+
return {
|
|
2945
|
+
baseUrl: base,
|
|
2946
|
+
request,
|
|
2947
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
2948
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
2949
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
2950
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
2951
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
2952
|
+
};
|
|
3008
2953
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
import { homedir as homedir5 } from "os";
|
|
3014
|
-
import { join as join6, basename as basename3 } from "path";
|
|
3015
|
-
var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
|
|
3016
|
-
var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
|
|
3017
|
-
function geminiTmpDir() {
|
|
3018
|
-
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
3019
|
-
}
|
|
3020
|
-
function geminiHistoryDir() {
|
|
3021
|
-
return process.env["HASNA_ECONOMY_GEMINI_HISTORY_DIR"] ?? DEFAULT_GEMINI_HISTORY_DIR;
|
|
3022
|
-
}
|
|
3023
|
-
function numberField(...values) {
|
|
3024
|
-
for (const value of values) {
|
|
3025
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
3026
|
-
return value;
|
|
3027
|
-
}
|
|
3028
|
-
return 0;
|
|
3029
|
-
}
|
|
3030
|
-
function listProjectDirs(...roots) {
|
|
3031
|
-
const dirs = new Set;
|
|
3032
|
-
for (const root of roots) {
|
|
3033
|
-
if (!existsSync7(root))
|
|
3034
|
-
continue;
|
|
3035
|
-
try {
|
|
3036
|
-
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
3037
|
-
if (entry.isDirectory())
|
|
3038
|
-
dirs.add(join6(root, entry.name));
|
|
3039
|
-
}
|
|
3040
|
-
} catch {}
|
|
2954
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
2955
|
+
const resolution = resolveClientTransport(name, env);
|
|
2956
|
+
if (resolution.misconfigured) {
|
|
2957
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
|
|
3041
2958
|
}
|
|
3042
|
-
|
|
2959
|
+
if (resolution.transport === "local" || !resolution.baseUrl) {
|
|
2960
|
+
return { transport: "local", client: null, resolution };
|
|
2961
|
+
}
|
|
2962
|
+
const keys = clientTransportEnvKeys(name);
|
|
2963
|
+
const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
|
|
2964
|
+
if (!apiKey) {
|
|
2965
|
+
throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
transport: "cloud-http",
|
|
2969
|
+
client: createHasnaHttpTransport({
|
|
2970
|
+
name,
|
|
2971
|
+
baseUrl: resolution.baseUrl,
|
|
2972
|
+
apiKey,
|
|
2973
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
2974
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
2975
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
2976
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
2977
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
2978
|
+
}),
|
|
2979
|
+
resolution
|
|
2980
|
+
};
|
|
3043
2981
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
2982
|
+
|
|
2983
|
+
// src/lib/contracts-client/storage.ts
|
|
2984
|
+
function resourcePath(resource) {
|
|
2985
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
2986
|
+
if (!trimmed)
|
|
2987
|
+
throw new Error("resource must be a non-empty path segment");
|
|
2988
|
+
return `/${trimmed}`;
|
|
2989
|
+
}
|
|
2990
|
+
function entityPath(resource, id) {
|
|
2991
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
2992
|
+
throw new Error("id must be a non-empty string");
|
|
2993
|
+
}
|
|
2994
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
2995
|
+
}
|
|
2996
|
+
function newIdempotencyKey() {
|
|
2997
|
+
const g = globalThis;
|
|
2998
|
+
if (g.crypto?.randomUUID)
|
|
2999
|
+
return g.crypto.randomUUID();
|
|
3000
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
3001
|
+
}
|
|
3002
|
+
function extractItems(raw) {
|
|
3003
|
+
if (Array.isArray(raw))
|
|
3004
|
+
return raw;
|
|
3005
|
+
if (raw && typeof raw === "object") {
|
|
3006
|
+
const obj = raw;
|
|
3007
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
3008
|
+
if (Array.isArray(obj[key]))
|
|
3009
|
+
return obj[key];
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return [];
|
|
3013
|
+
}
|
|
3014
|
+
function extractTotal(raw) {
|
|
3015
|
+
if (raw && typeof raw === "object") {
|
|
3016
|
+
const obj = raw;
|
|
3017
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
3018
|
+
if (typeof obj[key] === "number")
|
|
3019
|
+
return obj[key];
|
|
3079
3020
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3021
|
+
}
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
function extractCursor(raw) {
|
|
3025
|
+
if (raw && typeof raw === "object") {
|
|
3026
|
+
const obj = raw;
|
|
3027
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
3028
|
+
if (typeof obj[key] === "string")
|
|
3029
|
+
return obj[key];
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
3034
|
+
function createHasnaStorageClient(name, transport) {
|
|
3035
|
+
return {
|
|
3036
|
+
name,
|
|
3037
|
+
baseUrl: transport.baseUrl,
|
|
3038
|
+
transport,
|
|
3039
|
+
async list(resource, options = {}) {
|
|
3040
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
3041
|
+
return {
|
|
3042
|
+
items: extractItems(raw),
|
|
3043
|
+
total: extractTotal(raw),
|
|
3044
|
+
cursor: extractCursor(raw),
|
|
3045
|
+
raw
|
|
3046
|
+
};
|
|
3047
|
+
},
|
|
3048
|
+
async get(resource, id, options = {}) {
|
|
3083
3049
|
try {
|
|
3084
|
-
|
|
3085
|
-
} catch {
|
|
3086
|
-
|
|
3050
|
+
return await transport.get(entityPath(resource, id), options);
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
3053
|
+
return null;
|
|
3054
|
+
throw error;
|
|
3087
3055
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3056
|
+
},
|
|
3057
|
+
async create(resource, body, options = {}) {
|
|
3058
|
+
const { idempotencyKey, ...rest } = options;
|
|
3059
|
+
return transport.post(resourcePath(resource), body, {
|
|
3060
|
+
...rest,
|
|
3061
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
3062
|
+
});
|
|
3063
|
+
},
|
|
3064
|
+
async update(resource, id, patch, options = {}) {
|
|
3065
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
3066
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
3067
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
3068
|
+
},
|
|
3069
|
+
async delete(resource, id, options = {}) {
|
|
3092
3070
|
try {
|
|
3093
|
-
|
|
3094
|
-
} catch {
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
if (!sessionId)
|
|
3099
|
-
continue;
|
|
3100
|
-
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
3101
|
-
const projectPath = projectRoot(projectDir, chatData);
|
|
3102
|
-
const projectName = projectPath ? basename3(projectPath) : "";
|
|
3103
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
3104
|
-
if (!existing) {
|
|
3105
|
-
const session = {
|
|
3106
|
-
id: sessionId,
|
|
3107
|
-
agent: "gemini",
|
|
3108
|
-
project_path: projectPath,
|
|
3109
|
-
project_name: projectName,
|
|
3110
|
-
started_at: startTime,
|
|
3111
|
-
ended_at: chatData.lastUpdated ?? null,
|
|
3112
|
-
total_cost_usd: 0,
|
|
3113
|
-
total_tokens: 0,
|
|
3114
|
-
request_count: 0,
|
|
3115
|
-
machine_id: machineId
|
|
3116
|
-
};
|
|
3117
|
-
upsertSession(db, withAccount(session, account));
|
|
3118
|
-
totalSessions++;
|
|
3119
|
-
}
|
|
3120
|
-
touchedSessions.add(sessionId);
|
|
3121
|
-
for (const [index, message] of (chatData.messages ?? []).entries()) {
|
|
3122
|
-
const usage = message.usage ?? message.usageMetadata ?? message.response?.usageMetadata;
|
|
3123
|
-
if (!usage)
|
|
3124
|
-
continue;
|
|
3125
|
-
const model = message.model ?? message.response?.modelVersion ?? message.response?.model ?? chatData.model;
|
|
3126
|
-
if (!model)
|
|
3127
|
-
continue;
|
|
3128
|
-
const toolUsePromptTokens = numberField(usage.toolUsePromptTokenCount, usage.tool_use_prompt_token_count);
|
|
3129
|
-
const inputTotal = numberField(usage.inputTokens, usage.input_tokens, usage.promptTokenCount, usage.prompt_token_count) + toolUsePromptTokens;
|
|
3130
|
-
const cacheReadTokens = numberField(usage.cachedInputTokens, usage.cache_read_tokens, usage.cachedContentTokenCount, usage.cached_content_token_count);
|
|
3131
|
-
const inputTokens = Math.max(inputTotal - cacheReadTokens, 0);
|
|
3132
|
-
const thoughtsTokens = numberField(usage.thoughtsTokenCount, usage.thoughts_token_count);
|
|
3133
|
-
const outputTokens = numberField(usage.outputTokens, usage.output_tokens, usage.candidatesTokenCount, usage.candidates_token_count) + thoughtsTokens;
|
|
3134
|
-
const totalTokens = numberField(usage.totalTokens, usage.total_tokens, usage.totalTokenCount, usage.total_token_count);
|
|
3135
|
-
if (inputTokens + outputTokens + cacheReadTokens + totalTokens === 0)
|
|
3136
|
-
continue;
|
|
3137
|
-
const computedCost = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, 0);
|
|
3138
|
-
const costUsd = numberField(message.costUsd, message.cost_usd) || computedCost;
|
|
3139
|
-
const timestamp = message.timestamp ?? chatData.lastUpdated ?? startTime;
|
|
3140
|
-
const requestId = `gemini-${sessionId}-${message.id ?? index}`;
|
|
3141
|
-
upsertRequest(db, withAccount({
|
|
3142
|
-
id: requestId,
|
|
3143
|
-
agent: "gemini",
|
|
3144
|
-
session_id: sessionId,
|
|
3145
|
-
model,
|
|
3146
|
-
input_tokens: inputTokens,
|
|
3147
|
-
output_tokens: outputTokens,
|
|
3148
|
-
cache_read_tokens: cacheReadTokens,
|
|
3149
|
-
cache_create_tokens: 0,
|
|
3150
|
-
cost_usd: costUsd,
|
|
3151
|
-
cost_basis: defaultCostBasisForAgent("gemini"),
|
|
3152
|
-
duration_ms: 0,
|
|
3153
|
-
timestamp,
|
|
3154
|
-
source_request_id: message.id ?? requestId,
|
|
3155
|
-
machine_id: machineId
|
|
3156
|
-
}, account));
|
|
3157
|
-
totalRequests++;
|
|
3071
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
3074
|
+
return;
|
|
3075
|
+
throw error;
|
|
3158
3076
|
}
|
|
3159
|
-
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
3160
3077
|
}
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function resolveStorageClient(name, env = process.env, overrides) {
|
|
3081
|
+
const wired = createClientTransport(name, env, overrides);
|
|
3082
|
+
if (wired.transport === "cloud-http") {
|
|
3083
|
+
return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
|
|
3161
3084
|
}
|
|
3162
|
-
|
|
3163
|
-
|
|
3085
|
+
return { transport: "local", client: null };
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
// src/lib/cloud-storage.ts
|
|
3089
|
+
var ECONOMY_APP = "economy";
|
|
3090
|
+
var cache;
|
|
3091
|
+
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3092
|
+
const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
|
|
3093
|
+
return resolved.transport === "cloud-http" ? { active: true, client: resolved.client } : { active: false, client: null };
|
|
3094
|
+
}
|
|
3095
|
+
function economyCloudStorage(env = process.env) {
|
|
3096
|
+
if (cache === undefined)
|
|
3097
|
+
cache = resolveEconomyCloudStorage(env);
|
|
3098
|
+
return cache;
|
|
3099
|
+
}
|
|
3100
|
+
function cleanQuery(query) {
|
|
3101
|
+
if (!query)
|
|
3102
|
+
return;
|
|
3103
|
+
const out = {};
|
|
3104
|
+
for (const [k, v] of Object.entries(query)) {
|
|
3105
|
+
if (v !== undefined && v !== null && v !== "")
|
|
3106
|
+
out[k] = v;
|
|
3164
3107
|
}
|
|
3165
|
-
return
|
|
3108
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
3109
|
+
}
|
|
3110
|
+
async function cloudListItems(storage, resource, query) {
|
|
3111
|
+
const cleaned = cleanQuery(query);
|
|
3112
|
+
const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
|
|
3113
|
+
return res.items;
|
|
3114
|
+
}
|
|
3115
|
+
async function cloudObject(storage, path, query) {
|
|
3116
|
+
const cleaned = cleanQuery(query);
|
|
3117
|
+
const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
|
|
3118
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
3119
|
+
return raw.data;
|
|
3120
|
+
}
|
|
3121
|
+
return raw;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/lib/store/index.ts
|
|
3125
|
+
class LocalStore {
|
|
3126
|
+
transport = "local";
|
|
3127
|
+
_db;
|
|
3128
|
+
db() {
|
|
3129
|
+
if (!this._db) {
|
|
3130
|
+
this._db = openDatabase();
|
|
3131
|
+
ensurePricingSeeded(this._db);
|
|
3132
|
+
}
|
|
3133
|
+
return this._db;
|
|
3134
|
+
}
|
|
3135
|
+
async summary(period, machine) {
|
|
3136
|
+
return querySummary(this.db(), period, machine);
|
|
3137
|
+
}
|
|
3138
|
+
async sessions(filter) {
|
|
3139
|
+
return querySessions(this.db(), filter);
|
|
3140
|
+
}
|
|
3141
|
+
async topSessions(n, agent, since) {
|
|
3142
|
+
return queryTopSessions(this.db(), n, agent, since);
|
|
3143
|
+
}
|
|
3144
|
+
async sessionDetail(id) {
|
|
3145
|
+
return getSessionDetail(this.db(), id);
|
|
3146
|
+
}
|
|
3147
|
+
async modelBreakdown(query = {}) {
|
|
3148
|
+
return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
|
|
3149
|
+
}
|
|
3150
|
+
async agentBreakdown(query = {}) {
|
|
3151
|
+
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3152
|
+
}
|
|
3153
|
+
async projectBreakdown(query = {}) {
|
|
3154
|
+
return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3155
|
+
}
|
|
3156
|
+
async accountBreakdown(query = {}) {
|
|
3157
|
+
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
3158
|
+
}
|
|
3159
|
+
async accounts(period) {
|
|
3160
|
+
return queryAccountBreakdown(this.db(), period);
|
|
3161
|
+
}
|
|
3162
|
+
async daily(days, machine) {
|
|
3163
|
+
return queryDailyBreakdown(this.db(), days, machine);
|
|
3164
|
+
}
|
|
3165
|
+
async machines() {
|
|
3166
|
+
return listMachines(this.db());
|
|
3167
|
+
}
|
|
3168
|
+
async fleet(period) {
|
|
3169
|
+
return {
|
|
3170
|
+
summary: querySummary(this.db(), period, undefined, true),
|
|
3171
|
+
machines: listMachines(this.db(), period),
|
|
3172
|
+
registry: listMachineRegistry(this.db())
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
async billingSummary(period) {
|
|
3176
|
+
return queryBillingSummary(this.db(), period);
|
|
3177
|
+
}
|
|
3178
|
+
async billingDiff(period) {
|
|
3179
|
+
return queryBillingDiff(this.db(), period);
|
|
3180
|
+
}
|
|
3181
|
+
async usage(period, agent) {
|
|
3182
|
+
const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
3183
|
+
const summary = querySummary(this.db(), period, undefined, true, agent);
|
|
3184
|
+
return { snapshots, summary };
|
|
3185
|
+
}
|
|
3186
|
+
async savings(period, agent) {
|
|
3187
|
+
return querySavingsSummary(this.db(), period, agent);
|
|
3188
|
+
}
|
|
3189
|
+
async listBudgets() {
|
|
3190
|
+
return getBudgetStatuses(this.db());
|
|
3191
|
+
}
|
|
3192
|
+
async listGoals() {
|
|
3193
|
+
return getGoalStatuses(this.db());
|
|
3194
|
+
}
|
|
3195
|
+
async listPricing() {
|
|
3196
|
+
return listModelPricing(this.db());
|
|
3197
|
+
}
|
|
3198
|
+
async listSubscriptions() {
|
|
3199
|
+
return listSubscriptions(this.db());
|
|
3200
|
+
}
|
|
3201
|
+
async listProjects() {
|
|
3202
|
+
return queryProjectBreakdown(this.db());
|
|
3203
|
+
}
|
|
3204
|
+
async brief(query = {}) {
|
|
3205
|
+
return buildBrief(this.db(), {
|
|
3206
|
+
since: query.since,
|
|
3207
|
+
machine: query.machine,
|
|
3208
|
+
currentMachineId: query.currentMachineId,
|
|
3209
|
+
localSyncAt: query.localSyncAt
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
async projectDetail(nameOrPath) {
|
|
3213
|
+
return queryProjectDetail(this.db(), nameOrPath);
|
|
3214
|
+
}
|
|
3215
|
+
async exportRows(type, period) {
|
|
3216
|
+
return queryExportRows(this.db(), type, period);
|
|
3217
|
+
}
|
|
3218
|
+
async rangeStats(from, to) {
|
|
3219
|
+
return queryRangeStats(this.db(), from, to);
|
|
3220
|
+
}
|
|
3221
|
+
async forecast() {
|
|
3222
|
+
return queryForecast(this.db());
|
|
3223
|
+
}
|
|
3224
|
+
async efficiency() {
|
|
3225
|
+
return queryModelEfficiency(this.db());
|
|
3226
|
+
}
|
|
3227
|
+
async recentRequests(since) {
|
|
3228
|
+
return queryRequestsSince(this.db(), since);
|
|
3229
|
+
}
|
|
3230
|
+
async estimate(input) {
|
|
3231
|
+
return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
3232
|
+
}
|
|
3233
|
+
async setBudget(input) {
|
|
3234
|
+
const now = new Date().toISOString();
|
|
3235
|
+
const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
|
|
3236
|
+
upsertBudget(this.db(), budget);
|
|
3237
|
+
return budget.id;
|
|
3238
|
+
}
|
|
3239
|
+
async removeBudget(id) {
|
|
3240
|
+
deleteBudget(this.db(), id);
|
|
3241
|
+
}
|
|
3242
|
+
async setGoal(input) {
|
|
3243
|
+
const now = new Date().toISOString();
|
|
3244
|
+
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
3245
|
+
}
|
|
3246
|
+
async removeGoal(id) {
|
|
3247
|
+
deleteGoal(this.db(), id);
|
|
3248
|
+
}
|
|
3249
|
+
async setPricing(input) {
|
|
3250
|
+
upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
3251
|
+
}
|
|
3252
|
+
async removePricing(model) {
|
|
3253
|
+
deleteModelPricing(this.db(), model);
|
|
3254
|
+
}
|
|
3255
|
+
async setSubscription(input) {
|
|
3256
|
+
const now = new Date().toISOString();
|
|
3257
|
+
const row = {
|
|
3258
|
+
id: input.id ?? randomUUID(),
|
|
3259
|
+
provider: input.provider,
|
|
3260
|
+
plan: input.plan,
|
|
3261
|
+
agent: input.agent,
|
|
3262
|
+
monthly_fee_usd: input.monthly_fee_usd,
|
|
3263
|
+
included_usage_usd: input.included_usage_usd,
|
|
3264
|
+
billing_cycle_start: input.billing_cycle_start,
|
|
3265
|
+
reset_policy: input.reset_policy,
|
|
3266
|
+
active: input.active,
|
|
3267
|
+
created_at: now,
|
|
3268
|
+
updated_at: now
|
|
3269
|
+
};
|
|
3270
|
+
upsertSubscription(this.db(), row);
|
|
3271
|
+
return row;
|
|
3272
|
+
}
|
|
3273
|
+
async removeSubscription(id) {
|
|
3274
|
+
deleteSubscription(this.db(), id);
|
|
3275
|
+
}
|
|
3276
|
+
async addProject(path, name) {
|
|
3277
|
+
upsertProject(this.db(), {
|
|
3278
|
+
id: randomUUID(),
|
|
3279
|
+
path,
|
|
3280
|
+
name,
|
|
3281
|
+
description: null,
|
|
3282
|
+
tags: [],
|
|
3283
|
+
created_at: new Date().toISOString()
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
async renameProject(path, name) {
|
|
3287
|
+
const existing = getProject(this.db(), path);
|
|
3288
|
+
if (!existing)
|
|
3289
|
+
throw new Error("Project not found");
|
|
3290
|
+
upsertProject(this.db(), { ...existing, name });
|
|
3291
|
+
}
|
|
3292
|
+
async removeProject(path) {
|
|
3293
|
+
deleteProject(this.db(), path);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
function q(params) {
|
|
3297
|
+
const out = {};
|
|
3298
|
+
for (const [k, v] of Object.entries(params))
|
|
3299
|
+
if (v !== undefined && v !== null && v !== "")
|
|
3300
|
+
out[k] = v;
|
|
3301
|
+
return out;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
class ApiStore {
|
|
3305
|
+
cloud;
|
|
3306
|
+
transport = "cloud-http";
|
|
3307
|
+
constructor(cloud) {
|
|
3308
|
+
this.cloud = cloud;
|
|
3309
|
+
}
|
|
3310
|
+
async summary(period, machine) {
|
|
3311
|
+
return cloudObject(this.cloud, "/summary", q({ period, machine }));
|
|
3312
|
+
}
|
|
3313
|
+
async sessions(filter) {
|
|
3314
|
+
return cloudListItems(this.cloud, "sessions", q({ ...filter }));
|
|
3315
|
+
}
|
|
3316
|
+
async topSessions(n, agent, since) {
|
|
3317
|
+
return cloudListItems(this.cloud, "top", q({ n, agent, since }));
|
|
3318
|
+
}
|
|
3319
|
+
async sessionDetail(id) {
|
|
3320
|
+
const matches = await cloudListItems(this.cloud, "sessions", q({ search: id, limit: 1 }));
|
|
3321
|
+
const session = matches[0];
|
|
3322
|
+
if (!session)
|
|
3323
|
+
return null;
|
|
3324
|
+
const requests = await cloudObject(this.cloud, `/sessions/${encodeURIComponent(String(session["id"]))}/requests`);
|
|
3325
|
+
return { session, requests: requests ?? [] };
|
|
3326
|
+
}
|
|
3327
|
+
async modelBreakdown(query = {}) {
|
|
3328
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "model", period: query.period, since: query.since }));
|
|
3329
|
+
}
|
|
3330
|
+
async agentBreakdown(query = {}) {
|
|
3331
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
|
|
3332
|
+
}
|
|
3333
|
+
async projectBreakdown(query = {}) {
|
|
3334
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
|
|
3335
|
+
}
|
|
3336
|
+
async accountBreakdown(query = {}) {
|
|
3337
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
3338
|
+
}
|
|
3339
|
+
async accounts(period) {
|
|
3340
|
+
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
3341
|
+
}
|
|
3342
|
+
async daily(days, machine) {
|
|
3343
|
+
return cloudListItems(this.cloud, "daily", q({ days, machine }));
|
|
3344
|
+
}
|
|
3345
|
+
async machines() {
|
|
3346
|
+
return cloudListItems(this.cloud, "machines");
|
|
3347
|
+
}
|
|
3348
|
+
async fleet(period) {
|
|
3349
|
+
const data = await cloudObject(this.cloud, "/fleet", q({ period }));
|
|
3350
|
+
return {
|
|
3351
|
+
summary: data.summary,
|
|
3352
|
+
machines: data.machines ?? [],
|
|
3353
|
+
registry: data.registry ?? []
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
async billingSummary(period) {
|
|
3357
|
+
return cloudObject(this.cloud, "/billing", q({ period }));
|
|
3358
|
+
}
|
|
3359
|
+
async billingDiff(period) {
|
|
3360
|
+
return cloudObject(this.cloud, "/billing/diff", q({ period }));
|
|
3361
|
+
}
|
|
3362
|
+
async usage(period, agent) {
|
|
3363
|
+
return cloudObject(this.cloud, "/usage", q({ period, agent }));
|
|
3364
|
+
}
|
|
3365
|
+
async savings(period, agent) {
|
|
3366
|
+
return cloudObject(this.cloud, "/savings", q({ period, agent }));
|
|
3367
|
+
}
|
|
3368
|
+
async listBudgets() {
|
|
3369
|
+
return cloudListItems(this.cloud, "budgets");
|
|
3370
|
+
}
|
|
3371
|
+
async listGoals() {
|
|
3372
|
+
return cloudListItems(this.cloud, "goals");
|
|
3373
|
+
}
|
|
3374
|
+
async listPricing() {
|
|
3375
|
+
return cloudListItems(this.cloud, "pricing");
|
|
3376
|
+
}
|
|
3377
|
+
async listSubscriptions() {
|
|
3378
|
+
return cloudListItems(this.cloud, "subscriptions");
|
|
3379
|
+
}
|
|
3380
|
+
async listProjects() {
|
|
3381
|
+
return cloudListItems(this.cloud, "projects");
|
|
3382
|
+
}
|
|
3383
|
+
async brief(query = {}) {
|
|
3384
|
+
return cloudObject(this.cloud, "/brief", q({ since: query.since, machine: query.machine }));
|
|
3385
|
+
}
|
|
3386
|
+
async projectDetail(nameOrPath) {
|
|
3387
|
+
return cloudObject(this.cloud, "/projects/detail", q({ q: nameOrPath }));
|
|
3388
|
+
}
|
|
3389
|
+
async exportRows(type, period) {
|
|
3390
|
+
return cloudListItems(this.cloud, "export", q({ type, period }));
|
|
3391
|
+
}
|
|
3392
|
+
async rangeStats(from, to) {
|
|
3393
|
+
return cloudObject(this.cloud, "/compare", q({ from, to }));
|
|
3394
|
+
}
|
|
3395
|
+
async forecast() {
|
|
3396
|
+
return cloudObject(this.cloud, "/forecast");
|
|
3397
|
+
}
|
|
3398
|
+
async efficiency() {
|
|
3399
|
+
return cloudListItems(this.cloud, "efficiency");
|
|
3400
|
+
}
|
|
3401
|
+
async recentRequests(since) {
|
|
3402
|
+
return cloudListItems(this.cloud, "requests", q({ since }));
|
|
3403
|
+
}
|
|
3404
|
+
async estimate(input) {
|
|
3405
|
+
const rows = await this.listPricing();
|
|
3406
|
+
return estimateCostFromRows(rows, input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
3407
|
+
}
|
|
3408
|
+
async setBudget(input) {
|
|
3409
|
+
const created = await this.cloud.client.create("budgets", {
|
|
3410
|
+
project_path: input.project_path,
|
|
3411
|
+
...input.agent ? { agent: input.agent } : {},
|
|
3412
|
+
period: input.period,
|
|
3413
|
+
limit_usd: input.limit_usd,
|
|
3414
|
+
alert_at_percent: input.alert_at_percent
|
|
3415
|
+
});
|
|
3416
|
+
return created?.id ?? "";
|
|
3417
|
+
}
|
|
3418
|
+
async removeBudget(id) {
|
|
3419
|
+
await this.cloud.client.delete("budgets", id);
|
|
3420
|
+
}
|
|
3421
|
+
async setGoal(input) {
|
|
3422
|
+
await this.cloud.client.create("goals", {
|
|
3423
|
+
period: input.period,
|
|
3424
|
+
project_path: input.project_path,
|
|
3425
|
+
...input.agent ? { agent: input.agent } : {},
|
|
3426
|
+
limit_usd: input.limit_usd
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
async removeGoal(id) {
|
|
3430
|
+
await this.cloud.client.delete("goals", id);
|
|
3431
|
+
}
|
|
3432
|
+
async setPricing(input) {
|
|
3433
|
+
await this.cloud.client.create("pricing", { ...input, updated_at: new Date().toISOString() });
|
|
3434
|
+
}
|
|
3435
|
+
async removePricing(model) {
|
|
3436
|
+
await this.cloud.client.delete("pricing", model);
|
|
3437
|
+
}
|
|
3438
|
+
async setSubscription(input) {
|
|
3439
|
+
const now = new Date().toISOString();
|
|
3440
|
+
const row = {
|
|
3441
|
+
id: input.id ?? randomUUID(),
|
|
3442
|
+
provider: input.provider,
|
|
3443
|
+
plan: input.plan,
|
|
3444
|
+
agent: input.agent,
|
|
3445
|
+
monthly_fee_usd: input.monthly_fee_usd,
|
|
3446
|
+
included_usage_usd: input.included_usage_usd,
|
|
3447
|
+
billing_cycle_start: input.billing_cycle_start,
|
|
3448
|
+
reset_policy: input.reset_policy,
|
|
3449
|
+
active: input.active,
|
|
3450
|
+
created_at: now,
|
|
3451
|
+
updated_at: now
|
|
3452
|
+
};
|
|
3453
|
+
await this.cloud.client.create("subscriptions", row);
|
|
3454
|
+
return row;
|
|
3455
|
+
}
|
|
3456
|
+
async removeSubscription(id) {
|
|
3457
|
+
await this.cloud.client.delete("subscriptions", id);
|
|
3458
|
+
}
|
|
3459
|
+
async addProject(path, name) {
|
|
3460
|
+
await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
|
|
3461
|
+
}
|
|
3462
|
+
async renameProject(path, name) {
|
|
3463
|
+
await this.cloud.client.delete("project-registry", path);
|
|
3464
|
+
await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
|
|
3465
|
+
}
|
|
3466
|
+
async removeProject(path) {
|
|
3467
|
+
await this.cloud.client.delete("project-registry", path);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function getStore(env = process.env) {
|
|
3471
|
+
const cloud = economyCloudStorage(env);
|
|
3472
|
+
return cloud.active ? new ApiStore(cloud) : new LocalStore;
|
|
3473
|
+
}
|
|
3474
|
+
function isCloudStore(env = process.env) {
|
|
3475
|
+
return economyCloudStorage(env).active;
|
|
3166
3476
|
}
|
|
3167
3477
|
export {
|
|
3168
|
-
|
|
3169
|
-
verifySqliteIntegrity,
|
|
3170
|
-
upsertUsageSnapshot,
|
|
3171
|
-
upsertSubscription,
|
|
3172
|
-
upsertSession,
|
|
3173
|
-
upsertRequest,
|
|
3174
|
-
upsertProject,
|
|
3175
|
-
upsertModelPricing,
|
|
3176
|
-
upsertGoal,
|
|
3177
|
-
upsertBudget,
|
|
3178
|
-
upsertBillingDaily,
|
|
3179
|
-
syncOpenProjectsRegistry,
|
|
3180
|
-
setIngestState,
|
|
3181
|
-
setActiveModel,
|
|
3182
|
-
seedModelPricing,
|
|
3183
|
-
runEconomyFleetSync,
|
|
3184
|
-
rollupSession,
|
|
3185
|
-
readCodexModel,
|
|
3186
|
-
queryZeroCostTokenizedModels,
|
|
3187
|
-
queryUsageSnapshots,
|
|
3188
|
-
queryTopSessions,
|
|
3189
|
-
querySummary,
|
|
3190
|
-
querySessions,
|
|
3191
|
-
queryRequestsSince,
|
|
3192
|
-
queryProjectBreakdown,
|
|
3193
|
-
queryModelBreakdown,
|
|
3194
|
-
queryHourlyBreakdown,
|
|
3195
|
-
queryDailyBreakdown,
|
|
3196
|
-
queryBillingSummary,
|
|
3197
|
-
queryAgentBreakdown,
|
|
3198
|
-
queryAccountBreakdown,
|
|
3199
|
-
openDatabase,
|
|
3200
|
-
normalizeModelName,
|
|
3201
|
-
mergePeerDatabase,
|
|
3202
|
-
listSubscriptions,
|
|
3203
|
-
listProjects,
|
|
3204
|
-
listModelPricing,
|
|
3205
|
-
listMachines,
|
|
3206
|
-
listMachineRegistry,
|
|
3207
|
-
listGoals,
|
|
3208
|
-
listBudgets,
|
|
3209
|
-
isSafeRemoteSnapshotPath,
|
|
3478
|
+
isCloudStore,
|
|
3210
3479
|
isAgent,
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
ingestGemini,
|
|
3214
|
-
ingestCodex,
|
|
3215
|
-
ingestClaude,
|
|
3216
|
-
getProject,
|
|
3217
|
-
getPricingFromDb,
|
|
3218
|
-
getPricing,
|
|
3219
|
-
getModelPricing,
|
|
3220
|
-
getMachineId,
|
|
3221
|
-
getIngestState,
|
|
3222
|
-
getGoalStatuses,
|
|
3223
|
-
getDbPath,
|
|
3224
|
-
getDataDir,
|
|
3225
|
-
getBudgetStatuses,
|
|
3226
|
-
getActiveModel,
|
|
3227
|
-
gatherTrainingData,
|
|
3228
|
-
ensurePricingSeeded,
|
|
3229
|
-
deleteSubscription,
|
|
3230
|
-
deleteProject,
|
|
3231
|
-
deleteModelPricing,
|
|
3232
|
-
deleteGoal,
|
|
3233
|
-
deleteBudget,
|
|
3234
|
-
dedupeRequests,
|
|
3235
|
-
computeCostFromDb,
|
|
3236
|
-
computeCost,
|
|
3237
|
-
clearBillingRange,
|
|
3238
|
-
clearActiveModel,
|
|
3239
|
-
DEFAULT_PRICING,
|
|
3240
|
-
DEFAULT_MODEL,
|
|
3480
|
+
getStore,
|
|
3481
|
+
LocalStore,
|
|
3241
3482
|
COST_BASIS,
|
|
3483
|
+
ApiStore,
|
|
3242
3484
|
AGENTS
|
|
3243
3485
|
};
|