@hasna/economy 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +595 -208
- package/dist/index.js +1010 -0
- package/dist/mcp/index.js +126 -12
- package/dist/server/index.js +940 -0
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -107,12 +107,32 @@ var init_pricing = __esm(() => {
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
// src/db/database.ts
|
|
110
|
-
import { Database } from "
|
|
111
|
-
import { existsSync, mkdirSync } from "fs";
|
|
110
|
+
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
111
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
112
112
|
import { homedir } from "os";
|
|
113
113
|
import { join } from "path";
|
|
114
|
+
function getDataDir() {
|
|
115
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
116
|
+
const newDir = join(home, ".hasna", "economy");
|
|
117
|
+
const oldDir = join(home, ".economy");
|
|
118
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
119
|
+
mkdirSync(newDir, { recursive: true });
|
|
120
|
+
for (const file of readdirSync(oldDir)) {
|
|
121
|
+
const oldPath = join(oldDir, file);
|
|
122
|
+
if (statSync(oldPath).isFile()) {
|
|
123
|
+
copyFileSync(oldPath, join(newDir, file));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
mkdirSync(newDir, { recursive: true });
|
|
128
|
+
return newDir;
|
|
129
|
+
}
|
|
114
130
|
function getDbPath() {
|
|
115
|
-
|
|
131
|
+
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
132
|
+
return process.env["HASNA_ECONOMY_DB_PATH"];
|
|
133
|
+
if (process.env["ECONOMY_DB"])
|
|
134
|
+
return process.env["ECONOMY_DB"];
|
|
135
|
+
return join(getDataDir(), "economy.db");
|
|
116
136
|
}
|
|
117
137
|
function openDatabase(dbPath, skipSeed = false) {
|
|
118
138
|
const path = dbPath ?? getDbPath();
|
|
@@ -211,12 +231,24 @@ function initSchema(db) {
|
|
|
211
231
|
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
212
232
|
updated_at TEXT NOT NULL
|
|
213
233
|
);
|
|
234
|
+
|
|
235
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
236
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
237
|
+
message TEXT NOT NULL,
|
|
238
|
+
email TEXT,
|
|
239
|
+
category TEXT DEFAULT 'general',
|
|
240
|
+
version TEXT,
|
|
241
|
+
machine_id TEXT,
|
|
242
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
243
|
+
);
|
|
214
244
|
`);
|
|
215
245
|
}
|
|
216
246
|
function periodWhere(period) {
|
|
217
247
|
switch (period) {
|
|
218
248
|
case "today":
|
|
219
249
|
return `DATE(timestamp) = DATE('now')`;
|
|
250
|
+
case "yesterday":
|
|
251
|
+
return `DATE(timestamp) = DATE('now', '-1 day')`;
|
|
220
252
|
case "week":
|
|
221
253
|
return `timestamp >= DATE('now', '-7 days')`;
|
|
222
254
|
case "month":
|
|
@@ -231,6 +263,8 @@ function sessionPeriodWhere(period) {
|
|
|
231
263
|
switch (period) {
|
|
232
264
|
case "today":
|
|
233
265
|
return `DATE(started_at) = DATE('now')`;
|
|
266
|
+
case "yesterday":
|
|
267
|
+
return `DATE(started_at) = DATE('now', '-1 day')`;
|
|
234
268
|
case "week":
|
|
235
269
|
return `started_at >= DATE('now', '-7 days')`;
|
|
236
270
|
case "month":
|
|
@@ -510,9 +544,9 @@ function seedModelPricing(db, defaults) {
|
|
|
510
544
|
var init_database = () => {};
|
|
511
545
|
|
|
512
546
|
// src/ingest/claude.ts
|
|
513
|
-
import { readdirSync, readFileSync, existsSync as
|
|
547
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
514
548
|
import { homedir as homedir2 } from "os";
|
|
515
|
-
import { join as
|
|
549
|
+
import { join as join4, basename } from "path";
|
|
516
550
|
function autoDetectProject(cwd, projects) {
|
|
517
551
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
518
552
|
}
|
|
@@ -523,11 +557,11 @@ function collectJsonlFiles(projectDir) {
|
|
|
523
557
|
const files = [];
|
|
524
558
|
function walk(dir) {
|
|
525
559
|
try {
|
|
526
|
-
for (const entry of
|
|
560
|
+
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
527
561
|
if (entry.isDirectory())
|
|
528
|
-
walk(
|
|
562
|
+
walk(join4(dir, entry.name));
|
|
529
563
|
else if (entry.name.endsWith(".jsonl"))
|
|
530
|
-
files.push(
|
|
564
|
+
files.push(join4(dir, entry.name));
|
|
531
565
|
}
|
|
532
566
|
} catch {}
|
|
533
567
|
}
|
|
@@ -535,7 +569,7 @@ function collectJsonlFiles(projectDir) {
|
|
|
535
569
|
return files;
|
|
536
570
|
}
|
|
537
571
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
538
|
-
if (!
|
|
572
|
+
if (!existsSync3(PROJECTS_DIR)) {
|
|
539
573
|
if (verbose)
|
|
540
574
|
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
541
575
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -544,16 +578,16 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
544
578
|
let totalRequests = 0;
|
|
545
579
|
const touchedSessions = new Set;
|
|
546
580
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
547
|
-
const projectDirs =
|
|
581
|
+
const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
548
582
|
for (const projectDirEntry of projectDirs) {
|
|
549
|
-
const projectDirPath =
|
|
583
|
+
const projectDirPath = join4(PROJECTS_DIR, projectDirEntry.name);
|
|
550
584
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
551
585
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
552
586
|
for (const filePath of jsonlFiles) {
|
|
553
587
|
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
554
588
|
let fileMtime = "0";
|
|
555
589
|
try {
|
|
556
|
-
fileMtime =
|
|
590
|
+
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
557
591
|
} catch {
|
|
558
592
|
continue;
|
|
559
593
|
}
|
|
@@ -562,7 +596,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
562
596
|
continue;
|
|
563
597
|
let lines;
|
|
564
598
|
try {
|
|
565
|
-
lines =
|
|
599
|
+
lines = readFileSync2(filePath, "utf-8").split(`
|
|
566
600
|
`).filter((l) => l.trim());
|
|
567
601
|
} catch {
|
|
568
602
|
continue;
|
|
@@ -648,16 +682,16 @@ var PROJECTS_DIR;
|
|
|
648
682
|
var init_claude = __esm(() => {
|
|
649
683
|
init_database();
|
|
650
684
|
init_pricing();
|
|
651
|
-
PROJECTS_DIR =
|
|
685
|
+
PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
|
|
652
686
|
});
|
|
653
687
|
|
|
654
688
|
// src/ingest/codex.ts
|
|
655
|
-
import { existsSync as
|
|
689
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
656
690
|
import { homedir as homedir3 } from "os";
|
|
657
|
-
import { join as
|
|
691
|
+
import { join as join5, basename as basename2 } from "path";
|
|
658
692
|
import { Database as Database2 } from "bun:sqlite";
|
|
659
693
|
async function ingestCodex(db, verbose = false) {
|
|
660
|
-
if (!
|
|
694
|
+
if (!existsSync4(CODEX_DB_PATH)) {
|
|
661
695
|
if (verbose)
|
|
662
696
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
663
697
|
return { sessions: 0 };
|
|
@@ -701,43 +735,42 @@ async function ingestCodex(db, verbose = false) {
|
|
|
701
735
|
var CODEX_DB_PATH, CODEX_CONFIG_PATH;
|
|
702
736
|
var init_codex = __esm(() => {
|
|
703
737
|
init_database();
|
|
704
|
-
CODEX_DB_PATH =
|
|
705
|
-
CODEX_CONFIG_PATH =
|
|
738
|
+
CODEX_DB_PATH = join5(homedir3(), ".codex", "state_5.sqlite");
|
|
739
|
+
CODEX_CONFIG_PATH = join5(homedir3(), ".codex", "config.toml");
|
|
706
740
|
});
|
|
707
741
|
|
|
708
742
|
// src/lib/config.ts
|
|
709
743
|
var exports_config = {};
|
|
710
744
|
__export(exports_config, {
|
|
711
745
|
setConfigValue: () => setConfigValue,
|
|
712
|
-
saveConfig: () =>
|
|
713
|
-
loadConfig: () =>
|
|
746
|
+
saveConfig: () => saveConfig2,
|
|
747
|
+
loadConfig: () => loadConfig2,
|
|
714
748
|
getConfigValue: () => getConfigValue
|
|
715
749
|
});
|
|
716
|
-
import { existsSync as
|
|
717
|
-
import {
|
|
718
|
-
|
|
719
|
-
function loadConfig() {
|
|
750
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
751
|
+
import { join as join7 } from "path";
|
|
752
|
+
function loadConfig2() {
|
|
720
753
|
try {
|
|
721
|
-
if (
|
|
722
|
-
const raw =
|
|
754
|
+
if (existsSync6(CONFIG_PATH2)) {
|
|
755
|
+
const raw = readFileSync5(CONFIG_PATH2, "utf-8");
|
|
723
756
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
724
757
|
}
|
|
725
758
|
} catch {}
|
|
726
759
|
return { ...DEFAULTS };
|
|
727
760
|
}
|
|
728
|
-
function
|
|
729
|
-
const dir =
|
|
730
|
-
if (!
|
|
731
|
-
|
|
732
|
-
|
|
761
|
+
function saveConfig2(config) {
|
|
762
|
+
const dir = CONFIG_PATH2.substring(0, CONFIG_PATH2.lastIndexOf("/"));
|
|
763
|
+
if (!existsSync6(dir))
|
|
764
|
+
mkdirSync3(dir, { recursive: true });
|
|
765
|
+
writeFileSync2(CONFIG_PATH2, JSON.stringify(config, null, 2) + `
|
|
733
766
|
`);
|
|
734
767
|
}
|
|
735
768
|
function getConfigValue(key) {
|
|
736
|
-
const config =
|
|
769
|
+
const config = loadConfig2();
|
|
737
770
|
return config[key] ?? null;
|
|
738
771
|
}
|
|
739
772
|
function setConfigValue(key, value) {
|
|
740
|
-
const config =
|
|
773
|
+
const config = loadConfig2();
|
|
741
774
|
let parsed = value;
|
|
742
775
|
if (value === "true")
|
|
743
776
|
parsed = true;
|
|
@@ -753,11 +786,12 @@ function setConfigValue(key, value) {
|
|
|
753
786
|
} catch {}
|
|
754
787
|
}
|
|
755
788
|
config[key] = parsed;
|
|
756
|
-
|
|
789
|
+
saveConfig2(config);
|
|
757
790
|
}
|
|
758
|
-
var
|
|
791
|
+
var CONFIG_PATH2, DEFAULTS;
|
|
759
792
|
var init_config = __esm(() => {
|
|
760
|
-
|
|
793
|
+
init_database();
|
|
794
|
+
CONFIG_PATH2 = join7(getDataDir(), "config.json");
|
|
761
795
|
DEFAULTS = {
|
|
762
796
|
port: 3456,
|
|
763
797
|
"default-period": "today",
|
|
@@ -774,7 +808,7 @@ __export(exports_webhooks, {
|
|
|
774
808
|
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
775
809
|
});
|
|
776
810
|
async function checkAndFireWebhooks(db) {
|
|
777
|
-
const config =
|
|
811
|
+
const config = loadConfig2();
|
|
778
812
|
const url = config["webhook-url"];
|
|
779
813
|
if (!url)
|
|
780
814
|
return;
|
|
@@ -819,9 +853,9 @@ var exports_watch = {};
|
|
|
819
853
|
__export(exports_watch, {
|
|
820
854
|
watchCosts: () => watchCosts
|
|
821
855
|
});
|
|
822
|
-
import
|
|
856
|
+
import chalk2 from "chalk";
|
|
823
857
|
function fmt(usd) {
|
|
824
|
-
return
|
|
858
|
+
return chalk2.green(`$${usd.toFixed(4)}`);
|
|
825
859
|
}
|
|
826
860
|
function notify(title, body) {
|
|
827
861
|
try {
|
|
@@ -831,12 +865,12 @@ function notify(title, body) {
|
|
|
831
865
|
}
|
|
832
866
|
function renderHeader(todayUsd, weekUsd) {
|
|
833
867
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
834
|
-
console.log(
|
|
835
|
-
console.log(
|
|
868
|
+
console.log(chalk2.bold.cyan(" economy watch") + chalk2.dim(" \u2014 live cost stream"));
|
|
869
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
836
870
|
console.log(` Today: ${fmt(todayUsd)} Week: ${fmt(weekUsd)}`);
|
|
837
|
-
console.log(
|
|
838
|
-
console.log(
|
|
839
|
-
console.log(
|
|
871
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
872
|
+
console.log(chalk2.dim(" [agent] cost model tokens project"));
|
|
873
|
+
console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
840
874
|
}
|
|
841
875
|
async function watchCosts(opts) {
|
|
842
876
|
const db = openDatabase();
|
|
@@ -848,7 +882,7 @@ async function watchCosts(opts) {
|
|
|
848
882
|
const initialSummaryToday = querySummary(db, "today");
|
|
849
883
|
const initialSummaryWeek = querySummary(db, "week");
|
|
850
884
|
renderHeader(initialSummaryToday.total_usd, initialSummaryWeek.total_usd);
|
|
851
|
-
console.log(
|
|
885
|
+
console.log(chalk2.dim(`
|
|
852
886
|
Polling every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
853
887
|
`));
|
|
854
888
|
async function poll() {
|
|
@@ -860,7 +894,7 @@ async function watchCosts(opts) {
|
|
|
860
894
|
for (const req of newRequests) {
|
|
861
895
|
if (opts.agent && req.agent !== opts.agent)
|
|
862
896
|
continue;
|
|
863
|
-
const agentLabel = req.agent === "claude" ?
|
|
897
|
+
const agentLabel = req.agent === "claude" ? chalk2.blue("[claude]") : chalk2.yellow("[codex] ");
|
|
864
898
|
const tokens = req.input_tokens + req.output_tokens;
|
|
865
899
|
const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
866
900
|
const line = ` ${agentLabel} ${fmt(req.cost_usd).padEnd(14)}${req.model.substring(0, 24).padEnd(26)}${tokStr.padEnd(10)}${req.session_id.substring(0, 12)}`;
|
|
@@ -888,15 +922,15 @@ async function watchCosts(opts) {
|
|
|
888
922
|
for (const line of lines)
|
|
889
923
|
console.log(line);
|
|
890
924
|
if (lines.length === 0)
|
|
891
|
-
console.log(
|
|
892
|
-
console.log(
|
|
925
|
+
console.log(chalk2.dim(" Waiting for new requests..."));
|
|
926
|
+
console.log(chalk2.dim(`
|
|
893
927
|
Last updated: ${new Date().toLocaleTimeString()} \u2014 polling every ${opts.interval}s \u2014 Ctrl+C to exit`));
|
|
894
928
|
}
|
|
895
929
|
await poll();
|
|
896
930
|
const timer = setInterval(poll, opts.interval * 1000);
|
|
897
931
|
process.on("SIGINT", () => {
|
|
898
932
|
clearInterval(timer);
|
|
899
|
-
console.log(
|
|
933
|
+
console.log(chalk2.dim(`
|
|
900
934
|
|
|
901
935
|
Stopped watching.`));
|
|
902
936
|
process.exit(0);
|
|
@@ -1098,15 +1132,15 @@ function startServer(port = 3456) {
|
|
|
1098
1132
|
return apiHandler(req);
|
|
1099
1133
|
}
|
|
1100
1134
|
try {
|
|
1101
|
-
const { existsSync:
|
|
1102
|
-
if (
|
|
1135
|
+
const { existsSync: existsSync7 } = await import("fs");
|
|
1136
|
+
if (existsSync7(dashboardDir)) {
|
|
1103
1137
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
1104
1138
|
const fullPath = dashboardDir + filePath;
|
|
1105
|
-
if (
|
|
1139
|
+
if (existsSync7(fullPath)) {
|
|
1106
1140
|
return new Response(Bun.file(fullPath));
|
|
1107
1141
|
}
|
|
1108
1142
|
const indexPath = dashboardDir + "/index.html";
|
|
1109
|
-
if (
|
|
1143
|
+
if (existsSync7(indexPath)) {
|
|
1110
1144
|
return new Response(Bun.file(indexPath));
|
|
1111
1145
|
}
|
|
1112
1146
|
}
|
|
@@ -1137,16 +1171,16 @@ __export(exports_menubar, {
|
|
|
1137
1171
|
menubarStart: () => menubarStart,
|
|
1138
1172
|
menubarInstall: () => menubarInstall
|
|
1139
1173
|
});
|
|
1140
|
-
import
|
|
1174
|
+
import chalk3 from "chalk";
|
|
1141
1175
|
import { execSync } from "child_process";
|
|
1142
|
-
import { existsSync as
|
|
1176
|
+
import { existsSync as existsSync7, writeFileSync as writeFileSync3 } from "fs";
|
|
1143
1177
|
import { tmpdir, arch } from "os";
|
|
1144
|
-
import { join as
|
|
1178
|
+
import { join as join8 } from "path";
|
|
1145
1179
|
function getArch() {
|
|
1146
1180
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
1147
1181
|
}
|
|
1148
1182
|
function isInstalled() {
|
|
1149
|
-
return
|
|
1183
|
+
return existsSync7(APP_PATH);
|
|
1150
1184
|
}
|
|
1151
1185
|
function isRunning() {
|
|
1152
1186
|
try {
|
|
@@ -1158,13 +1192,13 @@ function isRunning() {
|
|
|
1158
1192
|
}
|
|
1159
1193
|
async function menubarInstall(opts) {
|
|
1160
1194
|
if (isInstalled() && !opts.force) {
|
|
1161
|
-
console.log(
|
|
1162
|
-
console.log(
|
|
1195
|
+
console.log(chalk3.yellow("Economy Bar is already installed. Use --force to reinstall."));
|
|
1196
|
+
console.log(chalk3.dim(` Location: ${APP_PATH}`));
|
|
1163
1197
|
return;
|
|
1164
1198
|
}
|
|
1165
1199
|
const cpuArch = getArch();
|
|
1166
|
-
console.log(
|
|
1167
|
-
console.log(
|
|
1200
|
+
console.log(chalk3.cyan(`\u2192 Detecting architecture: ${cpuArch}`));
|
|
1201
|
+
console.log(chalk3.cyan("\u2192 Fetching latest release info..."));
|
|
1168
1202
|
let assetUrl;
|
|
1169
1203
|
try {
|
|
1170
1204
|
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
|
|
@@ -1180,24 +1214,24 @@ async function menubarInstall(opts) {
|
|
|
1180
1214
|
throw new Error(`No asset found for ${assetName}. Check releases at https://github.com/${REPO}/releases`);
|
|
1181
1215
|
assetUrl = asset.browser_download_url;
|
|
1182
1216
|
} catch (e) {
|
|
1183
|
-
console.error(
|
|
1217
|
+
console.error(chalk3.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
1184
1218
|
process.exit(1);
|
|
1185
1219
|
}
|
|
1186
|
-
const zipPath =
|
|
1187
|
-
const extractDir =
|
|
1188
|
-
console.log(
|
|
1220
|
+
const zipPath = join8(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
1221
|
+
const extractDir = join8(tmpdir(), "economy-bar-extracted");
|
|
1222
|
+
console.log(chalk3.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
1189
1223
|
try {
|
|
1190
1224
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
1191
1225
|
if (!res.ok)
|
|
1192
1226
|
throw new Error(`Download failed: ${res.status}`);
|
|
1193
1227
|
const buffer = await res.arrayBuffer();
|
|
1194
|
-
|
|
1195
|
-
console.log(
|
|
1228
|
+
writeFileSync3(zipPath, Buffer.from(buffer));
|
|
1229
|
+
console.log(chalk3.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
1196
1230
|
} catch (e) {
|
|
1197
|
-
console.error(
|
|
1231
|
+
console.error(chalk3.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1198
1232
|
process.exit(1);
|
|
1199
1233
|
}
|
|
1200
|
-
console.log(
|
|
1234
|
+
console.log(chalk3.cyan("\u2192 Installing to /Applications..."));
|
|
1201
1235
|
try {
|
|
1202
1236
|
execSync(`rm -rf "${extractDir}" && mkdir -p "${extractDir}"`, { stdio: "ignore" });
|
|
1203
1237
|
execSync(`unzip -q "${zipPath}" -d "${extractDir}"`, { stdio: "ignore" });
|
|
@@ -1206,24 +1240,24 @@ async function menubarInstall(opts) {
|
|
|
1206
1240
|
execSync(`cp -R "${extractDir}/Economy Bar.app" /Applications/`, { stdio: "ignore" });
|
|
1207
1241
|
execSync(`xattr -rd com.apple.quarantine "${APP_PATH}" 2>/dev/null || true`, { stdio: "ignore" });
|
|
1208
1242
|
execSync(`rm -rf "${zipPath}" "${extractDir}"`, { stdio: "ignore" });
|
|
1209
|
-
console.log(
|
|
1243
|
+
console.log(chalk3.green(`\u2713 Installed to ${APP_PATH}`));
|
|
1210
1244
|
} catch (e) {
|
|
1211
|
-
console.error(
|
|
1245
|
+
console.error(chalk3.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1212
1246
|
process.exit(1);
|
|
1213
1247
|
}
|
|
1214
|
-
console.log(
|
|
1248
|
+
console.log(chalk3.cyan("\u2192 Launching Economy Bar..."));
|
|
1215
1249
|
try {
|
|
1216
1250
|
execSync(`open "${APP_PATH}"`, { stdio: "ignore" });
|
|
1217
|
-
console.log(
|
|
1251
|
+
console.log(chalk3.bold.green(`
|
|
1218
1252
|
\u2713 Economy Bar is running in your menu bar!`));
|
|
1219
|
-
console.log(
|
|
1253
|
+
console.log(chalk3.dim(" Make sure economy serve is running: economy serve"));
|
|
1220
1254
|
} catch (e) {
|
|
1221
|
-
console.log(
|
|
1255
|
+
console.log(chalk3.yellow("\u26A0 Installed but could not auto-launch. Open from /Applications manually."));
|
|
1222
1256
|
}
|
|
1223
1257
|
}
|
|
1224
1258
|
function menubarUninstall() {
|
|
1225
1259
|
if (!isInstalled()) {
|
|
1226
|
-
console.log(
|
|
1260
|
+
console.log(chalk3.yellow("Economy Bar is not installed."));
|
|
1227
1261
|
return;
|
|
1228
1262
|
}
|
|
1229
1263
|
if (isRunning()) {
|
|
@@ -1233,46 +1267,398 @@ function menubarUninstall() {
|
|
|
1233
1267
|
} catch {}
|
|
1234
1268
|
}
|
|
1235
1269
|
execSync(`rm -rf "${APP_PATH}"`, { stdio: "ignore" });
|
|
1236
|
-
console.log(
|
|
1270
|
+
console.log(chalk3.green("\u2713 Economy Bar uninstalled"));
|
|
1237
1271
|
}
|
|
1238
1272
|
function menubarStart() {
|
|
1239
1273
|
if (!isInstalled()) {
|
|
1240
|
-
console.error(
|
|
1274
|
+
console.error(chalk3.red("Economy Bar is not installed. Run: economy menubar install"));
|
|
1241
1275
|
process.exit(1);
|
|
1242
1276
|
}
|
|
1243
1277
|
execSync(`open "${APP_PATH}"`, { stdio: "ignore" });
|
|
1244
|
-
console.log(
|
|
1278
|
+
console.log(chalk3.green("\u2713 Economy Bar launched"));
|
|
1245
1279
|
}
|
|
1246
1280
|
function menubarStop() {
|
|
1247
1281
|
if (!isRunning()) {
|
|
1248
|
-
console.log(
|
|
1282
|
+
console.log(chalk3.yellow("Economy Bar is not running."));
|
|
1249
1283
|
return;
|
|
1250
1284
|
}
|
|
1251
1285
|
try {
|
|
1252
1286
|
execSync(`osascript -e 'quit app "Economy Bar"'`, { stdio: "ignore" });
|
|
1253
|
-
console.log(
|
|
1287
|
+
console.log(chalk3.green("\u2713 Economy Bar stopped"));
|
|
1254
1288
|
} catch {
|
|
1255
|
-
console.log(
|
|
1289
|
+
console.log(chalk3.yellow("Could not quit Economy Bar gracefully"));
|
|
1256
1290
|
}
|
|
1257
1291
|
}
|
|
1258
1292
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
|
|
1259
1293
|
var init_menubar = () => {};
|
|
1260
1294
|
|
|
1295
|
+
// src/cli/index.ts
|
|
1296
|
+
import { Command } from "commander";
|
|
1297
|
+
import chalk4 from "chalk";
|
|
1298
|
+
|
|
1299
|
+
// src/cli/brains.ts
|
|
1300
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
1301
|
+
import { join as join3 } from "path";
|
|
1302
|
+
import chalk from "chalk";
|
|
1303
|
+
|
|
1304
|
+
// src/lib/gatherer.ts
|
|
1305
|
+
init_database();
|
|
1306
|
+
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
1307
|
+
var gatherTrainingData = async (options = {}) => {
|
|
1308
|
+
const limit = options.limit ?? 500;
|
|
1309
|
+
const examples = [];
|
|
1310
|
+
try {
|
|
1311
|
+
const db = openDatabase();
|
|
1312
|
+
const periods = ["today", "week", "month", "all"];
|
|
1313
|
+
for (const period of periods) {
|
|
1314
|
+
try {
|
|
1315
|
+
const s = querySummary(db, period);
|
|
1316
|
+
examples.push({
|
|
1317
|
+
messages: [
|
|
1318
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1319
|
+
{ role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
|
|
1320
|
+
{
|
|
1321
|
+
role: "assistant",
|
|
1322
|
+
content: `${period === "all" ? "Total" : period.charAt(0).toUpperCase() + period.slice(1)} AI spending: $${s.total_usd.toFixed(4)} across ${s.sessions} session(s), ${s.requests} request(s), ${s.tokens.toLocaleString()} tokens.`
|
|
1323
|
+
}
|
|
1324
|
+
]
|
|
1325
|
+
});
|
|
1326
|
+
} catch {}
|
|
1327
|
+
}
|
|
1328
|
+
const sessions = querySessions(db, {
|
|
1329
|
+
limit: Math.min(Math.floor(limit / 4), 50),
|
|
1330
|
+
since: options.since?.toISOString().substring(0, 10)
|
|
1331
|
+
});
|
|
1332
|
+
for (const s of sessions) {
|
|
1333
|
+
examples.push({
|
|
1334
|
+
messages: [
|
|
1335
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1336
|
+
{
|
|
1337
|
+
role: "user",
|
|
1338
|
+
content: `How much did the session "${s.id.substring(0, 12)}" cost?`
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
role: "assistant",
|
|
1342
|
+
content: `Session ${s.id.substring(0, 12)} (${s.agent}, project: ${s.project_name || "unknown"}): $${s.total_cost_usd.toFixed(4)}, ${s.total_tokens.toLocaleString()} tokens, ${s.request_count} requests. Started: ${s.started_at.substring(0, 16)}.`
|
|
1343
|
+
}
|
|
1344
|
+
]
|
|
1345
|
+
});
|
|
1346
|
+
examples.push({
|
|
1347
|
+
messages: [
|
|
1348
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1349
|
+
{
|
|
1350
|
+
role: "user",
|
|
1351
|
+
content: `What was the token usage for session ${s.id.substring(0, 12)}?`
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
role: "assistant",
|
|
1355
|
+
content: `Session ${s.id.substring(0, 12)} used ${s.total_tokens.toLocaleString()} tokens across ${s.request_count} requests on project "${s.project_name || "unknown"}" (${s.agent}).`
|
|
1356
|
+
}
|
|
1357
|
+
]
|
|
1358
|
+
});
|
|
1359
|
+
if (examples.length >= limit)
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
const modelBreakdown = queryModelBreakdown(db);
|
|
1363
|
+
if (modelBreakdown.length > 0) {
|
|
1364
|
+
const topModels = modelBreakdown.slice(0, 5);
|
|
1365
|
+
examples.push({
|
|
1366
|
+
messages: [
|
|
1367
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1368
|
+
{ role: "user", content: "Which AI models have I spent the most on?" },
|
|
1369
|
+
{
|
|
1370
|
+
role: "assistant",
|
|
1371
|
+
content: `Model cost breakdown (top ${topModels.length}):
|
|
1372
|
+
${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
|
|
1373
|
+
`)}`
|
|
1374
|
+
}
|
|
1375
|
+
]
|
|
1376
|
+
});
|
|
1377
|
+
for (const m of topModels) {
|
|
1378
|
+
examples.push({
|
|
1379
|
+
messages: [
|
|
1380
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1381
|
+
{ role: "user", content: `How much have I spent on ${m.model}?` },
|
|
1382
|
+
{
|
|
1383
|
+
role: "assistant",
|
|
1384
|
+
content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
|
|
1385
|
+
}
|
|
1386
|
+
]
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const projectBreakdown = queryProjectBreakdown(db);
|
|
1391
|
+
if (projectBreakdown.length > 0) {
|
|
1392
|
+
const topProjects = projectBreakdown.slice(0, 5);
|
|
1393
|
+
examples.push({
|
|
1394
|
+
messages: [
|
|
1395
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1396
|
+
{ role: "user", content: "Which projects are costing the most?" },
|
|
1397
|
+
{
|
|
1398
|
+
role: "assistant",
|
|
1399
|
+
content: `Project cost breakdown (top ${topProjects.length}):
|
|
1400
|
+
${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
|
|
1401
|
+
`)}`
|
|
1402
|
+
}
|
|
1403
|
+
]
|
|
1404
|
+
});
|
|
1405
|
+
for (const p of topProjects.slice(0, 3)) {
|
|
1406
|
+
examples.push({
|
|
1407
|
+
messages: [
|
|
1408
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1409
|
+
{ role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
|
|
1410
|
+
{
|
|
1411
|
+
role: "assistant",
|
|
1412
|
+
content: `Project "${p.project_name}": $${p.cost_usd.toFixed(4)} across ${p.sessions} session(s) and ${p.requests.toLocaleString()} requests. Last active: ${p.last_active?.substring(0, 10) ?? "unknown"}.`
|
|
1413
|
+
}
|
|
1414
|
+
]
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const budgets = getBudgetStatuses(db);
|
|
1420
|
+
if (budgets.length > 0) {
|
|
1421
|
+
examples.push({
|
|
1422
|
+
messages: [
|
|
1423
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1424
|
+
{ role: "user", content: "How am I tracking against my AI spending budgets?" },
|
|
1425
|
+
{
|
|
1426
|
+
role: "assistant",
|
|
1427
|
+
content: `Budget status:
|
|
1428
|
+
${budgets.map((b) => `- ${b.project_path ?? "global"} (${b.period}): $${b.current_spend_usd.toFixed(4)} / $${b.limit_usd.toFixed(2)} (${b.percent_used.toFixed(1)}%) \u2014 ${b.is_over_limit ? "OVER LIMIT" : b.is_over_alert ? "ALERT" : "OK"}`).join(`
|
|
1429
|
+
`)}`
|
|
1430
|
+
}
|
|
1431
|
+
]
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
} catch {}
|
|
1435
|
+
try {
|
|
1436
|
+
const goals = getGoalStatuses(db);
|
|
1437
|
+
if (goals.length > 0) {
|
|
1438
|
+
examples.push({
|
|
1439
|
+
messages: [
|
|
1440
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1441
|
+
{ role: "user", content: "Am I on track with my AI cost reduction goals?" },
|
|
1442
|
+
{
|
|
1443
|
+
role: "assistant",
|
|
1444
|
+
content: `Goal progress:
|
|
1445
|
+
${goals.map((g) => `- ${g.period} goal (${g.project_path ?? g.agent ?? "global"}): $${g.current_spend_usd.toFixed(4)} / $${g.limit_usd.toFixed(2)} (${g.percent_used.toFixed(1)}%) \u2014 ${g.is_over ? "OVER" : g.is_at_risk ? "AT RISK" : "ON TRACK"}`).join(`
|
|
1446
|
+
`)}`
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
} catch {}
|
|
1452
|
+
const weekSummary = querySummary(db, "week");
|
|
1453
|
+
const monthSummary = querySummary(db, "month");
|
|
1454
|
+
if (weekSummary.total_usd > 0) {
|
|
1455
|
+
const projectedMonthly = weekSummary.total_usd / 7 * 30;
|
|
1456
|
+
examples.push({
|
|
1457
|
+
messages: [
|
|
1458
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1459
|
+
{ role: "user", content: "Based on my usage, what will I spend this month?" },
|
|
1460
|
+
{
|
|
1461
|
+
role: "assistant",
|
|
1462
|
+
content: `Based on your last 7 days ($${weekSummary.total_usd.toFixed(4)}), projected monthly spend is approximately $${projectedMonthly.toFixed(2)}. Current month-to-date: $${monthSummary.total_usd.toFixed(4)}.`
|
|
1463
|
+
}
|
|
1464
|
+
]
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
if (modelBreakdown.length > 1) {
|
|
1468
|
+
const expensive = modelBreakdown[0];
|
|
1469
|
+
const cheaper = modelBreakdown[modelBreakdown.length - 1];
|
|
1470
|
+
examples.push({
|
|
1471
|
+
messages: [
|
|
1472
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1473
|
+
{ role: "user", content: "How can I reduce my AI API costs?" },
|
|
1474
|
+
{
|
|
1475
|
+
role: "assistant",
|
|
1476
|
+
content: `Your most expensive model is ${expensive.model} at $${expensive.cost_usd.toFixed(4)}. Consider switching some workloads to ${cheaper.model} ($${cheaper.cost_usd.toFixed(4)}) for cost savings. Cache frequently repeated prompts to reduce cache-miss costs.`
|
|
1477
|
+
}
|
|
1478
|
+
]
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
} catch {}
|
|
1482
|
+
const finalExamples = examples.slice(0, limit);
|
|
1483
|
+
return { source: "economy", examples: finalExamples, count: finalExamples.length };
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
// src/lib/model-config.ts
|
|
1487
|
+
init_database();
|
|
1488
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1489
|
+
import { join as join2 } from "path";
|
|
1490
|
+
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
1491
|
+
var CONFIG_PATH = join2(getDataDir(), "config.json");
|
|
1492
|
+
function loadConfig() {
|
|
1493
|
+
try {
|
|
1494
|
+
if (existsSync2(CONFIG_PATH)) {
|
|
1495
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1496
|
+
}
|
|
1497
|
+
} catch {}
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
function saveConfig(config) {
|
|
1501
|
+
const dir = getDataDir();
|
|
1502
|
+
if (!existsSync2(dir))
|
|
1503
|
+
mkdirSync2(dir, { recursive: true });
|
|
1504
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
1505
|
+
`);
|
|
1506
|
+
}
|
|
1507
|
+
function getActiveModel() {
|
|
1508
|
+
return loadConfig().activeModel ?? DEFAULT_MODEL;
|
|
1509
|
+
}
|
|
1510
|
+
function setActiveModel(id) {
|
|
1511
|
+
const config = loadConfig();
|
|
1512
|
+
config.activeModel = id;
|
|
1513
|
+
saveConfig(config);
|
|
1514
|
+
}
|
|
1515
|
+
function clearActiveModel() {
|
|
1516
|
+
const config = loadConfig();
|
|
1517
|
+
delete config.activeModel;
|
|
1518
|
+
saveConfig(config);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/cli/brains.ts
|
|
1522
|
+
init_database();
|
|
1523
|
+
function registerBrainsCommand(program) {
|
|
1524
|
+
const brainsCmd = program.command("brains").description("Fine-tune an AI model on your economy cost data");
|
|
1525
|
+
brainsCmd.command("gather").description("Gather training data from economy cost data and write to ~/.hasna/economy/training/").option("--limit <n>", "Maximum number of training examples", "500").option("--output <path>", "Output file path (default: ~/.hasna/economy/training/training-<timestamp>.jsonl)").action(async (opts) => {
|
|
1526
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : 500;
|
|
1527
|
+
console.log(chalk.cyan(`Gathering up to ${limit} training examples from economy data...`));
|
|
1528
|
+
try {
|
|
1529
|
+
const result = await gatherTrainingData({ limit });
|
|
1530
|
+
if (result.count === 0) {
|
|
1531
|
+
console.log(chalk.yellow("No training examples found. Make sure you have cost data synced."));
|
|
1532
|
+
console.log(chalk.dim("Run: economy sync"));
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const defaultDir = join3(getDataDir(), "training");
|
|
1536
|
+
await mkdir(defaultDir, { recursive: true });
|
|
1537
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1538
|
+
const outputPath = opts.output ?? join3(defaultDir, `training-${timestamp}.jsonl`);
|
|
1539
|
+
const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
|
|
1540
|
+
`);
|
|
1541
|
+
await writeFile(outputPath, jsonl, "utf-8");
|
|
1542
|
+
console.log(chalk.green(`\u2713 Gathered ${result.count} training examples`));
|
|
1543
|
+
console.log(chalk.dim(` Written to: ${outputPath}`));
|
|
1544
|
+
console.log(`
|
|
1545
|
+
${chalk.dim("Next step:")} economy brains train --base-model gpt-4o-mini`);
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
1548
|
+
process.exit(1);
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
brainsCmd.command("train").description("Start a fine-tuning job using gathered training data").option("--base-model <model>", "Base model to fine-tune", "gpt-4o-mini").option("--name <name>", "Name for the fine-tuned model", "economy-assistant").option("--dataset <path>", "Path to JSONL training file (default: latest in ~/.hasna/economy/training/)").action(async (opts) => {
|
|
1552
|
+
const baseModel = opts.baseModel ?? "gpt-4o-mini";
|
|
1553
|
+
const name = opts.name ?? "economy-assistant";
|
|
1554
|
+
console.log(chalk.cyan("Starting fine-tuning job..."));
|
|
1555
|
+
console.log(chalk.dim(` Base model: ${baseModel}`));
|
|
1556
|
+
console.log(chalk.dim(` Name: ${name}`));
|
|
1557
|
+
let datasetPath = opts.dataset;
|
|
1558
|
+
if (!datasetPath) {
|
|
1559
|
+
const { readdirSync: readdirSync2 } = await import("fs");
|
|
1560
|
+
const trainingDir = join3(getDataDir(), "training");
|
|
1561
|
+
try {
|
|
1562
|
+
const files = readdirSync2(trainingDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
1563
|
+
if (files.length === 0) {
|
|
1564
|
+
console.error(chalk.red("No training data found. Run: economy brains gather"));
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
datasetPath = join3(trainingDir, files[0]);
|
|
1568
|
+
console.log(chalk.dim(` Dataset: ${datasetPath}`));
|
|
1569
|
+
} catch {
|
|
1570
|
+
console.error(chalk.red("Training directory not found. Run: economy brains gather"));
|
|
1571
|
+
process.exit(1);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
try {
|
|
1575
|
+
const brains = await import("@hasna/brains");
|
|
1576
|
+
const startFinetune = brains["startFinetune"] ?? brains["start_finetune"];
|
|
1577
|
+
if (typeof startFinetune !== "function") {
|
|
1578
|
+
console.error(chalk.red("@hasna/brains not found or startFinetune not exported."));
|
|
1579
|
+
console.error(chalk.dim("Install with: bun add @hasna/brains"));
|
|
1580
|
+
process.exit(1);
|
|
1581
|
+
}
|
|
1582
|
+
const job = await startFinetune({
|
|
1583
|
+
provider: "openai",
|
|
1584
|
+
baseModel,
|
|
1585
|
+
name,
|
|
1586
|
+
dataset: datasetPath
|
|
1587
|
+
});
|
|
1588
|
+
const jobId = job["id"] ?? job["fine_tune_job_id"] ?? job["jobId"];
|
|
1589
|
+
console.log(chalk.green(`\u2713 Fine-tuning job started: ${String(jobId ?? "unknown")}`));
|
|
1590
|
+
console.log(`
|
|
1591
|
+
${chalk.dim("Check status:")} economy brains status ${String(jobId ?? "")}`);
|
|
1592
|
+
console.log(`${chalk.dim("When complete, set model:")} economy brains model set <model-id>`);
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
console.error(chalk.red(`Error starting fine-tune: ${e instanceof Error ? e.message : String(e)}`));
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
const modelCmd = brainsCmd.command("model").description("View or set the active fine-tuned model").action(() => {
|
|
1599
|
+
const active = getActiveModel();
|
|
1600
|
+
const isDefault = active === DEFAULT_MODEL;
|
|
1601
|
+
console.log(`Active model: ${chalk.cyan(active)}${isDefault ? chalk.dim(" (default)") : chalk.green(" (fine-tuned)")}`);
|
|
1602
|
+
if (isDefault) {
|
|
1603
|
+
console.log(chalk.dim(`
|
|
1604
|
+
To set a fine-tuned model: economy brains model set <model-id>`));
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
modelCmd.command("set <id>").description("Set the active fine-tuned model ID").action((id) => {
|
|
1608
|
+
setActiveModel(id);
|
|
1609
|
+
console.log(chalk.green(`\u2713 Active model set to: ${id}`));
|
|
1610
|
+
console.log(chalk.dim(" Economy AI analysis will now use this model."));
|
|
1611
|
+
});
|
|
1612
|
+
modelCmd.command("clear").description(`Reset to default model (${DEFAULT_MODEL})`).action(() => {
|
|
1613
|
+
clearActiveModel();
|
|
1614
|
+
console.log(chalk.green(`\u2713 Active model cleared, using default: ${DEFAULT_MODEL}`));
|
|
1615
|
+
});
|
|
1616
|
+
brainsCmd.command("status [job-id]").description("Check the status of a fine-tuning job").option("--provider <provider>", "Provider: openai|thinker-labs", "openai").action(async (jobId, opts) => {
|
|
1617
|
+
if (!jobId) {
|
|
1618
|
+
console.error(chalk.red("Usage: economy brains status <job-id>"));
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
const brains = await import("@hasna/brains");
|
|
1623
|
+
const getFinetuneStatus = brains["getFinetuneStatus"] ?? brains["get_finetune_status"];
|
|
1624
|
+
if (typeof getFinetuneStatus !== "function") {
|
|
1625
|
+
console.error(chalk.red("@hasna/brains not installed. Run: bun add @hasna/brains"));
|
|
1626
|
+
process.exit(1);
|
|
1627
|
+
}
|
|
1628
|
+
const status = await getFinetuneStatus({
|
|
1629
|
+
jobId,
|
|
1630
|
+
provider: opts.provider ?? "openai"
|
|
1631
|
+
});
|
|
1632
|
+
console.log(`Job ${chalk.cyan(jobId)}:`);
|
|
1633
|
+
console.log(` Status: ${String(status["status"] ?? "unknown")}`);
|
|
1634
|
+
if (status["fine_tuned_model"]) {
|
|
1635
|
+
console.log(` Fine-tuned model: ${chalk.green(String(status["fine_tuned_model"]))}`);
|
|
1636
|
+
console.log(`
|
|
1637
|
+
${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tuned_model"])}`);
|
|
1638
|
+
}
|
|
1639
|
+
if (status["error"]) {
|
|
1640
|
+
console.log(chalk.red(` Error: ${String(status["error"])}`));
|
|
1641
|
+
}
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1261
1649
|
// src/cli/index.ts
|
|
1262
1650
|
init_database();
|
|
1263
1651
|
init_claude();
|
|
1264
1652
|
init_codex();
|
|
1265
|
-
import { Command } from "commander";
|
|
1266
|
-
import chalk3 from "chalk";
|
|
1267
1653
|
|
|
1268
1654
|
// src/ingest/gemini.ts
|
|
1269
1655
|
init_database();
|
|
1270
|
-
import { readdirSync as
|
|
1656
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
1271
1657
|
import { homedir as homedir4 } from "os";
|
|
1272
|
-
import { join as
|
|
1273
|
-
var GEMINI_TMP_DIR =
|
|
1658
|
+
import { join as join6 } from "path";
|
|
1659
|
+
var GEMINI_TMP_DIR = join6(homedir4(), ".gemini", "tmp");
|
|
1274
1660
|
async function ingestGemini(db, verbose) {
|
|
1275
|
-
if (!
|
|
1661
|
+
if (!existsSync5(GEMINI_TMP_DIR)) {
|
|
1276
1662
|
if (verbose)
|
|
1277
1663
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
1278
1664
|
return { sessions: 0 };
|
|
@@ -1281,17 +1667,17 @@ async function ingestGemini(db, verbose) {
|
|
|
1281
1667
|
const touchedSessions = new Set;
|
|
1282
1668
|
let projectHashDirs = [];
|
|
1283
1669
|
try {
|
|
1284
|
-
projectHashDirs =
|
|
1670
|
+
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join6(GEMINI_TMP_DIR, d.name));
|
|
1285
1671
|
} catch {
|
|
1286
1672
|
return { sessions: 0 };
|
|
1287
1673
|
}
|
|
1288
1674
|
for (const projectDir of projectHashDirs) {
|
|
1289
|
-
const chatsDir =
|
|
1290
|
-
if (!
|
|
1675
|
+
const chatsDir = join6(projectDir, "chats");
|
|
1676
|
+
if (!existsSync5(chatsDir))
|
|
1291
1677
|
continue;
|
|
1292
1678
|
let chatFiles = [];
|
|
1293
1679
|
try {
|
|
1294
|
-
chatFiles =
|
|
1680
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
|
|
1295
1681
|
} catch {
|
|
1296
1682
|
continue;
|
|
1297
1683
|
}
|
|
@@ -1299,7 +1685,7 @@ async function ingestGemini(db, verbose) {
|
|
|
1299
1685
|
const stateKey = filePath.replace(homedir4(), "~");
|
|
1300
1686
|
let fileMtime = "0";
|
|
1301
1687
|
try {
|
|
1302
|
-
fileMtime =
|
|
1688
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
1303
1689
|
} catch {
|
|
1304
1690
|
continue;
|
|
1305
1691
|
}
|
|
@@ -1308,7 +1694,7 @@ async function ingestGemini(db, verbose) {
|
|
|
1308
1694
|
continue;
|
|
1309
1695
|
let chatData;
|
|
1310
1696
|
try {
|
|
1311
|
-
chatData = JSON.parse(
|
|
1697
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
1312
1698
|
} catch {
|
|
1313
1699
|
continue;
|
|
1314
1700
|
}
|
|
@@ -1376,7 +1762,7 @@ function fmt2(usd) {
|
|
|
1376
1762
|
} else {
|
|
1377
1763
|
formatted = "$0.00";
|
|
1378
1764
|
}
|
|
1379
|
-
return
|
|
1765
|
+
return chalk4.green(formatted);
|
|
1380
1766
|
}
|
|
1381
1767
|
function fmtTokens(n) {
|
|
1382
1768
|
if (n >= 1e9)
|
|
@@ -1421,13 +1807,13 @@ function printSummary(label, period) {
|
|
|
1421
1807
|
ensurePricingSeeded(db);
|
|
1422
1808
|
const s = querySummary(db, period);
|
|
1423
1809
|
console.log();
|
|
1424
|
-
console.log(
|
|
1810
|
+
console.log(chalk4.bold.cyan(` ${label}`));
|
|
1425
1811
|
console.log();
|
|
1426
1812
|
printTable(["Metric", "Value"], [
|
|
1427
1813
|
["Total cost", fmt2(s.total_usd)],
|
|
1428
|
-
["Sessions",
|
|
1429
|
-
["Requests",
|
|
1430
|
-
["Tokens",
|
|
1814
|
+
["Sessions", chalk4.yellow(fmtCount(s.sessions))],
|
|
1815
|
+
["Requests", chalk4.yellow(fmtCount(s.requests))],
|
|
1816
|
+
["Tokens", chalk4.yellow(fmtTokens(s.tokens))]
|
|
1431
1817
|
]);
|
|
1432
1818
|
console.log();
|
|
1433
1819
|
}
|
|
@@ -1444,7 +1830,7 @@ program.action(async () => {
|
|
|
1444
1830
|
}, {});
|
|
1445
1831
|
const dailyValues = Object.values(daily);
|
|
1446
1832
|
console.log();
|
|
1447
|
-
console.log(
|
|
1833
|
+
console.log(chalk4.bold.cyan(" Economy"));
|
|
1448
1834
|
console.log();
|
|
1449
1835
|
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
1450
1836
|
["Today", fmt2(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens(t.tokens)],
|
|
@@ -1453,13 +1839,13 @@ program.action(async () => {
|
|
|
1453
1839
|
]);
|
|
1454
1840
|
if (dailyValues.length > 0) {
|
|
1455
1841
|
console.log(`
|
|
1456
|
-
${
|
|
1842
|
+
${chalk4.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1457
1843
|
}
|
|
1458
1844
|
if (projects.length > 0) {
|
|
1459
1845
|
console.log(`
|
|
1460
|
-
${
|
|
1846
|
+
${chalk4.dim("Top projects:")}`);
|
|
1461
1847
|
for (const p of projects) {
|
|
1462
|
-
console.log(` ${
|
|
1848
|
+
console.log(` ${chalk4.white(p.project_name.padEnd(25))} ${fmt2(p.cost_usd)}`);
|
|
1463
1849
|
}
|
|
1464
1850
|
}
|
|
1465
1851
|
console.log();
|
|
@@ -1470,32 +1856,32 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1470
1856
|
if (opts.force) {
|
|
1471
1857
|
db.exec(`DELETE FROM ingest_state WHERE source = 'claude'`);
|
|
1472
1858
|
if (opts.verbose)
|
|
1473
|
-
console.log(
|
|
1859
|
+
console.log(chalk4.dim("Cleared ingest cache"));
|
|
1474
1860
|
}
|
|
1475
1861
|
const anySpecific = opts.claude || opts.codex || opts.gemini;
|
|
1476
1862
|
const doClaude = opts.claude || !anySpecific;
|
|
1477
1863
|
const doCodex = opts.codex || !anySpecific;
|
|
1478
1864
|
const doGemini = opts.gemini || !anySpecific;
|
|
1479
1865
|
if (doClaude) {
|
|
1480
|
-
process.stdout.write(
|
|
1866
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting Claude Code telemetry... "));
|
|
1481
1867
|
const r = await ingestClaude(db, opts.verbose);
|
|
1482
|
-
console.log(
|
|
1868
|
+
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
1483
1869
|
}
|
|
1484
1870
|
if (doCodex) {
|
|
1485
|
-
process.stdout.write(
|
|
1871
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
|
|
1486
1872
|
const r = await ingestCodex(db, opts.verbose);
|
|
1487
|
-
console.log(
|
|
1873
|
+
console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
|
|
1488
1874
|
}
|
|
1489
1875
|
if (doGemini) {
|
|
1490
|
-
process.stdout.write(
|
|
1876
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting Gemini CLI sessions... "));
|
|
1491
1877
|
const r = await ingestGemini(db, opts.verbose);
|
|
1492
|
-
console.log(
|
|
1878
|
+
console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
|
|
1493
1879
|
}
|
|
1494
1880
|
try {
|
|
1495
1881
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
1496
1882
|
await checkAndFireWebhooks2(db);
|
|
1497
1883
|
} catch {}
|
|
1498
|
-
console.log(
|
|
1884
|
+
console.log(chalk4.bold.green(`
|
|
1499
1885
|
\u2713 Sync complete`));
|
|
1500
1886
|
});
|
|
1501
1887
|
program.command("today").description("Cost summary for today").action(async () => {
|
|
@@ -1522,7 +1908,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
1522
1908
|
search: opts.search
|
|
1523
1909
|
});
|
|
1524
1910
|
if (sessions.length === 0) {
|
|
1525
|
-
console.log(
|
|
1911
|
+
console.log(chalk4.yellow("No sessions found."));
|
|
1526
1912
|
return;
|
|
1527
1913
|
}
|
|
1528
1914
|
const f = opts.format ?? "table";
|
|
@@ -1544,13 +1930,13 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
1544
1930
|
}
|
|
1545
1931
|
console.log();
|
|
1546
1932
|
printTable(["Session ID", "Agent", "Project", "Cost", "Tokens", "Requests", "Started"], sessions.map((s) => [
|
|
1547
|
-
|
|
1548
|
-
s.agent === "claude" ?
|
|
1549
|
-
|
|
1933
|
+
chalk4.dim(s.id.substring(0, 12)),
|
|
1934
|
+
s.agent === "claude" ? chalk4.blue("claude") : chalk4.yellow("codex"),
|
|
1935
|
+
chalk4.white(s.project_name || chalk4.dim("unknown")),
|
|
1550
1936
|
fmt2(s.total_cost_usd),
|
|
1551
|
-
|
|
1937
|
+
chalk4.cyan(fmtTokens(s.total_tokens)),
|
|
1552
1938
|
fmtCount(s.request_count),
|
|
1553
|
-
|
|
1939
|
+
chalk4.dim(s.started_at.substring(0, 16))
|
|
1554
1940
|
]));
|
|
1555
1941
|
console.log();
|
|
1556
1942
|
});
|
|
@@ -1561,17 +1947,17 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
1561
1947
|
if (sinceDate)
|
|
1562
1948
|
sessions = sessions.filter((s) => s.started_at >= sinceDate);
|
|
1563
1949
|
if (sessions.length === 0) {
|
|
1564
|
-
console.log(
|
|
1950
|
+
console.log(chalk4.yellow("No sessions found. Run `economy sync` first."));
|
|
1565
1951
|
return;
|
|
1566
1952
|
}
|
|
1567
1953
|
console.log();
|
|
1568
1954
|
printTable(["#", "Project", "Agent", "Cost", "Tokens", "Started"], sessions.map((s, i) => [
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
s.agent === "claude" ?
|
|
1955
|
+
chalk4.dim(String(i + 1)),
|
|
1956
|
+
chalk4.white(s.project_name || chalk4.dim("unknown")),
|
|
1957
|
+
s.agent === "claude" ? chalk4.blue("claude") : chalk4.yellow("codex"),
|
|
1572
1958
|
fmt2(s.total_cost_usd),
|
|
1573
|
-
|
|
1574
|
-
|
|
1959
|
+
chalk4.cyan(fmtTokens(s.total_tokens)),
|
|
1960
|
+
chalk4.dim(s.started_at.substring(0, 16))
|
|
1575
1961
|
]));
|
|
1576
1962
|
console.log();
|
|
1577
1963
|
});
|
|
@@ -1591,10 +1977,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, or pro
|
|
|
1591
1977
|
GROUP BY project_path ORDER BY cost_usd DESC
|
|
1592
1978
|
`).all(sinceDate) : queryProjectBreakdown(db);
|
|
1593
1979
|
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], rows.map((r) => [
|
|
1594
|
-
|
|
1980
|
+
chalk4.white(r.project_name || chalk4.dim("unknown")),
|
|
1595
1981
|
String(r.sessions),
|
|
1596
1982
|
String(r.requests),
|
|
1597
|
-
|
|
1983
|
+
chalk4.cyan(fmtTokens(r.total_tokens)),
|
|
1598
1984
|
fmt2(r.cost_usd)
|
|
1599
1985
|
]));
|
|
1600
1986
|
} else {
|
|
@@ -1609,10 +1995,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, or pro
|
|
|
1609
1995
|
GROUP BY model, agent ORDER BY cost_usd DESC
|
|
1610
1996
|
`).all(sinceDate) : queryModelBreakdown(db);
|
|
1611
1997
|
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], rows.map((r) => [
|
|
1612
|
-
|
|
1613
|
-
r.agent === "claude" ?
|
|
1998
|
+
chalk4.white(r.model),
|
|
1999
|
+
r.agent === "claude" ? chalk4.blue("claude") : chalk4.yellow("codex"),
|
|
1614
2000
|
String(r.requests),
|
|
1615
|
-
|
|
2001
|
+
chalk4.cyan(fmtTokens(r.total_tokens)),
|
|
1616
2002
|
fmt2(r.cost_usd)
|
|
1617
2003
|
]));
|
|
1618
2004
|
}
|
|
@@ -1625,7 +2011,7 @@ program.command("watch").description("Live stream of incoming costs").option("--
|
|
|
1625
2011
|
var budgetCmd = program.command("budget").description("Manage spending budgets");
|
|
1626
2012
|
budgetCmd.command("set").description("Set a budget").option("--project <path>", "Project path (omit for global)").option("--period <period>", "Period: daily|weekly|monthly", "monthly").option("--limit <usd>", "Budget limit in USD").option("--alert <percent>", "Alert threshold %", "80").option("--agent <agent>", "Limit to agent (claude|codex)").action((opts) => {
|
|
1627
2013
|
if (!opts.limit) {
|
|
1628
|
-
console.error(
|
|
2014
|
+
console.error(chalk4.red("--limit is required"));
|
|
1629
2015
|
process.exit(1);
|
|
1630
2016
|
}
|
|
1631
2017
|
const db = openDatabase();
|
|
@@ -1640,22 +2026,22 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
1640
2026
|
created_at: now,
|
|
1641
2027
|
updated_at: now
|
|
1642
2028
|
});
|
|
1643
|
-
console.log(
|
|
2029
|
+
console.log(chalk4.green(`\u2713 Budget set: ${opts.project ?? "global"} \u2014 ${opts.period} $${opts.limit}`));
|
|
1644
2030
|
});
|
|
1645
2031
|
budgetCmd.command("list").description("List all budgets").action(() => {
|
|
1646
2032
|
const db = openDatabase();
|
|
1647
2033
|
const statuses = getBudgetStatuses(db);
|
|
1648
2034
|
if (statuses.length === 0) {
|
|
1649
|
-
console.log(
|
|
2035
|
+
console.log(chalk4.yellow("No budgets set."));
|
|
1650
2036
|
return;
|
|
1651
2037
|
}
|
|
1652
2038
|
console.log();
|
|
1653
2039
|
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], statuses.map((b) => {
|
|
1654
2040
|
const pct = b.percent_used.toFixed(1);
|
|
1655
|
-
const status = b.is_over_limit ?
|
|
1656
|
-
const pctColor = b.is_over_limit ?
|
|
2041
|
+
const status = b.is_over_limit ? chalk4.red("OVER") : b.is_over_alert ? chalk4.yellow("ALERT") : chalk4.green("OK");
|
|
2042
|
+
const pctColor = b.is_over_limit ? chalk4.red(pct + "%") : b.is_over_alert ? chalk4.yellow(pct + "%") : chalk4.green(pct + "%");
|
|
1657
2043
|
return [
|
|
1658
|
-
|
|
2044
|
+
chalk4.white(b.project_path ?? "global"),
|
|
1659
2045
|
b.period,
|
|
1660
2046
|
fmt2(b.limit_usd),
|
|
1661
2047
|
fmt2(b.current_spend_usd),
|
|
@@ -1668,7 +2054,7 @@ budgetCmd.command("list").description("List all budgets").action(() => {
|
|
|
1668
2054
|
budgetCmd.command("remove <id>").description("Remove a budget by ID").action((id) => {
|
|
1669
2055
|
const db = openDatabase();
|
|
1670
2056
|
deleteBudget(db, id);
|
|
1671
|
-
console.log(
|
|
2057
|
+
console.log(chalk4.green(`\u2713 Budget removed`));
|
|
1672
2058
|
});
|
|
1673
2059
|
var projectCmd = program.command("project").description("Manage tracked projects");
|
|
1674
2060
|
projectCmd.command("add <path>").description("Add a project").option("--name <name>", "Human-readable name").action((path, opts) => {
|
|
@@ -1682,46 +2068,46 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
|
|
|
1682
2068
|
tags: [],
|
|
1683
2069
|
created_at: new Date().toISOString()
|
|
1684
2070
|
});
|
|
1685
|
-
console.log(
|
|
2071
|
+
console.log(chalk4.green(`\u2713 Project added: ${path}`));
|
|
1686
2072
|
});
|
|
1687
2073
|
projectCmd.command("list").description("List all projects with costs").action(() => {
|
|
1688
2074
|
const db = openDatabase();
|
|
1689
2075
|
const projects = queryProjectBreakdown(db);
|
|
1690
2076
|
if (projects.length === 0) {
|
|
1691
|
-
console.log(
|
|
2077
|
+
console.log(chalk4.yellow("No projects tracked yet."));
|
|
1692
2078
|
return;
|
|
1693
2079
|
}
|
|
1694
2080
|
console.log();
|
|
1695
2081
|
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], projects.map((p) => [
|
|
1696
|
-
|
|
1697
|
-
|
|
2082
|
+
chalk4.white(p.project_name || chalk4.dim("unknown")),
|
|
2083
|
+
chalk4.dim(p.project_path.substring(0, 40)),
|
|
1698
2084
|
String(p.sessions),
|
|
1699
2085
|
fmt2(p.cost_usd),
|
|
1700
|
-
|
|
2086
|
+
chalk4.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
1701
2087
|
]));
|
|
1702
2088
|
console.log();
|
|
1703
2089
|
});
|
|
1704
2090
|
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action((path) => {
|
|
1705
2091
|
const db = openDatabase();
|
|
1706
2092
|
deleteProject(db, path);
|
|
1707
|
-
console.log(
|
|
2093
|
+
console.log(chalk4.green(`\u2713 Project removed`));
|
|
1708
2094
|
});
|
|
1709
2095
|
projectCmd.command("rename <path> <name>").description("Rename a project").action((path, name) => {
|
|
1710
2096
|
const db = openDatabase();
|
|
1711
2097
|
const existing = getProject(db, path);
|
|
1712
2098
|
if (!existing) {
|
|
1713
|
-
console.error(
|
|
2099
|
+
console.error(chalk4.red("Project not found"));
|
|
1714
2100
|
process.exit(1);
|
|
1715
2101
|
}
|
|
1716
2102
|
upsertProject(db, { ...existing, name });
|
|
1717
|
-
console.log(
|
|
2103
|
+
console.log(chalk4.green(`\u2713 Renamed to: ${name}`));
|
|
1718
2104
|
});
|
|
1719
2105
|
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
1720
2106
|
await autoSync();
|
|
1721
2107
|
const db = openDatabase();
|
|
1722
2108
|
const sessions = db.prepare(`SELECT * FROM sessions WHERE project_name LIKE ? OR project_path LIKE ? ORDER BY started_at DESC`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1723
2109
|
if (sessions.length === 0) {
|
|
1724
|
-
console.log(
|
|
2110
|
+
console.log(chalk4.yellow(`No sessions found for: ${nameOrPath}`));
|
|
1725
2111
|
return;
|
|
1726
2112
|
}
|
|
1727
2113
|
const projectName = sessions[0]["project_name"] || nameOrPath;
|
|
@@ -1743,8 +2129,8 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
1743
2129
|
GROUP BY r.model ORDER BY cost DESC LIMIT 5
|
|
1744
2130
|
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1745
2131
|
console.log();
|
|
1746
|
-
console.log(
|
|
1747
|
-
console.log(
|
|
2132
|
+
console.log(chalk4.bold.cyan(` ${projectName}`));
|
|
2133
|
+
console.log(chalk4.dim(` ${projectPath}`));
|
|
1748
2134
|
console.log();
|
|
1749
2135
|
printTable(["Metric", "Value"], [
|
|
1750
2136
|
["Total cost", fmt2(totalCost)],
|
|
@@ -1753,21 +2139,21 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
1753
2139
|
]);
|
|
1754
2140
|
if (dailyValues.length > 0) {
|
|
1755
2141
|
console.log(`
|
|
1756
|
-
${
|
|
2142
|
+
${chalk4.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1757
2143
|
}
|
|
1758
2144
|
if (models.length > 0) {
|
|
1759
2145
|
console.log(`
|
|
1760
|
-
${
|
|
2146
|
+
${chalk4.dim("Model breakdown:")}`);
|
|
1761
2147
|
for (const m of models) {
|
|
1762
|
-
console.log(` ${
|
|
2148
|
+
console.log(` ${chalk4.white(m.model.padEnd(30))} ${fmt2(m.cost)} (${fmtCount(m.reqs)} reqs)`);
|
|
1763
2149
|
}
|
|
1764
2150
|
}
|
|
1765
2151
|
const topSessions = sessions.sort((a, b) => b["total_cost_usd"] - a["total_cost_usd"]).slice(0, 5);
|
|
1766
2152
|
if (topSessions.length > 0) {
|
|
1767
2153
|
console.log(`
|
|
1768
|
-
${
|
|
2154
|
+
${chalk4.dim("Top sessions:")}`);
|
|
1769
2155
|
for (const s of topSessions) {
|
|
1770
|
-
console.log(` ${
|
|
2156
|
+
console.log(` ${chalk4.dim(s["id"].substring(0, 12))} ${fmt2(s["total_cost_usd"])} ${chalk4.dim(String(s["started_at"]).substring(0, 16))}`);
|
|
1771
2157
|
}
|
|
1772
2158
|
}
|
|
1773
2159
|
console.log();
|
|
@@ -1776,18 +2162,18 @@ var configCmd = program.command("config").description("Manage economy configurat
|
|
|
1776
2162
|
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
1777
2163
|
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1778
2164
|
setConfigValue2(_key, _value);
|
|
1779
|
-
console.log(
|
|
2165
|
+
console.log(chalk4.green(`\u2713 ${_key} = ${_value}`));
|
|
1780
2166
|
});
|
|
1781
2167
|
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
1782
2168
|
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1783
|
-
console.log(getConfigValue2(key) ??
|
|
2169
|
+
console.log(getConfigValue2(key) ?? chalk4.dim("(not set)"));
|
|
1784
2170
|
});
|
|
1785
2171
|
configCmd.command("webhook-test").description("Send a test payload to the configured webhook URL").action(async () => {
|
|
1786
|
-
const { loadConfig:
|
|
1787
|
-
const config =
|
|
2172
|
+
const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
2173
|
+
const config = loadConfig3();
|
|
1788
2174
|
const url = config["webhook-url"];
|
|
1789
2175
|
if (!url) {
|
|
1790
|
-
console.log(
|
|
2176
|
+
console.log(chalk4.yellow("No webhook-url configured. Run: economy config set webhook-url <url>"));
|
|
1791
2177
|
return;
|
|
1792
2178
|
}
|
|
1793
2179
|
const payload = {
|
|
@@ -1804,21 +2190,21 @@ configCmd.command("webhook-test").description("Send a test payload to the config
|
|
|
1804
2190
|
});
|
|
1805
2191
|
const text = await res.text().catch(() => "");
|
|
1806
2192
|
if (res.ok) {
|
|
1807
|
-
console.log(
|
|
2193
|
+
console.log(chalk4.green(`\u2713 Webhook responded: HTTP ${res.status}`));
|
|
1808
2194
|
if (text)
|
|
1809
|
-
console.log(
|
|
2195
|
+
console.log(chalk4.dim(text.slice(0, 200)));
|
|
1810
2196
|
} else {
|
|
1811
|
-
console.log(
|
|
2197
|
+
console.log(chalk4.red(`\u2717 Webhook failed: HTTP ${res.status}`));
|
|
1812
2198
|
if (text)
|
|
1813
|
-
console.log(
|
|
2199
|
+
console.log(chalk4.dim(text.slice(0, 200)));
|
|
1814
2200
|
}
|
|
1815
2201
|
} catch (e) {
|
|
1816
|
-
console.log(
|
|
2202
|
+
console.log(chalk4.red(`\u2717 Request failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1817
2203
|
}
|
|
1818
2204
|
});
|
|
1819
2205
|
configCmd.action(async () => {
|
|
1820
|
-
const { loadConfig:
|
|
1821
|
-
const config =
|
|
2206
|
+
const { loadConfig: loadConfig3 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
2207
|
+
const config = loadConfig3();
|
|
1822
2208
|
console.log();
|
|
1823
2209
|
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
1824
2210
|
console.log();
|
|
@@ -1830,18 +2216,18 @@ pricingCmd.command("list").description("List all model prices").action(() => {
|
|
|
1830
2216
|
const rows = listModelPricing(db);
|
|
1831
2217
|
console.log();
|
|
1832
2218
|
printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "Out/1k"], rows.map((r) => [
|
|
1833
|
-
|
|
2219
|
+
chalk4.white(r.model),
|
|
1834
2220
|
fmt2(r.input_per_1m),
|
|
1835
2221
|
fmt2(r.output_per_1m),
|
|
1836
2222
|
fmt2(r.cache_read_per_1m),
|
|
1837
2223
|
fmt2(r.cache_write_per_1m),
|
|
1838
|
-
|
|
2224
|
+
chalk4.dim(fmt2(r.output_per_1m / 1000))
|
|
1839
2225
|
]));
|
|
1840
2226
|
console.log();
|
|
1841
2227
|
});
|
|
1842
2228
|
pricingCmd.command("set <model>").description("Set pricing for a model").option("--input <usd>", "Input price per 1M tokens").option("--output <usd>", "Output price per 1M tokens").option("--cache-read <usd>", "Cache read price per 1M tokens", "0").option("--cache-write <usd>", "Cache write price per 1M tokens", "0").action((model, opts) => {
|
|
1843
2229
|
if (!opts.input || !opts.output) {
|
|
1844
|
-
console.error(
|
|
2230
|
+
console.error(chalk4.red("--input and --output are required"));
|
|
1845
2231
|
process.exit(1);
|
|
1846
2232
|
}
|
|
1847
2233
|
const db = openDatabase();
|
|
@@ -1854,12 +2240,12 @@ pricingCmd.command("set <model>").description("Set pricing for a model").option(
|
|
|
1854
2240
|
cache_write_per_1m: Number(opts.cacheWrite ?? 0),
|
|
1855
2241
|
updated_at: new Date().toISOString()
|
|
1856
2242
|
});
|
|
1857
|
-
console.log(
|
|
2243
|
+
console.log(chalk4.green(`\u2713 Pricing updated for ${model}`));
|
|
1858
2244
|
});
|
|
1859
2245
|
pricingCmd.command("remove <model>").description("Remove pricing for a model").action((model) => {
|
|
1860
2246
|
const db = openDatabase();
|
|
1861
2247
|
deleteModelPricing(db, model);
|
|
1862
|
-
console.log(
|
|
2248
|
+
console.log(chalk4.green(`\u2713 Pricing removed for ${model}`));
|
|
1863
2249
|
});
|
|
1864
2250
|
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
1865
2251
|
const port = Number(opts.port ?? 3456);
|
|
@@ -1875,7 +2261,7 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
1875
2261
|
serverRunning = res.ok;
|
|
1876
2262
|
} catch {}
|
|
1877
2263
|
if (!serverRunning) {
|
|
1878
|
-
console.log(
|
|
2264
|
+
console.log(chalk4.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
1879
2265
|
const { spawn } = await import("child_process");
|
|
1880
2266
|
const { resolve, dirname } = await import("path");
|
|
1881
2267
|
const serveScript = resolve(dirname(process.argv[1]), "..", "server", "index.js");
|
|
@@ -1898,29 +2284,29 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
1898
2284
|
attempts++;
|
|
1899
2285
|
}
|
|
1900
2286
|
if (serverRunning) {
|
|
1901
|
-
console.log(
|
|
2287
|
+
console.log(chalk4.green(`\u2713 Server started`));
|
|
1902
2288
|
} else {
|
|
1903
|
-
console.log(
|
|
2289
|
+
console.log(chalk4.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
1904
2290
|
}
|
|
1905
2291
|
}
|
|
1906
|
-
console.log(
|
|
2292
|
+
console.log(chalk4.cyan(`Opening ${url}`));
|
|
1907
2293
|
try {
|
|
1908
2294
|
execSync2(`open ${url}`);
|
|
1909
2295
|
} catch {
|
|
1910
|
-
console.log(
|
|
2296
|
+
console.log(chalk4.yellow(`Open your browser at ${url}`));
|
|
1911
2297
|
}
|
|
1912
2298
|
});
|
|
1913
2299
|
program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--all", "Install into all agents").action(async (opts) => {
|
|
1914
2300
|
const doAll = opts.all || !opts.claude && !opts.codex;
|
|
1915
2301
|
if (opts.claude || doAll) {
|
|
1916
|
-
console.log(
|
|
2302
|
+
console.log(chalk4.bold.cyan(`
|
|
1917
2303
|
Claude Code:`));
|
|
1918
|
-
console.log(
|
|
2304
|
+
console.log(chalk4.white(" claude mcp add --transport stdio --scope user economy -- economy-mcp"));
|
|
1919
2305
|
}
|
|
1920
2306
|
if (opts.codex || doAll) {
|
|
1921
|
-
console.log(
|
|
2307
|
+
console.log(chalk4.bold.yellow(`
|
|
1922
2308
|
Codex (~/.codex/config.toml):`));
|
|
1923
|
-
console.log(
|
|
2309
|
+
console.log(chalk4.white(` [mcp_servers.economy]
|
|
1924
2310
|
command = "economy-mcp"
|
|
1925
2311
|
args = []`));
|
|
1926
2312
|
}
|
|
@@ -1931,12 +2317,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
1931
2317
|
const db = openDatabase();
|
|
1932
2318
|
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `%${id}%`);
|
|
1933
2319
|
if (!session) {
|
|
1934
|
-
console.log(
|
|
2320
|
+
console.log(chalk4.red(`Session not found: ${id}`));
|
|
1935
2321
|
process.exit(1);
|
|
1936
2322
|
}
|
|
1937
2323
|
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1938
2324
|
console.log();
|
|
1939
|
-
console.log(
|
|
2325
|
+
console.log(chalk4.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
1940
2326
|
console.log();
|
|
1941
2327
|
printTable(["Field", "Value"], [
|
|
1942
2328
|
["Agent", String(session["agent"])],
|
|
@@ -1948,12 +2334,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
1948
2334
|
["Requests", fmtCount(session["request_count"])]
|
|
1949
2335
|
]);
|
|
1950
2336
|
if (requests.length > 0) {
|
|
1951
|
-
console.log(
|
|
2337
|
+
console.log(chalk4.dim(`
|
|
1952
2338
|
Requests (${requests.length}):
|
|
1953
2339
|
`));
|
|
1954
2340
|
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
1955
|
-
|
|
1956
|
-
|
|
2341
|
+
chalk4.dim(String(r["timestamp"]).substring(11, 19)),
|
|
2342
|
+
chalk4.white(String(r["model"]).substring(0, 22)),
|
|
1957
2343
|
fmtTokens(r["input_tokens"]),
|
|
1958
2344
|
fmtTokens(r["output_tokens"]),
|
|
1959
2345
|
fmtTokens(r["cache_read_tokens"]),
|
|
@@ -1961,7 +2347,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
1961
2347
|
fmt2(r["cost_usd"])
|
|
1962
2348
|
]));
|
|
1963
2349
|
if (requests.length > 50)
|
|
1964
|
-
console.log(
|
|
2350
|
+
console.log(chalk4.dim(` ... and ${requests.length - 50} more requests`));
|
|
1965
2351
|
}
|
|
1966
2352
|
console.log();
|
|
1967
2353
|
});
|
|
@@ -1989,9 +2375,9 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
1989
2375
|
}
|
|
1990
2376
|
}
|
|
1991
2377
|
if (opts.output) {
|
|
1992
|
-
const { writeFileSync:
|
|
1993
|
-
|
|
1994
|
-
console.log(
|
|
2378
|
+
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
2379
|
+
writeFileSync4(opts.output, csv);
|
|
2380
|
+
console.log(chalk4.green(`\u2713 Exported to ${opts.output}`));
|
|
1995
2381
|
} else {
|
|
1996
2382
|
process.stdout.write(csv);
|
|
1997
2383
|
}
|
|
@@ -2052,11 +2438,11 @@ program.command("compare <period1> <period2>").description("Compare two periods
|
|
|
2052
2438
|
const d = v1 - v2;
|
|
2053
2439
|
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
2054
2440
|
const sign = d >= 0 ? "+" : "";
|
|
2055
|
-
const color = d > 0 ?
|
|
2441
|
+
const color = d > 0 ? chalk4.red : d < 0 ? chalk4.green : chalk4.dim;
|
|
2056
2442
|
return color(`${sign}${pct}%`);
|
|
2057
2443
|
}
|
|
2058
2444
|
console.log();
|
|
2059
|
-
console.log(
|
|
2445
|
+
console.log(chalk4.bold.cyan(` ${p1} vs ${p2}`));
|
|
2060
2446
|
console.log();
|
|
2061
2447
|
printTable(["Metric", p1, p2, "Change"], [
|
|
2062
2448
|
["Cost", fmt2(a.cost), fmt2(b.cost), delta(a.cost, b.cost)],
|
|
@@ -2085,12 +2471,12 @@ program.command("forecast").description("Project end-of-month cost based on curr
|
|
|
2085
2471
|
const cheapest = dailyCosts[0];
|
|
2086
2472
|
const mostExpensive = dailyCosts[dailyCosts.length - 1];
|
|
2087
2473
|
console.log();
|
|
2088
|
-
console.log(
|
|
2474
|
+
console.log(chalk4.bold.cyan(` Forecast (${dayOfMonth} of ${daysInMonth} days)`));
|
|
2089
2475
|
console.log();
|
|
2090
2476
|
printTable(["Metric", "Value"], [
|
|
2091
2477
|
["Spent so far", fmt2(monthSoFar.cost)],
|
|
2092
2478
|
["Daily average", fmt2(dailyAvg)],
|
|
2093
|
-
[
|
|
2479
|
+
[chalk4.bold("Projected total"), chalk4.bold(fmt2(projected).replace(chalk4.green(""), ""))],
|
|
2094
2480
|
["Last 7-day rate", `${fmt2(last7DailyAvg)}/day \u2192 ${fmt2(last7Projected)}`],
|
|
2095
2481
|
["Cheapest day", cheapest ? `${fmt2(cheapest.cost)} (${cheapest.d})` : "\u2014"],
|
|
2096
2482
|
["Most expensive", mostExpensive ? `${fmt2(mostExpensive.cost)} (${mostExpensive.d})` : "\u2014"]
|
|
@@ -2107,14 +2493,14 @@ program.command("efficiency").description("Show output/input token ratio per mod
|
|
|
2107
2493
|
FROM requests GROUP BY model ORDER BY cost DESC
|
|
2108
2494
|
`).all();
|
|
2109
2495
|
console.log();
|
|
2110
|
-
console.log(
|
|
2496
|
+
console.log(chalk4.bold.cyan(" Token Efficiency"));
|
|
2111
2497
|
console.log();
|
|
2112
2498
|
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
2113
2499
|
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
2114
2500
|
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
2115
2501
|
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
2116
2502
|
const costPer1kOutput = m.output > 0 ? fmt2(m.cost / m.output * 1000) : "\u2014";
|
|
2117
|
-
return [
|
|
2503
|
+
return [chalk4.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
2118
2504
|
}));
|
|
2119
2505
|
console.log();
|
|
2120
2506
|
});
|
|
@@ -2138,7 +2524,7 @@ menubarCmd.command("stop").description("Quit Economy Bar").action(async () => {
|
|
|
2138
2524
|
var goalCmd = program.command("goal").description("Manage spending goals");
|
|
2139
2525
|
goalCmd.command("set").description("Set a spending goal").option("--period <period>", "Period: day|week|month|year", "month").option("--limit <usd>", "Goal limit in USD").option("--project <path>", "Scope to project path").option("--agent <agent>", "Scope to agent").action((opts) => {
|
|
2140
2526
|
if (!opts.limit) {
|
|
2141
|
-
console.error(
|
|
2527
|
+
console.error(chalk4.red("--limit is required"));
|
|
2142
2528
|
process.exit(1);
|
|
2143
2529
|
}
|
|
2144
2530
|
const db = openDatabase();
|
|
@@ -2152,24 +2538,24 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
|
|
|
2152
2538
|
created_at: now,
|
|
2153
2539
|
updated_at: now
|
|
2154
2540
|
});
|
|
2155
|
-
console.log(
|
|
2541
|
+
console.log(chalk4.green(`\u2713 Goal set: ${opts.period ?? "month"} $${opts.limit}${opts.project ? ` (${opts.project})` : ""}`));
|
|
2156
2542
|
});
|
|
2157
2543
|
goalCmd.command("list").description("List all goals with progress").action(() => {
|
|
2158
2544
|
const db = openDatabase();
|
|
2159
2545
|
const statuses = getGoalStatuses(db);
|
|
2160
2546
|
if (statuses.length === 0) {
|
|
2161
|
-
console.log(
|
|
2547
|
+
console.log(chalk4.yellow("No goals set."));
|
|
2162
2548
|
return;
|
|
2163
2549
|
}
|
|
2164
2550
|
console.log();
|
|
2165
2551
|
printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], statuses.map((g) => {
|
|
2166
2552
|
const pct = g.percent_used.toFixed(1);
|
|
2167
2553
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
2168
|
-
const status = g.is_over ?
|
|
2169
|
-
const pctColor = g.is_over ?
|
|
2554
|
+
const status = g.is_over ? chalk4.red("OVER") : g.is_at_risk ? chalk4.yellow("AT RISK") : chalk4.green("ON TRACK");
|
|
2555
|
+
const pctColor = g.is_over ? chalk4.red(pct + "%") : g.is_at_risk ? chalk4.yellow(pct + "%") : chalk4.green(pct + "%");
|
|
2170
2556
|
return [
|
|
2171
2557
|
g.period,
|
|
2172
|
-
|
|
2558
|
+
chalk4.white(scope),
|
|
2173
2559
|
fmt2(g.limit_usd),
|
|
2174
2560
|
fmt2(g.current_spend_usd),
|
|
2175
2561
|
pctColor,
|
|
@@ -2181,13 +2567,13 @@ goalCmd.command("list").description("List all goals with progress").action(() =>
|
|
|
2181
2567
|
goalCmd.command("remove <id>").description("Remove a goal").action((id) => {
|
|
2182
2568
|
const db = openDatabase();
|
|
2183
2569
|
deleteGoal(db, id);
|
|
2184
|
-
console.log(
|
|
2570
|
+
console.log(chalk4.green(`\u2713 Goal removed`));
|
|
2185
2571
|
});
|
|
2186
2572
|
goalCmd.command("status").description("Quick goal progress summary").action(() => {
|
|
2187
2573
|
const db = openDatabase();
|
|
2188
2574
|
const statuses = getGoalStatuses(db);
|
|
2189
2575
|
if (statuses.length === 0) {
|
|
2190
|
-
console.log(
|
|
2576
|
+
console.log(chalk4.yellow("No goals set."));
|
|
2191
2577
|
return;
|
|
2192
2578
|
}
|
|
2193
2579
|
console.log();
|
|
@@ -2197,7 +2583,7 @@ goalCmd.command("status").description("Quick goal progress summary").action(() =
|
|
|
2197
2583
|
const barFilled = Math.round(pct / 10);
|
|
2198
2584
|
const barEmpty = 10 - barFilled;
|
|
2199
2585
|
const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(barEmpty);
|
|
2200
|
-
const statusStr = g.is_over ?
|
|
2586
|
+
const statusStr = g.is_over ? chalk4.red("\u2717 OVER") : g.is_at_risk ? chalk4.yellow("\u26A0 AT RISK") : chalk4.green("\u2713 ON TRACK");
|
|
2201
2587
|
const label = `${g.period} (${scope})`.padEnd(20);
|
|
2202
2588
|
console.log(` ${label} ${bar} ${fmt2(g.current_spend_usd)} / ${fmt2(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
|
|
2203
2589
|
}
|
|
@@ -2209,27 +2595,28 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
2209
2595
|
switch (type.toLowerCase()) {
|
|
2210
2596
|
case "budget":
|
|
2211
2597
|
deleteBudget(db, id);
|
|
2212
|
-
console.log(
|
|
2598
|
+
console.log(chalk4.green(`\u2713 Budget ${id} removed`));
|
|
2213
2599
|
break;
|
|
2214
2600
|
case "project":
|
|
2215
2601
|
deleteProject(db, id);
|
|
2216
|
-
console.log(
|
|
2602
|
+
console.log(chalk4.green(`\u2713 Project ${id} removed`));
|
|
2217
2603
|
break;
|
|
2218
2604
|
case "goal":
|
|
2219
2605
|
deleteGoal(db, id);
|
|
2220
|
-
console.log(
|
|
2606
|
+
console.log(chalk4.green(`\u2713 Goal ${id} removed`));
|
|
2221
2607
|
break;
|
|
2222
2608
|
case "pricing":
|
|
2223
2609
|
deleteModelPricing(db, id);
|
|
2224
|
-
console.log(
|
|
2610
|
+
console.log(chalk4.green(`\u2713 Pricing entry ${id} removed`));
|
|
2225
2611
|
break;
|
|
2226
2612
|
default:
|
|
2227
|
-
console.error(
|
|
2613
|
+
console.error(chalk4.red(`Unknown type: ${type}. Use: budget | project | goal | pricing`));
|
|
2228
2614
|
process.exit(1);
|
|
2229
2615
|
}
|
|
2230
2616
|
} catch (e) {
|
|
2231
|
-
console.error(
|
|
2617
|
+
console.error(chalk4.red(`Failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
2232
2618
|
process.exit(1);
|
|
2233
2619
|
}
|
|
2234
2620
|
});
|
|
2621
|
+
registerBrainsCommand(program);
|
|
2235
2622
|
program.parse();
|