@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/mcp/index.js
CHANGED
|
@@ -21,8 +21,10 @@ var __require = import.meta.require;
|
|
|
21
21
|
var exports_pricing = {};
|
|
22
22
|
__export(exports_pricing, {
|
|
23
23
|
normalizeModelName: () => normalizeModelName,
|
|
24
|
+
getPricingFromRows: () => getPricingFromRows,
|
|
24
25
|
getPricingFromDb: () => getPricingFromDb,
|
|
25
26
|
getPricing: () => getPricing,
|
|
27
|
+
estimateCostFromRows: () => estimateCostFromRows,
|
|
26
28
|
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
27
29
|
computeCostFromDb: () => computeCostFromDb,
|
|
28
30
|
computeCost: () => computeCost,
|
|
@@ -213,6 +215,25 @@ function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens
|
|
|
213
215
|
return 0;
|
|
214
216
|
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
215
217
|
}
|
|
218
|
+
function getPricingFromRows(rows, model) {
|
|
219
|
+
if (isFreeModel(model))
|
|
220
|
+
return FREE_PRICING;
|
|
221
|
+
for (const key of modelLookupKeys(model)) {
|
|
222
|
+
const row = rows.find((r) => r.model === key);
|
|
223
|
+
if (row)
|
|
224
|
+
return modelPricingFromDbRow(row);
|
|
225
|
+
}
|
|
226
|
+
const match = bestModelMatch(model, rows.map((r) => [r.model, r]));
|
|
227
|
+
if (!match)
|
|
228
|
+
return null;
|
|
229
|
+
return modelPricingFromDbRow(match);
|
|
230
|
+
}
|
|
231
|
+
function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
|
|
232
|
+
const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
|
|
233
|
+
if (!pricing)
|
|
234
|
+
return 0;
|
|
235
|
+
return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
236
|
+
}
|
|
216
237
|
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
217
238
|
if (isFreeModel(model))
|
|
218
239
|
return 0;
|
|
@@ -1371,6 +1392,148 @@ function queryAccountBreakdown(db, period = "all", machine) {
|
|
|
1371
1392
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1372
1393
|
return result;
|
|
1373
1394
|
}
|
|
1395
|
+
function queryProjectBreakdownSince(db, since) {
|
|
1396
|
+
return db.prepare(`
|
|
1397
|
+
SELECT project_path, project_name,
|
|
1398
|
+
COUNT(*) as sessions,
|
|
1399
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1400
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
1401
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1402
|
+
MAX(started_at) as last_active
|
|
1403
|
+
FROM sessions WHERE started_at >= ?
|
|
1404
|
+
GROUP BY project_path ORDER BY cost_usd DESC
|
|
1405
|
+
`).all(since);
|
|
1406
|
+
}
|
|
1407
|
+
function queryAgentBreakdownSince(db, since) {
|
|
1408
|
+
return db.prepare(`
|
|
1409
|
+
SELECT agent,
|
|
1410
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1411
|
+
COUNT(*) as requests,
|
|
1412
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1413
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1414
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1415
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1416
|
+
MAX(timestamp) as last_active
|
|
1417
|
+
FROM requests
|
|
1418
|
+
WHERE timestamp >= ?
|
|
1419
|
+
GROUP BY agent
|
|
1420
|
+
ORDER BY api_equivalent_usd DESC
|
|
1421
|
+
`).all(since);
|
|
1422
|
+
}
|
|
1423
|
+
function queryModelBreakdownSince(db, since) {
|
|
1424
|
+
return db.prepare(`
|
|
1425
|
+
SELECT model, agent,
|
|
1426
|
+
COUNT(*) as requests,
|
|
1427
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
1428
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
1429
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1430
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1431
|
+
FROM requests WHERE timestamp >= ?
|
|
1432
|
+
GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1433
|
+
`).all(since);
|
|
1434
|
+
}
|
|
1435
|
+
function queryAccountBreakdownSince(db, since) {
|
|
1436
|
+
return db.prepare(`
|
|
1437
|
+
WITH request_rows AS (
|
|
1438
|
+
SELECT
|
|
1439
|
+
r.session_id as session_id,
|
|
1440
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1441
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as raw_account_key,
|
|
1442
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as raw_account_name,
|
|
1443
|
+
LOWER(TRIM(COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''))) as raw_account_email,
|
|
1444
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1445
|
+
1 as requests,
|
|
1446
|
+
COALESCE(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens, 0) as total_tokens,
|
|
1447
|
+
COALESCE(r.cost_usd, 0) as cost_usd,
|
|
1448
|
+
COALESCE(NULLIF(r.cost_basis, ''), 'estimated') as cost_basis,
|
|
1449
|
+
r.timestamp as last_active
|
|
1450
|
+
FROM requests r
|
|
1451
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
1452
|
+
WHERE r.timestamp >= ?
|
|
1453
|
+
AND (
|
|
1454
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') != ''
|
|
1455
|
+
OR COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') != ''
|
|
1456
|
+
OR COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') != ''
|
|
1457
|
+
OR COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') != ''
|
|
1458
|
+
)
|
|
1459
|
+
),
|
|
1460
|
+
session_only_rows AS (
|
|
1461
|
+
SELECT
|
|
1462
|
+
s.id as session_id,
|
|
1463
|
+
COALESCE(NULLIF(s.agent, ''), NULLIF(s.account_tool, ''), 'unknown') as account_agent,
|
|
1464
|
+
s.account_key as raw_account_key,
|
|
1465
|
+
s.account_name as raw_account_name,
|
|
1466
|
+
LOWER(TRIM(COALESCE(s.account_email, ''))) as raw_account_email,
|
|
1467
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
1468
|
+
COALESCE(s.request_count, 0) as requests,
|
|
1469
|
+
COALESCE(s.total_tokens, 0) as total_tokens,
|
|
1470
|
+
COALESCE(s.total_cost_usd, 0) as cost_usd,
|
|
1471
|
+
'estimated' as cost_basis,
|
|
1472
|
+
s.started_at as last_active
|
|
1473
|
+
FROM sessions s
|
|
1474
|
+
WHERE s.started_at >= ?
|
|
1475
|
+
AND s.id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1476
|
+
AND (s.account_key != '' OR s.account_tool != '' OR s.account_name != '' OR s.account_email != '')
|
|
1477
|
+
),
|
|
1478
|
+
normalized AS (
|
|
1479
|
+
SELECT
|
|
1480
|
+
CASE
|
|
1481
|
+
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1482
|
+
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1483
|
+
ELSE raw_account_key
|
|
1484
|
+
END as account_key,
|
|
1485
|
+
account_agent as account_tool,
|
|
1486
|
+
raw_account_name as account_name,
|
|
1487
|
+
raw_account_email as account_email,
|
|
1488
|
+
account_source,
|
|
1489
|
+
session_id,
|
|
1490
|
+
requests,
|
|
1491
|
+
total_tokens,
|
|
1492
|
+
cost_usd,
|
|
1493
|
+
cost_basis,
|
|
1494
|
+
last_active
|
|
1495
|
+
FROM request_rows
|
|
1496
|
+
UNION ALL
|
|
1497
|
+
SELECT
|
|
1498
|
+
CASE
|
|
1499
|
+
WHEN raw_account_email != '' THEN account_agent || ':' || raw_account_email
|
|
1500
|
+
WHEN raw_account_name != '' THEN account_agent || ':' || raw_account_name
|
|
1501
|
+
ELSE raw_account_key
|
|
1502
|
+
END as account_key,
|
|
1503
|
+
account_agent as account_tool,
|
|
1504
|
+
raw_account_name as account_name,
|
|
1505
|
+
raw_account_email as account_email,
|
|
1506
|
+
account_source,
|
|
1507
|
+
session_id,
|
|
1508
|
+
requests,
|
|
1509
|
+
total_tokens,
|
|
1510
|
+
cost_usd,
|
|
1511
|
+
cost_basis,
|
|
1512
|
+
last_active
|
|
1513
|
+
FROM session_only_rows
|
|
1514
|
+
)
|
|
1515
|
+
SELECT account_key,
|
|
1516
|
+
account_tool,
|
|
1517
|
+
COALESCE(MAX(NULLIF(account_name, '')), MAX(NULLIF(account_email, '')), account_key) as account_name,
|
|
1518
|
+
NULLIF(account_email, '') as account_email,
|
|
1519
|
+
COALESCE(MAX(NULLIF(account_source, 'unknown')), 'unknown') as account_source,
|
|
1520
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
1521
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
1522
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1523
|
+
COALESCE(SUM(cost_usd), 0) as api_equivalent_usd,
|
|
1524
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as billable_usd,
|
|
1525
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'metered_api' THEN cost_usd ELSE 0 END), 0) as metered_api_usd,
|
|
1526
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'subscription_included' THEN cost_usd ELSE 0 END), 0) as subscription_included_usd,
|
|
1527
|
+
COALESCE(SUM(CASE WHEN cost_basis NOT IN ('metered_api', 'subscription_included', 'unknown') THEN cost_usd ELSE 0 END), 0) as estimated_usd,
|
|
1528
|
+
COALESCE(SUM(CASE WHEN cost_basis = 'unknown' THEN cost_usd ELSE 0 END), 0) as unknown_usd,
|
|
1529
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1530
|
+
MAX(last_active) as last_active
|
|
1531
|
+
FROM normalized
|
|
1532
|
+
WHERE account_key != ''
|
|
1533
|
+
GROUP BY account_key, account_tool, account_email
|
|
1534
|
+
ORDER BY api_equivalent_usd DESC
|
|
1535
|
+
`).all(since, since);
|
|
1536
|
+
}
|
|
1374
1537
|
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1375
1538
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1376
1539
|
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
@@ -1382,6 +1545,21 @@ function queryDailyBreakdown(db, days = 30, machine) {
|
|
|
1382
1545
|
ORDER BY date ASC
|
|
1383
1546
|
`).all(...params);
|
|
1384
1547
|
}
|
|
1548
|
+
function upsertProject(db, project) {
|
|
1549
|
+
db.prepare(`
|
|
1550
|
+
INSERT OR REPLACE INTO projects (id, path, name, description, tags, created_at)
|
|
1551
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1552
|
+
`).run(project.id, project.path, project.name, project.description ?? null, JSON.stringify(project.tags), project.created_at);
|
|
1553
|
+
}
|
|
1554
|
+
function getProject(db, path) {
|
|
1555
|
+
const row = db.prepare(`SELECT * FROM projects WHERE path = ?`).get(path);
|
|
1556
|
+
if (!row)
|
|
1557
|
+
return null;
|
|
1558
|
+
return { ...row, tags: JSON.parse(row["tags"] ?? "[]") };
|
|
1559
|
+
}
|
|
1560
|
+
function deleteProject(db, path) {
|
|
1561
|
+
db.prepare(`DELETE FROM projects WHERE path = ?`).run(path);
|
|
1562
|
+
}
|
|
1385
1563
|
function upsertBudget(db, budget) {
|
|
1386
1564
|
db.prepare(`
|
|
1387
1565
|
INSERT OR REPLACE INTO budgets
|
|
@@ -1468,6 +1646,9 @@ function getIngestState(db, source, key) {
|
|
|
1468
1646
|
function setIngestState(db, source, key, value) {
|
|
1469
1647
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1470
1648
|
}
|
|
1649
|
+
function queryRequestsSince(db, since) {
|
|
1650
|
+
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
1651
|
+
}
|
|
1471
1652
|
function queryBillingSummary(db, period) {
|
|
1472
1653
|
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";
|
|
1473
1654
|
const rows = db.prepare(`SELECT provider, SUM(cost_usd) as cost FROM billing_daily WHERE ${where} GROUP BY provider`).all();
|
|
@@ -1479,6 +1660,13 @@ function queryBillingSummary(db, period) {
|
|
|
1479
1660
|
}
|
|
1480
1661
|
return { total_usd: total, by_provider };
|
|
1481
1662
|
}
|
|
1663
|
+
function getSessionDetail(db, id) {
|
|
1664
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `${id}%`);
|
|
1665
|
+
if (!session)
|
|
1666
|
+
return null;
|
|
1667
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1668
|
+
return { session, requests };
|
|
1669
|
+
}
|
|
1482
1670
|
function listMachines(db, period = "all") {
|
|
1483
1671
|
const rWhere = requestPeriodWhere(period);
|
|
1484
1672
|
const sWhere = sessionPeriodWhere(period);
|
|
@@ -1597,6 +1785,9 @@ function queryUsageSnapshots(db, opts = {}) {
|
|
|
1597
1785
|
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1598
1786
|
return db.prepare(`SELECT * FROM usage_snapshots ${where} ORDER BY date DESC, agent, metric`).all(...params);
|
|
1599
1787
|
}
|
|
1788
|
+
function listMachineRegistry(db) {
|
|
1789
|
+
return db.prepare(`SELECT * FROM machines ORDER BY last_seen_at DESC`).all();
|
|
1790
|
+
}
|
|
1600
1791
|
function dedupeRequests(db) {
|
|
1601
1792
|
const dupes = db.prepare(`
|
|
1602
1793
|
SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
|
|
@@ -1620,194 +1811,6 @@ function dedupeRequests(db) {
|
|
|
1620
1811
|
}
|
|
1621
1812
|
var init_database = () => {};
|
|
1622
1813
|
|
|
1623
|
-
// src/db/pg-migrations.ts
|
|
1624
|
-
var exports_pg_migrations = {};
|
|
1625
|
-
__export(exports_pg_migrations, {
|
|
1626
|
-
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1627
|
-
});
|
|
1628
|
-
var PG_MIGRATIONS;
|
|
1629
|
-
var init_pg_migrations = __esm(() => {
|
|
1630
|
-
PG_MIGRATIONS = [
|
|
1631
|
-
`CREATE TABLE IF NOT EXISTS requests (
|
|
1632
|
-
id TEXT PRIMARY KEY,
|
|
1633
|
-
agent TEXT NOT NULL,
|
|
1634
|
-
session_id TEXT NOT NULL,
|
|
1635
|
-
model TEXT NOT NULL,
|
|
1636
|
-
input_tokens INTEGER DEFAULT 0,
|
|
1637
|
-
output_tokens INTEGER DEFAULT 0,
|
|
1638
|
-
cache_read_tokens INTEGER DEFAULT 0,
|
|
1639
|
-
cache_create_tokens INTEGER DEFAULT 0,
|
|
1640
|
-
cache_create_5m_tokens INTEGER DEFAULT 0,
|
|
1641
|
-
cache_create_1h_tokens INTEGER DEFAULT 0,
|
|
1642
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1643
|
-
duration_ms INTEGER DEFAULT 0,
|
|
1644
|
-
timestamp TEXT NOT NULL,
|
|
1645
|
-
source_request_id TEXT,
|
|
1646
|
-
machine_id TEXT DEFAULT '',
|
|
1647
|
-
account_key TEXT DEFAULT '',
|
|
1648
|
-
account_tool TEXT DEFAULT '',
|
|
1649
|
-
account_name TEXT DEFAULT '',
|
|
1650
|
-
account_email TEXT DEFAULT '',
|
|
1651
|
-
account_source TEXT DEFAULT ''
|
|
1652
|
-
)`,
|
|
1653
|
-
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1654
|
-
id TEXT PRIMARY KEY,
|
|
1655
|
-
agent TEXT NOT NULL,
|
|
1656
|
-
project_path TEXT DEFAULT '',
|
|
1657
|
-
project_name TEXT DEFAULT '',
|
|
1658
|
-
started_at TEXT NOT NULL,
|
|
1659
|
-
ended_at TEXT,
|
|
1660
|
-
total_cost_usd REAL DEFAULT 0,
|
|
1661
|
-
total_tokens INTEGER DEFAULT 0,
|
|
1662
|
-
request_count INTEGER DEFAULT 0,
|
|
1663
|
-
machine_id TEXT DEFAULT '',
|
|
1664
|
-
account_key TEXT DEFAULT '',
|
|
1665
|
-
account_tool TEXT DEFAULT '',
|
|
1666
|
-
account_name TEXT DEFAULT '',
|
|
1667
|
-
account_email TEXT DEFAULT '',
|
|
1668
|
-
account_source TEXT DEFAULT ''
|
|
1669
|
-
)`,
|
|
1670
|
-
`CREATE TABLE IF NOT EXISTS projects (
|
|
1671
|
-
id TEXT PRIMARY KEY,
|
|
1672
|
-
path TEXT UNIQUE NOT NULL,
|
|
1673
|
-
name TEXT NOT NULL,
|
|
1674
|
-
description TEXT,
|
|
1675
|
-
tags TEXT DEFAULT '[]',
|
|
1676
|
-
created_at TEXT NOT NULL
|
|
1677
|
-
)`,
|
|
1678
|
-
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1679
|
-
id TEXT PRIMARY KEY,
|
|
1680
|
-
project_path TEXT,
|
|
1681
|
-
agent TEXT,
|
|
1682
|
-
period TEXT NOT NULL,
|
|
1683
|
-
limit_usd REAL NOT NULL,
|
|
1684
|
-
alert_at_percent INTEGER DEFAULT 80,
|
|
1685
|
-
created_at TEXT NOT NULL,
|
|
1686
|
-
updated_at TEXT NOT NULL
|
|
1687
|
-
)`,
|
|
1688
|
-
`CREATE TABLE IF NOT EXISTS goals (
|
|
1689
|
-
id TEXT PRIMARY KEY,
|
|
1690
|
-
period TEXT NOT NULL,
|
|
1691
|
-
project_path TEXT,
|
|
1692
|
-
agent TEXT,
|
|
1693
|
-
limit_usd REAL NOT NULL,
|
|
1694
|
-
created_at TEXT NOT NULL,
|
|
1695
|
-
updated_at TEXT NOT NULL
|
|
1696
|
-
)`,
|
|
1697
|
-
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1698
|
-
source TEXT NOT NULL,
|
|
1699
|
-
key TEXT NOT NULL,
|
|
1700
|
-
value TEXT NOT NULL,
|
|
1701
|
-
PRIMARY KEY (source, key)
|
|
1702
|
-
)`,
|
|
1703
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1704
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1705
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1706
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1707
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1708
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1709
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1710
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1711
|
-
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1712
|
-
model TEXT PRIMARY KEY,
|
|
1713
|
-
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1714
|
-
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1715
|
-
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1716
|
-
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1717
|
-
cache_write_1h_per_1m REAL NOT NULL DEFAULT 0,
|
|
1718
|
-
cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0,
|
|
1719
|
-
updated_at TEXT NOT NULL
|
|
1720
|
-
)`,
|
|
1721
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_5m_tokens INTEGER DEFAULT 0`,
|
|
1722
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cache_create_1h_tokens INTEGER DEFAULT 0`,
|
|
1723
|
-
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`,
|
|
1724
|
-
`ALTER TABLE model_pricing ADD COLUMN IF NOT EXISTS cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`,
|
|
1725
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1726
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1727
|
-
message TEXT NOT NULL,
|
|
1728
|
-
email TEXT,
|
|
1729
|
-
category TEXT DEFAULT 'general',
|
|
1730
|
-
version TEXT,
|
|
1731
|
-
machine_id TEXT,
|
|
1732
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1733
|
-
)`,
|
|
1734
|
-
`CREATE TABLE IF NOT EXISTS billing_daily (
|
|
1735
|
-
date TEXT NOT NULL,
|
|
1736
|
-
provider TEXT NOT NULL,
|
|
1737
|
-
description TEXT DEFAULT '',
|
|
1738
|
-
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1739
|
-
updated_at TEXT NOT NULL,
|
|
1740
|
-
PRIMARY KEY (date, provider, description)
|
|
1741
|
-
)`,
|
|
1742
|
-
`CREATE INDEX IF NOT EXISTS idx_billing_date ON billing_daily(date)`,
|
|
1743
|
-
`CREATE INDEX IF NOT EXISTS idx_billing_provider ON billing_daily(provider)`,
|
|
1744
|
-
`CREATE TABLE IF NOT EXISTS subscriptions (
|
|
1745
|
-
id TEXT PRIMARY KEY,
|
|
1746
|
-
agent TEXT,
|
|
1747
|
-
provider TEXT NOT NULL,
|
|
1748
|
-
plan TEXT NOT NULL,
|
|
1749
|
-
monthly_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1750
|
-
included_usage_usd REAL NOT NULL DEFAULT 0,
|
|
1751
|
-
billing_cycle_start TEXT,
|
|
1752
|
-
reset_policy TEXT DEFAULT 'monthly',
|
|
1753
|
-
active INTEGER NOT NULL DEFAULT 1,
|
|
1754
|
-
created_at TEXT NOT NULL,
|
|
1755
|
-
updated_at TEXT NOT NULL
|
|
1756
|
-
)`,
|
|
1757
|
-
`CREATE TABLE IF NOT EXISTS usage_snapshots (
|
|
1758
|
-
id TEXT PRIMARY KEY,
|
|
1759
|
-
agent TEXT NOT NULL,
|
|
1760
|
-
date TEXT NOT NULL,
|
|
1761
|
-
metric TEXT NOT NULL,
|
|
1762
|
-
value REAL NOT NULL DEFAULT 0,
|
|
1763
|
-
unit TEXT DEFAULT '',
|
|
1764
|
-
machine_id TEXT DEFAULT '',
|
|
1765
|
-
updated_at TEXT NOT NULL
|
|
1766
|
-
)`,
|
|
1767
|
-
`CREATE TABLE IF NOT EXISTS savings_daily (
|
|
1768
|
-
date TEXT NOT NULL,
|
|
1769
|
-
agent TEXT DEFAULT '',
|
|
1770
|
-
api_equivalent_usd REAL NOT NULL DEFAULT 0,
|
|
1771
|
-
subscription_fee_usd REAL NOT NULL DEFAULT 0,
|
|
1772
|
-
included_consumed_usd REAL NOT NULL DEFAULT 0,
|
|
1773
|
-
on_demand_usd REAL NOT NULL DEFAULT 0,
|
|
1774
|
-
saved_usd REAL NOT NULL DEFAULT 0,
|
|
1775
|
-
updated_at TEXT NOT NULL,
|
|
1776
|
-
PRIMARY KEY (date, agent)
|
|
1777
|
-
)`,
|
|
1778
|
-
`CREATE TABLE IF NOT EXISTS machines (
|
|
1779
|
-
machine_id TEXT PRIMARY KEY,
|
|
1780
|
-
hostname TEXT NOT NULL,
|
|
1781
|
-
last_seen_at TEXT,
|
|
1782
|
-
last_push_at TEXT,
|
|
1783
|
-
last_pull_at TEXT,
|
|
1784
|
-
economy_version TEXT,
|
|
1785
|
-
updated_at TEXT NOT NULL
|
|
1786
|
-
)`,
|
|
1787
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS cost_basis TEXT DEFAULT 'estimated'`,
|
|
1788
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1789
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1790
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1791
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1792
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1793
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1794
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1795
|
-
`ALTER TABLE requests ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1796
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS attribution_tag TEXT DEFAULT ''`,
|
|
1797
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_key TEXT DEFAULT ''`,
|
|
1798
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_tool TEXT DEFAULT ''`,
|
|
1799
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_name TEXT DEFAULT ''`,
|
|
1800
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_email TEXT DEFAULT ''`,
|
|
1801
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS account_source TEXT DEFAULT ''`,
|
|
1802
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS updated_at TEXT DEFAULT ''`,
|
|
1803
|
-
`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS synced_at TEXT DEFAULT ''`,
|
|
1804
|
-
`CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date)`,
|
|
1805
|
-
`CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date)`,
|
|
1806
|
-
`CREATE INDEX IF NOT EXISTS idx_requests_account ON requests(account_key)`,
|
|
1807
|
-
`CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_key)`
|
|
1808
|
-
];
|
|
1809
|
-
});
|
|
1810
|
-
|
|
1811
1814
|
// src/mcp/index.ts
|
|
1812
1815
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1813
1816
|
|
|
@@ -1829,9 +1832,9 @@ var packageMetadata = getPackageMetadata();
|
|
|
1829
1832
|
|
|
1830
1833
|
// src/mcp/server.ts
|
|
1831
1834
|
init_database();
|
|
1832
|
-
import { randomUUID } from "crypto";
|
|
1833
1835
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1834
1836
|
import { registerCloudTools } from "@hasna/cloud";
|
|
1837
|
+
import { registerAgentTools } from "@hasna/agent-registry";
|
|
1835
1838
|
import { z } from "zod";
|
|
1836
1839
|
|
|
1837
1840
|
// src/ingest/claude.ts
|
|
@@ -3446,139 +3449,10 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
3446
3449
|
|
|
3447
3450
|
// src/lib/sync-all.ts
|
|
3448
3451
|
init_database();
|
|
3449
|
-
|
|
3450
|
-
// src/lib/cloud-sync.ts
|
|
3451
|
-
init_database();
|
|
3452
|
-
var CLOUD_TABLES = [
|
|
3453
|
-
"requests",
|
|
3454
|
-
"sessions",
|
|
3455
|
-
"projects",
|
|
3456
|
-
"budgets",
|
|
3457
|
-
"goals",
|
|
3458
|
-
"model_pricing",
|
|
3459
|
-
"billing_daily",
|
|
3460
|
-
"subscriptions",
|
|
3461
|
-
"usage_snapshots",
|
|
3462
|
-
"savings_daily",
|
|
3463
|
-
"machines",
|
|
3464
|
-
"ingest_state"
|
|
3465
|
-
];
|
|
3466
|
-
function getCloudDatabaseUrl() {
|
|
3467
|
-
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
3468
|
-
}
|
|
3469
|
-
function isCloudAutoEnabled() {
|
|
3470
|
-
return process.env["ECONOMY_CLOUD_AUTO"] === "1" || process.env["ECONOMY_CLOUD_AUTO"] === "true";
|
|
3471
|
-
}
|
|
3472
|
-
function getCloudPullIntervalMinutes() {
|
|
3473
|
-
const raw = process.env["ECONOMY_CLOUD_PULL_INTERVAL"];
|
|
3474
|
-
if (!raw)
|
|
3475
|
-
return 15;
|
|
3476
|
-
const n = Number(raw);
|
|
3477
|
-
return Number.isFinite(n) && n > 0 ? n : 15;
|
|
3478
|
-
}
|
|
3479
|
-
async function getCloudPg() {
|
|
3480
|
-
const url = getCloudDatabaseUrl();
|
|
3481
|
-
if (!url) {
|
|
3482
|
-
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
3483
|
-
}
|
|
3484
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3485
|
-
return new PgAdapterAsync(url);
|
|
3486
|
-
}
|
|
3487
|
-
async function runCloudMigrations(cloud) {
|
|
3488
|
-
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
3489
|
-
for (const sql of PG_MIGRATIONS2) {
|
|
3490
|
-
await cloud.run(sql);
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
async function cloudPush(opts) {
|
|
3494
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3495
|
-
const cloud = await getCloudPg();
|
|
3496
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3497
|
-
await runCloudMigrations(cloud);
|
|
3498
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3499
|
-
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3500
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3501
|
-
touchMachineRegistry(local, "push");
|
|
3502
|
-
local.close();
|
|
3503
|
-
await cloud.close();
|
|
3504
|
-
return { rows, machine: getMachineId() };
|
|
3505
|
-
}
|
|
3506
|
-
async function cloudPull(opts) {
|
|
3507
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3508
|
-
const cloud = await getCloudPg();
|
|
3509
|
-
const local = new SqliteAdapter(getDbPath());
|
|
3510
|
-
await runCloudMigrations(cloud);
|
|
3511
|
-
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3512
|
-
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3513
|
-
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3514
|
-
touchMachineRegistry(local, "pull");
|
|
3515
|
-
local.close();
|
|
3516
|
-
await cloud.close();
|
|
3517
|
-
setLastCloudPull();
|
|
3518
|
-
return { rows, machine: getMachineId() };
|
|
3519
|
-
}
|
|
3520
|
-
function setLastCloudPull(at = new Date().toISOString()) {
|
|
3521
|
-
const db = openDatabase();
|
|
3522
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
3523
|
-
}
|
|
3524
|
-
function getLastCloudPull() {
|
|
3525
|
-
const db = openDatabase();
|
|
3526
|
-
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
3527
|
-
return row?.value ?? null;
|
|
3528
|
-
}
|
|
3529
|
-
function shouldPullFromCloud() {
|
|
3530
|
-
if (!getCloudDatabaseUrl())
|
|
3531
|
-
return false;
|
|
3532
|
-
const last = getLastCloudPull();
|
|
3533
|
-
if (!last)
|
|
3534
|
-
return true;
|
|
3535
|
-
const ageMs = Date.now() - new Date(last).getTime();
|
|
3536
|
-
return ageMs > getCloudPullIntervalMinutes() * 60000;
|
|
3537
|
-
}
|
|
3538
|
-
async function maybePullFromCloud() {
|
|
3539
|
-
if (!shouldPullFromCloud())
|
|
3540
|
-
return false;
|
|
3541
|
-
try {
|
|
3542
|
-
await cloudPull();
|
|
3543
|
-
return true;
|
|
3544
|
-
} catch {
|
|
3545
|
-
return false;
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3548
|
-
async function maybePushAfterIngest() {
|
|
3549
|
-
if (!isCloudAutoEnabled() || !getCloudDatabaseUrl())
|
|
3550
|
-
return false;
|
|
3551
|
-
try {
|
|
3552
|
-
await cloudPush();
|
|
3553
|
-
return true;
|
|
3554
|
-
} catch {
|
|
3555
|
-
return false;
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
function touchMachineRegistry(db, direction) {
|
|
3559
|
-
const now = new Date().toISOString();
|
|
3560
|
-
const machine = getMachineId();
|
|
3561
|
-
db.prepare(`
|
|
3562
|
-
INSERT INTO machines (machine_id, hostname, last_seen_at, last_push_at, last_pull_at, economy_version, updated_at)
|
|
3563
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3564
|
-
ON CONFLICT(machine_id) DO UPDATE SET
|
|
3565
|
-
hostname = excluded.hostname,
|
|
3566
|
-
last_seen_at = excluded.last_seen_at,
|
|
3567
|
-
last_push_at = CASE WHEN ? = 'push' THEN excluded.last_push_at ELSE machines.last_push_at END,
|
|
3568
|
-
last_pull_at = CASE WHEN ? = 'pull' THEN excluded.last_pull_at ELSE machines.last_pull_at END,
|
|
3569
|
-
economy_version = excluded.economy_version,
|
|
3570
|
-
updated_at = excluded.updated_at
|
|
3571
|
-
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
3572
|
-
}
|
|
3573
|
-
|
|
3574
|
-
// src/lib/sync-all.ts
|
|
3575
3452
|
async function syncAll(db, opts = {}) {
|
|
3576
3453
|
const anySpecific = Boolean(opts.claude || opts.takumi || opts.codex || opts.gemini || opts.opencode || opts.cursor || opts.pi || opts.hermes);
|
|
3577
3454
|
const all = !anySpecific;
|
|
3578
|
-
const
|
|
3579
|
-
if (useCloud)
|
|
3580
|
-
await maybePullFromCloud();
|
|
3581
|
-
const result = { deduped: 0, cloudPulled: false, cloudPushed: false };
|
|
3455
|
+
const result = { deduped: 0 };
|
|
3582
3456
|
if (all || opts.claude) {
|
|
3583
3457
|
result.claude = await ingestClaude(db, opts.verbose);
|
|
3584
3458
|
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
@@ -3601,8 +3475,6 @@ async function syncAll(db, opts = {}) {
|
|
|
3601
3475
|
result.hermes = await ingestHermes(db, opts.verbose);
|
|
3602
3476
|
if (opts.dedupe !== false)
|
|
3603
3477
|
result.deduped = dedupeRequests(db);
|
|
3604
|
-
if (useCloud)
|
|
3605
|
-
result.cloudPushed = await maybePushAfterIngest();
|
|
3606
3478
|
return result;
|
|
3607
3479
|
}
|
|
3608
3480
|
|
|
@@ -3619,7 +3491,73 @@ var AGENTS = [
|
|
|
3619
3491
|
];
|
|
3620
3492
|
|
|
3621
3493
|
// src/mcp/server.ts
|
|
3494
|
+
init_pricing();
|
|
3495
|
+
|
|
3496
|
+
// src/lib/store/index.ts
|
|
3497
|
+
init_database();
|
|
3498
|
+
init_pricing();
|
|
3499
|
+
import { randomUUID } from "crypto";
|
|
3500
|
+
|
|
3501
|
+
// src/lib/billing-diff.ts
|
|
3622
3502
|
init_database();
|
|
3503
|
+
var PROVIDER_TO_AGENT = {
|
|
3504
|
+
anthropic: "claude",
|
|
3505
|
+
openai: "codex",
|
|
3506
|
+
gemini: "gemini",
|
|
3507
|
+
google: "gemini"
|
|
3508
|
+
};
|
|
3509
|
+
function queryBillingDiff(db, period, thresholdPct = 15) {
|
|
3510
|
+
const estimated = querySummary(db, period, undefined, true);
|
|
3511
|
+
const actual = queryBillingSummary(db, period);
|
|
3512
|
+
const delta = estimated.total_usd - actual.total_usd;
|
|
3513
|
+
const deltaPct = actual.total_usd > 0 ? delta / actual.total_usd * 100 : 0;
|
|
3514
|
+
const agentRows = db.prepare(`
|
|
3515
|
+
SELECT agent, COALESCE(SUM(cost_usd), 0) as estimated_usd
|
|
3516
|
+
FROM requests
|
|
3517
|
+
WHERE ${periodWhere3(period, "timestamp")}
|
|
3518
|
+
GROUP BY agent
|
|
3519
|
+
`).all();
|
|
3520
|
+
const by_agent = agentRows.map((row) => {
|
|
3521
|
+
const provider = Object.entries(PROVIDER_TO_AGENT).find(([, a]) => a === row.agent)?.[0];
|
|
3522
|
+
const actualUsd = provider ? actual.by_provider[provider] ?? 0 : 0;
|
|
3523
|
+
const rowDelta = row.estimated_usd - actualUsd;
|
|
3524
|
+
const rowPct = actualUsd > 0 ? rowDelta / actualUsd * 100 : 0;
|
|
3525
|
+
return {
|
|
3526
|
+
agent: row.agent,
|
|
3527
|
+
estimated_usd: row.estimated_usd,
|
|
3528
|
+
actual_usd: actualUsd,
|
|
3529
|
+
delta_usd: rowDelta,
|
|
3530
|
+
delta_pct: rowPct
|
|
3531
|
+
};
|
|
3532
|
+
}).sort((a, b) => Math.abs(b.delta_usd) - Math.abs(a.delta_usd));
|
|
3533
|
+
return {
|
|
3534
|
+
period,
|
|
3535
|
+
estimated_usd: estimated.total_usd,
|
|
3536
|
+
actual_usd: actual.total_usd,
|
|
3537
|
+
delta_usd: delta,
|
|
3538
|
+
delta_pct: deltaPct,
|
|
3539
|
+
threshold_pct: thresholdPct,
|
|
3540
|
+
is_alert: Math.abs(deltaPct) > thresholdPct,
|
|
3541
|
+
by_agent,
|
|
3542
|
+
by_provider: actual.by_provider
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
function periodWhere3(period, column) {
|
|
3546
|
+
switch (period) {
|
|
3547
|
+
case "today":
|
|
3548
|
+
return `DATE(${column}) = DATE('now')`;
|
|
3549
|
+
case "yesterday":
|
|
3550
|
+
return `DATE(${column}) = DATE('now', '-1 day')`;
|
|
3551
|
+
case "week":
|
|
3552
|
+
return `${column} >= DATE('now', 'weekday 0', '-7 days')`;
|
|
3553
|
+
case "month":
|
|
3554
|
+
return `${column} >= DATE('now', 'start of month')`;
|
|
3555
|
+
case "year":
|
|
3556
|
+
return `${column} >= DATE('now', 'start of year')`;
|
|
3557
|
+
case "all":
|
|
3558
|
+
return "1=1";
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3623
3561
|
|
|
3624
3562
|
// src/lib/periods.ts
|
|
3625
3563
|
function ymd(date) {
|
|
@@ -3649,110 +3587,1631 @@ function usageSnapshotFilterForPeriod(period) {
|
|
|
3649
3587
|
}
|
|
3650
3588
|
}
|
|
3651
3589
|
|
|
3652
|
-
// src/
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
"
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
"
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
"
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
|
|
3724
|
-
search_tools: "query substring -> tool name list",
|
|
3725
|
-
describe_tools: "names[] -> one-line parameter hints",
|
|
3726
|
-
get_goals: "no params -> goal progress summary",
|
|
3727
|
-
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
3728
|
-
remove_goal: "id -> delete goal",
|
|
3729
|
-
register_agent: "name, session_id? -> register agent session",
|
|
3730
|
-
heartbeat: "agent_id -> update last_seen_at",
|
|
3731
|
-
set_focus: "agent_id, project_id? -> set active project context",
|
|
3732
|
-
list_agents: "no params -> registered agent list",
|
|
3733
|
-
send_feedback: "message, email?, category? -> save feedback locally"
|
|
3590
|
+
// src/lib/brief.ts
|
|
3591
|
+
init_database();
|
|
3592
|
+
var ZERO_REQUEST_TOTALS = {
|
|
3593
|
+
sessions: 0,
|
|
3594
|
+
request_sessions: 0,
|
|
3595
|
+
requests: 0,
|
|
3596
|
+
input_tokens: 0,
|
|
3597
|
+
output_tokens: 0,
|
|
3598
|
+
cache_read_tokens: 0,
|
|
3599
|
+
cache_create_tokens: 0,
|
|
3600
|
+
cache_create_5m_tokens: 0,
|
|
3601
|
+
cache_create_1h_tokens: 0,
|
|
3602
|
+
total_tokens: 0,
|
|
3603
|
+
cost_usd: 0,
|
|
3604
|
+
last_active: null
|
|
3605
|
+
};
|
|
3606
|
+
var ZERO_SESSION_TOTALS = {
|
|
3607
|
+
sessions: 0,
|
|
3608
|
+
requests: 0,
|
|
3609
|
+
total_tokens: 0,
|
|
3610
|
+
cost_usd: 0,
|
|
3611
|
+
last_active: null
|
|
3612
|
+
};
|
|
3613
|
+
function formatAge(timestamp, now) {
|
|
3614
|
+
if (!timestamp)
|
|
3615
|
+
return "never";
|
|
3616
|
+
const t = new Date(timestamp).getTime();
|
|
3617
|
+
if (!Number.isFinite(t))
|
|
3618
|
+
return "unknown";
|
|
3619
|
+
const ageMs = Math.max(0, now.getTime() - t);
|
|
3620
|
+
const minutes = Math.floor(ageMs / 60000);
|
|
3621
|
+
if (minutes < 1)
|
|
3622
|
+
return "<1m";
|
|
3623
|
+
if (minutes < 60)
|
|
3624
|
+
return `${minutes}m`;
|
|
3625
|
+
const hours = Math.floor(minutes / 60);
|
|
3626
|
+
if (hours < 48)
|
|
3627
|
+
return `${hours}h`;
|
|
3628
|
+
return `${Math.floor(hours / 24)}d`;
|
|
3629
|
+
}
|
|
3630
|
+
function machineFilter(machine) {
|
|
3631
|
+
if (!machine || machine === "all")
|
|
3632
|
+
return;
|
|
3633
|
+
return machine;
|
|
3634
|
+
}
|
|
3635
|
+
function requestPeriodWhere2(period) {
|
|
3636
|
+
if (period === "today")
|
|
3637
|
+
return `DATE(timestamp) = DATE('now')`;
|
|
3638
|
+
return `timestamp >= DATE('now', 'weekday 0', '-7 days')`;
|
|
3639
|
+
}
|
|
3640
|
+
function sessionPeriodWhere2(period) {
|
|
3641
|
+
if (period === "today")
|
|
3642
|
+
return `DATE(started_at) = DATE('now')`;
|
|
3643
|
+
return `started_at >= DATE('now', 'weekday 0', '-7 days')`;
|
|
3644
|
+
}
|
|
3645
|
+
function baseRequestTotals(row) {
|
|
3646
|
+
return {
|
|
3647
|
+
...ZERO_REQUEST_TOTALS,
|
|
3648
|
+
...row,
|
|
3649
|
+
sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
3650
|
+
request_sessions: Number(row?.request_sessions ?? row?.sessions ?? 0),
|
|
3651
|
+
requests: Number(row?.requests ?? 0),
|
|
3652
|
+
input_tokens: Number(row?.input_tokens ?? 0),
|
|
3653
|
+
output_tokens: Number(row?.output_tokens ?? 0),
|
|
3654
|
+
cache_read_tokens: Number(row?.cache_read_tokens ?? 0),
|
|
3655
|
+
cache_create_tokens: Number(row?.cache_create_tokens ?? 0),
|
|
3656
|
+
cache_create_5m_tokens: Number(row?.cache_create_5m_tokens ?? 0),
|
|
3657
|
+
cache_create_1h_tokens: Number(row?.cache_create_1h_tokens ?? 0),
|
|
3658
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
3659
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
3660
|
+
last_active: row?.last_active ?? null
|
|
3734
3661
|
};
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3662
|
+
}
|
|
3663
|
+
function baseSessionTotals(row) {
|
|
3664
|
+
return {
|
|
3665
|
+
sessions: Number(row?.sessions ?? 0),
|
|
3666
|
+
requests: Number(row?.requests ?? 0),
|
|
3667
|
+
total_tokens: Number(row?.total_tokens ?? 0),
|
|
3668
|
+
cost_usd: Number(row?.cost_usd ?? 0),
|
|
3669
|
+
last_active: row?.last_active ?? null
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
function latestTimestamp(...values) {
|
|
3673
|
+
return values.filter(Boolean).sort().at(-1) ?? null;
|
|
3674
|
+
}
|
|
3675
|
+
function combineTotals(requests, sessions) {
|
|
3676
|
+
return {
|
|
3677
|
+
sessions: requests.request_sessions + sessions.sessions,
|
|
3678
|
+
requests: requests.requests + sessions.requests,
|
|
3679
|
+
input_tokens: requests.input_tokens,
|
|
3680
|
+
output_tokens: requests.output_tokens,
|
|
3681
|
+
cache_read_tokens: requests.cache_read_tokens,
|
|
3682
|
+
cache_create_tokens: requests.cache_create_tokens,
|
|
3683
|
+
cache_create_5m_tokens: requests.cache_create_5m_tokens,
|
|
3684
|
+
cache_create_1h_tokens: requests.cache_create_1h_tokens,
|
|
3685
|
+
total_tokens: requests.total_tokens + sessions.total_tokens,
|
|
3686
|
+
cost_usd: requests.cost_usd + sessions.cost_usd,
|
|
3687
|
+
last_active: latestTimestamp(requests.last_active, sessions.last_active)
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
function emptyTotals() {
|
|
3691
|
+
return combineTotals(ZERO_REQUEST_TOTALS, ZERO_SESSION_TOTALS);
|
|
3692
|
+
}
|
|
3693
|
+
function requestTotalsSql(where, machine) {
|
|
3694
|
+
const params = [];
|
|
3695
|
+
let machineClause = "";
|
|
3696
|
+
if (machine) {
|
|
3697
|
+
machineClause = " AND machine_id = ?";
|
|
3698
|
+
params.push(machine);
|
|
3750
3699
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3700
|
+
return {
|
|
3701
|
+
sql: `
|
|
3702
|
+
SELECT
|
|
3703
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
3704
|
+
COUNT(*) as requests,
|
|
3705
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
3706
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
3707
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
3708
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
3709
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
3710
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
3711
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
3712
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
3713
|
+
MAX(timestamp) as last_active
|
|
3714
|
+
FROM requests
|
|
3715
|
+
WHERE ${where}${machineClause}
|
|
3716
|
+
`,
|
|
3717
|
+
params
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
function sessionOnlyTotalsSql(where, machine) {
|
|
3721
|
+
const params = [];
|
|
3722
|
+
let machineClause = "";
|
|
3723
|
+
if (machine) {
|
|
3724
|
+
machineClause = " AND machine_id = ?";
|
|
3725
|
+
params.push(machine);
|
|
3726
|
+
}
|
|
3727
|
+
return {
|
|
3728
|
+
sql: `
|
|
3729
|
+
SELECT
|
|
3730
|
+
COUNT(*) as sessions,
|
|
3731
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
3732
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
3733
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
3734
|
+
MAX(started_at) as last_active
|
|
3735
|
+
FROM sessions
|
|
3736
|
+
WHERE ${where}${machineClause}
|
|
3737
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
3738
|
+
`,
|
|
3739
|
+
params
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function queryRequestTotals(db, where, params, machine) {
|
|
3743
|
+
const q = requestTotalsSql(where, machine);
|
|
3744
|
+
return baseRequestTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
3745
|
+
}
|
|
3746
|
+
function querySessionOnlyTotals(db, where, params, machine) {
|
|
3747
|
+
const q = sessionOnlyTotalsSql(where, machine);
|
|
3748
|
+
return baseSessionTotals(db.prepare(q.sql).get(...params, ...q.params));
|
|
3749
|
+
}
|
|
3750
|
+
function queryPeriodTotals(db, period, machine) {
|
|
3751
|
+
const summary = querySummary(db, period, machine);
|
|
3752
|
+
const requests = queryRequestTotals(db, requestPeriodWhere2(period), [], machine);
|
|
3753
|
+
const sessions = querySessionOnlyTotals(db, sessionPeriodWhere2(period), [], machine);
|
|
3754
|
+
const totals = combineTotals(requests, sessions);
|
|
3755
|
+
return {
|
|
3756
|
+
...totals,
|
|
3757
|
+
sessions: summary.sessions,
|
|
3758
|
+
requests: summary.requests,
|
|
3759
|
+
total_tokens: summary.tokens,
|
|
3760
|
+
cost_usd: summary.total_usd
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
function querySinceTotals(db, since, machine) {
|
|
3764
|
+
const requests = queryRequestTotals(db, "timestamp >= ?", [since], machine);
|
|
3765
|
+
const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
|
|
3766
|
+
return combineTotals(requests, sessions);
|
|
3767
|
+
}
|
|
3768
|
+
function parseSinceDate(input, now) {
|
|
3769
|
+
const value = input.trim();
|
|
3770
|
+
const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
|
|
3771
|
+
if (match) {
|
|
3772
|
+
const amount = Number(match[1]);
|
|
3773
|
+
const unit = match[2].toLowerCase();
|
|
3774
|
+
const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
|
|
3775
|
+
return {
|
|
3776
|
+
timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
|
|
3777
|
+
label: value
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
const parsed = new Date(value);
|
|
3781
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
3782
|
+
throw new Error("--since must be a duration like 24h, 7d, 30m, 2w, or an ISO date");
|
|
3783
|
+
}
|
|
3784
|
+
return { timestamp: parsed.toISOString(), label: value };
|
|
3785
|
+
}
|
|
3786
|
+
function queryMachineSinceRows(db, since, machine) {
|
|
3787
|
+
const knownMachines = new Set;
|
|
3788
|
+
for (const m of listMachines(db, "all")) {
|
|
3789
|
+
if (m.machine_id)
|
|
3790
|
+
knownMachines.add(m.machine_id);
|
|
3791
|
+
}
|
|
3792
|
+
for (const m of listMachineRegistry(db)) {
|
|
3793
|
+
if (m.machine_id)
|
|
3794
|
+
knownMachines.add(m.machine_id);
|
|
3795
|
+
}
|
|
3796
|
+
if (machine)
|
|
3797
|
+
knownMachines.add(machine);
|
|
3798
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
3799
|
+
const requestRows = db.prepare(`
|
|
3800
|
+
SELECT
|
|
3801
|
+
machine_id,
|
|
3802
|
+
COUNT(DISTINCT session_id) as request_sessions,
|
|
3803
|
+
COUNT(*) as requests,
|
|
3804
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
3805
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
3806
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
3807
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
3808
|
+
COALESCE(SUM(COALESCE(cache_create_5m_tokens, cache_create_tokens)), 0) as cache_create_5m_tokens,
|
|
3809
|
+
COALESCE(SUM(COALESCE(cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
3810
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
3811
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
3812
|
+
MAX(timestamp) as last_active
|
|
3813
|
+
FROM requests
|
|
3814
|
+
WHERE timestamp >= ?
|
|
3815
|
+
AND machine_id != ''
|
|
3816
|
+
${requestMachineClause}
|
|
3817
|
+
GROUP BY machine_id
|
|
3818
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
3819
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
3820
|
+
const sessionRows = db.prepare(`
|
|
3821
|
+
SELECT
|
|
3822
|
+
machine_id,
|
|
3823
|
+
COUNT(*) as sessions,
|
|
3824
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
3825
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
3826
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
3827
|
+
MAX(started_at) as last_active
|
|
3828
|
+
FROM sessions
|
|
3829
|
+
WHERE started_at >= ?
|
|
3830
|
+
AND machine_id != ''
|
|
3831
|
+
${sessionMachineClause}
|
|
3832
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
3833
|
+
GROUP BY machine_id
|
|
3834
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
3835
|
+
const groups = new Map;
|
|
3836
|
+
for (const row of requestRows) {
|
|
3837
|
+
knownMachines.add(row.machine_id);
|
|
3838
|
+
groups.set(row.machine_id, combineTotals(baseRequestTotals(row), ZERO_SESSION_TOTALS));
|
|
3839
|
+
}
|
|
3840
|
+
for (const row of sessionRows) {
|
|
3841
|
+
knownMachines.add(row.machine_id);
|
|
3842
|
+
const current = groups.get(row.machine_id) ?? emptyTotals();
|
|
3843
|
+
const sessions = baseSessionTotals(row);
|
|
3844
|
+
groups.set(row.machine_id, {
|
|
3845
|
+
...current,
|
|
3846
|
+
sessions: current.sessions + sessions.sessions,
|
|
3847
|
+
requests: current.requests + sessions.requests,
|
|
3848
|
+
total_tokens: current.total_tokens + sessions.total_tokens,
|
|
3849
|
+
cost_usd: current.cost_usd + sessions.cost_usd,
|
|
3850
|
+
last_active: latestTimestamp(current.last_active, sessions.last_active)
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
const lastData = queryMachineLastData(db, machine);
|
|
3854
|
+
const rows = [...knownMachines].filter((m) => !machine || m === machine).map((machineId) => {
|
|
3855
|
+
const totals = groups.get(machineId) ?? emptyTotals();
|
|
3856
|
+
const lastDataAt = lastData.get(machineId) ?? totals.last_active;
|
|
3857
|
+
return {
|
|
3858
|
+
machine_id: machineId,
|
|
3859
|
+
...totals,
|
|
3860
|
+
last_data_at: lastDataAt,
|
|
3861
|
+
last_data_age: ""
|
|
3862
|
+
};
|
|
3863
|
+
});
|
|
3864
|
+
return rows.sort((a, b) => b.cost_usd - a.cost_usd || a.machine_id.localeCompare(b.machine_id));
|
|
3865
|
+
}
|
|
3866
|
+
function queryMachineLastData(db, machine) {
|
|
3867
|
+
const filter = machine ? " AND machine_id = ?" : "";
|
|
3868
|
+
const requestRows = db.prepare(`
|
|
3869
|
+
SELECT machine_id, MAX(timestamp) as last_at
|
|
3870
|
+
FROM requests
|
|
3871
|
+
WHERE machine_id != ''${filter}
|
|
3872
|
+
GROUP BY machine_id
|
|
3873
|
+
`).all(...machine ? [machine] : []);
|
|
3874
|
+
const sessionRows = db.prepare(`
|
|
3875
|
+
SELECT machine_id, MAX(started_at) as last_at
|
|
3876
|
+
FROM sessions
|
|
3877
|
+
WHERE machine_id != ''${filter}
|
|
3878
|
+
GROUP BY machine_id
|
|
3879
|
+
`).all(...machine ? [machine] : []);
|
|
3880
|
+
const result = new Map;
|
|
3881
|
+
for (const row of [...requestRows, ...sessionRows]) {
|
|
3882
|
+
result.set(row.machine_id, latestTimestamp(result.get(row.machine_id), row.last_at));
|
|
3883
|
+
}
|
|
3884
|
+
return result;
|
|
3885
|
+
}
|
|
3886
|
+
function queryAgentSinceRows(db, since, machine) {
|
|
3887
|
+
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
3888
|
+
const sessionMachineClause = machine ? " AND machine_id = ?" : "";
|
|
3889
|
+
const rows = db.prepare(`
|
|
3890
|
+
WITH request_rows AS (
|
|
3891
|
+
SELECT
|
|
3892
|
+
agent,
|
|
3893
|
+
session_id,
|
|
3894
|
+
1 as requests,
|
|
3895
|
+
input_tokens,
|
|
3896
|
+
output_tokens,
|
|
3897
|
+
cache_read_tokens,
|
|
3898
|
+
cache_create_tokens,
|
|
3899
|
+
COALESCE(cache_create_5m_tokens, cache_create_tokens) as cache_create_5m_tokens,
|
|
3900
|
+
COALESCE(cache_create_1h_tokens, 0) as cache_create_1h_tokens,
|
|
3901
|
+
input_tokens + output_tokens + cache_read_tokens + cache_create_tokens as total_tokens,
|
|
3902
|
+
cost_usd,
|
|
3903
|
+
timestamp as last_active
|
|
3904
|
+
FROM requests
|
|
3905
|
+
WHERE timestamp >= ?${requestMachineClause}
|
|
3906
|
+
),
|
|
3907
|
+
session_only_rows AS (
|
|
3908
|
+
SELECT
|
|
3909
|
+
agent,
|
|
3910
|
+
id as session_id,
|
|
3911
|
+
COALESCE(request_count, 0) as requests,
|
|
3912
|
+
0 as input_tokens,
|
|
3913
|
+
0 as output_tokens,
|
|
3914
|
+
0 as cache_read_tokens,
|
|
3915
|
+
0 as cache_create_tokens,
|
|
3916
|
+
0 as cache_create_5m_tokens,
|
|
3917
|
+
0 as cache_create_1h_tokens,
|
|
3918
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
3919
|
+
COALESCE(total_cost_usd, 0) as cost_usd,
|
|
3920
|
+
started_at as last_active
|
|
3921
|
+
FROM sessions
|
|
3922
|
+
WHERE started_at >= ?${sessionMachineClause}
|
|
3923
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = sessions.id)
|
|
3924
|
+
),
|
|
3925
|
+
combined AS (
|
|
3926
|
+
SELECT * FROM request_rows
|
|
3927
|
+
UNION ALL
|
|
3928
|
+
SELECT * FROM session_only_rows
|
|
3929
|
+
)
|
|
3930
|
+
SELECT
|
|
3931
|
+
agent,
|
|
3932
|
+
COUNT(DISTINCT session_id) as sessions,
|
|
3933
|
+
COALESCE(SUM(requests), 0) as requests,
|
|
3934
|
+
COALESCE(SUM(input_tokens), 0) as input_tokens,
|
|
3935
|
+
COALESCE(SUM(output_tokens), 0) as output_tokens,
|
|
3936
|
+
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
|
|
3937
|
+
COALESCE(SUM(cache_create_tokens), 0) as cache_create_tokens,
|
|
3938
|
+
COALESCE(SUM(cache_create_5m_tokens), 0) as cache_create_5m_tokens,
|
|
3939
|
+
COALESCE(SUM(cache_create_1h_tokens), 0) as cache_create_1h_tokens,
|
|
3940
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
3941
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
3942
|
+
MAX(last_active) as last_active
|
|
3943
|
+
FROM combined
|
|
3944
|
+
GROUP BY agent
|
|
3945
|
+
ORDER BY cost_usd DESC
|
|
3946
|
+
`).all(...machine ? [since, machine, since, machine] : [since, since]);
|
|
3947
|
+
if (rows.length > 0)
|
|
3948
|
+
return rows;
|
|
3949
|
+
return queryAgentBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(agentBreakdownToBriefRow);
|
|
3950
|
+
}
|
|
3951
|
+
function agentBreakdownToBriefRow(row) {
|
|
3952
|
+
return {
|
|
3953
|
+
agent: row.agent,
|
|
3954
|
+
sessions: row.sessions,
|
|
3955
|
+
requests: row.requests,
|
|
3956
|
+
input_tokens: 0,
|
|
3957
|
+
output_tokens: 0,
|
|
3958
|
+
cache_read_tokens: 0,
|
|
3959
|
+
cache_create_tokens: 0,
|
|
3960
|
+
cache_create_5m_tokens: 0,
|
|
3961
|
+
cache_create_1h_tokens: 0,
|
|
3962
|
+
total_tokens: row.total_tokens,
|
|
3963
|
+
cost_usd: row.cost_usd,
|
|
3964
|
+
last_active: row.last_active
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
function normalizeAccountEmail2(email) {
|
|
3968
|
+
return email.trim().toLowerCase();
|
|
3969
|
+
}
|
|
3970
|
+
function accountKey2(row) {
|
|
3971
|
+
const agent = row.agent || row.account_tool || "unknown";
|
|
3972
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
3973
|
+
if (email)
|
|
3974
|
+
return `${agent}:${email}`;
|
|
3975
|
+
if (row.account_name)
|
|
3976
|
+
return `${agent}:${row.account_name}`;
|
|
3977
|
+
return row.account_key || `${agent}:unknown`;
|
|
3978
|
+
}
|
|
3979
|
+
function addAccountRow(groups, row) {
|
|
3980
|
+
if (!row.account_key && !row.account_tool && !row.account_name && !row.account_email)
|
|
3981
|
+
return;
|
|
3982
|
+
const key = accountKey2(row);
|
|
3983
|
+
const email = normalizeAccountEmail2(row.account_email);
|
|
3984
|
+
const current = groups.get(key) ?? {
|
|
3985
|
+
account_key: key,
|
|
3986
|
+
account_tool: row.agent || row.account_tool || "unknown",
|
|
3987
|
+
account_name: row.account_name || email || row.account_key || "unknown",
|
|
3988
|
+
account_email: email || null,
|
|
3989
|
+
account_source: row.account_source || "unknown",
|
|
3990
|
+
sessions: 0,
|
|
3991
|
+
requests: 0,
|
|
3992
|
+
input_tokens: 0,
|
|
3993
|
+
output_tokens: 0,
|
|
3994
|
+
cache_read_tokens: 0,
|
|
3995
|
+
cache_create_tokens: 0,
|
|
3996
|
+
cache_create_5m_tokens: 0,
|
|
3997
|
+
cache_create_1h_tokens: 0,
|
|
3998
|
+
total_tokens: 0,
|
|
3999
|
+
cost_usd: 0,
|
|
4000
|
+
last_active: null
|
|
4001
|
+
};
|
|
4002
|
+
current.sessions += Number(row.sessions ?? 0);
|
|
4003
|
+
if (!current.account_email && email)
|
|
4004
|
+
current.account_email = email;
|
|
4005
|
+
if ((!current.account_name || current.account_name === "unknown") && (row.account_name || email)) {
|
|
4006
|
+
current.account_name = row.account_name || email;
|
|
4007
|
+
}
|
|
4008
|
+
if ((!current.account_source || current.account_source === "unknown") && row.account_source && row.account_source !== "unknown") {
|
|
4009
|
+
current.account_source = row.account_source;
|
|
4010
|
+
}
|
|
4011
|
+
current.requests += Number(row.requests ?? 0);
|
|
4012
|
+
current.input_tokens += Number(row.input_tokens ?? 0);
|
|
4013
|
+
current.output_tokens += Number(row.output_tokens ?? 0);
|
|
4014
|
+
current.cache_read_tokens += Number(row.cache_read_tokens ?? 0);
|
|
4015
|
+
current.cache_create_tokens += Number(row.cache_create_tokens ?? 0);
|
|
4016
|
+
current.cache_create_5m_tokens += Number(row.cache_create_5m_tokens ?? 0);
|
|
4017
|
+
current.cache_create_1h_tokens += Number(row.cache_create_1h_tokens ?? 0);
|
|
4018
|
+
current.total_tokens += Number(row.total_tokens ?? 0);
|
|
4019
|
+
current.cost_usd += Number(row.cost_usd ?? 0);
|
|
4020
|
+
current.last_active = latestTimestamp(current.last_active, row.last_active);
|
|
4021
|
+
groups.set(key, current);
|
|
4022
|
+
}
|
|
4023
|
+
function queryAccountSinceRows(db, since, machine) {
|
|
4024
|
+
const requestMachineClause = machine ? " AND r.machine_id = ?" : "";
|
|
4025
|
+
const sessionMachineClause = machine ? " AND s.machine_id = ?" : "";
|
|
4026
|
+
const requestRows = db.prepare(`
|
|
4027
|
+
SELECT
|
|
4028
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), '') as agent,
|
|
4029
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), '') as account_key,
|
|
4030
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), '') as account_tool,
|
|
4031
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), '') as account_name,
|
|
4032
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), '') as account_email,
|
|
4033
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
4034
|
+
COUNT(DISTINCT r.session_id) as sessions,
|
|
4035
|
+
COUNT(*) as requests,
|
|
4036
|
+
COALESCE(SUM(r.input_tokens), 0) as input_tokens,
|
|
4037
|
+
COALESCE(SUM(r.output_tokens), 0) as output_tokens,
|
|
4038
|
+
COALESCE(SUM(r.cache_read_tokens), 0) as cache_read_tokens,
|
|
4039
|
+
COALESCE(SUM(r.cache_create_tokens), 0) as cache_create_tokens,
|
|
4040
|
+
COALESCE(SUM(COALESCE(r.cache_create_5m_tokens, r.cache_create_tokens, 0)), 0) as cache_create_5m_tokens,
|
|
4041
|
+
COALESCE(SUM(COALESCE(r.cache_create_1h_tokens, 0)), 0) as cache_create_1h_tokens,
|
|
4042
|
+
COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
|
|
4043
|
+
COALESCE(SUM(r.cost_usd), 0) as cost_usd,
|
|
4044
|
+
MAX(r.timestamp) as last_active
|
|
4045
|
+
FROM requests r
|
|
4046
|
+
LEFT JOIN sessions s ON s.id = r.session_id
|
|
4047
|
+
WHERE r.timestamp >= ?${requestMachineClause}
|
|
4048
|
+
GROUP BY
|
|
4049
|
+
COALESCE(NULLIF(r.agent, ''), NULLIF(s.agent, ''), ''),
|
|
4050
|
+
COALESCE(NULLIF(r.account_key, ''), NULLIF(s.account_key, ''), ''),
|
|
4051
|
+
COALESCE(NULLIF(r.account_tool, ''), NULLIF(s.account_tool, ''), ''),
|
|
4052
|
+
COALESCE(NULLIF(r.account_name, ''), NULLIF(s.account_name, ''), ''),
|
|
4053
|
+
COALESCE(NULLIF(r.account_email, ''), NULLIF(s.account_email, ''), ''),
|
|
4054
|
+
COALESCE(NULLIF(r.account_source, ''), NULLIF(s.account_source, ''), 'unknown')
|
|
4055
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4056
|
+
const sessionRows = db.prepare(`
|
|
4057
|
+
SELECT
|
|
4058
|
+
s.agent as agent,
|
|
4059
|
+
s.account_key as account_key,
|
|
4060
|
+
s.account_tool as account_tool,
|
|
4061
|
+
s.account_name as account_name,
|
|
4062
|
+
COALESCE(s.account_email, '') as account_email,
|
|
4063
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown') as account_source,
|
|
4064
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
4065
|
+
COALESCE(SUM(s.request_count), 0) as requests,
|
|
4066
|
+
0 as input_tokens,
|
|
4067
|
+
0 as output_tokens,
|
|
4068
|
+
0 as cache_read_tokens,
|
|
4069
|
+
0 as cache_create_tokens,
|
|
4070
|
+
0 as cache_create_5m_tokens,
|
|
4071
|
+
0 as cache_create_1h_tokens,
|
|
4072
|
+
COALESCE(SUM(s.total_tokens), 0) as total_tokens,
|
|
4073
|
+
COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
|
|
4074
|
+
MAX(s.started_at) as last_active
|
|
4075
|
+
FROM sessions s
|
|
4076
|
+
WHERE s.started_at >= ?${sessionMachineClause}
|
|
4077
|
+
AND NOT EXISTS (SELECT 1 FROM requests r WHERE r.session_id = s.id)
|
|
4078
|
+
GROUP BY
|
|
4079
|
+
s.agent,
|
|
4080
|
+
s.account_key,
|
|
4081
|
+
s.account_tool,
|
|
4082
|
+
s.account_name,
|
|
4083
|
+
COALESCE(s.account_email, ''),
|
|
4084
|
+
COALESCE(NULLIF(s.account_source, ''), 'unknown')
|
|
4085
|
+
`).all(...machine ? [since, machine] : [since]);
|
|
4086
|
+
const groups = new Map;
|
|
4087
|
+
for (const row of requestRows)
|
|
4088
|
+
addAccountRow(groups, row);
|
|
4089
|
+
for (const row of sessionRows)
|
|
4090
|
+
addAccountRow(groups, row);
|
|
4091
|
+
const rows = [...groups.values()];
|
|
4092
|
+
rows.sort((a, b) => b.cost_usd - a.cost_usd || a.account_key.localeCompare(b.account_key));
|
|
4093
|
+
if (rows.length > 0)
|
|
4094
|
+
return rows;
|
|
4095
|
+
return queryAccountBreakdown(db, "all", machine).filter((row) => row.last_active >= since).map(accountBreakdownToBriefRow);
|
|
4096
|
+
}
|
|
4097
|
+
function accountBreakdownToBriefRow(row) {
|
|
4098
|
+
return {
|
|
4099
|
+
account_key: row.account_key,
|
|
4100
|
+
account_tool: row.account_tool,
|
|
4101
|
+
account_name: row.account_name,
|
|
4102
|
+
account_email: row.account_email,
|
|
4103
|
+
account_source: row.account_source,
|
|
4104
|
+
sessions: row.sessions,
|
|
4105
|
+
requests: row.requests,
|
|
4106
|
+
input_tokens: 0,
|
|
4107
|
+
output_tokens: 0,
|
|
4108
|
+
cache_read_tokens: 0,
|
|
4109
|
+
cache_create_tokens: 0,
|
|
4110
|
+
cache_create_5m_tokens: 0,
|
|
4111
|
+
cache_create_1h_tokens: 0,
|
|
4112
|
+
total_tokens: row.total_tokens,
|
|
4113
|
+
cost_usd: row.cost_usd,
|
|
4114
|
+
last_active: row.last_active
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
function queryFreshness(db, now, machine, localSync) {
|
|
4118
|
+
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
4119
|
+
const requestRows = db.prepare(`
|
|
4120
|
+
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
4121
|
+
FROM requests
|
|
4122
|
+
WHERE machine_id != ''${requestFilter}
|
|
4123
|
+
GROUP BY machine_id
|
|
4124
|
+
`).all(...machine ? [machine] : []);
|
|
4125
|
+
const registryRows = listMachineRegistry(db).filter((row) => !machine || row.machine_id === machine);
|
|
4126
|
+
const machines = new Map;
|
|
4127
|
+
for (const row of requestRows) {
|
|
4128
|
+
machines.set(row.machine_id, {
|
|
4129
|
+
machine_id: row.machine_id,
|
|
4130
|
+
max_request_at: row.max_request_at,
|
|
4131
|
+
request_age: formatAge(row.max_request_at, now),
|
|
4132
|
+
last_merge_sync_at: null,
|
|
4133
|
+
merge_sync_age: "never"
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
for (const row of registryRows) {
|
|
4137
|
+
const current = machines.get(row.machine_id) ?? {
|
|
4138
|
+
machine_id: row.machine_id,
|
|
4139
|
+
max_request_at: null,
|
|
4140
|
+
request_age: "never",
|
|
4141
|
+
last_merge_sync_at: null,
|
|
4142
|
+
merge_sync_age: "never"
|
|
4143
|
+
};
|
|
4144
|
+
const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
|
|
4145
|
+
current.last_merge_sync_at = syncAt;
|
|
4146
|
+
current.merge_sync_age = formatAge(syncAt, now);
|
|
4147
|
+
machines.set(row.machine_id, current);
|
|
4148
|
+
}
|
|
4149
|
+
if (localSync && (!machine || machine === localSync.machineId)) {
|
|
4150
|
+
const current = machines.get(localSync.machineId) ?? {
|
|
4151
|
+
machine_id: localSync.machineId,
|
|
4152
|
+
max_request_at: null,
|
|
4153
|
+
request_age: "never",
|
|
4154
|
+
last_merge_sync_at: null,
|
|
4155
|
+
merge_sync_age: "never"
|
|
4156
|
+
};
|
|
4157
|
+
current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
|
|
4158
|
+
current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
|
|
4159
|
+
machines.set(localSync.machineId, current);
|
|
4160
|
+
}
|
|
4161
|
+
const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
|
|
4162
|
+
return {
|
|
4163
|
+
machines: rows,
|
|
4164
|
+
max_request_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.max_request_at ?? "never"} (${row.request_age})`).join("; ") : "none",
|
|
4165
|
+
merge_sync_line: rows.length ? rows.map((row) => `${row.machine_id} ${row.last_merge_sync_at ?? "never"} (${row.merge_sync_age})`).join("; ") : "none"
|
|
4166
|
+
};
|
|
4167
|
+
}
|
|
4168
|
+
function buildBrief(db, opts = {}) {
|
|
4169
|
+
const now = opts.now ?? new Date;
|
|
4170
|
+
const machine = machineFilter(opts.machine);
|
|
4171
|
+
const sinceInput = opts.since ?? "24h";
|
|
4172
|
+
const since = parseSinceDate(sinceInput, now);
|
|
4173
|
+
const summaries = [
|
|
4174
|
+
{
|
|
4175
|
+
label: "Today",
|
|
4176
|
+
period: "today",
|
|
4177
|
+
...queryPeriodTotals(db, "today", machine)
|
|
4178
|
+
},
|
|
4179
|
+
{
|
|
4180
|
+
label: "Week",
|
|
4181
|
+
period: "week",
|
|
4182
|
+
...queryPeriodTotals(db, "week", machine)
|
|
4183
|
+
},
|
|
4184
|
+
{
|
|
4185
|
+
label: `Since ${since.label}`,
|
|
4186
|
+
period: "since",
|
|
4187
|
+
since: since.timestamp,
|
|
4188
|
+
...querySinceTotals(db, since.timestamp, machine)
|
|
4189
|
+
}
|
|
4190
|
+
];
|
|
4191
|
+
const machines = queryMachineSinceRows(db, since.timestamp, machine);
|
|
4192
|
+
for (const row of machines) {
|
|
4193
|
+
row.last_data_age = formatAge(row.last_data_at, now);
|
|
4194
|
+
}
|
|
4195
|
+
return {
|
|
4196
|
+
generated_at: now.toISOString(),
|
|
4197
|
+
machine: machine ?? "all",
|
|
4198
|
+
since: {
|
|
4199
|
+
input: sinceInput,
|
|
4200
|
+
timestamp: since.timestamp,
|
|
4201
|
+
label: since.label
|
|
4202
|
+
},
|
|
4203
|
+
summaries,
|
|
4204
|
+
machines,
|
|
4205
|
+
agents: queryAgentSinceRows(db, since.timestamp, machine),
|
|
4206
|
+
accounts: queryAccountSinceRows(db, since.timestamp, machine),
|
|
4207
|
+
freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
// src/lib/analytics.ts
|
|
4212
|
+
function queryProjectDetail(db, nameOrPath) {
|
|
4213
|
+
const like = `%${nameOrPath}%`;
|
|
4214
|
+
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
4215
|
+
FROM sessions
|
|
4216
|
+
WHERE project_name LIKE ? OR project_path LIKE ?
|
|
4217
|
+
ORDER BY started_at DESC`).all(like, like);
|
|
4218
|
+
if (sessions.length === 0)
|
|
4219
|
+
return null;
|
|
4220
|
+
const first = sessions[0];
|
|
4221
|
+
const totalCost = sessions.reduce((s, r) => s + (r.total_cost_usd ?? 0), 0);
|
|
4222
|
+
const totalTokens = sessions.reduce((s, r) => s + (r.total_tokens ?? 0), 0);
|
|
4223
|
+
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
4224
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
4225
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
4226
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
4227
|
+
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
4228
|
+
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
4229
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
4230
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
4231
|
+
GROUP BY r.model ORDER BY cost_usd DESC LIMIT 5`).all(like, like);
|
|
4232
|
+
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 }));
|
|
4233
|
+
return {
|
|
4234
|
+
project_name: first.project_name || nameOrPath,
|
|
4235
|
+
project_path: first.project_path || "",
|
|
4236
|
+
total_cost_usd: totalCost,
|
|
4237
|
+
total_tokens: totalTokens,
|
|
4238
|
+
sessions: sessions.length,
|
|
4239
|
+
daily,
|
|
4240
|
+
models,
|
|
4241
|
+
top_sessions
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
function exportDateWhere(column, period) {
|
|
4245
|
+
switch (period) {
|
|
4246
|
+
case "today":
|
|
4247
|
+
return `DATE(${column}) = DATE('now')`;
|
|
4248
|
+
case "week":
|
|
4249
|
+
return `${column} >= DATE('now', '-7 days')`;
|
|
4250
|
+
case "all":
|
|
4251
|
+
return "1=1";
|
|
4252
|
+
default:
|
|
4253
|
+
return `${column} >= DATE('now', '-30 days')`;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
function queryExportRows(db, type, period) {
|
|
4257
|
+
if (type === "requests") {
|
|
4258
|
+
const where2 = exportDateWhere("timestamp", period);
|
|
4259
|
+
return db.prepare(`SELECT * FROM requests WHERE ${where2} ORDER BY timestamp ASC`).all();
|
|
4260
|
+
}
|
|
4261
|
+
const where = exportDateWhere("started_at", period);
|
|
4262
|
+
return db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
4263
|
+
}
|
|
4264
|
+
function queryRangeStats(db, from, to) {
|
|
4265
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests,
|
|
4266
|
+
COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens
|
|
4267
|
+
FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
4268
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
4269
|
+
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
4270
|
+
}
|
|
4271
|
+
function queryForecast(db, now = new Date) {
|
|
4272
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
4273
|
+
const dayOfMonth = now.getDate();
|
|
4274
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
4275
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
4276
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
4277
|
+
const projected = dailyAvg * daysInMonth;
|
|
4278
|
+
const sevenDaysAgo = new Date(now);
|
|
4279
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
4280
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
4281
|
+
const last7DailyAvg = last7.cost / 7;
|
|
4282
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
4283
|
+
FROM requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
|
|
4284
|
+
return {
|
|
4285
|
+
day_of_month: dayOfMonth,
|
|
4286
|
+
days_in_month: daysInMonth,
|
|
4287
|
+
spent_so_far_usd: monthSoFar.cost,
|
|
4288
|
+
daily_avg_usd: dailyAvg,
|
|
4289
|
+
projected_usd: projected,
|
|
4290
|
+
last7_daily_avg_usd: last7DailyAvg,
|
|
4291
|
+
last7_projected_usd: last7DailyAvg * daysInMonth,
|
|
4292
|
+
cheapest_day: dailyCosts[0] ?? null,
|
|
4293
|
+
most_expensive_day: dailyCosts[dailyCosts.length - 1] ?? null
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
function queryModelEfficiency(db) {
|
|
4297
|
+
return db.prepare(`SELECT model,
|
|
4298
|
+
COALESCE(SUM(input_tokens),0) as input,
|
|
4299
|
+
COALESCE(SUM(output_tokens),0) as output,
|
|
4300
|
+
COALESCE(SUM(cache_read_tokens),0) as cache_read,
|
|
4301
|
+
COALESCE(SUM(cache_create_tokens),0) as cache_write,
|
|
4302
|
+
COUNT(*) as requests,
|
|
4303
|
+
COALESCE(SUM(cost_usd),0) as cost
|
|
4304
|
+
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
// src/lib/contracts-client/mode.ts
|
|
4308
|
+
var DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
|
|
4309
|
+
function normalizeStorageMode(value) {
|
|
4310
|
+
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
4311
|
+
if (normalized === "local")
|
|
4312
|
+
return { mode: "local", deprecatedAlias: null };
|
|
4313
|
+
if (normalized === "cloud")
|
|
4314
|
+
return { mode: "cloud", deprecatedAlias: null };
|
|
4315
|
+
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
4316
|
+
return { mode: "cloud", deprecatedAlias: normalized };
|
|
4317
|
+
}
|
|
4318
|
+
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
4319
|
+
}
|
|
4320
|
+
function envToken(name) {
|
|
4321
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// src/lib/contracts-client/transport.ts
|
|
4325
|
+
function defaultCloudBaseUrl(name) {
|
|
4326
|
+
return `https://${name}.hasna.xyz`;
|
|
4327
|
+
}
|
|
4328
|
+
function clientTransportEnvKeys(name) {
|
|
4329
|
+
const token = envToken(name);
|
|
4330
|
+
return {
|
|
4331
|
+
modeKeys: [
|
|
4332
|
+
`HASNA_${token}_STORAGE_MODE`,
|
|
4333
|
+
`HASNA_${token}_MODE`,
|
|
4334
|
+
`${token}_STORAGE_MODE`,
|
|
4335
|
+
`${token}_MODE`
|
|
4336
|
+
],
|
|
4337
|
+
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
4338
|
+
apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
4341
|
+
function firstEnv(env, keys) {
|
|
4342
|
+
for (const key of keys) {
|
|
4343
|
+
const value = env[key]?.trim();
|
|
4344
|
+
if (value)
|
|
4345
|
+
return { key, value };
|
|
4346
|
+
}
|
|
4347
|
+
return null;
|
|
4348
|
+
}
|
|
4349
|
+
function toV1BaseUrl(apiUrl) {
|
|
4350
|
+
const url = new URL(apiUrl);
|
|
4351
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
4352
|
+
throw new Error("API URL must use http or https.");
|
|
4353
|
+
}
|
|
4354
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
4355
|
+
if (path.endsWith("/v1"))
|
|
4356
|
+
path = path.slice(0, -"/v1".length);
|
|
4357
|
+
url.pathname = `${path}/v1`;
|
|
4358
|
+
url.search = "";
|
|
4359
|
+
url.hash = "";
|
|
4360
|
+
return url.toString().replace(/\/+$/, "");
|
|
4361
|
+
}
|
|
4362
|
+
function resolveClientTransport(name, env = process.env) {
|
|
4363
|
+
const keys = clientTransportEnvKeys(name);
|
|
4364
|
+
const modeHit = firstEnv(env, keys.modeKeys);
|
|
4365
|
+
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
4366
|
+
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
4367
|
+
let mode = "local";
|
|
4368
|
+
let deprecatedAlias = null;
|
|
4369
|
+
let modeSource = "default";
|
|
4370
|
+
const warnings = [];
|
|
4371
|
+
if (modeHit) {
|
|
4372
|
+
const normalized = normalizeStorageMode(modeHit.value);
|
|
4373
|
+
mode = normalized.mode;
|
|
4374
|
+
deprecatedAlias = normalized.deprecatedAlias;
|
|
4375
|
+
modeSource = modeHit.key;
|
|
4376
|
+
if (deprecatedAlias) {
|
|
4377
|
+
warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
|
|
4378
|
+
}
|
|
4379
|
+
} else if (urlHit && keyHit) {
|
|
4380
|
+
mode = "cloud";
|
|
4381
|
+
modeSource = `${urlHit.key}+${keyHit.key}`;
|
|
4382
|
+
}
|
|
4383
|
+
if (mode === "local") {
|
|
4384
|
+
return {
|
|
4385
|
+
transport: "local",
|
|
4386
|
+
mode,
|
|
4387
|
+
deprecatedAlias,
|
|
4388
|
+
modeSource,
|
|
4389
|
+
baseUrl: null,
|
|
4390
|
+
apiUrlSource: null,
|
|
4391
|
+
apiKeyPresent: Boolean(keyHit),
|
|
4392
|
+
apiKeySource: keyHit ? keyHit.key : null,
|
|
4393
|
+
misconfigured: false,
|
|
4394
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
if (!keyHit) {
|
|
4398
|
+
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.`);
|
|
4399
|
+
return {
|
|
4400
|
+
transport: "local",
|
|
4401
|
+
mode,
|
|
4402
|
+
deprecatedAlias,
|
|
4403
|
+
modeSource,
|
|
4404
|
+
baseUrl: null,
|
|
4405
|
+
apiUrlSource: null,
|
|
4406
|
+
apiKeyPresent: false,
|
|
4407
|
+
apiKeySource: null,
|
|
4408
|
+
misconfigured: true,
|
|
4409
|
+
warning: warnings.join(" ")
|
|
4410
|
+
};
|
|
4411
|
+
}
|
|
4412
|
+
const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
|
|
4413
|
+
const apiUrlSource = urlHit ? urlHit.key : "default";
|
|
4414
|
+
let baseUrl;
|
|
4415
|
+
try {
|
|
4416
|
+
baseUrl = toV1BaseUrl(rawUrl);
|
|
4417
|
+
} catch (error) {
|
|
4418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4419
|
+
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
4420
|
+
return {
|
|
4421
|
+
transport: "local",
|
|
4422
|
+
mode,
|
|
4423
|
+
deprecatedAlias,
|
|
4424
|
+
modeSource,
|
|
4425
|
+
baseUrl: null,
|
|
4426
|
+
apiUrlSource: null,
|
|
4427
|
+
apiKeyPresent: true,
|
|
4428
|
+
apiKeySource: keyHit.key,
|
|
4429
|
+
misconfigured: true,
|
|
4430
|
+
warning: warnings.join(" ")
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
return {
|
|
4434
|
+
transport: "cloud-http",
|
|
4435
|
+
mode,
|
|
4436
|
+
deprecatedAlias,
|
|
4437
|
+
modeSource,
|
|
4438
|
+
baseUrl,
|
|
4439
|
+
apiUrlSource,
|
|
4440
|
+
apiKeyPresent: true,
|
|
4441
|
+
apiKeySource: keyHit.key,
|
|
4442
|
+
misconfigured: false,
|
|
4443
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
4444
|
+
};
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
class HasnaHttpError extends Error {
|
|
4448
|
+
status;
|
|
4449
|
+
method;
|
|
4450
|
+
path;
|
|
4451
|
+
body;
|
|
4452
|
+
constructor(method, path, status, body) {
|
|
4453
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
|
|
4454
|
+
this.name = "HasnaHttpError";
|
|
4455
|
+
this.status = status;
|
|
4456
|
+
this.method = method;
|
|
4457
|
+
this.path = path;
|
|
4458
|
+
this.body = body;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
4462
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4463
|
+
function appendQuery(path, query) {
|
|
4464
|
+
if (!query)
|
|
4465
|
+
return path;
|
|
4466
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
4467
|
+
if (!(query instanceof URLSearchParams)) {
|
|
4468
|
+
for (const [key, value] of Object.entries(query)) {
|
|
4469
|
+
if (value === null || value === undefined)
|
|
4470
|
+
continue;
|
|
4471
|
+
if (Array.isArray(value)) {
|
|
4472
|
+
for (const v of value)
|
|
4473
|
+
params.append(key, String(v));
|
|
4474
|
+
} else {
|
|
4475
|
+
params.append(key, String(value));
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4479
|
+
const qs = params.toString();
|
|
4480
|
+
if (!qs)
|
|
4481
|
+
return path;
|
|
4482
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
4483
|
+
}
|
|
4484
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4485
|
+
function createHasnaHttpTransport(options) {
|
|
4486
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
4487
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
4488
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
4489
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
4490
|
+
const defaultRetry = options.retry;
|
|
4491
|
+
function resolveRetry(callRetry) {
|
|
4492
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
4493
|
+
if (chosen === false)
|
|
4494
|
+
return null;
|
|
4495
|
+
const r = chosen ?? {};
|
|
4496
|
+
return {
|
|
4497
|
+
retries: r.retries ?? 2,
|
|
4498
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
4499
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
4500
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
async function once(method, rel, url, body, opts) {
|
|
4504
|
+
const headers = {
|
|
4505
|
+
"x-api-key": options.apiKey,
|
|
4506
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
4507
|
+
Accept: "application/json",
|
|
4508
|
+
...options.headers ?? {},
|
|
4509
|
+
...opts.headers ?? {}
|
|
4510
|
+
};
|
|
4511
|
+
if (opts.idempotencyKey)
|
|
4512
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
4513
|
+
const init = { method, headers };
|
|
4514
|
+
if (body !== undefined) {
|
|
4515
|
+
headers["Content-Type"] = "application/json";
|
|
4516
|
+
init.body = JSON.stringify(body);
|
|
4517
|
+
}
|
|
4518
|
+
const controller = new AbortController;
|
|
4519
|
+
const onAbort = () => controller.abort();
|
|
4520
|
+
if (opts.signal) {
|
|
4521
|
+
if (opts.signal.aborted)
|
|
4522
|
+
controller.abort();
|
|
4523
|
+
else
|
|
4524
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
4525
|
+
}
|
|
4526
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
4527
|
+
init.signal = controller.signal;
|
|
4528
|
+
let response;
|
|
4529
|
+
try {
|
|
4530
|
+
response = await fetchImpl(url, init);
|
|
4531
|
+
} catch (error) {
|
|
4532
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
4533
|
+
if (opts.signal?.aborted)
|
|
4534
|
+
return { ok: false, retryable: false, error: err };
|
|
4535
|
+
return { ok: false, retryable: true, error: err };
|
|
4536
|
+
} finally {
|
|
4537
|
+
clearTimeout(timer);
|
|
4538
|
+
if (opts.signal)
|
|
4539
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
4540
|
+
}
|
|
4541
|
+
const text = await response.text();
|
|
4542
|
+
let parsed = undefined;
|
|
4543
|
+
if (text.length > 0) {
|
|
4544
|
+
try {
|
|
4545
|
+
parsed = JSON.parse(text);
|
|
4546
|
+
} catch {
|
|
4547
|
+
parsed = text;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
if (!response.ok) {
|
|
4551
|
+
const retry = resolveRetry(opts.retry);
|
|
4552
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
4553
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
4554
|
+
}
|
|
4555
|
+
return { ok: true, value: parsed };
|
|
4556
|
+
}
|
|
4557
|
+
async function request(method, path, body, opts = {}) {
|
|
4558
|
+
const upper = method.toUpperCase();
|
|
4559
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
4560
|
+
const url = `${base}${rel}`;
|
|
4561
|
+
const retry = resolveRetry(opts.retry);
|
|
4562
|
+
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
4563
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
4564
|
+
let last = null;
|
|
4565
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
4566
|
+
const result = await once(upper, rel, url, body, opts);
|
|
4567
|
+
if (result.ok)
|
|
4568
|
+
return result.value;
|
|
4569
|
+
last = result;
|
|
4570
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
4571
|
+
if (!canRetry)
|
|
4572
|
+
break;
|
|
4573
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
4574
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
4575
|
+
await sleep(backoff + jitter);
|
|
4576
|
+
}
|
|
4577
|
+
throw last.error;
|
|
4578
|
+
}
|
|
4579
|
+
return {
|
|
4580
|
+
baseUrl: base,
|
|
4581
|
+
request,
|
|
4582
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
4583
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
4584
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
4585
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
4586
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
4587
|
+
};
|
|
4588
|
+
}
|
|
4589
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
4590
|
+
const resolution = resolveClientTransport(name, env);
|
|
4591
|
+
if (resolution.misconfigured) {
|
|
4592
|
+
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
|
|
4593
|
+
}
|
|
4594
|
+
if (resolution.transport === "local" || !resolution.baseUrl) {
|
|
4595
|
+
return { transport: "local", client: null, resolution };
|
|
4596
|
+
}
|
|
4597
|
+
const keys = clientTransportEnvKeys(name);
|
|
4598
|
+
const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
|
|
4599
|
+
if (!apiKey) {
|
|
4600
|
+
throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
|
|
4601
|
+
}
|
|
4602
|
+
return {
|
|
4603
|
+
transport: "cloud-http",
|
|
4604
|
+
client: createHasnaHttpTransport({
|
|
4605
|
+
name,
|
|
4606
|
+
baseUrl: resolution.baseUrl,
|
|
4607
|
+
apiKey,
|
|
4608
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
4609
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
4610
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
4611
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
4612
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
4613
|
+
}),
|
|
4614
|
+
resolution
|
|
4615
|
+
};
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// src/lib/contracts-client/storage.ts
|
|
4619
|
+
function resourcePath(resource) {
|
|
4620
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
4621
|
+
if (!trimmed)
|
|
4622
|
+
throw new Error("resource must be a non-empty path segment");
|
|
4623
|
+
return `/${trimmed}`;
|
|
4624
|
+
}
|
|
4625
|
+
function entityPath(resource, id) {
|
|
4626
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
4627
|
+
throw new Error("id must be a non-empty string");
|
|
4628
|
+
}
|
|
4629
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
4630
|
+
}
|
|
4631
|
+
function newIdempotencyKey() {
|
|
4632
|
+
const g = globalThis;
|
|
4633
|
+
if (g.crypto?.randomUUID)
|
|
4634
|
+
return g.crypto.randomUUID();
|
|
4635
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
4636
|
+
}
|
|
4637
|
+
function extractItems(raw) {
|
|
4638
|
+
if (Array.isArray(raw))
|
|
4639
|
+
return raw;
|
|
4640
|
+
if (raw && typeof raw === "object") {
|
|
4641
|
+
const obj = raw;
|
|
4642
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
4643
|
+
if (Array.isArray(obj[key]))
|
|
4644
|
+
return obj[key];
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
return [];
|
|
4648
|
+
}
|
|
4649
|
+
function extractTotal(raw) {
|
|
4650
|
+
if (raw && typeof raw === "object") {
|
|
4651
|
+
const obj = raw;
|
|
4652
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
4653
|
+
if (typeof obj[key] === "number")
|
|
4654
|
+
return obj[key];
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4659
|
+
function extractCursor(raw) {
|
|
4660
|
+
if (raw && typeof raw === "object") {
|
|
4661
|
+
const obj = raw;
|
|
4662
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
4663
|
+
if (typeof obj[key] === "string")
|
|
4664
|
+
return obj[key];
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
return null;
|
|
4668
|
+
}
|
|
4669
|
+
function createHasnaStorageClient(name, transport) {
|
|
4670
|
+
return {
|
|
4671
|
+
name,
|
|
4672
|
+
baseUrl: transport.baseUrl,
|
|
4673
|
+
transport,
|
|
4674
|
+
async list(resource, options = {}) {
|
|
4675
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
4676
|
+
return {
|
|
4677
|
+
items: extractItems(raw),
|
|
4678
|
+
total: extractTotal(raw),
|
|
4679
|
+
cursor: extractCursor(raw),
|
|
4680
|
+
raw
|
|
4681
|
+
};
|
|
4682
|
+
},
|
|
4683
|
+
async get(resource, id, options = {}) {
|
|
4684
|
+
try {
|
|
4685
|
+
return await transport.get(entityPath(resource, id), options);
|
|
4686
|
+
} catch (error) {
|
|
4687
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
4688
|
+
return null;
|
|
4689
|
+
throw error;
|
|
4690
|
+
}
|
|
4691
|
+
},
|
|
4692
|
+
async create(resource, body, options = {}) {
|
|
4693
|
+
const { idempotencyKey, ...rest } = options;
|
|
4694
|
+
return transport.post(resourcePath(resource), body, {
|
|
4695
|
+
...rest,
|
|
4696
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
4697
|
+
});
|
|
4698
|
+
},
|
|
4699
|
+
async update(resource, id, patch, options = {}) {
|
|
4700
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
4701
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
4702
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
4703
|
+
},
|
|
4704
|
+
async delete(resource, id, options = {}) {
|
|
4705
|
+
try {
|
|
4706
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
4707
|
+
} catch (error) {
|
|
4708
|
+
if (error instanceof HasnaHttpError && error.status === 404)
|
|
4709
|
+
return;
|
|
4710
|
+
throw error;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
function resolveStorageClient(name, env = process.env, overrides) {
|
|
4716
|
+
const wired = createClientTransport(name, env, overrides);
|
|
4717
|
+
if (wired.transport === "cloud-http") {
|
|
4718
|
+
return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
|
|
4719
|
+
}
|
|
4720
|
+
return { transport: "local", client: null };
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
// src/lib/cloud-storage.ts
|
|
4724
|
+
var ECONOMY_APP = "economy";
|
|
4725
|
+
var cache;
|
|
4726
|
+
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
4727
|
+
const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
|
|
4728
|
+
return resolved.transport === "cloud-http" ? { active: true, client: resolved.client } : { active: false, client: null };
|
|
4729
|
+
}
|
|
4730
|
+
function economyCloudStorage(env = process.env) {
|
|
4731
|
+
if (cache === undefined)
|
|
4732
|
+
cache = resolveEconomyCloudStorage(env);
|
|
4733
|
+
return cache;
|
|
4734
|
+
}
|
|
4735
|
+
function cleanQuery(query) {
|
|
4736
|
+
if (!query)
|
|
4737
|
+
return;
|
|
4738
|
+
const out = {};
|
|
4739
|
+
for (const [k, v] of Object.entries(query)) {
|
|
4740
|
+
if (v !== undefined && v !== null && v !== "")
|
|
4741
|
+
out[k] = v;
|
|
4742
|
+
}
|
|
4743
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
4744
|
+
}
|
|
4745
|
+
async function cloudListItems(storage, resource, query) {
|
|
4746
|
+
const cleaned = cleanQuery(query);
|
|
4747
|
+
const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
|
|
4748
|
+
return res.items;
|
|
4749
|
+
}
|
|
4750
|
+
async function cloudObject(storage, path, query) {
|
|
4751
|
+
const cleaned = cleanQuery(query);
|
|
4752
|
+
const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
|
|
4753
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
4754
|
+
return raw.data;
|
|
4755
|
+
}
|
|
4756
|
+
return raw;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4759
|
+
// src/lib/store/index.ts
|
|
4760
|
+
class LocalStore {
|
|
4761
|
+
transport = "local";
|
|
4762
|
+
_db;
|
|
4763
|
+
db() {
|
|
4764
|
+
if (!this._db) {
|
|
4765
|
+
this._db = openDatabase();
|
|
4766
|
+
ensurePricingSeeded(this._db);
|
|
4767
|
+
}
|
|
4768
|
+
return this._db;
|
|
4769
|
+
}
|
|
4770
|
+
async summary(period, machine) {
|
|
4771
|
+
return querySummary(this.db(), period, machine);
|
|
4772
|
+
}
|
|
4773
|
+
async sessions(filter) {
|
|
4774
|
+
return querySessions(this.db(), filter);
|
|
4775
|
+
}
|
|
4776
|
+
async topSessions(n, agent, since) {
|
|
4777
|
+
return queryTopSessions(this.db(), n, agent, since);
|
|
4778
|
+
}
|
|
4779
|
+
async sessionDetail(id) {
|
|
4780
|
+
return getSessionDetail(this.db(), id);
|
|
4781
|
+
}
|
|
4782
|
+
async modelBreakdown(query = {}) {
|
|
4783
|
+
return query.since ? queryModelBreakdownSince(this.db(), query.since) : queryModelBreakdown(this.db());
|
|
4784
|
+
}
|
|
4785
|
+
async agentBreakdown(query = {}) {
|
|
4786
|
+
return query.since ? queryAgentBreakdownSince(this.db(), query.since) : queryAgentBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4787
|
+
}
|
|
4788
|
+
async projectBreakdown(query = {}) {
|
|
4789
|
+
return query.since ? queryProjectBreakdownSince(this.db(), query.since) : queryProjectBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4790
|
+
}
|
|
4791
|
+
async accountBreakdown(query = {}) {
|
|
4792
|
+
return query.since ? queryAccountBreakdownSince(this.db(), query.since) : queryAccountBreakdown(this.db(), query.period ?? "all", query.machine);
|
|
4793
|
+
}
|
|
4794
|
+
async accounts(period) {
|
|
4795
|
+
return queryAccountBreakdown(this.db(), period);
|
|
4796
|
+
}
|
|
4797
|
+
async daily(days, machine) {
|
|
4798
|
+
return queryDailyBreakdown(this.db(), days, machine);
|
|
4799
|
+
}
|
|
4800
|
+
async machines() {
|
|
4801
|
+
return listMachines(this.db());
|
|
4802
|
+
}
|
|
4803
|
+
async fleet(period) {
|
|
4804
|
+
return {
|
|
4805
|
+
summary: querySummary(this.db(), period, undefined, true),
|
|
4806
|
+
machines: listMachines(this.db(), period),
|
|
4807
|
+
registry: listMachineRegistry(this.db())
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
async billingSummary(period) {
|
|
4811
|
+
return queryBillingSummary(this.db(), period);
|
|
4812
|
+
}
|
|
4813
|
+
async billingDiff(period) {
|
|
4814
|
+
return queryBillingDiff(this.db(), period);
|
|
4815
|
+
}
|
|
4816
|
+
async usage(period, agent) {
|
|
4817
|
+
const snapshots = queryUsageSnapshots(this.db(), { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
4818
|
+
const summary = querySummary(this.db(), period, undefined, true, agent);
|
|
4819
|
+
return { snapshots, summary };
|
|
4820
|
+
}
|
|
4821
|
+
async savings(period, agent) {
|
|
4822
|
+
return querySavingsSummary(this.db(), period, agent);
|
|
4823
|
+
}
|
|
4824
|
+
async listBudgets() {
|
|
4825
|
+
return getBudgetStatuses(this.db());
|
|
4826
|
+
}
|
|
4827
|
+
async listGoals() {
|
|
4828
|
+
return getGoalStatuses(this.db());
|
|
4829
|
+
}
|
|
4830
|
+
async listPricing() {
|
|
4831
|
+
return listModelPricing(this.db());
|
|
4832
|
+
}
|
|
4833
|
+
async listSubscriptions() {
|
|
4834
|
+
return listSubscriptions(this.db());
|
|
4835
|
+
}
|
|
4836
|
+
async listProjects() {
|
|
4837
|
+
return queryProjectBreakdown(this.db());
|
|
4838
|
+
}
|
|
4839
|
+
async brief(query = {}) {
|
|
4840
|
+
return buildBrief(this.db(), {
|
|
4841
|
+
since: query.since,
|
|
4842
|
+
machine: query.machine,
|
|
4843
|
+
currentMachineId: query.currentMachineId,
|
|
4844
|
+
localSyncAt: query.localSyncAt
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
async projectDetail(nameOrPath) {
|
|
4848
|
+
return queryProjectDetail(this.db(), nameOrPath);
|
|
4849
|
+
}
|
|
4850
|
+
async exportRows(type, period) {
|
|
4851
|
+
return queryExportRows(this.db(), type, period);
|
|
4852
|
+
}
|
|
4853
|
+
async rangeStats(from, to) {
|
|
4854
|
+
return queryRangeStats(this.db(), from, to);
|
|
4855
|
+
}
|
|
4856
|
+
async forecast() {
|
|
4857
|
+
return queryForecast(this.db());
|
|
4858
|
+
}
|
|
4859
|
+
async efficiency() {
|
|
4860
|
+
return queryModelEfficiency(this.db());
|
|
4861
|
+
}
|
|
4862
|
+
async recentRequests(since) {
|
|
4863
|
+
return queryRequestsSince(this.db(), since);
|
|
4864
|
+
}
|
|
4865
|
+
async estimate(input) {
|
|
4866
|
+
return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
4867
|
+
}
|
|
4868
|
+
async setBudget(input) {
|
|
4869
|
+
const now = new Date().toISOString();
|
|
4870
|
+
const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
|
|
4871
|
+
upsertBudget(this.db(), budget);
|
|
4872
|
+
return budget.id;
|
|
4873
|
+
}
|
|
4874
|
+
async removeBudget(id) {
|
|
4875
|
+
deleteBudget(this.db(), id);
|
|
4876
|
+
}
|
|
4877
|
+
async setGoal(input) {
|
|
4878
|
+
const now = new Date().toISOString();
|
|
4879
|
+
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
|
|
4880
|
+
}
|
|
4881
|
+
async removeGoal(id) {
|
|
4882
|
+
deleteGoal(this.db(), id);
|
|
4883
|
+
}
|
|
4884
|
+
async setPricing(input) {
|
|
4885
|
+
upsertModelPricing(this.db(), { ...input, updated_at: new Date().toISOString() });
|
|
4886
|
+
}
|
|
4887
|
+
async removePricing(model) {
|
|
4888
|
+
deleteModelPricing(this.db(), model);
|
|
4889
|
+
}
|
|
4890
|
+
async setSubscription(input) {
|
|
4891
|
+
const now = new Date().toISOString();
|
|
4892
|
+
const row = {
|
|
4893
|
+
id: input.id ?? randomUUID(),
|
|
4894
|
+
provider: input.provider,
|
|
4895
|
+
plan: input.plan,
|
|
4896
|
+
agent: input.agent,
|
|
4897
|
+
monthly_fee_usd: input.monthly_fee_usd,
|
|
4898
|
+
included_usage_usd: input.included_usage_usd,
|
|
4899
|
+
billing_cycle_start: input.billing_cycle_start,
|
|
4900
|
+
reset_policy: input.reset_policy,
|
|
4901
|
+
active: input.active,
|
|
4902
|
+
created_at: now,
|
|
4903
|
+
updated_at: now
|
|
4904
|
+
};
|
|
4905
|
+
upsertSubscription(this.db(), row);
|
|
4906
|
+
return row;
|
|
4907
|
+
}
|
|
4908
|
+
async removeSubscription(id) {
|
|
4909
|
+
deleteSubscription(this.db(), id);
|
|
4910
|
+
}
|
|
4911
|
+
async addProject(path, name) {
|
|
4912
|
+
upsertProject(this.db(), {
|
|
4913
|
+
id: randomUUID(),
|
|
4914
|
+
path,
|
|
4915
|
+
name,
|
|
4916
|
+
description: null,
|
|
4917
|
+
tags: [],
|
|
4918
|
+
created_at: new Date().toISOString()
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
async renameProject(path, name) {
|
|
4922
|
+
const existing = getProject(this.db(), path);
|
|
4923
|
+
if (!existing)
|
|
4924
|
+
throw new Error("Project not found");
|
|
4925
|
+
upsertProject(this.db(), { ...existing, name });
|
|
4926
|
+
}
|
|
4927
|
+
async removeProject(path) {
|
|
4928
|
+
deleteProject(this.db(), path);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
function q(params) {
|
|
4932
|
+
const out = {};
|
|
4933
|
+
for (const [k, v] of Object.entries(params))
|
|
4934
|
+
if (v !== undefined && v !== null && v !== "")
|
|
4935
|
+
out[k] = v;
|
|
4936
|
+
return out;
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
class ApiStore {
|
|
4940
|
+
cloud;
|
|
4941
|
+
transport = "cloud-http";
|
|
4942
|
+
constructor(cloud) {
|
|
4943
|
+
this.cloud = cloud;
|
|
4944
|
+
}
|
|
4945
|
+
async summary(period, machine) {
|
|
4946
|
+
return cloudObject(this.cloud, "/summary", q({ period, machine }));
|
|
4947
|
+
}
|
|
4948
|
+
async sessions(filter) {
|
|
4949
|
+
return cloudListItems(this.cloud, "sessions", q({ ...filter }));
|
|
4950
|
+
}
|
|
4951
|
+
async topSessions(n, agent, since) {
|
|
4952
|
+
return cloudListItems(this.cloud, "top", q({ n, agent, since }));
|
|
4953
|
+
}
|
|
4954
|
+
async sessionDetail(id) {
|
|
4955
|
+
const matches = await cloudListItems(this.cloud, "sessions", q({ search: id, limit: 1 }));
|
|
4956
|
+
const session = matches[0];
|
|
4957
|
+
if (!session)
|
|
4958
|
+
return null;
|
|
4959
|
+
const requests = await cloudObject(this.cloud, `/sessions/${encodeURIComponent(String(session["id"]))}/requests`);
|
|
4960
|
+
return { session, requests: requests ?? [] };
|
|
4961
|
+
}
|
|
4962
|
+
async modelBreakdown(query = {}) {
|
|
4963
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "model", period: query.period, since: query.since }));
|
|
4964
|
+
}
|
|
4965
|
+
async agentBreakdown(query = {}) {
|
|
4966
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "agent", period: query.period, since: query.since }));
|
|
4967
|
+
}
|
|
4968
|
+
async projectBreakdown(query = {}) {
|
|
4969
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "project", period: query.period, since: query.since }));
|
|
4970
|
+
}
|
|
4971
|
+
async accountBreakdown(query = {}) {
|
|
4972
|
+
return cloudListItems(this.cloud, "breakdown", q({ by: "account", period: query.period, since: query.since }));
|
|
4973
|
+
}
|
|
4974
|
+
async accounts(period) {
|
|
4975
|
+
return cloudListItems(this.cloud, "accounts", q({ period }));
|
|
4976
|
+
}
|
|
4977
|
+
async daily(days, machine) {
|
|
4978
|
+
return cloudListItems(this.cloud, "daily", q({ days, machine }));
|
|
4979
|
+
}
|
|
4980
|
+
async machines() {
|
|
4981
|
+
return cloudListItems(this.cloud, "machines");
|
|
4982
|
+
}
|
|
4983
|
+
async fleet(period) {
|
|
4984
|
+
const data = await cloudObject(this.cloud, "/fleet", q({ period }));
|
|
4985
|
+
return {
|
|
4986
|
+
summary: data.summary,
|
|
4987
|
+
machines: data.machines ?? [],
|
|
4988
|
+
registry: data.registry ?? []
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4991
|
+
async billingSummary(period) {
|
|
4992
|
+
return cloudObject(this.cloud, "/billing", q({ period }));
|
|
4993
|
+
}
|
|
4994
|
+
async billingDiff(period) {
|
|
4995
|
+
return cloudObject(this.cloud, "/billing/diff", q({ period }));
|
|
4996
|
+
}
|
|
4997
|
+
async usage(period, agent) {
|
|
4998
|
+
return cloudObject(this.cloud, "/usage", q({ period, agent }));
|
|
4999
|
+
}
|
|
5000
|
+
async savings(period, agent) {
|
|
5001
|
+
return cloudObject(this.cloud, "/savings", q({ period, agent }));
|
|
5002
|
+
}
|
|
5003
|
+
async listBudgets() {
|
|
5004
|
+
return cloudListItems(this.cloud, "budgets");
|
|
5005
|
+
}
|
|
5006
|
+
async listGoals() {
|
|
5007
|
+
return cloudListItems(this.cloud, "goals");
|
|
5008
|
+
}
|
|
5009
|
+
async listPricing() {
|
|
5010
|
+
return cloudListItems(this.cloud, "pricing");
|
|
5011
|
+
}
|
|
5012
|
+
async listSubscriptions() {
|
|
5013
|
+
return cloudListItems(this.cloud, "subscriptions");
|
|
5014
|
+
}
|
|
5015
|
+
async listProjects() {
|
|
5016
|
+
return cloudListItems(this.cloud, "projects");
|
|
5017
|
+
}
|
|
5018
|
+
async brief(query = {}) {
|
|
5019
|
+
return cloudObject(this.cloud, "/brief", q({ since: query.since, machine: query.machine }));
|
|
5020
|
+
}
|
|
5021
|
+
async projectDetail(nameOrPath) {
|
|
5022
|
+
return cloudObject(this.cloud, "/projects/detail", q({ q: nameOrPath }));
|
|
5023
|
+
}
|
|
5024
|
+
async exportRows(type, period) {
|
|
5025
|
+
return cloudListItems(this.cloud, "export", q({ type, period }));
|
|
5026
|
+
}
|
|
5027
|
+
async rangeStats(from, to) {
|
|
5028
|
+
return cloudObject(this.cloud, "/compare", q({ from, to }));
|
|
5029
|
+
}
|
|
5030
|
+
async forecast() {
|
|
5031
|
+
return cloudObject(this.cloud, "/forecast");
|
|
5032
|
+
}
|
|
5033
|
+
async efficiency() {
|
|
5034
|
+
return cloudListItems(this.cloud, "efficiency");
|
|
5035
|
+
}
|
|
5036
|
+
async recentRequests(since) {
|
|
5037
|
+
return cloudListItems(this.cloud, "requests", q({ since }));
|
|
5038
|
+
}
|
|
5039
|
+
async estimate(input) {
|
|
5040
|
+
const rows = await this.listPricing();
|
|
5041
|
+
return estimateCostFromRows(rows, input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
5042
|
+
}
|
|
5043
|
+
async setBudget(input) {
|
|
5044
|
+
const created = await this.cloud.client.create("budgets", {
|
|
5045
|
+
project_path: input.project_path,
|
|
5046
|
+
...input.agent ? { agent: input.agent } : {},
|
|
5047
|
+
period: input.period,
|
|
5048
|
+
limit_usd: input.limit_usd,
|
|
5049
|
+
alert_at_percent: input.alert_at_percent
|
|
5050
|
+
});
|
|
5051
|
+
return created?.id ?? "";
|
|
5052
|
+
}
|
|
5053
|
+
async removeBudget(id) {
|
|
5054
|
+
await this.cloud.client.delete("budgets", id);
|
|
5055
|
+
}
|
|
5056
|
+
async setGoal(input) {
|
|
5057
|
+
await this.cloud.client.create("goals", {
|
|
5058
|
+
period: input.period,
|
|
5059
|
+
project_path: input.project_path,
|
|
5060
|
+
...input.agent ? { agent: input.agent } : {},
|
|
5061
|
+
limit_usd: input.limit_usd
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
async removeGoal(id) {
|
|
5065
|
+
await this.cloud.client.delete("goals", id);
|
|
5066
|
+
}
|
|
5067
|
+
async setPricing(input) {
|
|
5068
|
+
await this.cloud.client.create("pricing", { ...input, updated_at: new Date().toISOString() });
|
|
5069
|
+
}
|
|
5070
|
+
async removePricing(model) {
|
|
5071
|
+
await this.cloud.client.delete("pricing", model);
|
|
5072
|
+
}
|
|
5073
|
+
async setSubscription(input) {
|
|
5074
|
+
const now = new Date().toISOString();
|
|
5075
|
+
const row = {
|
|
5076
|
+
id: input.id ?? randomUUID(),
|
|
5077
|
+
provider: input.provider,
|
|
5078
|
+
plan: input.plan,
|
|
5079
|
+
agent: input.agent,
|
|
5080
|
+
monthly_fee_usd: input.monthly_fee_usd,
|
|
5081
|
+
included_usage_usd: input.included_usage_usd,
|
|
5082
|
+
billing_cycle_start: input.billing_cycle_start,
|
|
5083
|
+
reset_policy: input.reset_policy,
|
|
5084
|
+
active: input.active,
|
|
5085
|
+
created_at: now,
|
|
5086
|
+
updated_at: now
|
|
5087
|
+
};
|
|
5088
|
+
await this.cloud.client.create("subscriptions", row);
|
|
5089
|
+
return row;
|
|
5090
|
+
}
|
|
5091
|
+
async removeSubscription(id) {
|
|
5092
|
+
await this.cloud.client.delete("subscriptions", id);
|
|
5093
|
+
}
|
|
5094
|
+
async addProject(path, name) {
|
|
5095
|
+
await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
|
|
5096
|
+
}
|
|
5097
|
+
async renameProject(path, name) {
|
|
5098
|
+
await this.cloud.client.delete("project-registry", path);
|
|
5099
|
+
await this.cloud.client.create("project-registry", { path, name, description: null, tags: [] });
|
|
5100
|
+
}
|
|
5101
|
+
async removeProject(path) {
|
|
5102
|
+
await this.cloud.client.delete("project-registry", path);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
function getStore(env = process.env) {
|
|
5106
|
+
const cloud = economyCloudStorage(env);
|
|
5107
|
+
return cloud.active ? new ApiStore(cloud) : new LocalStore;
|
|
5108
|
+
}
|
|
5109
|
+
function isCloudStore(env = process.env) {
|
|
5110
|
+
return economyCloudStorage(env).active;
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
// src/mcp/server.ts
|
|
5114
|
+
var MCP_NAME = "economy";
|
|
5115
|
+
var DEFAULT_MCP_HTTP_PORT = 8860;
|
|
5116
|
+
function buildServer() {
|
|
5117
|
+
const db = openDatabase();
|
|
5118
|
+
ensurePricingSeeded(db);
|
|
5119
|
+
const store = getStore();
|
|
5120
|
+
const server = new McpServer({
|
|
5121
|
+
name: MCP_NAME,
|
|
5122
|
+
version: packageMetadata.version
|
|
5123
|
+
});
|
|
5124
|
+
const TOOL_NAMES = [
|
|
5125
|
+
"get_cost_summary",
|
|
5126
|
+
"get_sessions",
|
|
5127
|
+
"get_top_sessions",
|
|
5128
|
+
"get_model_breakdown",
|
|
5129
|
+
"get_project_breakdown",
|
|
5130
|
+
"get_agent_breakdown",
|
|
5131
|
+
"get_account_breakdown",
|
|
5132
|
+
"get_budget_status",
|
|
5133
|
+
"set_budget",
|
|
5134
|
+
"remove_budget",
|
|
5135
|
+
"get_pricing",
|
|
5136
|
+
"set_pricing",
|
|
5137
|
+
"remove_pricing",
|
|
5138
|
+
"get_daily",
|
|
5139
|
+
"get_billing_summary",
|
|
5140
|
+
"get_session_detail",
|
|
5141
|
+
"get_usage",
|
|
5142
|
+
"get_savings",
|
|
5143
|
+
"list_subscriptions",
|
|
5144
|
+
"set_subscription",
|
|
5145
|
+
"remove_subscription",
|
|
5146
|
+
"sync",
|
|
5147
|
+
"search_tools",
|
|
5148
|
+
"describe_tools",
|
|
5149
|
+
"get_goals",
|
|
5150
|
+
"set_goal",
|
|
5151
|
+
"remove_goal",
|
|
5152
|
+
"list_machines",
|
|
5153
|
+
"register_agent",
|
|
5154
|
+
"heartbeat",
|
|
5155
|
+
"set_focus",
|
|
5156
|
+
"list_agents",
|
|
5157
|
+
"send_feedback"
|
|
5158
|
+
];
|
|
5159
|
+
const TOOL_DESCRIPTIONS = {
|
|
5160
|
+
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
5161
|
+
get_sessions: `agent(${AGENTS.join("|")}), project(partial), account?(key/name/email), machine?(hostname), limit(20) -> compact session table`,
|
|
5162
|
+
get_top_sessions: `n(10), agent(${AGENTS.join("|")}) -> top sessions by cost`,
|
|
5163
|
+
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
5164
|
+
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
5165
|
+
get_project_breakdown: "period?(today|week|month|year|all) -> project_name, sessions, tokens, cost",
|
|
5166
|
+
get_agent_breakdown: "period?(today|week|month|year|all) -> agent, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5167
|
+
get_account_breakdown: "period?(today|week|month|year|all) -> account profile, sessions, requests, tokens, api-equivalent, billable, included",
|
|
5168
|
+
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
5169
|
+
set_budget: "period(daily|weekly|monthly), limit_usd, project_path?, agent?, alert_at_percent? -> create budget",
|
|
5170
|
+
remove_budget: "id -> delete budget",
|
|
5171
|
+
get_pricing: "no params -> model pricing rows with input, output, cache read/write, and cache storage rates",
|
|
5172
|
+
set_pricing: "model, input_per_1m, output_per_1m, cache_read_per_1m?, cache_write_per_1m?, cache_write_1h_per_1m?, cache_storage_per_1m_hour? -> create/update pricing",
|
|
5173
|
+
remove_pricing: "model -> delete pricing row",
|
|
5174
|
+
get_daily: "days(30) -> daily cost table grouped by date and agent",
|
|
5175
|
+
get_billing_summary: "period(today|yesterday|week|month|year|all) -> actual provider billing totals",
|
|
5176
|
+
get_session_detail: "session_id(prefix ok) -> per-request breakdown with model, tokens, cost",
|
|
5177
|
+
get_usage: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> usage snapshots and all-machine summary`,
|
|
5178
|
+
get_savings: `period(today|week|month|year|all), agent?(${AGENTS.join("|")}) -> subscription/API-equivalent savings`,
|
|
5179
|
+
list_subscriptions: "no params -> configured subscription plans and included usage",
|
|
5180
|
+
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
|
|
5181
|
+
remove_subscription: "id -> delete subscription plan",
|
|
5182
|
+
sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
|
|
5183
|
+
search_tools: "query substring -> tool name list",
|
|
5184
|
+
describe_tools: "names[] -> one-line parameter hints",
|
|
5185
|
+
get_goals: "no params -> goal progress summary",
|
|
5186
|
+
set_goal: "period(day|week|month|year), limit_usd, project_path?, agent? -> create goal",
|
|
5187
|
+
remove_goal: "id -> delete goal",
|
|
5188
|
+
register_agent: "name, session_id? -> register agent session",
|
|
5189
|
+
heartbeat: "agent_id -> update last_seen_at",
|
|
5190
|
+
set_focus: "agent_id, project_id? -> set active project context",
|
|
5191
|
+
list_agents: "no params -> registered agent list",
|
|
5192
|
+
send_feedback: "message, email?, category? -> save feedback locally"
|
|
5193
|
+
};
|
|
5194
|
+
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
5195
|
+
const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
5196
|
+
function fmtSession(s) {
|
|
5197
|
+
const id = String(s["id"] ?? "").slice(0, 8);
|
|
5198
|
+
const agent = String(s["agent"] ?? "");
|
|
5199
|
+
const proj = String(s["project_name"] || s["project_path"] || "\u2014").slice(0, 20);
|
|
5200
|
+
const cost = fmtUsd(Number(s["total_cost_usd"] ?? 0));
|
|
5201
|
+
const tok = fmtTok(Number(s["total_tokens"] ?? 0));
|
|
5202
|
+
return `${id} ${agent.padEnd(6)} ${cost.padEnd(10)} ${tok.padEnd(8)} ${proj}`;
|
|
5203
|
+
}
|
|
5204
|
+
function text(text2) {
|
|
5205
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
5206
|
+
}
|
|
5207
|
+
function textError(message) {
|
|
5208
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
5209
|
+
}
|
|
5210
|
+
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
|
|
5211
|
+
const q2 = query?.toLowerCase();
|
|
5212
|
+
const matches = q2 ? TOOL_NAMES.filter((name) => name.includes(q2)) : [...TOOL_NAMES];
|
|
5213
|
+
return text(matches.join(", "));
|
|
5214
|
+
});
|
|
3756
5215
|
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
|
|
3757
5216
|
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
3758
5217
|
`);
|
|
@@ -3760,7 +5219,7 @@ function buildServer() {
|
|
|
3760
5219
|
});
|
|
3761
5220
|
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
|
|
3762
5221
|
const resolved = period ?? "today";
|
|
3763
|
-
const s =
|
|
5222
|
+
const s = await store.summary(resolved, machine);
|
|
3764
5223
|
const machineLabel = machine ? ` on ${machine}` : "";
|
|
3765
5224
|
return text([
|
|
3766
5225
|
`period: ${resolved}${machineLabel}`,
|
|
@@ -3779,13 +5238,7 @@ function buildServer() {
|
|
|
3779
5238
|
machine: z.string().optional(),
|
|
3780
5239
|
limit: z.number().int().positive().max(100).optional()
|
|
3781
5240
|
}, async ({ agent, project, account, machine, limit }) => {
|
|
3782
|
-
const sessions =
|
|
3783
|
-
agent,
|
|
3784
|
-
project,
|
|
3785
|
-
account,
|
|
3786
|
-
machine,
|
|
3787
|
-
limit: limit ?? 20
|
|
3788
|
-
});
|
|
5241
|
+
const sessions = await store.sessions({ agent, project, account, machine, limit: limit ?? 20 });
|
|
3789
5242
|
const lines = ["id agent cost tokens project"];
|
|
3790
5243
|
for (const session of sessions)
|
|
3791
5244
|
lines.push(fmtSession(session));
|
|
@@ -3796,14 +5249,14 @@ function buildServer() {
|
|
|
3796
5249
|
n: z.number().int().positive().max(100).optional(),
|
|
3797
5250
|
agent: z.enum(AGENTS).optional()
|
|
3798
5251
|
}, async ({ n, agent }) => {
|
|
3799
|
-
const sessions =
|
|
5252
|
+
const sessions = await store.topSessions(n ?? 10, agent);
|
|
3800
5253
|
const lines = ["rank id agent cost tokens project"];
|
|
3801
5254
|
sessions.forEach((session, i) => lines.push(`${String(i + 1).padEnd(5)} ${fmtSession(session)}`));
|
|
3802
5255
|
return text(lines.join(`
|
|
3803
5256
|
`));
|
|
3804
5257
|
});
|
|
3805
5258
|
server.tool("get_model_breakdown", "Cost per model. No params.", {}, async () => {
|
|
3806
|
-
const rows =
|
|
5259
|
+
const rows = await store.modelBreakdown();
|
|
3807
5260
|
const lines = ["model agent reqs tokens cost"];
|
|
3808
5261
|
for (const row of rows) {
|
|
3809
5262
|
lines.push(`${String(row["model"]).slice(0, 30).padEnd(31)}${String(row["agent"]).padEnd(10)}${String(row["requests"]).padEnd(8)}${fmtTok(Number(row["total_tokens"])).padEnd(9)}${fmtUsd(Number(row["cost_usd"]))}`);
|
|
@@ -3812,7 +5265,7 @@ function buildServer() {
|
|
|
3812
5265
|
`));
|
|
3813
5266
|
});
|
|
3814
5267
|
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all).", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3815
|
-
const rows =
|
|
5268
|
+
const rows = await store.projectBreakdown({ period: period ?? "all" });
|
|
3816
5269
|
const lines = ["project sessions tokens cost"];
|
|
3817
5270
|
for (const row of rows) {
|
|
3818
5271
|
const name = String(row["project_name"] || row["project_path"] || "\u2014").slice(0, 20);
|
|
@@ -3822,7 +5275,7 @@ function buildServer() {
|
|
|
3822
5275
|
`));
|
|
3823
5276
|
});
|
|
3824
5277
|
server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3825
|
-
const rows =
|
|
5278
|
+
const rows = await store.agentBreakdown({ period: period ?? "all" });
|
|
3826
5279
|
if (rows.length === 0)
|
|
3827
5280
|
return text("No agent usage yet.");
|
|
3828
5281
|
const lines = ["agent sessions requests tokens api_eq billable included"];
|
|
@@ -3833,7 +5286,7 @@ function buildServer() {
|
|
|
3833
5286
|
`));
|
|
3834
5287
|
});
|
|
3835
5288
|
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3836
|
-
const rows =
|
|
5289
|
+
const rows = await store.accountBreakdown({ period: period ?? "all" });
|
|
3837
5290
|
if (rows.length === 0)
|
|
3838
5291
|
return text("No account-attributed sessions yet.");
|
|
3839
5292
|
const lines = ["account agent sessions requests tokens api_eq billable included"];
|
|
@@ -3845,7 +5298,7 @@ function buildServer() {
|
|
|
3845
5298
|
`));
|
|
3846
5299
|
});
|
|
3847
5300
|
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. No params.", {}, async () => {
|
|
3848
|
-
const budgets =
|
|
5301
|
+
const budgets = await store.listBudgets();
|
|
3849
5302
|
if (budgets.length === 0)
|
|
3850
5303
|
return text("No budgets set.");
|
|
3851
5304
|
const lines = ["scope period spent limit used% status"];
|
|
@@ -3865,26 +5318,21 @@ function buildServer() {
|
|
|
3865
5318
|
agent: z.enum(AGENTS).optional(),
|
|
3866
5319
|
alert_at_percent: z.number().positive().max(100).optional()
|
|
3867
5320
|
}, async ({ period, limit_usd, project_path, agent, alert_at_percent }) => {
|
|
3868
|
-
const
|
|
3869
|
-
const id = randomUUID();
|
|
3870
|
-
upsertBudget(db, {
|
|
3871
|
-
id,
|
|
5321
|
+
const id = await store.setBudget({
|
|
3872
5322
|
project_path: project_path ?? null,
|
|
3873
5323
|
agent: agent ?? null,
|
|
3874
5324
|
period,
|
|
3875
5325
|
limit_usd,
|
|
3876
|
-
alert_at_percent: alert_at_percent ?? 80
|
|
3877
|
-
created_at: now,
|
|
3878
|
-
updated_at: now
|
|
5326
|
+
alert_at_percent: alert_at_percent ?? 80
|
|
3879
5327
|
});
|
|
3880
5328
|
return text(`Budget set: ${id}`);
|
|
3881
5329
|
});
|
|
3882
5330
|
server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
|
|
3883
|
-
|
|
5331
|
+
await store.removeBudget(id);
|
|
3884
5332
|
return text("Budget removed.");
|
|
3885
5333
|
});
|
|
3886
5334
|
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage.", {}, async () => {
|
|
3887
|
-
const rows =
|
|
5335
|
+
const rows = await store.listPricing();
|
|
3888
5336
|
const lines = ["model input output cache-r cache-w cache-1h storage-h"];
|
|
3889
5337
|
for (const row of rows) {
|
|
3890
5338
|
lines.push(`${row.model.slice(0, 30).padEnd(31)}` + `${fmtUsd(row.input_per_1m).padEnd(9)}` + `${fmtUsd(row.output_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_read_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_per_1m).padEnd(9)}` + `${fmtUsd(row.cache_write_1h_per_1m ?? 0).padEnd(9)}` + `${fmtUsd(row.cache_storage_per_1m_hour ?? 0)}`);
|
|
@@ -3904,24 +5352,23 @@ function buildServer() {
|
|
|
3904
5352
|
const model = input.model.trim();
|
|
3905
5353
|
if (!model)
|
|
3906
5354
|
return textError("model is required");
|
|
3907
|
-
|
|
5355
|
+
await store.setPricing({
|
|
3908
5356
|
model,
|
|
3909
5357
|
input_per_1m: input.input_per_1m,
|
|
3910
5358
|
output_per_1m: input.output_per_1m,
|
|
3911
5359
|
cache_read_per_1m: input.cache_read_per_1m ?? 0,
|
|
3912
5360
|
cache_write_per_1m: input.cache_write_per_1m ?? 0,
|
|
3913
5361
|
cache_write_1h_per_1m: input.cache_write_1h_per_1m ?? 0,
|
|
3914
|
-
cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0
|
|
3915
|
-
updated_at: new Date().toISOString()
|
|
5362
|
+
cache_storage_per_1m_hour: input.cache_storage_per_1m_hour ?? 0
|
|
3916
5363
|
});
|
|
3917
5364
|
return text(`Pricing set: ${model}`);
|
|
3918
5365
|
});
|
|
3919
5366
|
server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
|
|
3920
|
-
|
|
5367
|
+
await store.removePricing(model);
|
|
3921
5368
|
return text("Pricing removed.");
|
|
3922
5369
|
});
|
|
3923
5370
|
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
3924
|
-
const rows =
|
|
5371
|
+
const rows = await store.daily(days ?? 30);
|
|
3925
5372
|
const byDate = new Map;
|
|
3926
5373
|
for (const row of rows) {
|
|
3927
5374
|
const date = String(row["date"]);
|
|
@@ -3945,7 +5392,7 @@ function buildServer() {
|
|
|
3945
5392
|
`));
|
|
3946
5393
|
});
|
|
3947
5394
|
server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
3948
|
-
const summary =
|
|
5395
|
+
const summary = await store.billingSummary(period ?? "month");
|
|
3949
5396
|
const lines = ["provider billed"];
|
|
3950
5397
|
for (const [provider, cost] of Object.entries(summary.by_provider)) {
|
|
3951
5398
|
lines.push(`${provider.padEnd(11)}${fmtUsd(cost)}`);
|
|
@@ -3955,10 +5402,10 @@ function buildServer() {
|
|
|
3955
5402
|
`));
|
|
3956
5403
|
});
|
|
3957
5404
|
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok)", { session_id: z.string() }, async ({ session_id }) => {
|
|
3958
|
-
const
|
|
3959
|
-
if (!
|
|
5405
|
+
const detail = await store.sessionDetail(session_id);
|
|
5406
|
+
if (!detail)
|
|
3960
5407
|
return textError(`Session not found: ${session_id}`);
|
|
3961
|
-
const
|
|
5408
|
+
const { session, requests } = detail;
|
|
3962
5409
|
const lines = [
|
|
3963
5410
|
`session: ${String(session["id"]).slice(0, 16)}`,
|
|
3964
5411
|
`agent: ${session["agent"]} project: ${session["project_name"] || "\u2014"}`,
|
|
@@ -3966,13 +5413,16 @@ function buildServer() {
|
|
|
3966
5413
|
"",
|
|
3967
5414
|
"time model input output cache-r cache-5m cache-1h cost"
|
|
3968
5415
|
];
|
|
3969
|
-
for (const request of requests) {
|
|
5416
|
+
for (const request of requests.slice(0, 50)) {
|
|
3970
5417
|
lines.push(`${String(request["timestamp"]).slice(11, 19)} ` + `${String(request["model"]).slice(0, 22).padEnd(23)}` + `${fmtTok(Number(request["input_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["output_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_read_tokens"])).padEnd(9)}` + `${fmtTok(Number(request["cache_create_5m_tokens"] ?? request["cache_create_tokens"] ?? 0)).padEnd(9)}` + `${fmtTok(Number(request["cache_create_1h_tokens"] ?? 0)).padEnd(9)}` + `${fmtUsd(Number(request["cost_usd"]))}`);
|
|
3971
5418
|
}
|
|
3972
5419
|
return text(lines.join(`
|
|
3973
5420
|
`));
|
|
3974
5421
|
});
|
|
3975
5422
|
server.tool("sync", `Ingest new cost data. sources: all|${AGENTS.join("|")}`, { sources: z.enum(["all", ...AGENTS]).optional() }, async ({ sources }) => {
|
|
5423
|
+
if (isCloudStore()) {
|
|
5424
|
+
return text("cloud mode: ingest is a local-only operation; nothing to sync (reads/writes route to the cloud API)");
|
|
5425
|
+
}
|
|
3976
5426
|
const selected = sources ?? "all";
|
|
3977
5427
|
const opts = selected === "all" ? {} : { [selected]: true };
|
|
3978
5428
|
const result = await syncAll(db, opts);
|
|
@@ -3980,15 +5430,13 @@ function buildServer() {
|
|
|
3980
5430
|
});
|
|
3981
5431
|
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
3982
5432
|
const p = period ?? "month";
|
|
3983
|
-
|
|
3984
|
-
const summary = querySummary(db, p, undefined, true);
|
|
3985
|
-
return text(JSON.stringify({ snapshots: snaps, summary }, null, 2));
|
|
5433
|
+
return text(JSON.stringify(await store.usage(p, agent), null, 2));
|
|
3986
5434
|
});
|
|
3987
5435
|
server.tool("get_savings", "Subscription vs API savings summary", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
|
|
3988
|
-
return text(JSON.stringify(
|
|
5436
|
+
return text(JSON.stringify(await store.savings(period ?? "month", agent), null, 2));
|
|
3989
5437
|
});
|
|
3990
5438
|
server.tool("list_subscriptions", "List configured subscription plans used by savings calculations.", {}, async () => {
|
|
3991
|
-
const rows = listSubscriptions(
|
|
5439
|
+
const rows = await store.listSubscriptions();
|
|
3992
5440
|
if (rows.length === 0)
|
|
3993
5441
|
return text("No subscriptions configured.");
|
|
3994
5442
|
const lines = ["id provider plan agent fee included active"];
|
|
@@ -4009,9 +5457,8 @@ function buildServer() {
|
|
|
4009
5457
|
reset_policy: z.string().optional(),
|
|
4010
5458
|
active: z.boolean().optional()
|
|
4011
5459
|
}, async (input) => {
|
|
4012
|
-
const
|
|
4013
|
-
|
|
4014
|
-
id: input.id ?? randomUUID(),
|
|
5460
|
+
const row = await store.setSubscription({
|
|
5461
|
+
id: input.id,
|
|
4015
5462
|
provider: input.provider,
|
|
4016
5463
|
plan: input.plan,
|
|
4017
5464
|
agent: input.agent ?? null,
|
|
@@ -4019,23 +5466,20 @@ function buildServer() {
|
|
|
4019
5466
|
included_usage_usd: input.included_usage_usd ?? 0,
|
|
4020
5467
|
billing_cycle_start: input.billing_cycle_start ?? null,
|
|
4021
5468
|
reset_policy: input.reset_policy ?? "monthly",
|
|
4022
|
-
active: input.active === false ? 0 : 1
|
|
4023
|
-
|
|
4024
|
-
updated_at: now
|
|
4025
|
-
};
|
|
4026
|
-
upsertSubscription(db, row);
|
|
5469
|
+
active: input.active === false ? 0 : 1
|
|
5470
|
+
});
|
|
4027
5471
|
return text(JSON.stringify(row, null, 2));
|
|
4028
5472
|
});
|
|
4029
5473
|
server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
|
|
4030
|
-
|
|
5474
|
+
await store.removeSubscription(id);
|
|
4031
5475
|
return text("Subscription removed.");
|
|
4032
5476
|
});
|
|
4033
5477
|
server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
|
|
4034
|
-
const cost =
|
|
5478
|
+
const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
|
|
4035
5479
|
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
4036
5480
|
});
|
|
4037
5481
|
server.tool("get_goals", "All spending goals with current progress. No params.", {}, async () => {
|
|
4038
|
-
const goals =
|
|
5482
|
+
const goals = await store.listGoals();
|
|
4039
5483
|
if (goals.length === 0)
|
|
4040
5484
|
return text("No goals set.");
|
|
4041
5485
|
const lines = ["period scope limit spent used% status"];
|
|
@@ -4054,24 +5498,20 @@ function buildServer() {
|
|
|
4054
5498
|
project_path: z.string().optional(),
|
|
4055
5499
|
agent: z.string().optional()
|
|
4056
5500
|
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
4057
|
-
|
|
4058
|
-
upsertGoal(db, {
|
|
4059
|
-
id: randomUUID(),
|
|
5501
|
+
await store.setGoal({
|
|
4060
5502
|
period,
|
|
4061
5503
|
project_path: project_path ?? null,
|
|
4062
5504
|
agent: agent ?? null,
|
|
4063
|
-
limit_usd
|
|
4064
|
-
created_at: now,
|
|
4065
|
-
updated_at: now
|
|
5505
|
+
limit_usd
|
|
4066
5506
|
});
|
|
4067
5507
|
return text(`Goal set: ${period} $${limit_usd}`);
|
|
4068
5508
|
});
|
|
4069
5509
|
server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
|
|
4070
|
-
|
|
5510
|
+
await store.removeGoal(id);
|
|
4071
5511
|
return text("Goal removed.");
|
|
4072
5512
|
});
|
|
4073
5513
|
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
4074
|
-
const machines =
|
|
5514
|
+
const machines = await store.machines();
|
|
4075
5515
|
if (machines.length === 0)
|
|
4076
5516
|
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
4077
5517
|
const lines = ["machine sessions requests cost last_active"];
|
|
@@ -4083,32 +5523,7 @@ current machine: ${getMachineId()}`);
|
|
|
4083
5523
|
return text(lines.join(`
|
|
4084
5524
|
`));
|
|
4085
5525
|
});
|
|
4086
|
-
server
|
|
4087
|
-
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
4088
|
-
if (existing) {
|
|
4089
|
-
existing.last_seen_at = new Date().toISOString();
|
|
4090
|
-
return text(JSON.stringify(existing));
|
|
4091
|
-
}
|
|
4092
|
-
const id = Math.random().toString(36).slice(2, 10);
|
|
4093
|
-
const agent = { id, name, last_seen_at: new Date().toISOString() };
|
|
4094
|
-
_econAgents.set(id, agent);
|
|
4095
|
-
return text(JSON.stringify(agent));
|
|
4096
|
-
});
|
|
4097
|
-
server.tool("heartbeat", "Update last_seen_at.", { agent_id: z.string() }, async ({ agent_id }) => {
|
|
4098
|
-
const agent = _econAgents.get(agent_id);
|
|
4099
|
-
if (!agent)
|
|
4100
|
-
return textError("Agent not found");
|
|
4101
|
-
agent.last_seen_at = new Date().toISOString();
|
|
4102
|
-
return text(`\u2665 ${agent.name}`);
|
|
4103
|
-
});
|
|
4104
|
-
server.tool("set_focus", "Set active project context.", { agent_id: z.string(), project_id: z.string().optional().nullable() }, async ({ agent_id, project_id }) => {
|
|
4105
|
-
const agent = _econAgents.get(agent_id);
|
|
4106
|
-
if (!agent)
|
|
4107
|
-
return textError("Agent not found");
|
|
4108
|
-
agent.project_id = project_id ?? undefined;
|
|
4109
|
-
return text(project_id ? `Focus: ${project_id}` : "Focus cleared");
|
|
4110
|
-
});
|
|
4111
|
-
server.tool("list_agents", "List all registered agents.", {}, async () => text(JSON.stringify([..._econAgents.values()])));
|
|
5526
|
+
registerAgentTools(server, { includeFeedback: false });
|
|
4112
5527
|
server.tool("send_feedback", "Send feedback about this service.", {
|
|
4113
5528
|
message: z.string(),
|
|
4114
5529
|
email: z.string().optional(),
|
|
@@ -4126,48 +5541,31 @@ current machine: ${getMachineId()}`);
|
|
|
4126
5541
|
}
|
|
4127
5542
|
|
|
4128
5543
|
// src/mcp/http.ts
|
|
4129
|
-
import {
|
|
5544
|
+
import {
|
|
5545
|
+
healthPayload,
|
|
5546
|
+
isHttpMode as harnessIsHttpMode,
|
|
5547
|
+
isStdioMode as harnessIsStdioMode,
|
|
5548
|
+
resolveMcpHttpPort as harnessResolveMcpHttpPort
|
|
5549
|
+
} from "@hasna/mcp-harness";
|
|
5550
|
+
import { handleMcpHttpRequest as harnessHandleMcpHttpRequest } from "@hasna/mcp-harness/bun";
|
|
4130
5551
|
var MCP_HTTP_IDLE_TIMEOUT_SECONDS = 0;
|
|
4131
5552
|
function isHttpMode(argv = process.argv.slice(2)) {
|
|
4132
|
-
return argv
|
|
5553
|
+
return harnessIsHttpMode(argv);
|
|
4133
5554
|
}
|
|
4134
5555
|
function isStdioMode(argv = process.argv.slice(2)) {
|
|
4135
|
-
return argv
|
|
5556
|
+
return harnessIsStdioMode(argv);
|
|
4136
5557
|
}
|
|
4137
5558
|
function resolveHttpPort(argv = process.argv.slice(2)) {
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
if (arg === "--port" || arg === "-p") {
|
|
4141
|
-
const raw = argv[i + 1];
|
|
4142
|
-
if (!raw)
|
|
4143
|
-
throw new Error(`Invalid port: ${raw ?? ""}`);
|
|
4144
|
-
return parsePort(raw, "port");
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
4147
|
-
const fromEnv = process.env["MCP_HTTP_PORT"];
|
|
4148
|
-
if (fromEnv)
|
|
4149
|
-
return parsePort(fromEnv, "MCP_HTTP_PORT");
|
|
4150
|
-
return DEFAULT_MCP_HTTP_PORT;
|
|
4151
|
-
}
|
|
4152
|
-
function parsePort(raw, label) {
|
|
4153
|
-
const value = Number(raw);
|
|
4154
|
-
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
4155
|
-
throw new Error(`Invalid ${label}: ${raw}`);
|
|
4156
|
-
}
|
|
4157
|
-
return value;
|
|
5559
|
+
const normalized = argv.map((arg) => arg === "-p" ? "--port" : arg);
|
|
5560
|
+
return harnessResolveMcpHttpPort({ argv: normalized, default: DEFAULT_MCP_HTTP_PORT });
|
|
4158
5561
|
}
|
|
4159
5562
|
async function handleMcpHttpRequest(req) {
|
|
4160
5563
|
const url = new URL(req.url);
|
|
4161
5564
|
if (url.pathname === "/health" && req.method === "GET") {
|
|
4162
|
-
return Response.json(
|
|
5565
|
+
return Response.json(healthPayload(MCP_NAME));
|
|
4163
5566
|
}
|
|
4164
5567
|
if (url.pathname === "/mcp") {
|
|
4165
|
-
|
|
4166
|
-
sessionIdGenerator: undefined
|
|
4167
|
-
});
|
|
4168
|
-
const server = buildServer();
|
|
4169
|
-
await server.connect(transport);
|
|
4170
|
-
return transport.handleRequest(req);
|
|
5568
|
+
return harnessHandleMcpHttpRequest(req, buildServer);
|
|
4171
5569
|
}
|
|
4172
5570
|
return new Response("Not Found", { status: 404 });
|
|
4173
5571
|
}
|