@hasna/economy 0.2.41 → 0.2.43
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/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +705 -87
- package/dist/db/cloud.d.ts +28 -0
- package/dist/db/cloud.d.ts.map +1 -0
- package/dist/db/database.d.ts +4 -3
- package/dist/db/database.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 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +299 -49
- package/dist/lib/fleet-sync.d.ts +61 -0
- package/dist/lib/fleet-sync.d.ts.map +1 -0
- package/dist/mcp/index.js +66 -16
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/otel/index.js +41 -5
- package/dist/server/index.js +456 -44
- 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/storage.js +2746 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -4
package/dist/cli/index.js
CHANGED
|
@@ -528,6 +528,7 @@ __export(exports_database, {
|
|
|
528
528
|
setIngestState: () => setIngestState,
|
|
529
529
|
seedModelPricing: () => seedModelPricing,
|
|
530
530
|
rollupSession: () => rollupSession,
|
|
531
|
+
queryZeroCostTokenizedModels: () => queryZeroCostTokenizedModels,
|
|
531
532
|
queryUsageSnapshots: () => queryUsageSnapshots,
|
|
532
533
|
queryTopSessions: () => queryTopSessions,
|
|
533
534
|
querySummary: () => querySummary,
|
|
@@ -623,6 +624,32 @@ function getDbPath() {
|
|
|
623
624
|
return process.env["ECONOMY_DB"];
|
|
624
625
|
return join(getDataDir(), "economy.db");
|
|
625
626
|
}
|
|
627
|
+
function isSqliteBusyError(error) {
|
|
628
|
+
const candidate = error;
|
|
629
|
+
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
630
|
+
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
631
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
632
|
+
}
|
|
633
|
+
function retryDelayMs(attempt) {
|
|
634
|
+
return Math.min(1000, 50 * 2 ** attempt);
|
|
635
|
+
}
|
|
636
|
+
function withSqliteBusyRetry(operation, context) {
|
|
637
|
+
const maxAttempts = 8;
|
|
638
|
+
let lastError;
|
|
639
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
640
|
+
try {
|
|
641
|
+
return operation();
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (!isSqliteBusyError(error))
|
|
644
|
+
throw error;
|
|
645
|
+
lastError = error;
|
|
646
|
+
if (attempt === maxAttempts - 1)
|
|
647
|
+
break;
|
|
648
|
+
Bun.sleepSync(retryDelayMs(attempt));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
|
|
652
|
+
}
|
|
626
653
|
function openDatabase(dbPath, skipSeed = false) {
|
|
627
654
|
const path = dbPath ?? getDbPath();
|
|
628
655
|
if (path !== ":memory:") {
|
|
@@ -630,11 +657,21 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
630
657
|
if (dir && !existsSync(dir))
|
|
631
658
|
mkdirSync(dir, { recursive: true });
|
|
632
659
|
}
|
|
633
|
-
const db =
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
660
|
+
const db = withSqliteBusyRetry(() => {
|
|
661
|
+
const opened = new Database(path);
|
|
662
|
+
try {
|
|
663
|
+
opened.exec("PRAGMA busy_timeout = 10000");
|
|
664
|
+
opened.exec("PRAGMA journal_mode = WAL");
|
|
665
|
+
opened.exec("PRAGMA foreign_keys = ON");
|
|
666
|
+
initSchema(opened);
|
|
667
|
+
return opened;
|
|
668
|
+
} catch (error) {
|
|
669
|
+
try {
|
|
670
|
+
opened.close();
|
|
671
|
+
} catch {}
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
}, `opening ${path}`);
|
|
638
675
|
if (!skipSeed) {
|
|
639
676
|
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
640
677
|
}
|
|
@@ -996,36 +1033,50 @@ function querySessions(db, filter = {}) {
|
|
|
996
1033
|
SELECT * FROM sessions ${where} ORDER BY started_at DESC LIMIT ? OFFSET ?
|
|
997
1034
|
`).all(...params, limit, offset);
|
|
998
1035
|
}
|
|
999
|
-
function queryTopSessions(db, n = 10, agent) {
|
|
1036
|
+
function queryTopSessions(db, n = 10, agent, since) {
|
|
1037
|
+
const conditions = [];
|
|
1038
|
+
const params = [];
|
|
1000
1039
|
if (agent) {
|
|
1001
|
-
|
|
1040
|
+
conditions.push("agent = ?");
|
|
1041
|
+
params.push(agent);
|
|
1042
|
+
}
|
|
1043
|
+
if (since) {
|
|
1044
|
+
conditions.push("started_at >= ?");
|
|
1045
|
+
params.push(since);
|
|
1002
1046
|
}
|
|
1003
|
-
|
|
1047
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1048
|
+
return db.prepare(`SELECT * FROM sessions ${where} ORDER BY total_cost_usd DESC LIMIT ?`).all(...params, n);
|
|
1004
1049
|
}
|
|
1005
|
-
function querySummary(db, period, machine, allMachines = false) {
|
|
1050
|
+
function querySummary(db, period, machine, allMachines = false, agent) {
|
|
1006
1051
|
const rWhere = periodWhere(period);
|
|
1007
1052
|
const sWhere = sessionPeriodWhere(period);
|
|
1008
|
-
const machineClause = !allMachines && machine ?
|
|
1053
|
+
const machineClause = !allMachines && machine ? " AND machine_id = ?" : "";
|
|
1054
|
+
const agentClause = agent ? " AND agent = ?" : "";
|
|
1055
|
+
const params = [];
|
|
1056
|
+
if (!allMachines && machine)
|
|
1057
|
+
params.push(machine);
|
|
1058
|
+
if (agent)
|
|
1059
|
+
params.push(agent);
|
|
1009
1060
|
const r = db.prepare(`
|
|
1010
1061
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
1011
1062
|
COUNT(*) as requests,
|
|
1012
1063
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
1013
|
-
FROM requests WHERE ${rWhere}${machineClause}
|
|
1014
|
-
`).get();
|
|
1064
|
+
FROM requests WHERE ${rWhere}${machineClause}${agentClause}
|
|
1065
|
+
`).get(...params);
|
|
1015
1066
|
const codexTotals = db.prepare(`
|
|
1016
1067
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1017
1068
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
1018
1069
|
COALESCE(SUM(request_count), 0) as requests,
|
|
1019
1070
|
COUNT(*) as sessions
|
|
1020
1071
|
FROM sessions
|
|
1021
|
-
WHERE ${sWhere}${machineClause}
|
|
1072
|
+
WHERE ${sWhere}${machineClause}${agentClause}
|
|
1022
1073
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1023
|
-
`).get();
|
|
1074
|
+
`).get(...params);
|
|
1024
1075
|
const requestSessionCount = db.prepare(`
|
|
1025
1076
|
SELECT COUNT(DISTINCT session_id) as sessions
|
|
1026
1077
|
FROM requests
|
|
1027
|
-
WHERE ${rWhere}${machineClause}
|
|
1028
|
-
`).get();
|
|
1078
|
+
WHERE ${rWhere}${machineClause}${agentClause}
|
|
1079
|
+
`).get(...params);
|
|
1029
1080
|
return {
|
|
1030
1081
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
1031
1082
|
requests: r.requests + codexTotals.requests,
|
|
@@ -1034,6 +1085,21 @@ function querySummary(db, period, machine, allMachines = false) {
|
|
|
1034
1085
|
period
|
|
1035
1086
|
};
|
|
1036
1087
|
}
|
|
1088
|
+
function queryZeroCostTokenizedModels(db, limit = 10) {
|
|
1089
|
+
return db.prepare(`
|
|
1090
|
+
SELECT agent,
|
|
1091
|
+
model,
|
|
1092
|
+
COUNT(*) as requests,
|
|
1093
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1094
|
+
MAX(timestamp) as last_seen
|
|
1095
|
+
FROM requests
|
|
1096
|
+
WHERE cost_usd = 0
|
|
1097
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
|
|
1098
|
+
GROUP BY agent, model
|
|
1099
|
+
ORDER BY requests DESC, total_tokens DESC
|
|
1100
|
+
LIMIT ?
|
|
1101
|
+
`).all(limit);
|
|
1102
|
+
}
|
|
1037
1103
|
function queryModelBreakdown(db) {
|
|
1038
1104
|
return db.prepare(`
|
|
1039
1105
|
SELECT model, agent,
|
|
@@ -4270,15 +4336,15 @@ __export(exports_config, {
|
|
|
4270
4336
|
loadConfig: () => loadConfig2,
|
|
4271
4337
|
getConfigValue: () => getConfigValue
|
|
4272
4338
|
});
|
|
4273
|
-
import { existsSync as
|
|
4274
|
-
import { dirname as dirname2, join as
|
|
4339
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
4340
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
4275
4341
|
function getConfigPath() {
|
|
4276
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
4342
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
4277
4343
|
}
|
|
4278
4344
|
function loadConfig2() {
|
|
4279
4345
|
try {
|
|
4280
4346
|
const configPath = getConfigPath();
|
|
4281
|
-
if (
|
|
4347
|
+
if (existsSync14(configPath)) {
|
|
4282
4348
|
const raw = readFileSync11(configPath, "utf-8");
|
|
4283
4349
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
4284
4350
|
}
|
|
@@ -4288,9 +4354,9 @@ function loadConfig2() {
|
|
|
4288
4354
|
function saveConfig2(config) {
|
|
4289
4355
|
const configPath = getConfigPath();
|
|
4290
4356
|
const dir = dirname2(configPath);
|
|
4291
|
-
if (!
|
|
4292
|
-
|
|
4293
|
-
|
|
4357
|
+
if (!existsSync14(dir))
|
|
4358
|
+
mkdirSync4(dir, { recursive: true });
|
|
4359
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + `
|
|
4294
4360
|
`);
|
|
4295
4361
|
}
|
|
4296
4362
|
function getConfigValue(key) {
|
|
@@ -4432,7 +4498,7 @@ var init_webhooks = __esm(() => {
|
|
|
4432
4498
|
});
|
|
4433
4499
|
|
|
4434
4500
|
// src/lib/watch-paths.ts
|
|
4435
|
-
import { existsSync as
|
|
4501
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4436
4502
|
function getWatchPaths() {
|
|
4437
4503
|
const p = agentPaths();
|
|
4438
4504
|
const candidates = [
|
|
@@ -4445,7 +4511,7 @@ function getWatchPaths() {
|
|
|
4445
4511
|
p.piSessions,
|
|
4446
4512
|
p.hermesDir
|
|
4447
4513
|
];
|
|
4448
|
-
return candidates.filter((path) =>
|
|
4514
|
+
return candidates.filter((path) => existsSync15(path));
|
|
4449
4515
|
}
|
|
4450
4516
|
var init_watch_paths = __esm(() => {
|
|
4451
4517
|
init_paths();
|
|
@@ -4607,16 +4673,275 @@ var init_watch = __esm(() => {
|
|
|
4607
4673
|
init_notification();
|
|
4608
4674
|
});
|
|
4609
4675
|
|
|
4676
|
+
// src/db/sync-pg.ts
|
|
4677
|
+
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
4678
|
+
import { fileURLToPath } from "url";
|
|
4679
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
4680
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4681
|
+
import { translateSql, translateParams } from "@hasna/cloud";
|
|
4682
|
+
function translateSqliteDates(sql) {
|
|
4683
|
+
let s = sql;
|
|
4684
|
+
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
4685
|
+
s = s.replace(/DATE\('now',\s*'weekday 0',\s*'-7 days'\)/gi, isoDate("(date_trunc('week', now() + interval '1 day') - interval '1 day')"));
|
|
4686
|
+
s = s.replace(/DATE\('now',\s*'start of month'\)/gi, isoDate("date_trunc('month', now())"));
|
|
4687
|
+
s = s.replace(/DATE\('now',\s*'start of year'\)/gi, isoDate("date_trunc('year', now())"));
|
|
4688
|
+
s = s.replace(/DATE\('now',\s*'(-?\d+)\s*days?'\)/gi, (_m, n) => isoDate(`now() + interval '${n} days'`));
|
|
4689
|
+
s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
|
|
4690
|
+
s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
|
|
4691
|
+
s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
|
|
4692
|
+
const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
|
|
4693
|
+
s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt4, col) => {
|
|
4694
|
+
const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt4);
|
|
4695
|
+
return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
|
|
4696
|
+
});
|
|
4697
|
+
s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
|
|
4698
|
+
s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
|
|
4699
|
+
s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
|
|
4700
|
+
s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
|
|
4701
|
+
return s;
|
|
4702
|
+
}
|
|
4703
|
+
function resolveWorkerPath() {
|
|
4704
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4705
|
+
const candidates = [
|
|
4706
|
+
join14(here, "pg-sync-worker.ts"),
|
|
4707
|
+
join14(here, "pg-sync-worker.js"),
|
|
4708
|
+
join14(here, "..", "db", "pg-sync-worker.js")
|
|
4709
|
+
];
|
|
4710
|
+
for (const candidate of candidates)
|
|
4711
|
+
if (existsSync16(candidate))
|
|
4712
|
+
return candidate;
|
|
4713
|
+
return candidates[0];
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
class SyncPgAdapter {
|
|
4717
|
+
worker;
|
|
4718
|
+
port;
|
|
4719
|
+
counter = 0;
|
|
4720
|
+
constructor(dsn) {
|
|
4721
|
+
const workerPath = resolveWorkerPath();
|
|
4722
|
+
this.worker = new Worker(workerPath, { workerData: { dsn } });
|
|
4723
|
+
this.worker.on("error", (err) => console.error(JSON.stringify({ evt: "pg_worker_error", message: err.message })));
|
|
4724
|
+
const channel = new MessageChannel;
|
|
4725
|
+
this.port = channel.port1;
|
|
4726
|
+
this.worker.postMessage({ type: "init", port: channel.port2 }, [channel.port2]);
|
|
4727
|
+
}
|
|
4728
|
+
query(sql, params) {
|
|
4729
|
+
const pgSql = translateSqliteDates(translateSql(sql, "pg"));
|
|
4730
|
+
const pgParams = translateParams(params);
|
|
4731
|
+
const id = ++this.counter;
|
|
4732
|
+
const signal = new SharedArrayBuffer(4);
|
|
4733
|
+
const sig = new Int32Array(signal);
|
|
4734
|
+
Atomics.store(sig, 0, 0);
|
|
4735
|
+
this.worker.postMessage({ id, sql: pgSql, params: pgParams, signal });
|
|
4736
|
+
Atomics.wait(sig, 0, 0);
|
|
4737
|
+
let received = receiveMessageOnPort(this.port);
|
|
4738
|
+
for (let attempt = 0;!received && attempt < 1e6; attempt++)
|
|
4739
|
+
received = receiveMessageOnPort(this.port);
|
|
4740
|
+
if (!received)
|
|
4741
|
+
throw new Error("SyncPgAdapter: no reply from worker");
|
|
4742
|
+
const reply = received.message;
|
|
4743
|
+
if (!reply.ok)
|
|
4744
|
+
throw new Error(reply.error);
|
|
4745
|
+
return { rows: reply.rows, rowCount: reply.rowCount };
|
|
4746
|
+
}
|
|
4747
|
+
run(sql, ...params) {
|
|
4748
|
+
const result = this.query(sql, params);
|
|
4749
|
+
return { changes: result.rowCount, lastInsertRowid: result.rows?.[0]?.["id"] ?? 0 };
|
|
4750
|
+
}
|
|
4751
|
+
get(sql, ...params) {
|
|
4752
|
+
return this.query(sql, params).rows[0] ?? null;
|
|
4753
|
+
}
|
|
4754
|
+
all(sql, ...params) {
|
|
4755
|
+
return this.query(sql, params).rows;
|
|
4756
|
+
}
|
|
4757
|
+
exec(sql) {
|
|
4758
|
+
this.query(sql, []);
|
|
4759
|
+
}
|
|
4760
|
+
prepare(sql) {
|
|
4761
|
+
const self = this;
|
|
4762
|
+
return {
|
|
4763
|
+
run: (...params) => self.run(sql, ...params),
|
|
4764
|
+
get: (...params) => self.get(sql, ...params),
|
|
4765
|
+
all: (...params) => self.all(sql, ...params),
|
|
4766
|
+
finalize: () => {}
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
transaction(fn) {
|
|
4770
|
+
return fn();
|
|
4771
|
+
}
|
|
4772
|
+
close() {
|
|
4773
|
+
this.worker.terminate();
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
var init_sync_pg = () => {};
|
|
4777
|
+
|
|
4778
|
+
// src/db/cloud.ts
|
|
4779
|
+
import pg from "pg";
|
|
4780
|
+
function getCloudDatabaseUrl2() {
|
|
4781
|
+
return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
|
|
4782
|
+
}
|
|
4783
|
+
function isCloudMode() {
|
|
4784
|
+
const mode = process.env["HASNA_ECONOMY_STORAGE_MODE"]?.trim().toLowerCase();
|
|
4785
|
+
if (mode === "cloud")
|
|
4786
|
+
return true;
|
|
4787
|
+
if (mode === "local")
|
|
4788
|
+
return false;
|
|
4789
|
+
return Boolean(getCloudDatabaseUrl2());
|
|
4790
|
+
}
|
|
4791
|
+
function resolveSigningSecret() {
|
|
4792
|
+
return process.env["HASNA_ECONOMY_API_SIGNING_KEY"]?.trim() || process.env["HASNA_API_SIGNING_KEY"]?.trim() || process.env["API_KEY_SIGNING_SECRET"]?.trim() || undefined;
|
|
4793
|
+
}
|
|
4794
|
+
function openCloudDatabase(dsn = getCloudDatabaseUrl2()) {
|
|
4795
|
+
if (!dsn) {
|
|
4796
|
+
throw new Error("economy cloud mode requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
4797
|
+
}
|
|
4798
|
+
return new SyncPgAdapter(dsn);
|
|
4799
|
+
}
|
|
4800
|
+
function createCloudPool(dsn = getCloudDatabaseUrl2()) {
|
|
4801
|
+
if (!dsn)
|
|
4802
|
+
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
4803
|
+
const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
4804
|
+
const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
4805
|
+
pool.on("error", (err) => {
|
|
4806
|
+
console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
|
|
4807
|
+
});
|
|
4808
|
+
return pool;
|
|
4809
|
+
}
|
|
4810
|
+
function authClientFromPool(pool) {
|
|
4811
|
+
return {
|
|
4812
|
+
async many(sql, params = []) {
|
|
4813
|
+
const res = await pool.query(sql, params);
|
|
4814
|
+
return res.rows;
|
|
4815
|
+
},
|
|
4816
|
+
async get(sql, params = []) {
|
|
4817
|
+
const res = await pool.query(sql, params);
|
|
4818
|
+
return res.rows[0] ?? null;
|
|
4819
|
+
},
|
|
4820
|
+
async execute(sql, params = []) {
|
|
4821
|
+
await pool.query(sql, params);
|
|
4822
|
+
}
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
var init_cloud = __esm(() => {
|
|
4826
|
+
init_sync_pg();
|
|
4827
|
+
init_pg_migrations();
|
|
4828
|
+
});
|
|
4829
|
+
|
|
4830
|
+
// src/openapi.ts
|
|
4831
|
+
var openApiSpec;
|
|
4832
|
+
var init_openapi = __esm(() => {
|
|
4833
|
+
openApiSpec = {
|
|
4834
|
+
openapi: "3.0.3",
|
|
4835
|
+
info: {
|
|
4836
|
+
title: "@hasna/economy self-hosted API",
|
|
4837
|
+
version: "0.0.0",
|
|
4838
|
+
description: "AI coding cost tracker \u2014 self-hosted control plane. Cloud mode is PURE REMOTE per Amendment A1: the serve process reads/writes the shared RDS Postgres directly with API-key auth via @hasna/contracts. All /v1 routes require a valid `economy:*` scoped API key (x-api-key or Authorization: Bearer). The foundation probes /health, /ready, /version are open."
|
|
4839
|
+
},
|
|
4840
|
+
servers: [{ url: "https://economy.hasna.xyz", description: "prod" }, { url: "http://localhost:3456", description: "local" }],
|
|
4841
|
+
security: [{ apiKey: [] }, { bearer: [] }],
|
|
4842
|
+
components: {
|
|
4843
|
+
securitySchemes: {
|
|
4844
|
+
apiKey: { type: "apiKey", in: "header", name: "x-api-key" },
|
|
4845
|
+
bearer: { type: "http", scheme: "bearer" }
|
|
4846
|
+
},
|
|
4847
|
+
schemas: {
|
|
4848
|
+
Envelope: { type: "object", properties: { data: {}, meta: { type: "object", additionalProperties: true } }, required: ["data"] },
|
|
4849
|
+
Foundation: { type: "object", properties: { status: { type: "string" }, version: { type: "string" }, mode: { type: "string" }, service: { type: "string" } }, required: ["status", "version", "mode"] },
|
|
4850
|
+
Error: { type: "object", properties: { error: { type: "string" }, message: { type: "string" } }, required: ["error"] },
|
|
4851
|
+
MutationOk: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
4852
|
+
CostSummary: { type: "object", properties: { total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" }, session_count: { type: "number" }, period: { type: "string" } }, additionalProperties: true },
|
|
4853
|
+
Session: { type: "object", properties: { id: { type: "string" }, agent: { type: "string" }, project_path: { type: "string" }, project_name: { type: "string" }, started_at: { type: "string" }, ended_at: { type: "string", nullable: true }, total_cost_usd: { type: "number" }, total_tokens: { type: "number" }, request_count: { type: "number" } }, additionalProperties: true },
|
|
4854
|
+
ModelBreakdown: { type: "object", additionalProperties: true },
|
|
4855
|
+
ProjectBreakdown: { type: "object", additionalProperties: true },
|
|
4856
|
+
AgentBreakdown: { type: "object", additionalProperties: true },
|
|
4857
|
+
AccountBreakdown: { type: "object", additionalProperties: true },
|
|
4858
|
+
DailyPoint: { type: "object", additionalProperties: true },
|
|
4859
|
+
BudgetStatus: { type: "object", properties: { id: { type: "string" }, limit_usd: { type: "number" }, period: { type: "string" }, spent_usd: { type: "number" } }, additionalProperties: true },
|
|
4860
|
+
CreateBudgetInput: { type: "object", properties: { limit_usd: { type: "number" }, alert_at_percent: { type: "number" }, period: { type: "string", enum: ["daily", "weekly", "monthly", "day", "week", "month"] }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
|
|
4861
|
+
GoalStatus: { type: "object", additionalProperties: true },
|
|
4862
|
+
CreateGoalInput: { type: "object", properties: { period: { type: "string", enum: ["day", "week", "month", "year"] }, limit_usd: { type: "number" }, project_path: { type: "string", nullable: true }, agent: { type: "string", nullable: true } }, required: ["limit_usd"] },
|
|
4863
|
+
ModelPricing: { type: "object", additionalProperties: true },
|
|
4864
|
+
CreatePricingInput: { type: "object", properties: { model: { type: "string" }, input_per_1m: { type: "number" }, output_per_1m: { type: "number" } }, required: ["model", "input_per_1m", "output_per_1m"] },
|
|
4865
|
+
Subscription: { type: "object", additionalProperties: true },
|
|
4866
|
+
CreateSubscriptionInput: { type: "object", properties: { provider: { type: "string" }, plan: { type: "string" }, monthly_fee_usd: { type: "number" }, included_usage_usd: { type: "number" }, agent: { type: "string", nullable: true } }, required: ["provider", "plan"] },
|
|
4867
|
+
Project: { type: "object", additionalProperties: true },
|
|
4868
|
+
CreateProjectInput: { type: "object", properties: { path: { type: "string" }, name: { type: "string" }, description: { type: "string" }, tags: { type: "array", items: { type: "string" } } }, required: ["path"] },
|
|
4869
|
+
MachineInfo: { type: "object", additionalProperties: true },
|
|
4870
|
+
BillingSummary: { type: "object", additionalProperties: true }
|
|
4871
|
+
}
|
|
4872
|
+
},
|
|
4873
|
+
paths: {
|
|
4874
|
+
"/health": { get: { operationId: "health", summary: "Liveness probe", security: [], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
|
|
4875
|
+
"/ready": { get: { operationId: "ready", summary: "Readiness probe (storage reachable + migrated)", security: [], responses: { "200": { description: "ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } }, "503": { description: "not ready", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
|
|
4876
|
+
"/version": { get: { operationId: "version", summary: "Version probe", security: [], responses: { "200": { description: "version", content: { "application/json": { schema: { $ref: "#/components/schemas/Foundation" } } } } } } },
|
|
4877
|
+
"/v1/summary": { get: { operationId: "getSummary", summary: "Cost summary for a period", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/CostSummary" } } } } } } } } },
|
|
4878
|
+
"/v1/sessions": { get: { operationId: "listSessions", summary: "List sessions", parameters: [{ name: "agent", in: "query", schema: { type: "string" } }, { name: "project", in: "query", schema: { type: "string" } }, { name: "search", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }, { name: "account", in: "query", schema: { type: "string" } }, { name: "limit", in: "query", schema: { type: "integer" } }, { name: "offset", in: "query", schema: { type: "integer" } }, { name: "since", in: "query", schema: { type: "string" } }, { name: "fields", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
|
|
4879
|
+
"/v1/sessions/{id}/requests": { get: { operationId: "getSessionRequests", summary: "Requests for a session", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } }, "404": { description: "not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } } } },
|
|
4880
|
+
"/v1/top": { get: { operationId: "topSessions", summary: "Top sessions by cost", parameters: [{ name: "n", in: "query", schema: { type: "integer" } }, { name: "agent", in: "query", schema: { type: "string" } }, { name: "since", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Session" } } } } } } } } } },
|
|
4881
|
+
"/v1/models": { get: { operationId: "modelBreakdown", summary: "Cost by model", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelBreakdown" } } } } } } } } } },
|
|
4882
|
+
"/v1/projects": { get: { operationId: "projectBreakdown", summary: "Cost by project", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ProjectBreakdown" } } } } } } } } } },
|
|
4883
|
+
"/v1/accounts": { get: { operationId: "accountBreakdown", summary: "Cost by account", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/AccountBreakdown" } } } } } } } } } },
|
|
4884
|
+
"/v1/breakdown": { get: { operationId: "breakdown", summary: "Cost breakdown by dimension", parameters: [{ name: "by", in: "query", schema: { type: "string", enum: ["model", "project", "agent", "account"] } }, { name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4885
|
+
"/v1/daily": { get: { operationId: "daily", summary: "Daily cost points", parameters: [{ name: "days", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/DailyPoint" } } } } } } } } } },
|
|
4886
|
+
"/v1/hourly": { get: { operationId: "hourly", summary: "Hourly cost points", parameters: [{ name: "hours", in: "query", schema: { type: "integer" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4887
|
+
"/v1/machines": { get: { operationId: "listMachines", summary: "Known machines", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/MachineInfo" } } } } } } } } } },
|
|
4888
|
+
"/v1/fleet": { get: { operationId: "fleet", summary: "Fleet summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "machine", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4889
|
+
"/v1/billing": { get: { operationId: "billing", summary: "Provider billing summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BillingSummary" } } } } } } } } },
|
|
4890
|
+
"/v1/billing/diff": { get: { operationId: "billingDiff", summary: "Billing vs tracked diff", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "threshold", in: "query", schema: { type: "number" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4891
|
+
"/v1/usage": { get: { operationId: "usage", summary: "Usage snapshots + summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4892
|
+
"/v1/savings": { get: { operationId: "savings", summary: "Savings summary", parameters: [{ name: "period", in: "query", schema: { type: "string" } }, { name: "agent", in: "query", schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } },
|
|
4893
|
+
"/v1/budgets": {
|
|
4894
|
+
get: { operationId: "listBudgets", summary: "List budgets", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } } },
|
|
4895
|
+
post: { operationId: "createBudget", summary: "Create a budget", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateBudgetInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/BudgetStatus" } } } } } } } }
|
|
4896
|
+
},
|
|
4897
|
+
"/v1/budgets/{id}": { delete: { operationId: "deleteBudget", summary: "Delete a budget", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4898
|
+
"/v1/goals": {
|
|
4899
|
+
get: { operationId: "listGoals", summary: "List goals", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/GoalStatus" } } } } } } } } },
|
|
4900
|
+
post: { operationId: "createGoal", summary: "Create a goal", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateGoalInput" } } } }, responses: { "200": { description: "created", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/GoalStatus" } } } } } } } }
|
|
4901
|
+
},
|
|
4902
|
+
"/v1/goals/{id}": { delete: { operationId: "deleteGoal", summary: "Delete a goal", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4903
|
+
"/v1/pricing": {
|
|
4904
|
+
get: { operationId: "listPricing", summary: "List model pricing", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/ModelPricing" } } } } } } } } },
|
|
4905
|
+
post: { operationId: "upsertPricing", summary: "Upsert model pricing", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreatePricingInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/ModelPricing" } } } } } } } }
|
|
4906
|
+
},
|
|
4907
|
+
"/v1/pricing/{model}": { delete: { operationId: "deletePricing", summary: "Delete model pricing", parameters: [{ name: "model", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4908
|
+
"/v1/subscriptions": {
|
|
4909
|
+
get: { operationId: "listSubscriptions", summary: "List subscriptions", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Subscription" } } } } } } } } },
|
|
4910
|
+
post: { operationId: "upsertSubscription", summary: "Upsert a subscription", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateSubscriptionInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { $ref: "#/components/schemas/Subscription" } } } } } } } }
|
|
4911
|
+
},
|
|
4912
|
+
"/v1/subscriptions/{id}": { delete: { operationId: "deleteSubscription", summary: "Delete a subscription", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4913
|
+
"/v1/project-registry": {
|
|
4914
|
+
get: { operationId: "listProjectRegistry", summary: "List registered projects", responses: { "200": { description: "ok", content: { "application/json": { schema: { type: "object", properties: { data: { type: "array", items: { $ref: "#/components/schemas/Project" } } } } } } } } },
|
|
4915
|
+
post: { operationId: "upsertProject", summary: "Register a project", requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProjectInput" } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } }
|
|
4916
|
+
},
|
|
4917
|
+
"/v1/project-registry/{id}": { delete: { operationId: "deleteProject", summary: "Delete a registered project", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/MutationOk" } } } } } } },
|
|
4918
|
+
"/v1/sync": { post: { operationId: "sync", summary: "Trigger an ingest sync", requestBody: { required: false, content: { "application/json": { schema: { type: "object", properties: { sources: { type: "string" } } } } } }, responses: { "200": { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Envelope" } } } } } } }
|
|
4919
|
+
}
|
|
4920
|
+
};
|
|
4921
|
+
});
|
|
4922
|
+
|
|
4610
4923
|
// src/server/serve.ts
|
|
4611
4924
|
var exports_serve = {};
|
|
4612
4925
|
__export(exports_serve, {
|
|
4613
4926
|
startServer: () => startServer,
|
|
4927
|
+
openApiDocument: () => openApiDocument,
|
|
4614
4928
|
createServerFetch: () => createServerFetch,
|
|
4615
4929
|
createHandler: () => createHandler
|
|
4616
4930
|
});
|
|
4617
4931
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4618
|
-
import { existsSync as
|
|
4619
|
-
import { resolve, sep } from "path";
|
|
4932
|
+
import { existsSync as existsSync17 } from "fs";
|
|
4933
|
+
import { resolve as resolve2, sep } from "path";
|
|
4934
|
+
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
4935
|
+
function openApiDocument() {
|
|
4936
|
+
const spec = openApiSpec;
|
|
4937
|
+
return { ...spec, info: { ...spec["info"], version: packageMetadata.version } };
|
|
4938
|
+
}
|
|
4939
|
+
function serveMode() {
|
|
4940
|
+
return isCloudMode() ? "self_hosted" : "local";
|
|
4941
|
+
}
|
|
4942
|
+
function foundationEnvelope(status, extra = {}) {
|
|
4943
|
+
return { status, version: packageMetadata.version, mode: serveMode(), service: "economy", ...extra };
|
|
4944
|
+
}
|
|
4620
4945
|
function json(data, status = 200) {
|
|
4621
4946
|
return new Response(JSON.stringify(data), {
|
|
4622
4947
|
status,
|
|
@@ -4670,23 +4995,23 @@ function dashboardPath(root, pathname) {
|
|
|
4670
4995
|
return null;
|
|
4671
4996
|
}
|
|
4672
4997
|
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
4673
|
-
const rootPath =
|
|
4674
|
-
const filePath =
|
|
4998
|
+
const rootPath = resolve2(root);
|
|
4999
|
+
const filePath = resolve2(rootPath, relativePath);
|
|
4675
5000
|
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
4676
5001
|
}
|
|
4677
5002
|
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
4678
5003
|
return async function fetch2(req) {
|
|
4679
5004
|
const url = new URL(req.url);
|
|
4680
|
-
if (url.pathname.startsWith("/api") || url.pathname
|
|
5005
|
+
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
4681
5006
|
return apiHandler(req);
|
|
4682
5007
|
}
|
|
4683
|
-
if (
|
|
5008
|
+
if (existsSync17(dashboardDir)) {
|
|
4684
5009
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
4685
|
-
if (filePath &&
|
|
5010
|
+
if (filePath && existsSync17(filePath)) {
|
|
4686
5011
|
return new Response(Bun.file(filePath));
|
|
4687
5012
|
}
|
|
4688
5013
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
4689
|
-
if (indexPath &&
|
|
5014
|
+
if (indexPath && existsSync17(indexPath)) {
|
|
4690
5015
|
return new Response(Bun.file(indexPath));
|
|
4691
5016
|
}
|
|
4692
5017
|
}
|
|
@@ -4698,15 +5023,34 @@ function applyFields(obj, fields) {
|
|
|
4698
5023
|
return obj;
|
|
4699
5024
|
return Object.fromEntries(fields.map((f) => [f, obj[f] ?? null]));
|
|
4700
5025
|
}
|
|
4701
|
-
function createHandler(db) {
|
|
5026
|
+
function createHandler(db, options = {}) {
|
|
4702
5027
|
return async function handler(req) {
|
|
4703
5028
|
const url = new URL(req.url);
|
|
4704
|
-
const
|
|
5029
|
+
const rawPath = url.pathname;
|
|
5030
|
+
const path = rawPath.startsWith("/v1/") ? "/api" + rawPath.slice(3) : rawPath === "/v1" ? "/api" : rawPath;
|
|
4705
5031
|
const method = req.method;
|
|
4706
5032
|
if (method === "OPTIONS")
|
|
4707
5033
|
return new Response(null, { status: 204, headers: CORS });
|
|
4708
|
-
if (
|
|
5034
|
+
if (method === "GET" && (rawPath === "/health" || rawPath === "/healthz")) {
|
|
5035
|
+
return json(foundationEnvelope("ok"));
|
|
5036
|
+
}
|
|
5037
|
+
if (method === "GET" && (rawPath === "/version" || rawPath === "/v1/version")) {
|
|
5038
|
+
return json(foundationEnvelope("ok"));
|
|
5039
|
+
}
|
|
5040
|
+
if (method === "GET" && (rawPath === "/ready" || rawPath === "/readyz")) {
|
|
5041
|
+
const result = options.readyCheck ? await options.readyCheck() : { ready: true };
|
|
5042
|
+
return json(foundationEnvelope(result.ready ? "ready" : "not_ready", result.detail ? { detail: result.detail } : {}), result.ready ? 200 : 503);
|
|
5043
|
+
}
|
|
5044
|
+
if (method === "GET" && rawPath === "/openapi.json") {
|
|
5045
|
+
return json(openApiDocument());
|
|
5046
|
+
}
|
|
5047
|
+
if (options.authenticator) {
|
|
5048
|
+
const decision = await options.authenticator.authenticate(req.headers, { method, path: rawPath });
|
|
5049
|
+
if (!decision.ok)
|
|
5050
|
+
return json({ error: decision.reason ?? "unauthorized", message: decision.message }, decision.status || 401);
|
|
5051
|
+
} else if (!isAuthorizedRequest(req, path)) {
|
|
4709
5052
|
return err("Unauthorized", 401);
|
|
5053
|
+
}
|
|
4710
5054
|
if (path === "/health")
|
|
4711
5055
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
4712
5056
|
if (path === "/api/summary" && method === "GET") {
|
|
@@ -4771,7 +5115,8 @@ function createHandler(db) {
|
|
|
4771
5115
|
if (path === "/api/top" && method === "GET") {
|
|
4772
5116
|
const n = Number(url.searchParams.get("n") ?? 10);
|
|
4773
5117
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4774
|
-
|
|
5118
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
5119
|
+
return ok(queryTopSessions(db, n, agent, since));
|
|
4775
5120
|
}
|
|
4776
5121
|
if (path === "/api/models" && method === "GET") {
|
|
4777
5122
|
return ok(queryModelBreakdown(db));
|
|
@@ -5058,22 +5403,56 @@ function createHandler(db) {
|
|
|
5058
5403
|
return err("Not found", 404);
|
|
5059
5404
|
};
|
|
5060
5405
|
}
|
|
5406
|
+
function isLocalHost(host) {
|
|
5407
|
+
return ["127.0.0.1", "localhost", "::1"].includes(host);
|
|
5408
|
+
}
|
|
5061
5409
|
function startServer(port = 3456, options = {}) {
|
|
5062
|
-
const
|
|
5063
|
-
|
|
5064
|
-
const
|
|
5065
|
-
const
|
|
5410
|
+
const cloud = options.db ? false : isCloudMode();
|
|
5411
|
+
const hostname2 = options.hostname ?? (cloud ? process.env["ECONOMY_HOST"] ?? "0.0.0.0" : getServeBindHost());
|
|
5412
|
+
const log = options.log ?? console.log;
|
|
5413
|
+
const handlerOptions = {};
|
|
5414
|
+
let db;
|
|
5415
|
+
if (cloud) {
|
|
5416
|
+
db = openCloudDatabase();
|
|
5417
|
+
const pool = createCloudPool();
|
|
5418
|
+
const signingSecret = resolveSigningSecret();
|
|
5419
|
+
if (signingSecret) {
|
|
5420
|
+
const keys = new ApiKeyStore(authClientFromPool(pool));
|
|
5421
|
+
keys.ensureSchema().catch((e) => log(`api_keys ensureSchema: ${e instanceof Error ? e.message : String(e)}`));
|
|
5422
|
+
handlerOptions.authenticator = verifyApiKey({
|
|
5423
|
+
app: APP,
|
|
5424
|
+
signingSecret,
|
|
5425
|
+
isRevoked: keys.statusChecker(),
|
|
5426
|
+
audit: (event) => log(JSON.stringify({ evt: "api_auth", outcome: event.outcome, kid: event.kid, reason: event.reason, path: event.path, status: event.status }))
|
|
5427
|
+
});
|
|
5428
|
+
} else if (!isLocalHost(hostname2)) {
|
|
5429
|
+
throw new Error("economy-serve on a non-local host requires an API signing secret (HASNA_ECONOMY_API_SIGNING_KEY / API_KEY_SIGNING_SECRET)");
|
|
5430
|
+
}
|
|
5431
|
+
handlerOptions.readyCheck = async () => {
|
|
5432
|
+
try {
|
|
5433
|
+
const res = await pool.query("SELECT to_regclass($1) AS reg", ["public.requests"]);
|
|
5434
|
+
if (!res.rows[0]?.reg)
|
|
5435
|
+
return { ready: false, detail: "pending_migrations:requests" };
|
|
5436
|
+
return { ready: true };
|
|
5437
|
+
} catch (error) {
|
|
5438
|
+
return { ready: false, detail: error instanceof Error ? error.message : "storage_unreachable" };
|
|
5439
|
+
}
|
|
5440
|
+
};
|
|
5441
|
+
} else {
|
|
5442
|
+
db = options.db ?? openDatabase();
|
|
5443
|
+
ensurePricingSeeded(db);
|
|
5444
|
+
}
|
|
5445
|
+
const apiHandler = createHandler(db, handlerOptions);
|
|
5066
5446
|
const server = Bun.serve({
|
|
5067
5447
|
port,
|
|
5068
5448
|
hostname: hostname2,
|
|
5069
5449
|
fetch: createServerFetch(apiHandler, options.dashboardDir)
|
|
5070
5450
|
});
|
|
5071
5451
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
5072
|
-
const log = options.log ?? console.log;
|
|
5073
5452
|
log(`economy-serve listening on ${address}`);
|
|
5074
5453
|
return server;
|
|
5075
5454
|
}
|
|
5076
|
-
var CORS, AGENT_ERROR, SYNC_SOURCES, DEFAULT_DASHBOARD_DIR;
|
|
5455
|
+
var CORS, AGENT_ERROR, SYNC_SOURCES, DEFAULT_DASHBOARD_DIR, FOUNDATION_PATHS, APP = "economy";
|
|
5077
5456
|
var init_serve = __esm(() => {
|
|
5078
5457
|
init_database();
|
|
5079
5458
|
init_pricing();
|
|
@@ -5081,6 +5460,9 @@ var init_serve = __esm(() => {
|
|
|
5081
5460
|
init_sync_all();
|
|
5082
5461
|
init_billing_diff();
|
|
5083
5462
|
init_database();
|
|
5463
|
+
init_package_metadata();
|
|
5464
|
+
init_cloud();
|
|
5465
|
+
init_openapi();
|
|
5084
5466
|
CORS = {
|
|
5085
5467
|
"Access-Control-Allow-Origin": "*",
|
|
5086
5468
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
|
@@ -5089,6 +5471,7 @@ var init_serve = __esm(() => {
|
|
|
5089
5471
|
AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
5090
5472
|
SYNC_SOURCES = ["all", ...AGENTS];
|
|
5091
5473
|
DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
5474
|
+
FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
5092
5475
|
});
|
|
5093
5476
|
|
|
5094
5477
|
// src/cli/commands/menubar.ts
|
|
@@ -5101,14 +5484,14 @@ __export(exports_menubar, {
|
|
|
5101
5484
|
});
|
|
5102
5485
|
import chalk6 from "chalk";
|
|
5103
5486
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5104
|
-
import { cpSync, existsSync as
|
|
5487
|
+
import { cpSync, existsSync as existsSync18, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5105
5488
|
import { tmpdir, arch } from "os";
|
|
5106
|
-
import { join as
|
|
5489
|
+
import { join as join15 } from "path";
|
|
5107
5490
|
function getArch() {
|
|
5108
5491
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
5109
5492
|
}
|
|
5110
5493
|
function isInstalled() {
|
|
5111
|
-
return
|
|
5494
|
+
return existsSync18(APP_PATH);
|
|
5112
5495
|
}
|
|
5113
5496
|
function isRunning() {
|
|
5114
5497
|
try {
|
|
@@ -5145,15 +5528,15 @@ async function menubarInstall(opts) {
|
|
|
5145
5528
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
5146
5529
|
process.exit(1);
|
|
5147
5530
|
}
|
|
5148
|
-
const zipPath =
|
|
5149
|
-
const extractDir =
|
|
5531
|
+
const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
5532
|
+
const extractDir = join15(tmpdir(), "economy-bar-extracted");
|
|
5150
5533
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
5151
5534
|
try {
|
|
5152
5535
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
5153
5536
|
if (!res.ok)
|
|
5154
5537
|
throw new Error(`Download failed: ${res.status}`);
|
|
5155
5538
|
const buffer = await res.arrayBuffer();
|
|
5156
|
-
|
|
5539
|
+
writeFileSync4(zipPath, Buffer.from(buffer));
|
|
5157
5540
|
console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
5158
5541
|
} catch (e) {
|
|
5159
5542
|
console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5161,17 +5544,17 @@ async function menubarInstall(opts) {
|
|
|
5161
5544
|
}
|
|
5162
5545
|
console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
|
|
5163
5546
|
try {
|
|
5164
|
-
|
|
5165
|
-
|
|
5547
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
5548
|
+
mkdirSync5(extractDir, { recursive: true });
|
|
5166
5549
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
5167
5550
|
if (isInstalled())
|
|
5168
|
-
|
|
5169
|
-
cpSync(
|
|
5551
|
+
rmSync2(APP_PATH, { recursive: true, force: true });
|
|
5552
|
+
cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
5170
5553
|
try {
|
|
5171
5554
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
5172
5555
|
} catch {}
|
|
5173
|
-
|
|
5174
|
-
|
|
5556
|
+
rmSync2(zipPath, { force: true });
|
|
5557
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
5175
5558
|
console.log(chalk6.green(`\u2713 Installed to ${APP_PATH}`));
|
|
5176
5559
|
} catch (e) {
|
|
5177
5560
|
console.error(chalk6.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5198,7 +5581,7 @@ function menubarUninstall() {
|
|
|
5198
5581
|
execFileSync3("sleep", ["1"], { stdio: "ignore" });
|
|
5199
5582
|
} catch {}
|
|
5200
5583
|
}
|
|
5201
|
-
|
|
5584
|
+
rmSync2(APP_PATH, { recursive: true, force: true });
|
|
5202
5585
|
console.log(chalk6.green("\u2713 Economy Bar uninstalled"));
|
|
5203
5586
|
}
|
|
5204
5587
|
function menubarStart() {
|
|
@@ -6564,6 +6947,15 @@ init_paths();
|
|
|
6564
6947
|
function fmt2(usd) {
|
|
6565
6948
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
6566
6949
|
}
|
|
6950
|
+
function fmtTokens(n) {
|
|
6951
|
+
if (n >= 1e9)
|
|
6952
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
6953
|
+
if (n >= 1e6)
|
|
6954
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
6955
|
+
if (n >= 1000)
|
|
6956
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
6957
|
+
return n.toLocaleString("en-US");
|
|
6958
|
+
}
|
|
6567
6959
|
function parsePeriod(value, fallback = "month") {
|
|
6568
6960
|
const p = value ?? fallback;
|
|
6569
6961
|
if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
|
|
@@ -6578,7 +6970,7 @@ function registerExtendedCommands(program) {
|
|
|
6578
6970
|
const period = parsePeriod(periodArg, "month");
|
|
6579
6971
|
const agent = parseAgent(opts.agent, "--agent");
|
|
6580
6972
|
const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
6581
|
-
const summary = querySummary(db, period, undefined, true);
|
|
6973
|
+
const summary = querySummary(db, period, undefined, true, agent);
|
|
6582
6974
|
if (opts.json) {
|
|
6583
6975
|
console.log(JSON.stringify({ period, agent: agent ?? "all", snapshots: snaps, summary }, null, 2));
|
|
6584
6976
|
return;
|
|
@@ -6593,8 +6985,9 @@ function registerExtendedCommands(program) {
|
|
|
6593
6985
|
console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
|
|
6594
6986
|
}
|
|
6595
6987
|
}
|
|
6988
|
+
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
6596
6989
|
console.log(`
|
|
6597
|
-
${chalk4.dim(
|
|
6990
|
+
${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
|
|
6598
6991
|
console.log();
|
|
6599
6992
|
});
|
|
6600
6993
|
program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", "Filter by agent").option("--json", "Output JSON").action((periodArg, opts) => {
|
|
@@ -6676,8 +7069,19 @@ function registerExtendedCommands(program) {
|
|
|
6676
7069
|
}
|
|
6677
7070
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
6678
7071
|
checks.push({ ok: Boolean(getCloudDatabaseUrl()), msg: `cloud: ${getCloudDatabaseUrl() ? "ECONOMY_CLOUD_DATABASE_URL set" : "not configured"}` });
|
|
6679
|
-
const
|
|
6680
|
-
|
|
7072
|
+
const zeroCostBuckets = queryZeroCostTokenizedModels(db, 5);
|
|
7073
|
+
const zeroCost = db.prepare(`
|
|
7074
|
+
SELECT COUNT(*) as c
|
|
7075
|
+
FROM requests
|
|
7076
|
+
WHERE cost_usd = 0
|
|
7077
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
|
|
7078
|
+
`).get();
|
|
7079
|
+
const zeroCostSuffix = zeroCostBuckets.length > 0 ? `; top buckets: ${zeroCostBuckets.map((row) => {
|
|
7080
|
+
const pricing = getPricingFromDb(db, row.model);
|
|
7081
|
+
const status = pricing ? "pricing configured" : "missing pricing";
|
|
7082
|
+
return `${row.agent}/${row.model} ${row.requests} req ${fmtTokens(row.total_tokens)} tok (${status})`;
|
|
7083
|
+
}).join("; ")}` : "";
|
|
7084
|
+
checks.push({ ok: zeroCost.c === 0, msg: `zero-cost requests with tokens: ${zeroCost.c}${zeroCostSuffix}` });
|
|
6681
7085
|
const estimated = querySummary(db, "month", undefined, true);
|
|
6682
7086
|
const actual = queryBillingSummary(db, "month");
|
|
6683
7087
|
const drift = actual.total_usd > 0 ? Math.abs(estimated.total_usd - actual.total_usd) / actual.total_usd : 0;
|
|
@@ -7824,6 +8228,187 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
|
7824
8228
|
};
|
|
7825
8229
|
}
|
|
7826
8230
|
|
|
8231
|
+
// src/lib/fleet-sync.ts
|
|
8232
|
+
init_database();
|
|
8233
|
+
import { spawnSync } from "child_process";
|
|
8234
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
8235
|
+
import { join as join12, resolve } from "path";
|
|
8236
|
+
import { homedir as homedir9 } from "os";
|
|
8237
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
8238
|
+
var DEFAULT_TIMEOUT_MS = 120000;
|
|
8239
|
+
var DEFAULT_MACHINES = ["spark02", "apple03"];
|
|
8240
|
+
function runEconomyFleetSync(options = {}) {
|
|
8241
|
+
const generatedAt = (options.now ?? new Date).toISOString();
|
|
8242
|
+
const timeoutMs = normalizePositiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
8243
|
+
const runner = options.runner ?? spawnCommand;
|
|
8244
|
+
const dryRun = Boolean(options.dryRun);
|
|
8245
|
+
const machines = ((options.machines?.length) ? options.machines : DEFAULT_MACHINES).filter(Boolean);
|
|
8246
|
+
const cacheDir = resolve(options.cacheDir ?? join12(homedir9(), ".hasna", "economy", "fleet-sync-cache"));
|
|
8247
|
+
const remoteSnapshotDir = options.remoteSnapshotDir ?? "$HOME/.hasna/economy/fleet-sync-snapshots";
|
|
8248
|
+
const localSync = options.localSync !== false;
|
|
8249
|
+
const remoteSync = options.remoteSync !== false;
|
|
8250
|
+
const local = {
|
|
8251
|
+
status: dryRun ? "dry_run" : localSync ? "skipped" : "skipped",
|
|
8252
|
+
db_path: getDbPath()
|
|
8253
|
+
};
|
|
8254
|
+
if (localSync) {
|
|
8255
|
+
if (dryRun) {
|
|
8256
|
+
local.status = "dry_run";
|
|
8257
|
+
} else {
|
|
8258
|
+
local.sync = runner("economy", ["sync"], { timeoutMs });
|
|
8259
|
+
local.status = local.sync.code === 0 ? "synced" : "failed";
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
const remote = machines.map((machine) => syncMachine(machine, {
|
|
8263
|
+
generatedAt,
|
|
8264
|
+
timeoutMs,
|
|
8265
|
+
runner,
|
|
8266
|
+
dryRun,
|
|
8267
|
+
remoteSync,
|
|
8268
|
+
cacheDir,
|
|
8269
|
+
remoteSnapshotDir
|
|
8270
|
+
}));
|
|
8271
|
+
return {
|
|
8272
|
+
schema: "open-economy.fleet-sync.v1",
|
|
8273
|
+
generated_at: generatedAt,
|
|
8274
|
+
dry_run: dryRun,
|
|
8275
|
+
local,
|
|
8276
|
+
remote,
|
|
8277
|
+
summary: {
|
|
8278
|
+
machines: remote.length,
|
|
8279
|
+
merged: remote.filter((row) => row.status === "merged").length,
|
|
8280
|
+
failed: remote.filter((row) => row.status === "failed").length,
|
|
8281
|
+
dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
|
|
8282
|
+
}
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
function syncMachine(machine, context) {
|
|
8286
|
+
const safeMachine = machine.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
8287
|
+
const stamp = context.generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
8288
|
+
const snapshotName = `${safeMachine}-${stamp}.economy.db`;
|
|
8289
|
+
const remoteSnapshotPath = `${context.remoteSnapshotDir.replace(/\/$/, "")}/${snapshotName}`;
|
|
8290
|
+
const tmpLocal = join12(context.cacheDir, `${snapshotName}.tmp`);
|
|
8291
|
+
const verifiedLocal = join12(context.cacheDir, snapshotName);
|
|
8292
|
+
if (context.dryRun) {
|
|
8293
|
+
return { machine, status: "dry_run", remote_snapshot_path: remoteSnapshotPath };
|
|
8294
|
+
}
|
|
8295
|
+
mkdirSync3(context.cacheDir, { recursive: true });
|
|
8296
|
+
rmSync(tmpLocal, { force: true });
|
|
8297
|
+
const result = {
|
|
8298
|
+
machine,
|
|
8299
|
+
status: "failed",
|
|
8300
|
+
remote_snapshot_path: remoteSnapshotPath
|
|
8301
|
+
};
|
|
8302
|
+
if (context.remoteSync) {
|
|
8303
|
+
result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
|
|
8304
|
+
if (result.remote_sync.code !== 0) {
|
|
8305
|
+
result.error = "remote economy sync failed";
|
|
8306
|
+
return result;
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
|
|
8310
|
+
if (result.remote_snapshot.code !== 0) {
|
|
8311
|
+
result.error = "remote snapshot failed";
|
|
8312
|
+
return result;
|
|
8313
|
+
}
|
|
8314
|
+
const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
|
|
8315
|
+
if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
|
|
8316
|
+
result.error = `remote snapshot path refused: ${emittedPath}`;
|
|
8317
|
+
return result;
|
|
8318
|
+
}
|
|
8319
|
+
result.remote_snapshot_path = emittedPath;
|
|
8320
|
+
result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
|
|
8321
|
+
context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
|
|
8322
|
+
if (result.pull.code !== 0) {
|
|
8323
|
+
result.error = "snapshot pull failed; no cached DB merged";
|
|
8324
|
+
rmSync(tmpLocal, { force: true });
|
|
8325
|
+
return result;
|
|
8326
|
+
}
|
|
8327
|
+
const integrity = verifySqliteIntegrity(tmpLocal);
|
|
8328
|
+
result.local_integrity = integrity;
|
|
8329
|
+
if (!integrity.ok) {
|
|
8330
|
+
result.error = "pulled snapshot failed local integrity check";
|
|
8331
|
+
rmSync(tmpLocal, { force: true });
|
|
8332
|
+
return result;
|
|
8333
|
+
}
|
|
8334
|
+
renameSync(tmpLocal, verifiedLocal);
|
|
8335
|
+
const db = openDatabase();
|
|
8336
|
+
result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
|
|
8337
|
+
result.status = "merged";
|
|
8338
|
+
return result;
|
|
8339
|
+
}
|
|
8340
|
+
function remoteSnapshotCommand(remoteDir, filename) {
|
|
8341
|
+
const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
|
|
8342
|
+
const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
8343
|
+
return [
|
|
8344
|
+
"set -euo pipefail",
|
|
8345
|
+
'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
|
|
8346
|
+
`snapshot_dir=${dirExpr}`,
|
|
8347
|
+
`out="$snapshot_dir/${safeFilename}"`,
|
|
8348
|
+
'test -f "$db"',
|
|
8349
|
+
'mkdir -p "$snapshot_dir"',
|
|
8350
|
+
'rm -f "$out"',
|
|
8351
|
+
`sqlite3 "$db" "VACUUM INTO '$out'"`,
|
|
8352
|
+
'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
|
|
8353
|
+
'printf "%s\\n" "$out"'
|
|
8354
|
+
].join("; ");
|
|
8355
|
+
}
|
|
8356
|
+
function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
|
|
8357
|
+
if (!path || path.includes("\x00") || path.includes(`
|
|
8358
|
+
`) || path.includes("\r"))
|
|
8359
|
+
return false;
|
|
8360
|
+
if (!path.endsWith(".db"))
|
|
8361
|
+
return false;
|
|
8362
|
+
if (path.includes(".."))
|
|
8363
|
+
return false;
|
|
8364
|
+
const prefix = remoteSnapshotDir.replace(/\/$/, "");
|
|
8365
|
+
return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
|
|
8366
|
+
}
|
|
8367
|
+
function verifySqliteIntegrity(path) {
|
|
8368
|
+
if (!existsSync13(path))
|
|
8369
|
+
return { ok: false, detail: "file does not exist" };
|
|
8370
|
+
let db;
|
|
8371
|
+
try {
|
|
8372
|
+
db = new Database2(path, { readonly: true });
|
|
8373
|
+
const row = db.query("PRAGMA integrity_check").get();
|
|
8374
|
+
const detail = Object.values(row ?? {})[0] ?? "";
|
|
8375
|
+
return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
|
|
8376
|
+
} catch (error) {
|
|
8377
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
8378
|
+
} finally {
|
|
8379
|
+
db?.close();
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
function writeFleetSyncReport(reportDir, result) {
|
|
8383
|
+
mkdirSync3(reportDir, { recursive: true });
|
|
8384
|
+
const path = join12(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
|
|
8385
|
+
writeFileSync2(path, `${JSON.stringify(result, null, 2)}
|
|
8386
|
+
`);
|
|
8387
|
+
return path;
|
|
8388
|
+
}
|
|
8389
|
+
function spawnCommand(command, args, options) {
|
|
8390
|
+
const result = spawnSync(command, args, {
|
|
8391
|
+
encoding: "utf8",
|
|
8392
|
+
timeout: options.timeoutMs,
|
|
8393
|
+
maxBuffer: 1024 * 1024,
|
|
8394
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8395
|
+
});
|
|
8396
|
+
return {
|
|
8397
|
+
code: result.status,
|
|
8398
|
+
stdout: result.stdout ?? "",
|
|
8399
|
+
stderr: result.stderr ?? "",
|
|
8400
|
+
...result.error ? { error: result.error.message } : {}
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8403
|
+
function shellQuote(value) {
|
|
8404
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8405
|
+
}
|
|
8406
|
+
function normalizePositiveInteger(value, fallback) {
|
|
8407
|
+
if (value == null || !Number.isFinite(value) || value < 1)
|
|
8408
|
+
return fallback;
|
|
8409
|
+
return Math.floor(value);
|
|
8410
|
+
}
|
|
8411
|
+
|
|
7827
8412
|
// src/cli/index.ts
|
|
7828
8413
|
init_database();
|
|
7829
8414
|
init_database();
|
|
@@ -7863,7 +8448,7 @@ function fmt4(usd) {
|
|
|
7863
8448
|
}
|
|
7864
8449
|
return chalk7.green(formatted);
|
|
7865
8450
|
}
|
|
7866
|
-
function
|
|
8451
|
+
function fmtTokens2(n) {
|
|
7867
8452
|
if (n >= 1e9)
|
|
7868
8453
|
return `${(n / 1e9).toFixed(1)}B`;
|
|
7869
8454
|
if (n >= 1e6)
|
|
@@ -7971,7 +8556,7 @@ function printAccountBreakdown(rows) {
|
|
|
7971
8556
|
chalk7.dim(r.account_source || "unknown"),
|
|
7972
8557
|
String(r.sessions),
|
|
7973
8558
|
String(r.requests),
|
|
7974
|
-
chalk7.cyan(
|
|
8559
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
7975
8560
|
fmt4(r.api_equivalent_usd),
|
|
7976
8561
|
fmt4(r.billable_usd),
|
|
7977
8562
|
fmt4(r.subscription_included_usd)
|
|
@@ -7998,7 +8583,7 @@ function printSummary(label, period) {
|
|
|
7998
8583
|
["Total cost", fmt4(s.total_usd)],
|
|
7999
8584
|
["Sessions", chalk7.yellow(fmtCount(s.sessions))],
|
|
8000
8585
|
["Requests", chalk7.yellow(fmtCount(s.requests))],
|
|
8001
|
-
["Tokens", chalk7.yellow(
|
|
8586
|
+
["Tokens", chalk7.yellow(fmtTokens2(s.tokens))]
|
|
8002
8587
|
]);
|
|
8003
8588
|
console.log();
|
|
8004
8589
|
}
|
|
@@ -8018,9 +8603,9 @@ program.action(async () => {
|
|
|
8018
8603
|
console.log(chalk7.bold.cyan(" Economy"));
|
|
8019
8604
|
console.log();
|
|
8020
8605
|
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
8021
|
-
["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests),
|
|
8022
|
-
["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests),
|
|
8023
|
-
["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests),
|
|
8606
|
+
["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
|
|
8607
|
+
["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens2(w.tokens)],
|
|
8608
|
+
["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens2(m.tokens)]
|
|
8024
8609
|
]);
|
|
8025
8610
|
if (dailyValues.length > 0) {
|
|
8026
8611
|
console.log(`
|
|
@@ -8076,7 +8661,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
8076
8661
|
console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
8077
8662
|
}
|
|
8078
8663
|
if (opts.recalculate) {
|
|
8079
|
-
const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
8664
|
+
const { computeCostFromDb: computeCostFromDb2, getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
8080
8665
|
const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens FROM requests WHERE cost_usd = 0 AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
|
|
8081
8666
|
let fixed = 0;
|
|
8082
8667
|
for (const r of zeroRows) {
|
|
@@ -8098,6 +8683,16 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
8098
8683
|
}
|
|
8099
8684
|
}
|
|
8100
8685
|
console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
|
|
8686
|
+
const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
|
|
8687
|
+
if (zeroCostBuckets.length > 0) {
|
|
8688
|
+
console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
|
|
8689
|
+
for (const row of zeroCostBuckets) {
|
|
8690
|
+
const pricing = getPricingFromDb2(db, row.model);
|
|
8691
|
+
const status = pricing ? pricing.inputPer1M === 0 && pricing.outputPer1M === 0 && pricing.cacheReadPer1M === 0 && pricing.cacheWritePer1M === 0 ? "explicit zero/free pricing" : "pricing configured; inspect/recalculate" : "missing pricing";
|
|
8692
|
+
console.log(chalk7.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
|
|
8693
|
+
}
|
|
8694
|
+
console.log(chalk7.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
|
|
8695
|
+
}
|
|
8101
8696
|
}
|
|
8102
8697
|
try {
|
|
8103
8698
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
@@ -8140,7 +8735,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
8140
8735
|
const f = opts.format ?? "table";
|
|
8141
8736
|
if (f === "compact") {
|
|
8142
8737
|
for (const s of sessions)
|
|
8143
|
-
process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${
|
|
8738
|
+
process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${fmtTokens2(s.total_tokens)} ${s.project_name || "\u2014"}
|
|
8144
8739
|
`);
|
|
8145
8740
|
return;
|
|
8146
8741
|
}
|
|
@@ -8160,7 +8755,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
8160
8755
|
fmtAgent(s.agent),
|
|
8161
8756
|
chalk7.white(s.project_name || chalk7.dim("unknown")),
|
|
8162
8757
|
fmt4(s.total_cost_usd),
|
|
8163
|
-
chalk7.cyan(
|
|
8758
|
+
chalk7.cyan(fmtTokens2(s.total_tokens)),
|
|
8164
8759
|
fmtCount(s.request_count),
|
|
8165
8760
|
chalk7.dim(s.started_at.substring(0, 16))
|
|
8166
8761
|
]));
|
|
@@ -8171,9 +8766,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
8171
8766
|
const agent = parseOptionalCliAgent(opts.agent);
|
|
8172
8767
|
const db = openDatabase();
|
|
8173
8768
|
const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
8174
|
-
|
|
8175
|
-
if (sinceDate)
|
|
8176
|
-
sessions = sessions.filter((s) => s.started_at >= sinceDate);
|
|
8769
|
+
const sessions = queryTopSessions(db, count, agent, sinceDate);
|
|
8177
8770
|
if (sessions.length === 0) {
|
|
8178
8771
|
console.log(chalk7.yellow("No sessions found. Run `economy sync` first."));
|
|
8179
8772
|
return;
|
|
@@ -8184,7 +8777,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
8184
8777
|
chalk7.white(s.project_name || chalk7.dim("unknown")),
|
|
8185
8778
|
fmtAgent(s.agent),
|
|
8186
8779
|
fmt4(s.total_cost_usd),
|
|
8187
|
-
chalk7.cyan(
|
|
8780
|
+
chalk7.cyan(fmtTokens2(s.total_tokens)),
|
|
8188
8781
|
chalk7.dim(s.started_at.substring(0, 16))
|
|
8189
8782
|
]));
|
|
8190
8783
|
console.log();
|
|
@@ -8208,7 +8801,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8208
8801
|
chalk7.white(r.project_name || chalk7.dim("unknown")),
|
|
8209
8802
|
String(r.sessions),
|
|
8210
8803
|
String(r.requests),
|
|
8211
|
-
chalk7.cyan(
|
|
8804
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8212
8805
|
fmt4(r.cost_usd)
|
|
8213
8806
|
]));
|
|
8214
8807
|
} else if (opts.by === "agent") {
|
|
@@ -8230,7 +8823,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8230
8823
|
fmtAgent(r.agent),
|
|
8231
8824
|
String(r.sessions),
|
|
8232
8825
|
String(r.requests),
|
|
8233
|
-
chalk7.cyan(
|
|
8826
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8234
8827
|
fmt4(r.api_equivalent_usd),
|
|
8235
8828
|
fmt4(r.billable_usd),
|
|
8236
8829
|
fmt4(r.subscription_included_usd)
|
|
@@ -8352,7 +8945,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8352
8945
|
chalk7.white(r.model),
|
|
8353
8946
|
fmtAgent(r.agent),
|
|
8354
8947
|
String(r.requests),
|
|
8355
|
-
chalk7.cyan(
|
|
8948
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8356
8949
|
fmt4(r.cost_usd)
|
|
8357
8950
|
]));
|
|
8358
8951
|
}
|
|
@@ -8514,7 +9107,7 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
8514
9107
|
printTable(["Metric", "Value"], [
|
|
8515
9108
|
["Total cost", fmt4(totalCost)],
|
|
8516
9109
|
["Sessions", fmtCount(sessions.length)],
|
|
8517
|
-
["Total tokens",
|
|
9110
|
+
["Total tokens", fmtTokens2(totalTokens)]
|
|
8518
9111
|
]);
|
|
8519
9112
|
if (dailyValues.length > 0) {
|
|
8520
9113
|
console.log(`
|
|
@@ -8647,8 +9240,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
8647
9240
|
if (!serverRunning) {
|
|
8648
9241
|
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
8649
9242
|
const { spawn } = await import("child_process");
|
|
8650
|
-
const { resolve:
|
|
8651
|
-
const serveScript =
|
|
9243
|
+
const { resolve: resolve3, dirname: dirname4 } = await import("path");
|
|
9244
|
+
const serveScript = resolve3(dirname4(process.argv[1]), "..", "server", "index.js");
|
|
8652
9245
|
const child = spawn(process.execPath, [serveScript], {
|
|
8653
9246
|
detached: true,
|
|
8654
9247
|
stdio: "ignore",
|
|
@@ -8719,7 +9312,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
8719
9312
|
["Started", String(session["started_at"]).substring(0, 19)],
|
|
8720
9313
|
["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
|
|
8721
9314
|
["Total cost", fmt4(session["total_cost_usd"])],
|
|
8722
|
-
["Total tokens",
|
|
9315
|
+
["Total tokens", fmtTokens2(session["total_tokens"])],
|
|
8723
9316
|
["Requests", fmtCount(session["request_count"])]
|
|
8724
9317
|
]);
|
|
8725
9318
|
if (requests.length > 0) {
|
|
@@ -8729,10 +9322,10 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
8729
9322
|
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
8730
9323
|
chalk7.dim(String(r["timestamp"]).substring(11, 19)),
|
|
8731
9324
|
chalk7.white(String(r["model"]).substring(0, 22)),
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
9325
|
+
fmtTokens2(r["input_tokens"]),
|
|
9326
|
+
fmtTokens2(r["output_tokens"]),
|
|
9327
|
+
fmtTokens2(r["cache_read_tokens"]),
|
|
9328
|
+
fmtTokens2(r["cache_create_tokens"]),
|
|
8736
9329
|
fmt4(r["cost_usd"])
|
|
8737
9330
|
]));
|
|
8738
9331
|
if (requests.length > 50)
|
|
@@ -8763,6 +9356,31 @@ program.command("machines").description("List all machines that have synced data
|
|
|
8763
9356
|
${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
|
|
8764
9357
|
console.log();
|
|
8765
9358
|
});
|
|
9359
|
+
program.command("fleet-sync").description("Loop-safe cross-machine sync using remote SQLite snapshots").option("--machine <ids>", "Comma-separated remote machines", "spark02,apple03").option("--no-local-sync", "Skip local economy sync before remote merge").option("--no-remote-sync", "Skip remote economy sync before snapshot").option("--cache-dir <path>", "Directory for verified pulled DB snapshots").option("--report-dir <path>", "Directory for JSON evidence reports").option("--remote-snapshot-dir <path>", "Remote snapshot directory", "$HOME/.hasna/economy/fleet-sync-snapshots").option("--timeout-ms <n>", "Per remote command timeout", "120000").option("--dry-run", "Plan without syncing, pulling, or merging").option("--json", "Output JSON").action((opts) => {
|
|
9360
|
+
const result = runEconomyFleetSync({
|
|
9361
|
+
machines: opts.machine?.split(",").map((entry) => entry.trim()).filter(Boolean),
|
|
9362
|
+
localSync: opts.localSync !== false,
|
|
9363
|
+
remoteSync: opts.remoteSync !== false,
|
|
9364
|
+
cacheDir: opts.cacheDir,
|
|
9365
|
+
remoteSnapshotDir: opts.remoteSnapshotDir,
|
|
9366
|
+
timeoutMs: parsePositiveCliInteger(opts.timeoutMs, "--timeout-ms"),
|
|
9367
|
+
dryRun: opts.dryRun
|
|
9368
|
+
});
|
|
9369
|
+
const reportPath = opts.reportDir ? writeFleetSyncReport(opts.reportDir, result) : undefined;
|
|
9370
|
+
if (opts.json) {
|
|
9371
|
+
console.log(JSON.stringify({ ...result, ...reportPath ? { report_path: reportPath } : {} }, null, 2));
|
|
9372
|
+
return;
|
|
9373
|
+
}
|
|
9374
|
+
const status = result.summary.failed === 0 ? chalk7.green("ok") : chalk7.red("failed");
|
|
9375
|
+
console.log(`${status} machines=${result.summary.machines} merged=${result.summary.merged} failed=${result.summary.failed} dry_run=${result.summary.dry_run}`);
|
|
9376
|
+
if (reportPath)
|
|
9377
|
+
console.log(chalk7.dim(`report=${reportPath}`));
|
|
9378
|
+
for (const row of result.remote.filter((entry) => entry.status !== "merged").slice(0, 20)) {
|
|
9379
|
+
console.log(`${row.status === "failed" ? chalk7.red("failed") : chalk7.yellow(row.status)} ${row.machine} ${chalk7.dim(row.error ?? "")}`);
|
|
9380
|
+
}
|
|
9381
|
+
if (result.local.status === "failed" || result.summary.failed > 0)
|
|
9382
|
+
process.exitCode = 1;
|
|
9383
|
+
});
|
|
8766
9384
|
program.command("merge-db <source-db>").description("Merge another Economy SQLite database into this machine").option("--source-machine <id>", "Machine id to use for source rows that do not have one").option("--json", "Output JSON").action((sourceDb, opts) => {
|
|
8767
9385
|
const db = openDatabase();
|
|
8768
9386
|
const result = mergePeerDatabase(db, sourceDb, { sourceMachine: opts.sourceMachine });
|
|
@@ -8802,8 +9420,8 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
8802
9420
|
}
|
|
8803
9421
|
}
|
|
8804
9422
|
if (opts.output) {
|
|
8805
|
-
const { writeFileSync:
|
|
8806
|
-
|
|
9423
|
+
const { writeFileSync: writeFileSync5 } = await import("fs");
|
|
9424
|
+
writeFileSync5(opts.output, csv);
|
|
8807
9425
|
console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
|
|
8808
9426
|
} else {
|
|
8809
9427
|
process.stdout.write(csv);
|
|
@@ -8875,7 +9493,7 @@ program.command("compare <period1> <period2>").description("Compare two periods
|
|
|
8875
9493
|
["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
|
|
8876
9494
|
["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
|
|
8877
9495
|
["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
|
|
8878
|
-
["Tokens",
|
|
9496
|
+
["Tokens", fmtTokens2(a.tokens), fmtTokens2(b.tokens), delta(a.tokens, b.tokens)]
|
|
8879
9497
|
]);
|
|
8880
9498
|
console.log();
|
|
8881
9499
|
});
|