@hasna/economy 0.2.40 → 0.2.42
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 +30 -0
- package/CODE_OF_CONDUCT.md +23 -0
- package/CONTRIBUTING.md +39 -0
- package/README.md +4 -0
- package/SECURITY.md +31 -0
- package/dashboard/dist/assets/charts-BtCbslx4.js +36 -0
- package/dashboard/dist/assets/index-BDmtt3RI.css +1 -0
- package/dashboard/dist/assets/index-BFwRLqg7.js +6 -0
- package/dashboard/dist/assets/vendor-Cz8jb743.js +53 -0
- package/dashboard/dist/index.html +16 -0
- package/dashboard/dist/logo.jpg +0 -0
- package/dashboard/dist/vite.svg +1 -0
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +379 -77
- package/dist/db/database.d.ts +4 -3
- package/dist/db/database.d.ts.map +1 -1
- 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/otel/index.js +41 -5
- package/dist/server/index.js +68 -17
- 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 +15 -2
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();
|
|
@@ -4615,8 +4681,8 @@ __export(exports_serve, {
|
|
|
4615
4681
|
createHandler: () => createHandler
|
|
4616
4682
|
});
|
|
4617
4683
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4618
|
-
import { existsSync as
|
|
4619
|
-
import { resolve, sep } from "path";
|
|
4684
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4685
|
+
import { resolve as resolve2, sep } from "path";
|
|
4620
4686
|
function json(data, status = 200) {
|
|
4621
4687
|
return new Response(JSON.stringify(data), {
|
|
4622
4688
|
status,
|
|
@@ -4670,8 +4736,8 @@ function dashboardPath(root, pathname) {
|
|
|
4670
4736
|
return null;
|
|
4671
4737
|
}
|
|
4672
4738
|
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
4673
|
-
const rootPath =
|
|
4674
|
-
const filePath =
|
|
4739
|
+
const rootPath = resolve2(root);
|
|
4740
|
+
const filePath = resolve2(rootPath, relativePath);
|
|
4675
4741
|
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
4676
4742
|
}
|
|
4677
4743
|
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
@@ -4680,13 +4746,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
4680
4746
|
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
4681
4747
|
return apiHandler(req);
|
|
4682
4748
|
}
|
|
4683
|
-
if (
|
|
4749
|
+
if (existsSync16(dashboardDir)) {
|
|
4684
4750
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
4685
|
-
if (filePath &&
|
|
4751
|
+
if (filePath && existsSync16(filePath)) {
|
|
4686
4752
|
return new Response(Bun.file(filePath));
|
|
4687
4753
|
}
|
|
4688
4754
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
4689
|
-
if (indexPath &&
|
|
4755
|
+
if (indexPath && existsSync16(indexPath)) {
|
|
4690
4756
|
return new Response(Bun.file(indexPath));
|
|
4691
4757
|
}
|
|
4692
4758
|
}
|
|
@@ -4771,7 +4837,8 @@ function createHandler(db) {
|
|
|
4771
4837
|
if (path === "/api/top" && method === "GET") {
|
|
4772
4838
|
const n = Number(url.searchParams.get("n") ?? 10);
|
|
4773
4839
|
const agent = url.searchParams.get("agent") ?? undefined;
|
|
4774
|
-
|
|
4840
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
4841
|
+
return ok(queryTopSessions(db, n, agent, since));
|
|
4775
4842
|
}
|
|
4776
4843
|
if (path === "/api/models" && method === "GET") {
|
|
4777
4844
|
return ok(queryModelBreakdown(db));
|
|
@@ -5101,14 +5168,14 @@ __export(exports_menubar, {
|
|
|
5101
5168
|
});
|
|
5102
5169
|
import chalk6 from "chalk";
|
|
5103
5170
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5104
|
-
import { cpSync, existsSync as
|
|
5171
|
+
import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
5105
5172
|
import { tmpdir, arch } from "os";
|
|
5106
|
-
import { join as
|
|
5173
|
+
import { join as join14 } from "path";
|
|
5107
5174
|
function getArch() {
|
|
5108
5175
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
5109
5176
|
}
|
|
5110
5177
|
function isInstalled() {
|
|
5111
|
-
return
|
|
5178
|
+
return existsSync17(APP_PATH);
|
|
5112
5179
|
}
|
|
5113
5180
|
function isRunning() {
|
|
5114
5181
|
try {
|
|
@@ -5145,15 +5212,15 @@ async function menubarInstall(opts) {
|
|
|
5145
5212
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
5146
5213
|
process.exit(1);
|
|
5147
5214
|
}
|
|
5148
|
-
const zipPath =
|
|
5149
|
-
const extractDir =
|
|
5215
|
+
const zipPath = join14(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
5216
|
+
const extractDir = join14(tmpdir(), "economy-bar-extracted");
|
|
5150
5217
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
5151
5218
|
try {
|
|
5152
5219
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
5153
5220
|
if (!res.ok)
|
|
5154
5221
|
throw new Error(`Download failed: ${res.status}`);
|
|
5155
5222
|
const buffer = await res.arrayBuffer();
|
|
5156
|
-
|
|
5223
|
+
writeFileSync4(zipPath, Buffer.from(buffer));
|
|
5157
5224
|
console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
5158
5225
|
} catch (e) {
|
|
5159
5226
|
console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5161,17 +5228,17 @@ async function menubarInstall(opts) {
|
|
|
5161
5228
|
}
|
|
5162
5229
|
console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
|
|
5163
5230
|
try {
|
|
5164
|
-
|
|
5165
|
-
|
|
5231
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
5232
|
+
mkdirSync5(extractDir, { recursive: true });
|
|
5166
5233
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
5167
5234
|
if (isInstalled())
|
|
5168
|
-
|
|
5169
|
-
cpSync(
|
|
5235
|
+
rmSync2(APP_PATH, { recursive: true, force: true });
|
|
5236
|
+
cpSync(join14(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
5170
5237
|
try {
|
|
5171
5238
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
5172
5239
|
} catch {}
|
|
5173
|
-
|
|
5174
|
-
|
|
5240
|
+
rmSync2(zipPath, { force: true });
|
|
5241
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
5175
5242
|
console.log(chalk6.green(`\u2713 Installed to ${APP_PATH}`));
|
|
5176
5243
|
} catch (e) {
|
|
5177
5244
|
console.error(chalk6.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5198,7 +5265,7 @@ function menubarUninstall() {
|
|
|
5198
5265
|
execFileSync3("sleep", ["1"], { stdio: "ignore" });
|
|
5199
5266
|
} catch {}
|
|
5200
5267
|
}
|
|
5201
|
-
|
|
5268
|
+
rmSync2(APP_PATH, { recursive: true, force: true });
|
|
5202
5269
|
console.log(chalk6.green("\u2713 Economy Bar uninstalled"));
|
|
5203
5270
|
}
|
|
5204
5271
|
function menubarStart() {
|
|
@@ -6564,6 +6631,15 @@ init_paths();
|
|
|
6564
6631
|
function fmt2(usd) {
|
|
6565
6632
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
6566
6633
|
}
|
|
6634
|
+
function fmtTokens(n) {
|
|
6635
|
+
if (n >= 1e9)
|
|
6636
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
6637
|
+
if (n >= 1e6)
|
|
6638
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
6639
|
+
if (n >= 1000)
|
|
6640
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
6641
|
+
return n.toLocaleString("en-US");
|
|
6642
|
+
}
|
|
6567
6643
|
function parsePeriod(value, fallback = "month") {
|
|
6568
6644
|
const p = value ?? fallback;
|
|
6569
6645
|
if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
|
|
@@ -6578,7 +6654,7 @@ function registerExtendedCommands(program) {
|
|
|
6578
6654
|
const period = parsePeriod(periodArg, "month");
|
|
6579
6655
|
const agent = parseAgent(opts.agent, "--agent");
|
|
6580
6656
|
const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(period) });
|
|
6581
|
-
const summary = querySummary(db, period, undefined, true);
|
|
6657
|
+
const summary = querySummary(db, period, undefined, true, agent);
|
|
6582
6658
|
if (opts.json) {
|
|
6583
6659
|
console.log(JSON.stringify({ period, agent: agent ?? "all", snapshots: snaps, summary }, null, 2));
|
|
6584
6660
|
return;
|
|
@@ -6593,8 +6669,9 @@ function registerExtendedCommands(program) {
|
|
|
6593
6669
|
console.log(` ${chalk4.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk4.dim(s.date)}`);
|
|
6594
6670
|
}
|
|
6595
6671
|
}
|
|
6672
|
+
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
6596
6673
|
console.log(`
|
|
6597
|
-
${chalk4.dim(
|
|
6674
|
+
${chalk4.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
|
|
6598
6675
|
console.log();
|
|
6599
6676
|
});
|
|
6600
6677
|
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 +6753,19 @@ function registerExtendedCommands(program) {
|
|
|
6676
6753
|
}
|
|
6677
6754
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
6678
6755
|
checks.push({ ok: Boolean(getCloudDatabaseUrl()), msg: `cloud: ${getCloudDatabaseUrl() ? "ECONOMY_CLOUD_DATABASE_URL set" : "not configured"}` });
|
|
6679
|
-
const
|
|
6680
|
-
|
|
6756
|
+
const zeroCostBuckets = queryZeroCostTokenizedModels(db, 5);
|
|
6757
|
+
const zeroCost = db.prepare(`
|
|
6758
|
+
SELECT COUNT(*) as c
|
|
6759
|
+
FROM requests
|
|
6760
|
+
WHERE cost_usd = 0
|
|
6761
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)
|
|
6762
|
+
`).get();
|
|
6763
|
+
const zeroCostSuffix = zeroCostBuckets.length > 0 ? `; top buckets: ${zeroCostBuckets.map((row) => {
|
|
6764
|
+
const pricing = getPricingFromDb(db, row.model);
|
|
6765
|
+
const status = pricing ? "pricing configured" : "missing pricing";
|
|
6766
|
+
return `${row.agent}/${row.model} ${row.requests} req ${fmtTokens(row.total_tokens)} tok (${status})`;
|
|
6767
|
+
}).join("; ")}` : "";
|
|
6768
|
+
checks.push({ ok: zeroCost.c === 0, msg: `zero-cost requests with tokens: ${zeroCost.c}${zeroCostSuffix}` });
|
|
6681
6769
|
const estimated = querySummary(db, "month", undefined, true);
|
|
6682
6770
|
const actual = queryBillingSummary(db, "month");
|
|
6683
6771
|
const drift = actual.total_usd > 0 ? Math.abs(estimated.total_usd - actual.total_usd) / actual.total_usd : 0;
|
|
@@ -7824,6 +7912,187 @@ function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
|
7824
7912
|
};
|
|
7825
7913
|
}
|
|
7826
7914
|
|
|
7915
|
+
// src/lib/fleet-sync.ts
|
|
7916
|
+
init_database();
|
|
7917
|
+
import { spawnSync } from "child_process";
|
|
7918
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
7919
|
+
import { join as join12, resolve } from "path";
|
|
7920
|
+
import { homedir as homedir9 } from "os";
|
|
7921
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
7922
|
+
var DEFAULT_TIMEOUT_MS = 120000;
|
|
7923
|
+
var DEFAULT_MACHINES = ["spark02", "apple03"];
|
|
7924
|
+
function runEconomyFleetSync(options = {}) {
|
|
7925
|
+
const generatedAt = (options.now ?? new Date).toISOString();
|
|
7926
|
+
const timeoutMs = normalizePositiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
7927
|
+
const runner = options.runner ?? spawnCommand;
|
|
7928
|
+
const dryRun = Boolean(options.dryRun);
|
|
7929
|
+
const machines = ((options.machines?.length) ? options.machines : DEFAULT_MACHINES).filter(Boolean);
|
|
7930
|
+
const cacheDir = resolve(options.cacheDir ?? join12(homedir9(), ".hasna", "economy", "fleet-sync-cache"));
|
|
7931
|
+
const remoteSnapshotDir = options.remoteSnapshotDir ?? "$HOME/.hasna/economy/fleet-sync-snapshots";
|
|
7932
|
+
const localSync = options.localSync !== false;
|
|
7933
|
+
const remoteSync = options.remoteSync !== false;
|
|
7934
|
+
const local = {
|
|
7935
|
+
status: dryRun ? "dry_run" : localSync ? "skipped" : "skipped",
|
|
7936
|
+
db_path: getDbPath()
|
|
7937
|
+
};
|
|
7938
|
+
if (localSync) {
|
|
7939
|
+
if (dryRun) {
|
|
7940
|
+
local.status = "dry_run";
|
|
7941
|
+
} else {
|
|
7942
|
+
local.sync = runner("economy", ["sync"], { timeoutMs });
|
|
7943
|
+
local.status = local.sync.code === 0 ? "synced" : "failed";
|
|
7944
|
+
}
|
|
7945
|
+
}
|
|
7946
|
+
const remote = machines.map((machine) => syncMachine(machine, {
|
|
7947
|
+
generatedAt,
|
|
7948
|
+
timeoutMs,
|
|
7949
|
+
runner,
|
|
7950
|
+
dryRun,
|
|
7951
|
+
remoteSync,
|
|
7952
|
+
cacheDir,
|
|
7953
|
+
remoteSnapshotDir
|
|
7954
|
+
}));
|
|
7955
|
+
return {
|
|
7956
|
+
schema: "open-economy.fleet-sync.v1",
|
|
7957
|
+
generated_at: generatedAt,
|
|
7958
|
+
dry_run: dryRun,
|
|
7959
|
+
local,
|
|
7960
|
+
remote,
|
|
7961
|
+
summary: {
|
|
7962
|
+
machines: remote.length,
|
|
7963
|
+
merged: remote.filter((row) => row.status === "merged").length,
|
|
7964
|
+
failed: remote.filter((row) => row.status === "failed").length,
|
|
7965
|
+
dry_run: remote.filter((row) => row.status === "dry_run").length + (dryRun ? 1 : 0)
|
|
7966
|
+
}
|
|
7967
|
+
};
|
|
7968
|
+
}
|
|
7969
|
+
function syncMachine(machine, context) {
|
|
7970
|
+
const safeMachine = machine.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
7971
|
+
const stamp = context.generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
7972
|
+
const snapshotName = `${safeMachine}-${stamp}.economy.db`;
|
|
7973
|
+
const remoteSnapshotPath = `${context.remoteSnapshotDir.replace(/\/$/, "")}/${snapshotName}`;
|
|
7974
|
+
const tmpLocal = join12(context.cacheDir, `${snapshotName}.tmp`);
|
|
7975
|
+
const verifiedLocal = join12(context.cacheDir, snapshotName);
|
|
7976
|
+
if (context.dryRun) {
|
|
7977
|
+
return { machine, status: "dry_run", remote_snapshot_path: remoteSnapshotPath };
|
|
7978
|
+
}
|
|
7979
|
+
mkdirSync3(context.cacheDir, { recursive: true });
|
|
7980
|
+
rmSync(tmpLocal, { force: true });
|
|
7981
|
+
const result = {
|
|
7982
|
+
machine,
|
|
7983
|
+
status: "failed",
|
|
7984
|
+
remote_snapshot_path: remoteSnapshotPath
|
|
7985
|
+
};
|
|
7986
|
+
if (context.remoteSync) {
|
|
7987
|
+
result.remote_sync = context.runner("ssh", [machine, "economy sync"], { timeoutMs: context.timeoutMs });
|
|
7988
|
+
if (result.remote_sync.code !== 0) {
|
|
7989
|
+
result.error = "remote economy sync failed";
|
|
7990
|
+
return result;
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
result.remote_snapshot = context.runner("ssh", [machine, remoteSnapshotCommand(context.remoteSnapshotDir, snapshotName)], { timeoutMs: context.timeoutMs });
|
|
7994
|
+
if (result.remote_snapshot.code !== 0) {
|
|
7995
|
+
result.error = "remote snapshot failed";
|
|
7996
|
+
return result;
|
|
7997
|
+
}
|
|
7998
|
+
const emittedPath = result.remote_snapshot.stdout.trim().split(/\r?\n/).at(-1)?.trim() || remoteSnapshotPath;
|
|
7999
|
+
if (!isSafeRemoteSnapshotPath(emittedPath, context.remoteSnapshotDir)) {
|
|
8000
|
+
result.error = `remote snapshot path refused: ${emittedPath}`;
|
|
8001
|
+
return result;
|
|
8002
|
+
}
|
|
8003
|
+
result.remote_snapshot_path = emittedPath;
|
|
8004
|
+
result.pull = context.runner("scp", [`${machine}:${emittedPath}`, tmpLocal], { timeoutMs: context.timeoutMs });
|
|
8005
|
+
context.runner("ssh", [machine, `rm -f ${shellQuote(emittedPath)}`], { timeoutMs: context.timeoutMs });
|
|
8006
|
+
if (result.pull.code !== 0) {
|
|
8007
|
+
result.error = "snapshot pull failed; no cached DB merged";
|
|
8008
|
+
rmSync(tmpLocal, { force: true });
|
|
8009
|
+
return result;
|
|
8010
|
+
}
|
|
8011
|
+
const integrity = verifySqliteIntegrity(tmpLocal);
|
|
8012
|
+
result.local_integrity = integrity;
|
|
8013
|
+
if (!integrity.ok) {
|
|
8014
|
+
result.error = "pulled snapshot failed local integrity check";
|
|
8015
|
+
rmSync(tmpLocal, { force: true });
|
|
8016
|
+
return result;
|
|
8017
|
+
}
|
|
8018
|
+
renameSync(tmpLocal, verifiedLocal);
|
|
8019
|
+
const db = openDatabase();
|
|
8020
|
+
result.merge = mergePeerDatabase(db, verifiedLocal, { sourceMachine: machine });
|
|
8021
|
+
result.status = "merged";
|
|
8022
|
+
return result;
|
|
8023
|
+
}
|
|
8024
|
+
function remoteSnapshotCommand(remoteDir, filename) {
|
|
8025
|
+
const dirExpr = remoteDir.startsWith("$HOME/") ? `"${remoteDir.replace(/"/g, "\\\"")}"` : shellQuote(remoteDir);
|
|
8026
|
+
const safeFilename = filename.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
8027
|
+
return [
|
|
8028
|
+
"set -euo pipefail",
|
|
8029
|
+
'db="${HASNA_ECONOMY_DB_PATH:-${ECONOMY_DB:-$HOME/.hasna/economy/economy.db}}"',
|
|
8030
|
+
`snapshot_dir=${dirExpr}`,
|
|
8031
|
+
`out="$snapshot_dir/${safeFilename}"`,
|
|
8032
|
+
'test -f "$db"',
|
|
8033
|
+
'mkdir -p "$snapshot_dir"',
|
|
8034
|
+
'rm -f "$out"',
|
|
8035
|
+
`sqlite3 "$db" "VACUUM INTO '$out'"`,
|
|
8036
|
+
'test "$(sqlite3 "$out" "PRAGMA integrity_check;")" = "ok"',
|
|
8037
|
+
'printf "%s\\n" "$out"'
|
|
8038
|
+
].join("; ");
|
|
8039
|
+
}
|
|
8040
|
+
function isSafeRemoteSnapshotPath(path, remoteSnapshotDir = "$HOME/.hasna/economy/fleet-sync-snapshots") {
|
|
8041
|
+
if (!path || path.includes("\x00") || path.includes(`
|
|
8042
|
+
`) || path.includes("\r"))
|
|
8043
|
+
return false;
|
|
8044
|
+
if (!path.endsWith(".db"))
|
|
8045
|
+
return false;
|
|
8046
|
+
if (path.includes(".."))
|
|
8047
|
+
return false;
|
|
8048
|
+
const prefix = remoteSnapshotDir.replace(/\/$/, "");
|
|
8049
|
+
return path.startsWith(`${prefix}/`) || path.startsWith("$HOME/.hasna/economy/fleet-sync-snapshots/") || path.includes("/.hasna/economy/fleet-sync-snapshots/");
|
|
8050
|
+
}
|
|
8051
|
+
function verifySqliteIntegrity(path) {
|
|
8052
|
+
if (!existsSync13(path))
|
|
8053
|
+
return { ok: false, detail: "file does not exist" };
|
|
8054
|
+
let db;
|
|
8055
|
+
try {
|
|
8056
|
+
db = new Database2(path, { readonly: true });
|
|
8057
|
+
const row = db.query("PRAGMA integrity_check").get();
|
|
8058
|
+
const detail = Object.values(row ?? {})[0] ?? "";
|
|
8059
|
+
return { ok: detail.toLowerCase() === "ok", detail: detail || "no integrity result" };
|
|
8060
|
+
} catch (error) {
|
|
8061
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
8062
|
+
} finally {
|
|
8063
|
+
db?.close();
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
function writeFleetSyncReport(reportDir, result) {
|
|
8067
|
+
mkdirSync3(reportDir, { recursive: true });
|
|
8068
|
+
const path = join12(resolve(reportDir), `${result.generated_at.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.json`);
|
|
8069
|
+
writeFileSync2(path, `${JSON.stringify(result, null, 2)}
|
|
8070
|
+
`);
|
|
8071
|
+
return path;
|
|
8072
|
+
}
|
|
8073
|
+
function spawnCommand(command, args, options) {
|
|
8074
|
+
const result = spawnSync(command, args, {
|
|
8075
|
+
encoding: "utf8",
|
|
8076
|
+
timeout: options.timeoutMs,
|
|
8077
|
+
maxBuffer: 1024 * 1024,
|
|
8078
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8079
|
+
});
|
|
8080
|
+
return {
|
|
8081
|
+
code: result.status,
|
|
8082
|
+
stdout: result.stdout ?? "",
|
|
8083
|
+
stderr: result.stderr ?? "",
|
|
8084
|
+
...result.error ? { error: result.error.message } : {}
|
|
8085
|
+
};
|
|
8086
|
+
}
|
|
8087
|
+
function shellQuote(value) {
|
|
8088
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8089
|
+
}
|
|
8090
|
+
function normalizePositiveInteger(value, fallback) {
|
|
8091
|
+
if (value == null || !Number.isFinite(value) || value < 1)
|
|
8092
|
+
return fallback;
|
|
8093
|
+
return Math.floor(value);
|
|
8094
|
+
}
|
|
8095
|
+
|
|
7827
8096
|
// src/cli/index.ts
|
|
7828
8097
|
init_database();
|
|
7829
8098
|
init_database();
|
|
@@ -7863,7 +8132,7 @@ function fmt4(usd) {
|
|
|
7863
8132
|
}
|
|
7864
8133
|
return chalk7.green(formatted);
|
|
7865
8134
|
}
|
|
7866
|
-
function
|
|
8135
|
+
function fmtTokens2(n) {
|
|
7867
8136
|
if (n >= 1e9)
|
|
7868
8137
|
return `${(n / 1e9).toFixed(1)}B`;
|
|
7869
8138
|
if (n >= 1e6)
|
|
@@ -7971,7 +8240,7 @@ function printAccountBreakdown(rows) {
|
|
|
7971
8240
|
chalk7.dim(r.account_source || "unknown"),
|
|
7972
8241
|
String(r.sessions),
|
|
7973
8242
|
String(r.requests),
|
|
7974
|
-
chalk7.cyan(
|
|
8243
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
7975
8244
|
fmt4(r.api_equivalent_usd),
|
|
7976
8245
|
fmt4(r.billable_usd),
|
|
7977
8246
|
fmt4(r.subscription_included_usd)
|
|
@@ -7998,7 +8267,7 @@ function printSummary(label, period) {
|
|
|
7998
8267
|
["Total cost", fmt4(s.total_usd)],
|
|
7999
8268
|
["Sessions", chalk7.yellow(fmtCount(s.sessions))],
|
|
8000
8269
|
["Requests", chalk7.yellow(fmtCount(s.requests))],
|
|
8001
|
-
["Tokens", chalk7.yellow(
|
|
8270
|
+
["Tokens", chalk7.yellow(fmtTokens2(s.tokens))]
|
|
8002
8271
|
]);
|
|
8003
8272
|
console.log();
|
|
8004
8273
|
}
|
|
@@ -8018,9 +8287,9 @@ program.action(async () => {
|
|
|
8018
8287
|
console.log(chalk7.bold.cyan(" Economy"));
|
|
8019
8288
|
console.log();
|
|
8020
8289
|
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),
|
|
8290
|
+
["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
|
|
8291
|
+
["This Week", fmt4(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens2(w.tokens)],
|
|
8292
|
+
["This Month", fmt4(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens2(m.tokens)]
|
|
8024
8293
|
]);
|
|
8025
8294
|
if (dailyValues.length > 0) {
|
|
8026
8295
|
console.log(`
|
|
@@ -8076,7 +8345,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
8076
8345
|
console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
8077
8346
|
}
|
|
8078
8347
|
if (opts.recalculate) {
|
|
8079
|
-
const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
8348
|
+
const { computeCostFromDb: computeCostFromDb2, getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
8080
8349
|
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
8350
|
let fixed = 0;
|
|
8082
8351
|
for (const r of zeroRows) {
|
|
@@ -8098,6 +8367,16 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
8098
8367
|
}
|
|
8099
8368
|
}
|
|
8100
8369
|
console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
|
|
8370
|
+
const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
|
|
8371
|
+
if (zeroCostBuckets.length > 0) {
|
|
8372
|
+
console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
|
|
8373
|
+
for (const row of zeroCostBuckets) {
|
|
8374
|
+
const pricing = getPricingFromDb2(db, row.model);
|
|
8375
|
+
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";
|
|
8376
|
+
console.log(chalk7.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
|
|
8377
|
+
}
|
|
8378
|
+
console.log(chalk7.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
|
|
8379
|
+
}
|
|
8101
8380
|
}
|
|
8102
8381
|
try {
|
|
8103
8382
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
@@ -8140,7 +8419,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
8140
8419
|
const f = opts.format ?? "table";
|
|
8141
8420
|
if (f === "compact") {
|
|
8142
8421
|
for (const s of sessions)
|
|
8143
|
-
process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${
|
|
8422
|
+
process.stdout.write(`${s.id.slice(0, 8)} ${s.agent} ${fmt4(s.total_cost_usd)} ${fmtTokens2(s.total_tokens)} ${s.project_name || "\u2014"}
|
|
8144
8423
|
`);
|
|
8145
8424
|
return;
|
|
8146
8425
|
}
|
|
@@ -8160,7 +8439,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
8160
8439
|
fmtAgent(s.agent),
|
|
8161
8440
|
chalk7.white(s.project_name || chalk7.dim("unknown")),
|
|
8162
8441
|
fmt4(s.total_cost_usd),
|
|
8163
|
-
chalk7.cyan(
|
|
8442
|
+
chalk7.cyan(fmtTokens2(s.total_tokens)),
|
|
8164
8443
|
fmtCount(s.request_count),
|
|
8165
8444
|
chalk7.dim(s.started_at.substring(0, 16))
|
|
8166
8445
|
]));
|
|
@@ -8171,9 +8450,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
8171
8450
|
const agent = parseOptionalCliAgent(opts.agent);
|
|
8172
8451
|
const db = openDatabase();
|
|
8173
8452
|
const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
8174
|
-
|
|
8175
|
-
if (sinceDate)
|
|
8176
|
-
sessions = sessions.filter((s) => s.started_at >= sinceDate);
|
|
8453
|
+
const sessions = queryTopSessions(db, count, agent, sinceDate);
|
|
8177
8454
|
if (sessions.length === 0) {
|
|
8178
8455
|
console.log(chalk7.yellow("No sessions found. Run `economy sync` first."));
|
|
8179
8456
|
return;
|
|
@@ -8184,7 +8461,7 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
8184
8461
|
chalk7.white(s.project_name || chalk7.dim("unknown")),
|
|
8185
8462
|
fmtAgent(s.agent),
|
|
8186
8463
|
fmt4(s.total_cost_usd),
|
|
8187
|
-
chalk7.cyan(
|
|
8464
|
+
chalk7.cyan(fmtTokens2(s.total_tokens)),
|
|
8188
8465
|
chalk7.dim(s.started_at.substring(0, 16))
|
|
8189
8466
|
]));
|
|
8190
8467
|
console.log();
|
|
@@ -8208,7 +8485,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8208
8485
|
chalk7.white(r.project_name || chalk7.dim("unknown")),
|
|
8209
8486
|
String(r.sessions),
|
|
8210
8487
|
String(r.requests),
|
|
8211
|
-
chalk7.cyan(
|
|
8488
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8212
8489
|
fmt4(r.cost_usd)
|
|
8213
8490
|
]));
|
|
8214
8491
|
} else if (opts.by === "agent") {
|
|
@@ -8230,7 +8507,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8230
8507
|
fmtAgent(r.agent),
|
|
8231
8508
|
String(r.sessions),
|
|
8232
8509
|
String(r.requests),
|
|
8233
|
-
chalk7.cyan(
|
|
8510
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8234
8511
|
fmt4(r.api_equivalent_usd),
|
|
8235
8512
|
fmt4(r.billable_usd),
|
|
8236
8513
|
fmt4(r.subscription_included_usd)
|
|
@@ -8352,7 +8629,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
8352
8629
|
chalk7.white(r.model),
|
|
8353
8630
|
fmtAgent(r.agent),
|
|
8354
8631
|
String(r.requests),
|
|
8355
|
-
chalk7.cyan(
|
|
8632
|
+
chalk7.cyan(fmtTokens2(r.total_tokens)),
|
|
8356
8633
|
fmt4(r.cost_usd)
|
|
8357
8634
|
]));
|
|
8358
8635
|
}
|
|
@@ -8514,7 +8791,7 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
8514
8791
|
printTable(["Metric", "Value"], [
|
|
8515
8792
|
["Total cost", fmt4(totalCost)],
|
|
8516
8793
|
["Sessions", fmtCount(sessions.length)],
|
|
8517
|
-
["Total tokens",
|
|
8794
|
+
["Total tokens", fmtTokens2(totalTokens)]
|
|
8518
8795
|
]);
|
|
8519
8796
|
if (dailyValues.length > 0) {
|
|
8520
8797
|
console.log(`
|
|
@@ -8647,8 +8924,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
8647
8924
|
if (!serverRunning) {
|
|
8648
8925
|
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
8649
8926
|
const { spawn } = await import("child_process");
|
|
8650
|
-
const { resolve:
|
|
8651
|
-
const serveScript =
|
|
8927
|
+
const { resolve: resolve3, dirname: dirname3 } = await import("path");
|
|
8928
|
+
const serveScript = resolve3(dirname3(process.argv[1]), "..", "server", "index.js");
|
|
8652
8929
|
const child = spawn(process.execPath, [serveScript], {
|
|
8653
8930
|
detached: true,
|
|
8654
8931
|
stdio: "ignore",
|
|
@@ -8719,7 +8996,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
8719
8996
|
["Started", String(session["started_at"]).substring(0, 19)],
|
|
8720
8997
|
["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
|
|
8721
8998
|
["Total cost", fmt4(session["total_cost_usd"])],
|
|
8722
|
-
["Total tokens",
|
|
8999
|
+
["Total tokens", fmtTokens2(session["total_tokens"])],
|
|
8723
9000
|
["Requests", fmtCount(session["request_count"])]
|
|
8724
9001
|
]);
|
|
8725
9002
|
if (requests.length > 0) {
|
|
@@ -8729,10 +9006,10 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
8729
9006
|
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
8730
9007
|
chalk7.dim(String(r["timestamp"]).substring(11, 19)),
|
|
8731
9008
|
chalk7.white(String(r["model"]).substring(0, 22)),
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
9009
|
+
fmtTokens2(r["input_tokens"]),
|
|
9010
|
+
fmtTokens2(r["output_tokens"]),
|
|
9011
|
+
fmtTokens2(r["cache_read_tokens"]),
|
|
9012
|
+
fmtTokens2(r["cache_create_tokens"]),
|
|
8736
9013
|
fmt4(r["cost_usd"])
|
|
8737
9014
|
]));
|
|
8738
9015
|
if (requests.length > 50)
|
|
@@ -8763,6 +9040,31 @@ program.command("machines").description("List all machines that have synced data
|
|
|
8763
9040
|
${chalk7.dim("Current machine:")} ${chalk7.bold(current)}`);
|
|
8764
9041
|
console.log();
|
|
8765
9042
|
});
|
|
9043
|
+
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) => {
|
|
9044
|
+
const result = runEconomyFleetSync({
|
|
9045
|
+
machines: opts.machine?.split(",").map((entry) => entry.trim()).filter(Boolean),
|
|
9046
|
+
localSync: opts.localSync !== false,
|
|
9047
|
+
remoteSync: opts.remoteSync !== false,
|
|
9048
|
+
cacheDir: opts.cacheDir,
|
|
9049
|
+
remoteSnapshotDir: opts.remoteSnapshotDir,
|
|
9050
|
+
timeoutMs: parsePositiveCliInteger(opts.timeoutMs, "--timeout-ms"),
|
|
9051
|
+
dryRun: opts.dryRun
|
|
9052
|
+
});
|
|
9053
|
+
const reportPath = opts.reportDir ? writeFleetSyncReport(opts.reportDir, result) : undefined;
|
|
9054
|
+
if (opts.json) {
|
|
9055
|
+
console.log(JSON.stringify({ ...result, ...reportPath ? { report_path: reportPath } : {} }, null, 2));
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
const status = result.summary.failed === 0 ? chalk7.green("ok") : chalk7.red("failed");
|
|
9059
|
+
console.log(`${status} machines=${result.summary.machines} merged=${result.summary.merged} failed=${result.summary.failed} dry_run=${result.summary.dry_run}`);
|
|
9060
|
+
if (reportPath)
|
|
9061
|
+
console.log(chalk7.dim(`report=${reportPath}`));
|
|
9062
|
+
for (const row of result.remote.filter((entry) => entry.status !== "merged").slice(0, 20)) {
|
|
9063
|
+
console.log(`${row.status === "failed" ? chalk7.red("failed") : chalk7.yellow(row.status)} ${row.machine} ${chalk7.dim(row.error ?? "")}`);
|
|
9064
|
+
}
|
|
9065
|
+
if (result.local.status === "failed" || result.summary.failed > 0)
|
|
9066
|
+
process.exitCode = 1;
|
|
9067
|
+
});
|
|
8766
9068
|
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
9069
|
const db = openDatabase();
|
|
8768
9070
|
const result = mergePeerDatabase(db, sourceDb, { sourceMachine: opts.sourceMachine });
|
|
@@ -8802,8 +9104,8 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
8802
9104
|
}
|
|
8803
9105
|
}
|
|
8804
9106
|
if (opts.output) {
|
|
8805
|
-
const { writeFileSync:
|
|
8806
|
-
|
|
9107
|
+
const { writeFileSync: writeFileSync5 } = await import("fs");
|
|
9108
|
+
writeFileSync5(opts.output, csv);
|
|
8807
9109
|
console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
|
|
8808
9110
|
} else {
|
|
8809
9111
|
process.stdout.write(csv);
|
|
@@ -8875,7 +9177,7 @@ program.command("compare <period1> <period2>").description("Compare two periods
|
|
|
8875
9177
|
["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
|
|
8876
9178
|
["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
|
|
8877
9179
|
["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
|
|
8878
|
-
["Tokens",
|
|
9180
|
+
["Tokens", fmtTokens2(a.tokens), fmtTokens2(b.tokens), delta(a.tokens, b.tokens)]
|
|
8879
9181
|
]);
|
|
8880
9182
|
console.log();
|
|
8881
9183
|
});
|