@hasna/economy 0.3.27 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -16
- package/SECURITY.md +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +1182 -862
- package/dist/db/cloud.d.ts +16 -8
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +58 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +236 -27
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +19 -0
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/lib/accounts-store.d.ts +83 -0
- package/dist/lib/accounts-store.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +2 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +19 -0
- package/dist/lib/api-display-url.d.ts.map +1 -0
- package/dist/lib/autosync-gate.d.ts +15 -0
- package/dist/lib/autosync-gate.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -1
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts +166 -10
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/model-config.d.ts +1 -1
- package/dist/lib/model-config.d.ts.map +1 -1
- package/dist/lib/serve-auth.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +6 -3
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/test-hermetic-accounts.d.ts +3 -2
- package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +85 -0
- package/dist/mcp/agent-registry.d.ts.map +1 -0
- package/dist/mcp/harness.d.ts +26 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/index.js +1387 -311
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +468 -24
- package/dist/server/index.js +595 -164
- package/dist/server/serve.d.ts +0 -2
- package/dist/server/serve.d.ts.map +1 -1
- package/docs/cli.md +2 -3
- package/docs/configuration.md +23 -10
- package/docs/ingestion.md +1 -1
- package/docs/mcp.md +4 -2
- package/docs/otel.md +8 -2
- package/docs/rest-api.md +2 -2
- package/package.json +7 -11
- package/postinstall.js +82 -0
- package/dashboard/README.md +0 -26
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -595,7 +595,63 @@ import { execFileSync } from "child_process";
|
|
|
595
595
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
596
596
|
import { hostname, platform } from "os";
|
|
597
597
|
import { homedir } from "os";
|
|
598
|
-
import { join } from "path";
|
|
598
|
+
import { join, resolve } from "path";
|
|
599
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
600
|
+
import { join as pathsResolverJoin } from "path";
|
|
601
|
+
function pathsResolverAssertApp(app) {
|
|
602
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
603
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
604
|
+
}
|
|
605
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
606
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function pathsResolverAssertKind(kind) {
|
|
610
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
611
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function pathsResolverBaseDir(kind, options) {
|
|
615
|
+
pathsResolverAssertKind(kind);
|
|
616
|
+
const env = options.env ?? process.env;
|
|
617
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
618
|
+
if (typeof override === "string" && override.length > 0)
|
|
619
|
+
return override;
|
|
620
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
621
|
+
const platform2 = options.platform ?? process.platform;
|
|
622
|
+
if (platform2 === "darwin") {
|
|
623
|
+
switch (kind) {
|
|
624
|
+
case "config":
|
|
625
|
+
case "data":
|
|
626
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
627
|
+
case "cache":
|
|
628
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
629
|
+
case "state":
|
|
630
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
switch (kind) {
|
|
634
|
+
case "config":
|
|
635
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
636
|
+
case "data":
|
|
637
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
638
|
+
case "state":
|
|
639
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
640
|
+
case "cache":
|
|
641
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function pathsResolverResolve(kind, options) {
|
|
645
|
+
pathsResolverAssertApp(options.app);
|
|
646
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
647
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
648
|
+
}
|
|
649
|
+
function dataDir(options) {
|
|
650
|
+
return pathsResolverResolve("data", options);
|
|
651
|
+
}
|
|
652
|
+
function cacheDir(options) {
|
|
653
|
+
return pathsResolverResolve("cache", options);
|
|
654
|
+
}
|
|
599
655
|
function normalizeMachineId(value) {
|
|
600
656
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
601
657
|
return id && id.length > 0 ? id : null;
|
|
@@ -618,7 +674,7 @@ function macHostMachineId() {
|
|
|
618
674
|
return null;
|
|
619
675
|
}
|
|
620
676
|
function getMachineId() {
|
|
621
|
-
const envMachine = normalizeMachineId(process.env["
|
|
677
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
622
678
|
if (envMachine)
|
|
623
679
|
return envMachine;
|
|
624
680
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -626,21 +682,48 @@ function getMachineId() {
|
|
|
626
682
|
return macHostMachineId() ?? hostMachine;
|
|
627
683
|
return hostMachine;
|
|
628
684
|
}
|
|
629
|
-
function
|
|
685
|
+
function getHomeDir() {
|
|
630
686
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
687
|
+
if (!home)
|
|
688
|
+
throw new Error("Could not resolve the user home directory");
|
|
689
|
+
return home;
|
|
690
|
+
}
|
|
691
|
+
function getResolverDataRoot() {
|
|
692
|
+
return dataDir({
|
|
693
|
+
app: "economy",
|
|
694
|
+
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function getLegacyDataRoot() {
|
|
698
|
+
return join(getHomeDir(), ".hasna", "economy");
|
|
699
|
+
}
|
|
700
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
701
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
702
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
703
|
+
return true;
|
|
704
|
+
return existsSync(join(resolved, "economy.db"));
|
|
705
|
+
}
|
|
706
|
+
function getExactDataRoot() {
|
|
707
|
+
const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
|
|
708
|
+
if (dir && dir.trim())
|
|
709
|
+
return dir.trim();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
function getDataDir() {
|
|
713
|
+
const exact = getExactDataRoot();
|
|
714
|
+
const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
|
|
715
|
+
const oldDir = join(getHomeDir(), ".economy");
|
|
716
|
+
if (existsSync(oldDir) && !existsSync(effective)) {
|
|
717
|
+
mkdirSync(effective, { recursive: true });
|
|
635
718
|
for (const file of readdirSync(oldDir)) {
|
|
636
719
|
const oldPath = join(oldDir, file);
|
|
637
720
|
if (statSync(oldPath).isFile()) {
|
|
638
|
-
copyFileSync(oldPath, join(
|
|
721
|
+
copyFileSync(oldPath, join(effective, file));
|
|
639
722
|
}
|
|
640
723
|
}
|
|
641
724
|
}
|
|
642
|
-
mkdirSync(
|
|
643
|
-
return
|
|
725
|
+
mkdirSync(effective, { recursive: true });
|
|
726
|
+
return effective;
|
|
644
727
|
}
|
|
645
728
|
function getDbPath() {
|
|
646
729
|
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
@@ -1919,6 +2002,10 @@ function getIngestState(db, source, key) {
|
|
|
1919
2002
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1920
2003
|
return row?.value ?? null;
|
|
1921
2004
|
}
|
|
2005
|
+
function loadIngestState(db, source) {
|
|
2006
|
+
const rows = db.prepare(`SELECT key, value FROM ingest_state WHERE source = ?`).all(source);
|
|
2007
|
+
return new Map(rows.map((row) => [row.key, row.value]));
|
|
2008
|
+
}
|
|
1922
2009
|
function setIngestState(db, source, key, value) {
|
|
1923
2010
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1924
2011
|
}
|
|
@@ -2364,7 +2451,7 @@ function bulkIngest(db, body) {
|
|
|
2364
2451
|
const total = Object.values(ingested).reduce((a, b) => a + b, 0);
|
|
2365
2452
|
return { ingested, total };
|
|
2366
2453
|
}
|
|
2367
|
-
var numOr = (v, d = 0) => {
|
|
2454
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
|
|
2368
2455
|
if (v == null)
|
|
2369
2456
|
return d;
|
|
2370
2457
|
const num = Number(v);
|
|
@@ -2372,6 +2459,13 @@ var numOr = (v, d = 0) => {
|
|
|
2372
2459
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2373
2460
|
var init_database = __esm(() => {
|
|
2374
2461
|
init_sqlite_adapter();
|
|
2462
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
2463
|
+
config: "HASNA_CONFIG_HOME",
|
|
2464
|
+
data: "HASNA_DATA_HOME",
|
|
2465
|
+
state: "HASNA_STATE_HOME",
|
|
2466
|
+
cache: "HASNA_CACHE_HOME"
|
|
2467
|
+
};
|
|
2468
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2375
2469
|
});
|
|
2376
2470
|
|
|
2377
2471
|
// package.json
|
|
@@ -2379,8 +2473,8 @@ var package_default;
|
|
|
2379
2473
|
var init_package = __esm(() => {
|
|
2380
2474
|
package_default = {
|
|
2381
2475
|
name: "@hasna/economy",
|
|
2382
|
-
version: "0.
|
|
2383
|
-
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API
|
|
2476
|
+
version: "0.4.0",
|
|
2477
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
2384
2478
|
repository: {
|
|
2385
2479
|
type: "git",
|
|
2386
2480
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -2411,7 +2505,7 @@ var init_package = __esm(() => {
|
|
|
2411
2505
|
files: [
|
|
2412
2506
|
"dist",
|
|
2413
2507
|
"docs",
|
|
2414
|
-
"
|
|
2508
|
+
"postinstall.js",
|
|
2415
2509
|
"LICENSE",
|
|
2416
2510
|
"README.md",
|
|
2417
2511
|
"CHANGELOG.md",
|
|
@@ -2420,12 +2514,11 @@ var init_package = __esm(() => {
|
|
|
2420
2514
|
"CODE_OF_CONDUCT.md"
|
|
2421
2515
|
],
|
|
2422
2516
|
scripts: {
|
|
2423
|
-
build: "
|
|
2517
|
+
build: "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
|
|
2424
2518
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
2425
2519
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
2426
2520
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
2427
2521
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
2428
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
2429
2522
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
2430
2523
|
prepack: "bun run build && bun run artifact-scan",
|
|
2431
2524
|
typecheck: "tsc --noEmit",
|
|
@@ -2434,7 +2527,7 @@ var init_package = __esm(() => {
|
|
|
2434
2527
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
2435
2528
|
"dev:serve": "bun run src/server/index.ts",
|
|
2436
2529
|
"dev:otel": "bun run src/otel/index.ts",
|
|
2437
|
-
postinstall: "
|
|
2530
|
+
postinstall: "node postinstall.js"
|
|
2438
2531
|
},
|
|
2439
2532
|
keywords: [
|
|
2440
2533
|
"economy",
|
|
@@ -2455,11 +2548,8 @@ var init_package = __esm(() => {
|
|
|
2455
2548
|
access: "public"
|
|
2456
2549
|
},
|
|
2457
2550
|
dependencies: {
|
|
2458
|
-
"@hasna/
|
|
2459
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
2460
|
-
"@hasna/contracts": "0.13.4",
|
|
2551
|
+
"@hasna/contracts": "1.0.2",
|
|
2461
2552
|
"@hasna/events": "^0.1.16",
|
|
2462
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
2463
2553
|
"@hasna/projects": "1.0.0",
|
|
2464
2554
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2465
2555
|
chalk: "^5.4.1",
|
|
@@ -2468,7 +2558,7 @@ var init_package = __esm(() => {
|
|
|
2468
2558
|
zod: "^3.24.2"
|
|
2469
2559
|
},
|
|
2470
2560
|
devDependencies: {
|
|
2471
|
-
"@types/bun": "
|
|
2561
|
+
"@types/bun": "1.3.14",
|
|
2472
2562
|
"@types/pg": "^8.20.0",
|
|
2473
2563
|
"bun-types": "latest",
|
|
2474
2564
|
typescript: "^5.7.2"
|
|
@@ -3483,18 +3573,133 @@ function queryModelEfficiency(db) {
|
|
|
3483
3573
|
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
3484
3574
|
}
|
|
3485
3575
|
|
|
3486
|
-
// src/lib/contracts-client/storage.ts
|
|
3487
|
-
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
3488
|
-
var init_storage = () => {};
|
|
3489
|
-
|
|
3490
3576
|
// src/lib/cloud-storage.ts
|
|
3577
|
+
import {
|
|
3578
|
+
appConfigDiskValue,
|
|
3579
|
+
ClientTransportConfigurationError,
|
|
3580
|
+
clientTransportEnvKeys,
|
|
3581
|
+
createClientTransport,
|
|
3582
|
+
credentialDiskSources,
|
|
3583
|
+
credentialOverrideEnvKey,
|
|
3584
|
+
credentialPointerEnvKey,
|
|
3585
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
3586
|
+
keychainConfigValue
|
|
3587
|
+
} from "@hasna/contracts/client";
|
|
3588
|
+
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
3589
|
+
function assertNoRetiredStorageMode(env) {
|
|
3590
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
3591
|
+
if (!legacyKey)
|
|
3592
|
+
return;
|
|
3593
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
|
|
3594
|
+
}
|
|
3595
|
+
function envFlagSet(env, key) {
|
|
3596
|
+
const raw = env[key];
|
|
3597
|
+
if (raw === undefined)
|
|
3598
|
+
return false;
|
|
3599
|
+
const value = raw.trim().toLowerCase();
|
|
3600
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
3601
|
+
}
|
|
3602
|
+
function localStorageExplicitlyOptedIn(env) {
|
|
3603
|
+
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
|
|
3604
|
+
}
|
|
3605
|
+
function economyAuthorityEnvKeys() {
|
|
3606
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
3607
|
+
return [
|
|
3608
|
+
...keys.apiUrlKeys,
|
|
3609
|
+
...keys.apiKeyKeys,
|
|
3610
|
+
credentialOverrideEnvKey(ECONOMY_APP),
|
|
3611
|
+
credentialPointerEnvKey(ECONOMY_APP),
|
|
3612
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
3613
|
+
];
|
|
3614
|
+
}
|
|
3615
|
+
function hasEconomyEnvAuthorityIntent(env) {
|
|
3616
|
+
return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
|
|
3617
|
+
}
|
|
3618
|
+
function hostedAuthorityConfigured(env, options = {}) {
|
|
3619
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
3620
|
+
if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
|
|
3621
|
+
return true;
|
|
3622
|
+
if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
|
|
3623
|
+
return true;
|
|
3624
|
+
return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
|
|
3625
|
+
}
|
|
3626
|
+
function economyResolverEnv(env) {
|
|
3627
|
+
const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
3628
|
+
if (blanks.length === 0)
|
|
3629
|
+
return env;
|
|
3630
|
+
const next = { ...env };
|
|
3631
|
+
for (const key of blanks)
|
|
3632
|
+
delete next[key];
|
|
3633
|
+
return next;
|
|
3634
|
+
}
|
|
3635
|
+
function isAmbientEconomyEnv(env) {
|
|
3636
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
3637
|
+
return true;
|
|
3638
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
3639
|
+
}
|
|
3640
|
+
function economyResolverInputs(env, credentials = {}) {
|
|
3641
|
+
const normalised = economyResolverEnv(env);
|
|
3642
|
+
if (normalised === env)
|
|
3643
|
+
return { env: normalised, credentials };
|
|
3644
|
+
const keychain = { ...credentials.keychain };
|
|
3645
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
3646
|
+
keychain.enabled = isAmbientEconomyEnv(env);
|
|
3647
|
+
}
|
|
3648
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
3649
|
+
}
|
|
3650
|
+
function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
3651
|
+
const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
|
|
3652
|
+
const wired = createClientTransport(name, resolverEnv, {
|
|
3653
|
+
credentials,
|
|
3654
|
+
...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
3655
|
+
...overrides.headers ? { headers: overrides.headers } : {},
|
|
3656
|
+
...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
3657
|
+
...overrides.retry !== undefined ? { retry: overrides.retry } : {},
|
|
3658
|
+
...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
3659
|
+
});
|
|
3660
|
+
return {
|
|
3661
|
+
transport: "http",
|
|
3662
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
3663
|
+
resolution: wired.resolution
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
function localEconomyNotice() {
|
|
3667
|
+
return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
3668
|
+
}
|
|
3669
|
+
function credentialRequiredError(env, cause) {
|
|
3670
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
3671
|
+
const clientEnv = env;
|
|
3672
|
+
const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
|
|
3673
|
+
const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
|
|
3674
|
+
return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
|
|
3675
|
+
}
|
|
3491
3676
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3492
|
-
|
|
3493
|
-
|
|
3677
|
+
assertNoRetiredStorageMode(env);
|
|
3678
|
+
const clientEnv = env;
|
|
3679
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
3680
|
+
return { active: false, client: null };
|
|
3681
|
+
}
|
|
3682
|
+
let resolved;
|
|
3683
|
+
try {
|
|
3684
|
+
resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
3685
|
+
} catch (error) {
|
|
3686
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
3687
|
+
throw credentialRequiredError(env, error);
|
|
3688
|
+
}
|
|
3689
|
+
throw error;
|
|
3690
|
+
}
|
|
3691
|
+
return { active: true, client: resolved.client };
|
|
3494
3692
|
}
|
|
3495
3693
|
function economyCloudStorage(env = process.env) {
|
|
3496
|
-
if (cache === undefined)
|
|
3694
|
+
if (cache === undefined) {
|
|
3497
3695
|
cache = resolveEconomyCloudStorage(env);
|
|
3696
|
+
if (!cache.active && !localNoticePrinted) {
|
|
3697
|
+
localNoticePrinted = true;
|
|
3698
|
+
if (typeof process !== "undefined")
|
|
3699
|
+
process.stderr.write(`${localEconomyNotice()}
|
|
3700
|
+
`);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3498
3703
|
return cache;
|
|
3499
3704
|
}
|
|
3500
3705
|
function cleanQuery(query) {
|
|
@@ -3520,9 +3725,59 @@ async function cloudObject(storage, path, query) {
|
|
|
3520
3725
|
}
|
|
3521
3726
|
return raw;
|
|
3522
3727
|
}
|
|
3523
|
-
|
|
3728
|
+
function economyTransportReport(env = process.env, overrides) {
|
|
3729
|
+
const clientEnv = env;
|
|
3730
|
+
assertNoRetiredStorageMode(env);
|
|
3731
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
3732
|
+
return {
|
|
3733
|
+
ok: true,
|
|
3734
|
+
selected: true,
|
|
3735
|
+
transport: "sqlite",
|
|
3736
|
+
source: "local-opt-in",
|
|
3737
|
+
authority: null,
|
|
3738
|
+
issues: []
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
try {
|
|
3742
|
+
const resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
3743
|
+
return {
|
|
3744
|
+
ok: true,
|
|
3745
|
+
selected: true,
|
|
3746
|
+
transport: "http",
|
|
3747
|
+
source: resolved.resolution.transportSource,
|
|
3748
|
+
authority: {
|
|
3749
|
+
baseUrl: resolved.resolution.baseUrl,
|
|
3750
|
+
apiUrlSource: resolved.resolution.apiUrlSource,
|
|
3751
|
+
apiKeySource: resolved.resolution.apiKeySource,
|
|
3752
|
+
apiKeyTier: resolved.resolution.apiKeyTier,
|
|
3753
|
+
warning: resolved.resolution.warning
|
|
3754
|
+
},
|
|
3755
|
+
issues: []
|
|
3756
|
+
};
|
|
3757
|
+
} catch (error) {
|
|
3758
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
3759
|
+
return {
|
|
3760
|
+
ok: false,
|
|
3761
|
+
selected: true,
|
|
3762
|
+
transport: "http",
|
|
3763
|
+
source: null,
|
|
3764
|
+
authority: null,
|
|
3765
|
+
issues: [error.message]
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
throw error;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
var ECONOMY_APP = "economy", RETIRED_STORAGE_MODE_KEYS, LOCAL_STORAGE_OPT_IN_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT, cache, localNoticePrinted = false;
|
|
3524
3772
|
var init_cloud_storage = __esm(() => {
|
|
3525
|
-
|
|
3773
|
+
RETIRED_STORAGE_MODE_KEYS = [
|
|
3774
|
+
"HASNA_ECONOMY_STORAGE_MODE",
|
|
3775
|
+
"HASNA_ECONOMY_MODE",
|
|
3776
|
+
"ECONOMY_STORAGE_MODE",
|
|
3777
|
+
"ECONOMY_MODE"
|
|
3778
|
+
];
|
|
3779
|
+
LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
3780
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
3526
3781
|
});
|
|
3527
3782
|
|
|
3528
3783
|
// src/lib/store/index.ts
|
|
@@ -3923,7 +4178,7 @@ var init_store = __esm(() => {
|
|
|
3923
4178
|
|
|
3924
4179
|
// src/lib/serve-auth.ts
|
|
3925
4180
|
function getServeApiToken() {
|
|
3926
|
-
return process.env["
|
|
4181
|
+
return process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
3927
4182
|
}
|
|
3928
4183
|
function getServeBindHost() {
|
|
3929
4184
|
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
@@ -3955,7 +4210,7 @@ __export(exports_tui, {
|
|
|
3955
4210
|
buildWaybarJson: () => buildWaybarJson,
|
|
3956
4211
|
buildStatusLine: () => buildStatusLine
|
|
3957
4212
|
});
|
|
3958
|
-
import
|
|
4213
|
+
import chalk2 from "chalk";
|
|
3959
4214
|
function fmt(usd) {
|
|
3960
4215
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
3961
4216
|
}
|
|
@@ -4033,20 +4288,20 @@ async function runTui(opts) {
|
|
|
4033
4288
|
const render = async () => {
|
|
4034
4289
|
const data = await gatherStatusData();
|
|
4035
4290
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
4036
|
-
console.log(
|
|
4037
|
-
console.log(
|
|
4038
|
-
console.log(` Today ${
|
|
4291
|
+
console.log(chalk2.bold.cyan(" economy"));
|
|
4292
|
+
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"));
|
|
4293
|
+
console.log(` Today ${chalk2.green(fmt(data.today.total_usd))} ${data.today.sessions} sessions`);
|
|
4039
4294
|
console.log(` Week ${fmt(data.week.total_usd)} ${data.week.sessions} sessions`);
|
|
4040
4295
|
console.log(` Month ${fmt(data.month.total_usd)} saved ${fmt(data.savings.saved_usd)}`);
|
|
4041
|
-
console.log(
|
|
4296
|
+
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"));
|
|
4042
4297
|
console.log(` Top agent: ${data.topAgent}`);
|
|
4043
4298
|
if (data.quota)
|
|
4044
4299
|
console.log(` Quota: ${data.quota}`);
|
|
4045
4300
|
console.log(` Fleet: ${data.machineCount} machines`);
|
|
4046
4301
|
console.log(` Store: ${data.transport === "cloud-http" ? "self_hosted (cloud API)" : "local"}`);
|
|
4047
4302
|
if (getServeApiToken())
|
|
4048
|
-
console.log(
|
|
4049
|
-
console.log(
|
|
4303
|
+
console.log(chalk2.dim(" API auth: HASNA_ECONOMY_API_TOKEN set"));
|
|
4304
|
+
console.log(chalk2.dim(`
|
|
4050
4305
|
${opts.watch ? `Refreshing every ${interval}s \u2014 Ctrl+C to exit` : "Run with --watch for live refresh"}`));
|
|
4051
4306
|
};
|
|
4052
4307
|
await render();
|
|
@@ -4092,32 +4347,344 @@ var init_agents = __esm(() => {
|
|
|
4092
4347
|
|
|
4093
4348
|
// src/lib/paths.ts
|
|
4094
4349
|
import { homedir as homedir2 } from "os";
|
|
4095
|
-
import { join as
|
|
4096
|
-
function
|
|
4350
|
+
import { join as join2 } from "path";
|
|
4351
|
+
function getHomeDir2() {
|
|
4097
4352
|
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
|
|
4098
4353
|
}
|
|
4099
4354
|
function agentPaths() {
|
|
4100
|
-
const home =
|
|
4355
|
+
const home = getHomeDir2();
|
|
4101
4356
|
return {
|
|
4102
|
-
claudeProjects:
|
|
4103
|
-
claudeCredentials:
|
|
4104
|
-
takumiProjects:
|
|
4105
|
-
codexDir:
|
|
4106
|
-
codexDb:
|
|
4107
|
-
codexAuth:
|
|
4108
|
-
codexConfig:
|
|
4109
|
-
geminiTmp:
|
|
4110
|
-
geminiHistory:
|
|
4111
|
-
opencodeMessages:
|
|
4112
|
-
piSessions:
|
|
4113
|
-
hermesDir:
|
|
4114
|
-
hermesDb:
|
|
4357
|
+
claudeProjects: join2(home, ".claude", "projects"),
|
|
4358
|
+
claudeCredentials: join2(home, ".claude", ".credentials.json"),
|
|
4359
|
+
takumiProjects: join2(home, ".takumi", "projects"),
|
|
4360
|
+
codexDir: join2(home, ".codex"),
|
|
4361
|
+
codexDb: join2(home, ".codex", "state_5.sqlite"),
|
|
4362
|
+
codexAuth: join2(home, ".codex", "auth.json"),
|
|
4363
|
+
codexConfig: join2(home, ".codex", "config.toml"),
|
|
4364
|
+
geminiTmp: join2(home, ".gemini", "tmp"),
|
|
4365
|
+
geminiHistory: join2(home, ".gemini", "history"),
|
|
4366
|
+
opencodeMessages: join2(home, ".local", "share", "opencode", "storage", "message"),
|
|
4367
|
+
piSessions: join2(home, ".pi", "agent", "sessions"),
|
|
4368
|
+
hermesDir: join2(home, ".hermes"),
|
|
4369
|
+
hermesDb: join2(home, ".hermes", "state.db")
|
|
4115
4370
|
};
|
|
4116
4371
|
}
|
|
4117
4372
|
var init_paths = () => {};
|
|
4118
4373
|
|
|
4374
|
+
// src/lib/accounts-store.ts
|
|
4375
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
4376
|
+
import { homedir as homedir3 } from "os";
|
|
4377
|
+
import { join as join4 } from "path";
|
|
4378
|
+
import { z } from "zod";
|
|
4379
|
+
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
4380
|
+
function validateEnvPath(value, label) {
|
|
4381
|
+
const trimmed = value.trim();
|
|
4382
|
+
if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed))
|
|
4383
|
+
throw new Error(`invalid ${label}`);
|
|
4384
|
+
return trimmed;
|
|
4385
|
+
}
|
|
4386
|
+
function accountsHome() {
|
|
4387
|
+
const override = process.env.ACCOUNTS_HOME;
|
|
4388
|
+
if (override && override.trim())
|
|
4389
|
+
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
4390
|
+
return join4(homedir3(), ".hasna", "accounts");
|
|
4391
|
+
}
|
|
4392
|
+
function storePath() {
|
|
4393
|
+
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
4394
|
+
if (override && override.trim())
|
|
4395
|
+
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
4396
|
+
return join4(accountsHome(), "accounts.json");
|
|
4397
|
+
}
|
|
4398
|
+
function parseStoreFile() {
|
|
4399
|
+
const path = storePath();
|
|
4400
|
+
if (!existsSync3(path))
|
|
4401
|
+
return structuredClone(EMPTY_STORE);
|
|
4402
|
+
let raw;
|
|
4403
|
+
try {
|
|
4404
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
4405
|
+
} catch (err) {
|
|
4406
|
+
throw new Error(`could not parse store at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4407
|
+
}
|
|
4408
|
+
const parsed = storeSchema.safeParse(raw);
|
|
4409
|
+
if (!parsed.success) {
|
|
4410
|
+
throw new Error(`invalid store at ${path}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
|
|
4411
|
+
}
|
|
4412
|
+
return parsed.data;
|
|
4413
|
+
}
|
|
4414
|
+
function loadAppliedMap() {
|
|
4415
|
+
const applied = {};
|
|
4416
|
+
for (const [toolId, name] of Object.entries(parseStoreFile().applied)) {
|
|
4417
|
+
if (name && profileNameSchema.safeParse(name).success)
|
|
4418
|
+
applied[toolId] = name;
|
|
4419
|
+
}
|
|
4420
|
+
return applied;
|
|
4421
|
+
}
|
|
4422
|
+
function appliedProfileName(toolId) {
|
|
4423
|
+
return loadAppliedMap()[toolId];
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
class LocalStore2 {
|
|
4427
|
+
transport = "local";
|
|
4428
|
+
async listProfiles(tool) {
|
|
4429
|
+
const profiles = parseStoreFile().profiles;
|
|
4430
|
+
const filtered = tool ? profiles.filter((profile) => profile.tool === tool) : profiles;
|
|
4431
|
+
return filtered.slice().sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
4432
|
+
}
|
|
4433
|
+
async findProfile(name, tool) {
|
|
4434
|
+
const matches = parseStoreFile().profiles.filter((profile) => profile.name === name && (!tool || profile.tool === tool));
|
|
4435
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4436
|
+
}
|
|
4437
|
+
async currentProfile(tool) {
|
|
4438
|
+
const store = parseStoreFile();
|
|
4439
|
+
const name = store.current[tool];
|
|
4440
|
+
if (!name)
|
|
4441
|
+
return;
|
|
4442
|
+
return store.profiles.find((profile) => profile.name === name && profile.tool === tool);
|
|
4443
|
+
}
|
|
4444
|
+
async listTools() {
|
|
4445
|
+
const custom = parseStoreFile().tools;
|
|
4446
|
+
const byId = new Map;
|
|
4447
|
+
for (const tool of BUILTIN_TOOLS)
|
|
4448
|
+
byId.set(tool.id, tool);
|
|
4449
|
+
for (const tool of custom)
|
|
4450
|
+
byId.set(tool.id, tool);
|
|
4451
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
function assertNoRetiredStorageMode2(env) {
|
|
4455
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS2.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
4456
|
+
if (!legacyKey)
|
|
4457
|
+
return;
|
|
4458
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The accounts registry uses the local JSON store, or the HTTP API selected by ` + `HASNA_ACCOUNTS_API_URL + HASNA_ACCOUNTS_API_KEY.`);
|
|
4459
|
+
}
|
|
4460
|
+
function isNotFound(err) {
|
|
4461
|
+
return Boolean(err && typeof err === "object" && err.status === 404);
|
|
4462
|
+
}
|
|
4463
|
+
function sleep(ms) {
|
|
4464
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4465
|
+
}
|
|
4466
|
+
async function cloudRequest(baseUrl, apiKey, path, query) {
|
|
4467
|
+
const rel = path.startsWith("/") ? path : `/${path}`;
|
|
4468
|
+
const url = query && Object.keys(query).length > 0 ? `${baseUrl}${rel}?${new URLSearchParams(query).toString()}` : `${baseUrl}${rel}`;
|
|
4469
|
+
let lastError;
|
|
4470
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
4471
|
+
const controller = new AbortController;
|
|
4472
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
4473
|
+
let res;
|
|
4474
|
+
try {
|
|
4475
|
+
res = await fetch(url, {
|
|
4476
|
+
method: "GET",
|
|
4477
|
+
headers: {
|
|
4478
|
+
"x-api-key": apiKey,
|
|
4479
|
+
Authorization: `Bearer ${apiKey}`,
|
|
4480
|
+
Accept: "application/json"
|
|
4481
|
+
},
|
|
4482
|
+
signal: controller.signal
|
|
4483
|
+
});
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
4486
|
+
clearTimeout(timer);
|
|
4487
|
+
if (attempt < 2) {
|
|
4488
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
4489
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
4490
|
+
await sleep(backoff + jitter);
|
|
4491
|
+
}
|
|
4492
|
+
continue;
|
|
4493
|
+
} finally {
|
|
4494
|
+
clearTimeout(timer);
|
|
4495
|
+
}
|
|
4496
|
+
const text = await res.text();
|
|
4497
|
+
let parsed;
|
|
4498
|
+
if (text.length > 0) {
|
|
4499
|
+
try {
|
|
4500
|
+
parsed = JSON.parse(text);
|
|
4501
|
+
} catch {
|
|
4502
|
+
parsed = text;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
if (!res.ok) {
|
|
4506
|
+
lastError = new AccountsApiError("GET", rel, res.status, parsed);
|
|
4507
|
+
if (attempt < 2 && RETRY_STATUSES.includes(res.status)) {
|
|
4508
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
4509
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
4510
|
+
await sleep(backoff + jitter);
|
|
4511
|
+
continue;
|
|
4512
|
+
}
|
|
4513
|
+
break;
|
|
4514
|
+
}
|
|
4515
|
+
return parsed;
|
|
4516
|
+
}
|
|
4517
|
+
throw lastError;
|
|
4518
|
+
}
|
|
4519
|
+
function toProfile(account) {
|
|
4520
|
+
const profile = {
|
|
4521
|
+
name: String(account.name),
|
|
4522
|
+
tool: String(account.tool),
|
|
4523
|
+
dir: typeof account.dir === "string" ? account.dir : "",
|
|
4524
|
+
createdAt: String(account.createdAt)
|
|
4525
|
+
};
|
|
4526
|
+
if (typeof account.email === "string")
|
|
4527
|
+
profile.email = account.email;
|
|
4528
|
+
if (typeof account.displayName === "string")
|
|
4529
|
+
profile.displayName = account.displayName;
|
|
4530
|
+
if (typeof account.description === "string")
|
|
4531
|
+
profile.description = account.description;
|
|
4532
|
+
if (typeof account.lastUsedAt === "string")
|
|
4533
|
+
profile.lastUsedAt = account.lastUsedAt;
|
|
4534
|
+
return profile;
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
class ApiStore2 {
|
|
4538
|
+
baseUrl;
|
|
4539
|
+
apiKey;
|
|
4540
|
+
transport = "api";
|
|
4541
|
+
constructor(baseUrl, apiKey) {
|
|
4542
|
+
this.baseUrl = baseUrl;
|
|
4543
|
+
this.apiKey = apiKey;
|
|
4544
|
+
}
|
|
4545
|
+
async listProfiles(tool) {
|
|
4546
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/accounts", tool ? { tool } : undefined);
|
|
4547
|
+
const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
|
|
4548
|
+
return accounts.map((account) => toProfile(account)).sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
4549
|
+
}
|
|
4550
|
+
async findProfile(name, tool) {
|
|
4551
|
+
if (tool) {
|
|
4552
|
+
try {
|
|
4553
|
+
const account = await cloudRequest(this.baseUrl, this.apiKey, `/accounts/${encodeURIComponent(tool)}/${encodeURIComponent(name)}`);
|
|
4554
|
+
return account ? toProfile(account) : undefined;
|
|
4555
|
+
} catch (err) {
|
|
4556
|
+
if (isNotFound(err))
|
|
4557
|
+
return;
|
|
4558
|
+
throw err;
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
const matches = (await this.listProfiles()).filter((profile) => profile.name === name);
|
|
4562
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4563
|
+
}
|
|
4564
|
+
async currentProfile(tool) {
|
|
4565
|
+
let current;
|
|
4566
|
+
try {
|
|
4567
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, `/current/${encodeURIComponent(tool)}`);
|
|
4568
|
+
current = raw && typeof raw === "object" ? raw : null;
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
if (isNotFound(err))
|
|
4571
|
+
return;
|
|
4572
|
+
throw err;
|
|
4573
|
+
}
|
|
4574
|
+
if (!current || typeof current.name !== "string")
|
|
4575
|
+
return;
|
|
4576
|
+
return this.findProfile(current.name, tool);
|
|
4577
|
+
}
|
|
4578
|
+
async listTools() {
|
|
4579
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/tools");
|
|
4580
|
+
const tools = Array.isArray(raw?.tools) ? raw.tools : [];
|
|
4581
|
+
const custom = [];
|
|
4582
|
+
for (const item of tools) {
|
|
4583
|
+
if (item === null || typeof item !== "object")
|
|
4584
|
+
continue;
|
|
4585
|
+
const record = item;
|
|
4586
|
+
if (record.builtin !== false)
|
|
4587
|
+
continue;
|
|
4588
|
+
const parsed = toolDefSchema.safeParse(item);
|
|
4589
|
+
if (!parsed.success) {
|
|
4590
|
+
throw new Error(`invalid custom tool "${String(record.id)}" returned by accounts-serve: ` + parsed.error.issues.map((issue) => issue.message).join("; "));
|
|
4591
|
+
}
|
|
4592
|
+
custom.push(parsed.data);
|
|
4593
|
+
}
|
|
4594
|
+
const byId = new Map;
|
|
4595
|
+
for (const tool of BUILTIN_TOOLS)
|
|
4596
|
+
byId.set(tool.id, tool);
|
|
4597
|
+
for (const tool of custom)
|
|
4598
|
+
byId.set(tool.id, tool);
|
|
4599
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
function resolveStore(env = process.env, options = {}) {
|
|
4603
|
+
assertNoRetiredStorageMode2(env);
|
|
4604
|
+
const clientEnv = env;
|
|
4605
|
+
try {
|
|
4606
|
+
const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
|
|
4607
|
+
if (credential === null)
|
|
4608
|
+
return new LocalStore2;
|
|
4609
|
+
const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
|
|
4610
|
+
credentials: credential ? { ...options.credentials, apiKey: credential.apiKey } : options.credentials
|
|
4611
|
+
});
|
|
4612
|
+
return new ApiStore2(resolution.baseUrl, credential.apiKey);
|
|
4613
|
+
} catch (err) {
|
|
4614
|
+
console.warn(`accounts: API configuration unusable (${err instanceof Error ? err.message : String(err)}); using local store`);
|
|
4615
|
+
return new LocalStore2;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
var ACCOUNTS_APP = "accounts", profileNameSchema, profileSchema, toolDefSchema, storeSchema, EMPTY_STORE, BUILTIN_TOOLS, RETIRED_STORAGE_MODE_KEYS2, RETRY_STATUSES, AccountsApiError;
|
|
4619
|
+
var init_accounts_store = __esm(() => {
|
|
4620
|
+
profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
4621
|
+
profileSchema = z.object({
|
|
4622
|
+
name: z.string(),
|
|
4623
|
+
tool: z.string(),
|
|
4624
|
+
email: z.string().optional(),
|
|
4625
|
+
displayName: z.string().optional(),
|
|
4626
|
+
dir: z.string(),
|
|
4627
|
+
description: z.string().optional(),
|
|
4628
|
+
createdAt: z.string(),
|
|
4629
|
+
lastUsedAt: z.string().optional()
|
|
4630
|
+
});
|
|
4631
|
+
toolDefSchema = z.object({
|
|
4632
|
+
id: z.string(),
|
|
4633
|
+
label: z.string(),
|
|
4634
|
+
envVar: z.string(),
|
|
4635
|
+
defaultDir: z.string(),
|
|
4636
|
+
bin: z.string()
|
|
4637
|
+
});
|
|
4638
|
+
storeSchema = z.object({
|
|
4639
|
+
version: z.literal(1),
|
|
4640
|
+
current: z.record(z.string(), z.string()).default({}),
|
|
4641
|
+
applied: z.record(z.string(), z.string()).default({}),
|
|
4642
|
+
toolLocks: z.record(z.string(), z.string()).default({}),
|
|
4643
|
+
profiles: z.array(profileSchema).default([]),
|
|
4644
|
+
tools: z.array(toolDefSchema).default([])
|
|
4645
|
+
});
|
|
4646
|
+
EMPTY_STORE = {
|
|
4647
|
+
version: 1,
|
|
4648
|
+
current: {},
|
|
4649
|
+
applied: {},
|
|
4650
|
+
toolLocks: {},
|
|
4651
|
+
profiles: [],
|
|
4652
|
+
tools: []
|
|
4653
|
+
};
|
|
4654
|
+
BUILTIN_TOOLS = [
|
|
4655
|
+
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join4(homedir3(), ".claude"), bin: "claude" },
|
|
4656
|
+
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join4(homedir3(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
4657
|
+
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join4(homedir3(), ".codex"), bin: "codex" },
|
|
4658
|
+
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join4(homedir3(), ".codewith"), bin: "codewith" },
|
|
4659
|
+
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join4(homedir3(), ".takumi"), bin: "takumi" },
|
|
4660
|
+
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join4(homedir3(), ".gemini"), bin: "gemini" },
|
|
4661
|
+
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join4(homedir3(), ".config", "opencode"), bin: "opencode" },
|
|
4662
|
+
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join4(homedir3(), ".cursor"), bin: "cursor-agent" },
|
|
4663
|
+
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join4(homedir3(), ".pi"), bin: "pi" },
|
|
4664
|
+
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join4(homedir3(), ".hermes"), bin: "hermes" },
|
|
4665
|
+
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join4(homedir3(), ".kimi-code"), bin: "kimi" },
|
|
4666
|
+
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join4(homedir3(), ".grok"), bin: "grok" }
|
|
4667
|
+
];
|
|
4668
|
+
RETIRED_STORAGE_MODE_KEYS2 = [
|
|
4669
|
+
"HASNA_ACCOUNTS_STORAGE_MODE",
|
|
4670
|
+
"ACCOUNTS_STORAGE_MODE",
|
|
4671
|
+
"HASNA_ACCOUNTS_MODE",
|
|
4672
|
+
"ACCOUNTS_MODE"
|
|
4673
|
+
];
|
|
4674
|
+
RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
4675
|
+
AccountsApiError = class AccountsApiError extends Error {
|
|
4676
|
+
status;
|
|
4677
|
+
body;
|
|
4678
|
+
constructor(method, path, status, body) {
|
|
4679
|
+
super(`accounts cloud request failed: ${method} ${path} -> ${status}`);
|
|
4680
|
+
this.name = "AccountsApiError";
|
|
4681
|
+
this.status = status;
|
|
4682
|
+
this.body = body;
|
|
4683
|
+
}
|
|
4684
|
+
};
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4119
4687
|
// src/lib/accounts.ts
|
|
4120
|
-
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
4121
4688
|
function normalizeEmail(email) {
|
|
4122
4689
|
return (email ?? "").trim().toLowerCase();
|
|
4123
4690
|
}
|
|
@@ -4179,11 +4746,11 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
4179
4746
|
const normalized = normalizeDir(configuredDir);
|
|
4180
4747
|
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
4181
4748
|
}
|
|
4182
|
-
async function resolveAccountForAgent(agent, env = process.env) {
|
|
4749
|
+
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
4183
4750
|
const override = envOverride(agent, env);
|
|
4184
4751
|
if (override)
|
|
4185
4752
|
return override;
|
|
4186
|
-
const store = resolveStore(env);
|
|
4753
|
+
const store = resolveStore(env, options);
|
|
4187
4754
|
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
4188
4755
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
4189
4756
|
const tool = tools.get(toolId);
|
|
@@ -4218,6 +4785,7 @@ function withAccount(record, account) {
|
|
|
4218
4785
|
}
|
|
4219
4786
|
var AGENT_ACCOUNT_TOOLS;
|
|
4220
4787
|
var init_accounts = __esm(() => {
|
|
4788
|
+
init_accounts_store();
|
|
4221
4789
|
AGENT_ACCOUNT_TOOLS = {
|
|
4222
4790
|
claude: ["claude"],
|
|
4223
4791
|
takumi: ["takumi", "claude"],
|
|
@@ -4232,8 +4800,8 @@ var init_accounts = __esm(() => {
|
|
|
4232
4800
|
|
|
4233
4801
|
// src/ingest/claude.ts
|
|
4234
4802
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
4235
|
-
import { homedir as
|
|
4236
|
-
import { join as
|
|
4803
|
+
import { homedir as homedir4 } from "os";
|
|
4804
|
+
import { join as join5, basename } from "path";
|
|
4237
4805
|
function autoDetectProject(cwd, projects) {
|
|
4238
4806
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
4239
4807
|
}
|
|
@@ -4246,9 +4814,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
4246
4814
|
try {
|
|
4247
4815
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
4248
4816
|
if (entry.isDirectory())
|
|
4249
|
-
walk(
|
|
4817
|
+
walk(join5(dir, entry.name));
|
|
4250
4818
|
else if (entry.name.endsWith(".jsonl"))
|
|
4251
|
-
files.push(
|
|
4819
|
+
files.push(join5(dir, entry.name));
|
|
4252
4820
|
}
|
|
4253
4821
|
} catch {}
|
|
4254
4822
|
}
|
|
@@ -4273,9 +4841,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
4273
4841
|
const touchedSessions = new Set;
|
|
4274
4842
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
4275
4843
|
const account = await resolveAccountForAgent(agentName);
|
|
4844
|
+
const ingestState = loadIngestState(db, agentName);
|
|
4276
4845
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
4277
4846
|
for (const projectDirEntry of projectDirs) {
|
|
4278
|
-
const projectDirPath =
|
|
4847
|
+
const projectDirPath = join5(projectsDir, projectDirEntry.name);
|
|
4279
4848
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
4280
4849
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
4281
4850
|
for (const filePath of jsonlFiles) {
|
|
@@ -4286,7 +4855,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
4286
4855
|
} catch {
|
|
4287
4856
|
continue;
|
|
4288
4857
|
}
|
|
4289
|
-
const processed =
|
|
4858
|
+
const processed = ingestState.get(stateKey) ?? null;
|
|
4290
4859
|
if (processed === fileMtime)
|
|
4291
4860
|
continue;
|
|
4292
4861
|
let lines;
|
|
@@ -4412,14 +4981,14 @@ var init_claude = __esm(() => {
|
|
|
4412
4981
|
init_database();
|
|
4413
4982
|
init_pricing();
|
|
4414
4983
|
init_accounts();
|
|
4415
|
-
CLAUDE_PROJECTS_DIR =
|
|
4416
|
-
TAKUMI_PROJECTS_DIR =
|
|
4984
|
+
CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join5(homedir4(), ".claude", "projects");
|
|
4985
|
+
TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join5(homedir4(), ".takumi", "projects");
|
|
4417
4986
|
});
|
|
4418
4987
|
|
|
4419
4988
|
// src/ingest/codex.ts
|
|
4420
4989
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
4421
|
-
import { homedir as
|
|
4422
|
-
import { join as
|
|
4990
|
+
import { homedir as homedir5 } from "os";
|
|
4991
|
+
import { join as join6, basename as basename2 } from "path";
|
|
4423
4992
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
4424
4993
|
function codexDbPath() {
|
|
4425
4994
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -4665,16 +5234,16 @@ var init_codex = __esm(() => {
|
|
|
4665
5234
|
init_database();
|
|
4666
5235
|
init_pricing();
|
|
4667
5236
|
init_accounts();
|
|
4668
|
-
DEFAULT_CODEX_DB_PATH =
|
|
4669
|
-
DEFAULT_CODEX_CONFIG_PATH =
|
|
4670
|
-
DEFAULT_CODEWITH_DB_PATH =
|
|
4671
|
-
DEFAULT_CODEWITH_CONFIG_PATH =
|
|
5237
|
+
DEFAULT_CODEX_DB_PATH = join6(homedir5(), ".codex", "state_5.sqlite");
|
|
5238
|
+
DEFAULT_CODEX_CONFIG_PATH = join6(homedir5(), ".codex", "config.toml");
|
|
5239
|
+
DEFAULT_CODEWITH_DB_PATH = join6(homedir5(), ".codewith", "state_5.sqlite");
|
|
5240
|
+
DEFAULT_CODEWITH_CONFIG_PATH = join6(homedir5(), ".codewith", "config.toml");
|
|
4672
5241
|
});
|
|
4673
5242
|
|
|
4674
5243
|
// src/ingest/gemini.ts
|
|
4675
5244
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
4676
|
-
import { homedir as
|
|
4677
|
-
import { join as
|
|
5245
|
+
import { homedir as homedir6 } from "os";
|
|
5246
|
+
import { join as join7, basename as basename3 } from "path";
|
|
4678
5247
|
function geminiTmpDir() {
|
|
4679
5248
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
4680
5249
|
}
|
|
@@ -4696,7 +5265,7 @@ function listProjectDirs(...roots) {
|
|
|
4696
5265
|
try {
|
|
4697
5266
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
4698
5267
|
if (entry.isDirectory())
|
|
4699
|
-
dirs.add(
|
|
5268
|
+
dirs.add(join7(root, entry.name));
|
|
4700
5269
|
}
|
|
4701
5270
|
} catch {}
|
|
4702
5271
|
}
|
|
@@ -4707,7 +5276,7 @@ function projectRoot(projectDir, chatData) {
|
|
|
4707
5276
|
return chatData.projectPath;
|
|
4708
5277
|
if (chatData.project_path)
|
|
4709
5278
|
return chatData.project_path;
|
|
4710
|
-
const rootFile =
|
|
5279
|
+
const rootFile = join7(projectDir, ".project_root");
|
|
4711
5280
|
try {
|
|
4712
5281
|
if (existsSync6(rootFile))
|
|
4713
5282
|
return readFileSync4(rootFile, "utf-8").trim();
|
|
@@ -4729,17 +5298,17 @@ async function ingestGemini(db, verbose) {
|
|
|
4729
5298
|
const account = await resolveAccountForAgent("gemini");
|
|
4730
5299
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
4731
5300
|
for (const projectDir of projectDirs) {
|
|
4732
|
-
const chatsDir =
|
|
5301
|
+
const chatsDir = join7(projectDir, "chats");
|
|
4733
5302
|
if (!existsSync6(chatsDir))
|
|
4734
5303
|
continue;
|
|
4735
5304
|
let chatFiles = [];
|
|
4736
5305
|
try {
|
|
4737
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
5306
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join7(chatsDir, f));
|
|
4738
5307
|
} catch {
|
|
4739
5308
|
continue;
|
|
4740
5309
|
}
|
|
4741
5310
|
for (const filePath of chatFiles) {
|
|
4742
|
-
const stateKey = filePath.replace(
|
|
5311
|
+
const stateKey = filePath.replace(homedir6(), "~");
|
|
4743
5312
|
let fileMtime = "0";
|
|
4744
5313
|
try {
|
|
4745
5314
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -4830,19 +5399,19 @@ var init_gemini = __esm(() => {
|
|
|
4830
5399
|
init_database();
|
|
4831
5400
|
init_pricing();
|
|
4832
5401
|
init_accounts();
|
|
4833
|
-
DEFAULT_GEMINI_TMP_DIR =
|
|
4834
|
-
DEFAULT_GEMINI_HISTORY_DIR =
|
|
5402
|
+
DEFAULT_GEMINI_TMP_DIR = join7(homedir6(), ".gemini", "tmp");
|
|
5403
|
+
DEFAULT_GEMINI_HISTORY_DIR = join7(homedir6(), ".gemini", "history");
|
|
4835
5404
|
});
|
|
4836
5405
|
|
|
4837
5406
|
// src/ingest/opencode.ts
|
|
4838
5407
|
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
4839
|
-
import { homedir as
|
|
4840
|
-
import { join as
|
|
5408
|
+
import { homedir as homedir7 } from "os";
|
|
5409
|
+
import { join as join8 } from "path";
|
|
4841
5410
|
function walkJsonFiles(dir, acc = []) {
|
|
4842
5411
|
if (!existsSync7(dir))
|
|
4843
5412
|
return acc;
|
|
4844
5413
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
4845
|
-
const p =
|
|
5414
|
+
const p = join8(dir, entry.name);
|
|
4846
5415
|
if (entry.isDirectory())
|
|
4847
5416
|
walkJsonFiles(p, acc);
|
|
4848
5417
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4862,7 +5431,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
4862
5431
|
return null;
|
|
4863
5432
|
}
|
|
4864
5433
|
async function ingestOpenCode(db, verbose = false) {
|
|
4865
|
-
const messageDir =
|
|
5434
|
+
const messageDir = join8(OPENCODE_STORAGE, "message");
|
|
4866
5435
|
const files = walkJsonFiles(messageDir);
|
|
4867
5436
|
let requests = 0;
|
|
4868
5437
|
const touched = new Set;
|
|
@@ -4945,7 +5514,7 @@ var init_opencode = __esm(() => {
|
|
|
4945
5514
|
init_database();
|
|
4946
5515
|
init_pricing();
|
|
4947
5516
|
init_accounts();
|
|
4948
|
-
OPENCODE_STORAGE =
|
|
5517
|
+
OPENCODE_STORAGE = join8(homedir7(), ".local", "share", "opencode", "storage");
|
|
4949
5518
|
});
|
|
4950
5519
|
|
|
4951
5520
|
// src/ingest/cursor.ts
|
|
@@ -5074,13 +5643,13 @@ var init_cursor = __esm(() => {
|
|
|
5074
5643
|
|
|
5075
5644
|
// src/ingest/pi.ts
|
|
5076
5645
|
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
5077
|
-
import { homedir as
|
|
5078
|
-
import { join as
|
|
5646
|
+
import { homedir as homedir8 } from "os";
|
|
5647
|
+
import { join as join9 } from "path";
|
|
5079
5648
|
function walkSessions(dir, acc = []) {
|
|
5080
5649
|
if (!existsSync8(dir))
|
|
5081
5650
|
return acc;
|
|
5082
5651
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5083
|
-
const p =
|
|
5652
|
+
const p = join9(dir, entry.name);
|
|
5084
5653
|
if (entry.isDirectory())
|
|
5085
5654
|
walkSessions(p, acc);
|
|
5086
5655
|
else if (entry.name.endsWith(".json"))
|
|
@@ -5167,13 +5736,13 @@ var PI_SESSION_DIR;
|
|
|
5167
5736
|
var init_pi = __esm(() => {
|
|
5168
5737
|
init_database();
|
|
5169
5738
|
init_accounts();
|
|
5170
|
-
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
5739
|
+
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join9(homedir8(), ".pi", "agent", "sessions");
|
|
5171
5740
|
});
|
|
5172
5741
|
|
|
5173
5742
|
// src/ingest/hermes.ts
|
|
5174
5743
|
import { existsSync as existsSync9, statSync as statSync6 } from "fs";
|
|
5175
|
-
import { homedir as
|
|
5176
|
-
import { join as
|
|
5744
|
+
import { homedir as homedir9 } from "os";
|
|
5745
|
+
import { join as join10 } from "path";
|
|
5177
5746
|
function mapCostBasis(billingMode) {
|
|
5178
5747
|
if (billingMode === "subscription")
|
|
5179
5748
|
return "subscription_included";
|
|
@@ -5262,16 +5831,77 @@ var HERMES_DB;
|
|
|
5262
5831
|
var init_hermes = __esm(() => {
|
|
5263
5832
|
init_database();
|
|
5264
5833
|
init_accounts();
|
|
5265
|
-
HERMES_DB =
|
|
5834
|
+
HERMES_DB = join10(homedir9(), ".hermes", "state.db");
|
|
5266
5835
|
});
|
|
5267
5836
|
|
|
5268
5837
|
// src/ingest/loops.ts
|
|
5269
5838
|
import { existsSync as existsSync10 } from "fs";
|
|
5270
|
-
import { homedir as
|
|
5271
|
-
import { join as
|
|
5839
|
+
import { homedir as homedir10 } from "os";
|
|
5840
|
+
import { join as join11 } from "path";
|
|
5272
5841
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
5842
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
5843
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
5844
|
+
function pathsResolverAssertApp2(app) {
|
|
5845
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
5846
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
5847
|
+
}
|
|
5848
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
5849
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
function pathsResolverAssertKind2(kind) {
|
|
5853
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
5854
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
5858
|
+
pathsResolverAssertKind2(kind);
|
|
5859
|
+
const env = options.env ?? process.env;
|
|
5860
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
5861
|
+
if (typeof override === "string" && override.length > 0)
|
|
5862
|
+
return override;
|
|
5863
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
5864
|
+
const platform2 = options.platform ?? process.platform;
|
|
5865
|
+
if (platform2 === "darwin") {
|
|
5866
|
+
switch (kind) {
|
|
5867
|
+
case "config":
|
|
5868
|
+
case "data":
|
|
5869
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
5870
|
+
case "cache":
|
|
5871
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
5872
|
+
case "state":
|
|
5873
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
switch (kind) {
|
|
5877
|
+
case "config":
|
|
5878
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
5879
|
+
case "data":
|
|
5880
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
5881
|
+
case "state":
|
|
5882
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
5883
|
+
case "cache":
|
|
5884
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
function pathsResolverResolve2(kind, options) {
|
|
5888
|
+
pathsResolverAssertApp2(options.app);
|
|
5889
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
5890
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
5891
|
+
}
|
|
5892
|
+
function dataDir2(options) {
|
|
5893
|
+
return pathsResolverResolve2("data", options);
|
|
5894
|
+
}
|
|
5895
|
+
function defaultLoopsDbPath() {
|
|
5896
|
+
const resolved = dataDir2({
|
|
5897
|
+
app: "loops",
|
|
5898
|
+
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
5899
|
+
});
|
|
5900
|
+
const legacy = join11(process.env["HOME"] || process.env["USERPROFILE"] || homedir10(), ".hasna", "loops", "loops.db");
|
|
5901
|
+
return existsSync10(join11(resolved, "loops.db")) ? join11(resolved, "loops.db") : legacy;
|
|
5902
|
+
}
|
|
5273
5903
|
function loopsDbPath() {
|
|
5274
|
-
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ??
|
|
5904
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
5275
5905
|
}
|
|
5276
5906
|
function loopsModel() {
|
|
5277
5907
|
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
@@ -5447,11 +6077,17 @@ async function ingestLoops(db, verbose = false) {
|
|
|
5447
6077
|
rollupSession(db, sessionId);
|
|
5448
6078
|
return { sessions: sessions.size, requests };
|
|
5449
6079
|
}
|
|
5450
|
-
var
|
|
6080
|
+
var PATHS_RESOLVER_KIND_ENV2, PATHS_RESOLVER_APP_SLUG_RE2, LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
5451
6081
|
var init_loops = __esm(() => {
|
|
5452
6082
|
init_database();
|
|
5453
6083
|
init_pricing();
|
|
5454
|
-
|
|
6084
|
+
PATHS_RESOLVER_KIND_ENV2 = {
|
|
6085
|
+
config: "HASNA_CONFIG_HOME",
|
|
6086
|
+
data: "HASNA_DATA_HOME",
|
|
6087
|
+
state: "HASNA_STATE_HOME",
|
|
6088
|
+
cache: "HASNA_CACHE_HOME"
|
|
6089
|
+
};
|
|
6090
|
+
PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5455
6091
|
});
|
|
5456
6092
|
|
|
5457
6093
|
// src/ingest/claude-quota.ts
|
|
@@ -6092,26 +6728,26 @@ var init_open_projects = __esm(() => {
|
|
|
6092
6728
|
var exports_config = {};
|
|
6093
6729
|
__export(exports_config, {
|
|
6094
6730
|
setConfigValue: () => setConfigValue,
|
|
6095
|
-
saveConfig: () =>
|
|
6096
|
-
loadConfig: () =>
|
|
6731
|
+
saveConfig: () => saveConfig,
|
|
6732
|
+
loadConfig: () => loadConfig,
|
|
6097
6733
|
getConfigValue: () => getConfigValue
|
|
6098
6734
|
});
|
|
6099
|
-
import { existsSync as existsSync13, readFileSync as
|
|
6100
|
-
import { dirname as dirname2, join as
|
|
6735
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
6736
|
+
import { dirname as dirname2, join as join13 } from "path";
|
|
6101
6737
|
function getConfigPath() {
|
|
6102
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
6738
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
6103
6739
|
}
|
|
6104
|
-
function
|
|
6740
|
+
function loadConfig() {
|
|
6105
6741
|
try {
|
|
6106
6742
|
const configPath = getConfigPath();
|
|
6107
6743
|
if (existsSync13(configPath)) {
|
|
6108
|
-
const raw =
|
|
6744
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
6109
6745
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
6110
6746
|
}
|
|
6111
6747
|
} catch {}
|
|
6112
6748
|
return { ...DEFAULTS };
|
|
6113
6749
|
}
|
|
6114
|
-
function
|
|
6750
|
+
function saveConfig(config) {
|
|
6115
6751
|
const configPath = getConfigPath();
|
|
6116
6752
|
const dir = dirname2(configPath);
|
|
6117
6753
|
if (!existsSync13(dir))
|
|
@@ -6120,11 +6756,11 @@ function saveConfig2(config) {
|
|
|
6120
6756
|
`);
|
|
6121
6757
|
}
|
|
6122
6758
|
function getConfigValue(key) {
|
|
6123
|
-
const config =
|
|
6759
|
+
const config = loadConfig();
|
|
6124
6760
|
return config[key] ?? null;
|
|
6125
6761
|
}
|
|
6126
6762
|
function setConfigValue(key, value) {
|
|
6127
|
-
const config =
|
|
6763
|
+
const config = loadConfig();
|
|
6128
6764
|
let parsed = value;
|
|
6129
6765
|
if (value === "true")
|
|
6130
6766
|
parsed = true;
|
|
@@ -6140,7 +6776,7 @@ function setConfigValue(key, value) {
|
|
|
6140
6776
|
} catch {}
|
|
6141
6777
|
}
|
|
6142
6778
|
config[key] = parsed;
|
|
6143
|
-
|
|
6779
|
+
saveConfig(config);
|
|
6144
6780
|
}
|
|
6145
6781
|
var DEFAULTS;
|
|
6146
6782
|
var init_config = __esm(() => {
|
|
@@ -6198,7 +6834,7 @@ __export(exports_webhooks, {
|
|
|
6198
6834
|
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
6199
6835
|
});
|
|
6200
6836
|
async function checkAndFireWebhooks(db) {
|
|
6201
|
-
const config =
|
|
6837
|
+
const config = loadConfig();
|
|
6202
6838
|
const url = config["webhook-url"];
|
|
6203
6839
|
if (!url)
|
|
6204
6840
|
return;
|
|
@@ -6299,38 +6935,38 @@ var exports_watch = {};
|
|
|
6299
6935
|
__export(exports_watch, {
|
|
6300
6936
|
watchCosts: () => watchCosts
|
|
6301
6937
|
});
|
|
6302
|
-
import
|
|
6938
|
+
import chalk4 from "chalk";
|
|
6303
6939
|
import { watch } from "fs";
|
|
6304
6940
|
function fmt3(usd) {
|
|
6305
|
-
return
|
|
6941
|
+
return chalk4.green(`$${usd.toFixed(4)}`);
|
|
6306
6942
|
}
|
|
6307
6943
|
function renderHeader(todayUsd, weekUsd, mode) {
|
|
6308
6944
|
process.stdout.write("\x1B[H\x1B[2J");
|
|
6309
|
-
console.log(
|
|
6310
|
-
console.log(
|
|
6945
|
+
console.log(chalk4.bold.cyan(" economy watch") + chalk4.dim(` \u2014 live cost stream (${mode})`));
|
|
6946
|
+
console.log(chalk4.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"));
|
|
6311
6947
|
console.log(` Today: ${fmt3(todayUsd)} Week: ${fmt3(weekUsd)}`);
|
|
6312
|
-
console.log(
|
|
6313
|
-
console.log(
|
|
6314
|
-
console.log(
|
|
6948
|
+
console.log(chalk4.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"));
|
|
6949
|
+
console.log(chalk4.dim(" [agent] cost model tokens project"));
|
|
6950
|
+
console.log(chalk4.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"));
|
|
6315
6951
|
}
|
|
6316
6952
|
function agentLabel(agent) {
|
|
6317
6953
|
if (agent === "claude")
|
|
6318
|
-
return
|
|
6954
|
+
return chalk4.blue("[claude]");
|
|
6319
6955
|
if (agent === "codex")
|
|
6320
|
-
return
|
|
6956
|
+
return chalk4.yellow("[codex] ");
|
|
6321
6957
|
if (agent === "gemini")
|
|
6322
|
-
return
|
|
6958
|
+
return chalk4.green("[gemini]");
|
|
6323
6959
|
if (agent === "takumi")
|
|
6324
|
-
return
|
|
6960
|
+
return chalk4.magenta("[takumi]");
|
|
6325
6961
|
if (agent === "opencode")
|
|
6326
|
-
return
|
|
6962
|
+
return chalk4.cyan("[opncde]");
|
|
6327
6963
|
if (agent === "cursor")
|
|
6328
|
-
return
|
|
6964
|
+
return chalk4.white("[cursor]");
|
|
6329
6965
|
if (agent === "pi")
|
|
6330
|
-
return
|
|
6966
|
+
return chalk4.white("[pi ]");
|
|
6331
6967
|
if (agent === "hermes")
|
|
6332
|
-
return
|
|
6333
|
-
return
|
|
6968
|
+
return chalk4.white("[hermes]");
|
|
6969
|
+
return chalk4.gray(`[${agent.slice(0, 6).padEnd(6)}]`);
|
|
6334
6970
|
}
|
|
6335
6971
|
async function watchCosts(opts) {
|
|
6336
6972
|
const store = getStore();
|
|
@@ -6350,7 +6986,7 @@ async function watchCosts(opts) {
|
|
|
6350
6986
|
renderHeader(initialSummaryToday.total_usd, initialSummaryWeek.total_usd, mode);
|
|
6351
6987
|
if (daemon) {
|
|
6352
6988
|
const paths = getWatchPaths();
|
|
6353
|
-
console.log(
|
|
6989
|
+
console.log(chalk4.dim(`
|
|
6354
6990
|
Watching ${paths.length} paths \u2014 sync on change, refresh every ${opts.interval}s
|
|
6355
6991
|
`));
|
|
6356
6992
|
for (const p of paths) {
|
|
@@ -6363,11 +6999,11 @@ async function watchCosts(opts) {
|
|
|
6363
6999
|
}
|
|
6364
7000
|
}
|
|
6365
7001
|
} else if (cloud) {
|
|
6366
|
-
console.log(
|
|
7002
|
+
console.log(chalk4.dim(`
|
|
6367
7003
|
Streaming from cloud API every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
6368
7004
|
`));
|
|
6369
7005
|
} else {
|
|
6370
|
-
console.log(
|
|
7006
|
+
console.log(chalk4.dim(`
|
|
6371
7007
|
Polling every ${opts.interval}s \u2014 Ctrl+C to exit
|
|
6372
7008
|
`));
|
|
6373
7009
|
}
|
|
@@ -6415,9 +7051,9 @@ async function watchCosts(opts) {
|
|
|
6415
7051
|
for (const line of lines)
|
|
6416
7052
|
console.log(line);
|
|
6417
7053
|
if (lines.length === 0)
|
|
6418
|
-
console.log(
|
|
7054
|
+
console.log(chalk4.dim(" Waiting for new requests..."));
|
|
6419
7055
|
const suffix = daemon ? `file watch + ${opts.interval}s refresh` : cloud ? `cloud stream every ${opts.interval}s` : `polling every ${opts.interval}s`;
|
|
6420
|
-
console.log(
|
|
7056
|
+
console.log(chalk4.dim(`
|
|
6421
7057
|
Last updated: ${new Date().toLocaleTimeString()} \u2014 ${suffix} \u2014 Ctrl+C to exit`));
|
|
6422
7058
|
}
|
|
6423
7059
|
if (daemon)
|
|
@@ -6425,7 +7061,7 @@ async function watchCosts(opts) {
|
|
|
6425
7061
|
await poll();
|
|
6426
7062
|
const timer = setInterval(() => {
|
|
6427
7063
|
poll().catch((e) => {
|
|
6428
|
-
console.log(
|
|
7064
|
+
console.log(chalk4.dim(`
|
|
6429
7065
|
Poll error: ${e instanceof Error ? e.message : String(e)} \u2014 retrying next tick`));
|
|
6430
7066
|
});
|
|
6431
7067
|
}, opts.interval * 1000);
|
|
@@ -6433,7 +7069,7 @@ async function watchCosts(opts) {
|
|
|
6433
7069
|
clearInterval(timer);
|
|
6434
7070
|
if (ingestTimer)
|
|
6435
7071
|
clearTimeout(ingestTimer);
|
|
6436
|
-
console.log(
|
|
7072
|
+
console.log(chalk4.dim(`
|
|
6437
7073
|
|
|
6438
7074
|
Stopped watching.`));
|
|
6439
7075
|
process.exit(0);
|
|
@@ -6488,7 +7124,7 @@ function sqliteToPostgres(sql) {
|
|
|
6488
7124
|
// src/db/sync-pg.ts
|
|
6489
7125
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
6490
7126
|
import { fileURLToPath } from "url";
|
|
6491
|
-
import { dirname as dirname3, join as
|
|
7127
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
6492
7128
|
import { existsSync as existsSync15 } from "fs";
|
|
6493
7129
|
function translateSqliteDates(sql) {
|
|
6494
7130
|
let s = sql;
|
|
@@ -6514,9 +7150,9 @@ function translateSqliteDates(sql) {
|
|
|
6514
7150
|
function resolveWorkerPath() {
|
|
6515
7151
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
6516
7152
|
const candidates = [
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
7153
|
+
join14(here, "pg-sync-worker.ts"),
|
|
7154
|
+
join14(here, "pg-sync-worker.js"),
|
|
7155
|
+
join14(here, "..", "db", "pg-sync-worker.js")
|
|
6520
7156
|
];
|
|
6521
7157
|
for (const candidate of candidates)
|
|
6522
7158
|
if (existsSync15(candidate))
|
|
@@ -8327,12 +8963,9 @@ var exports_serve = {};
|
|
|
8327
8963
|
__export(exports_serve, {
|
|
8328
8964
|
startServer: () => startServer,
|
|
8329
8965
|
openApiDocument: () => openApiDocument,
|
|
8330
|
-
createServerFetch: () => createServerFetch,
|
|
8331
8966
|
createHandler: () => createHandler
|
|
8332
8967
|
});
|
|
8333
8968
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8334
|
-
import { existsSync as existsSync16 } from "fs";
|
|
8335
|
-
import { resolve, sep } from "path";
|
|
8336
8969
|
import { verifyApiKey, ApiKeyStore } from "@hasna/contracts/auth";
|
|
8337
8970
|
function openApiDocument() {
|
|
8338
8971
|
const spec = openApiSpec;
|
|
@@ -8393,37 +9026,6 @@ function requiredScopesForRequest(method, path) {
|
|
|
8393
9026
|
function stringArray(value) {
|
|
8394
9027
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
8395
9028
|
}
|
|
8396
|
-
function dashboardPath(root, pathname) {
|
|
8397
|
-
let decoded;
|
|
8398
|
-
try {
|
|
8399
|
-
decoded = decodeURIComponent(pathname);
|
|
8400
|
-
} catch {
|
|
8401
|
-
return null;
|
|
8402
|
-
}
|
|
8403
|
-
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
8404
|
-
const rootPath = resolve(root);
|
|
8405
|
-
const filePath = resolve(rootPath, relativePath);
|
|
8406
|
-
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
8407
|
-
}
|
|
8408
|
-
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
8409
|
-
return async function fetch2(req) {
|
|
8410
|
-
const url = new URL(req.url);
|
|
8411
|
-
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
8412
|
-
return apiHandler(req);
|
|
8413
|
-
}
|
|
8414
|
-
if (existsSync16(dashboardDir)) {
|
|
8415
|
-
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
8416
|
-
if (filePath && existsSync16(filePath)) {
|
|
8417
|
-
return new Response(Bun.file(filePath));
|
|
8418
|
-
}
|
|
8419
|
-
const indexPath = dashboardPath(dashboardDir, "/");
|
|
8420
|
-
if (indexPath && existsSync16(indexPath)) {
|
|
8421
|
-
return new Response(Bun.file(indexPath));
|
|
8422
|
-
}
|
|
8423
|
-
}
|
|
8424
|
-
return apiHandler(req);
|
|
8425
|
-
};
|
|
8426
|
-
}
|
|
8427
9029
|
function applyFields(obj, fields) {
|
|
8428
9030
|
if (!fields || fields.length === 0)
|
|
8429
9031
|
return obj;
|
|
@@ -8947,13 +9549,13 @@ function startServer(port = 3456, options = {}) {
|
|
|
8947
9549
|
const server = Bun.serve({
|
|
8948
9550
|
port,
|
|
8949
9551
|
hostname: hostname2,
|
|
8950
|
-
fetch:
|
|
9552
|
+
fetch: apiHandler
|
|
8951
9553
|
});
|
|
8952
9554
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
8953
9555
|
log(`economy-serve listening on ${address}`);
|
|
8954
9556
|
return server;
|
|
8955
9557
|
}
|
|
8956
|
-
var CORS, APP = "economy", ECONOMY_WRITE_SCOPE, AGENT_ERROR, SYNC_SOURCES
|
|
9558
|
+
var CORS, APP = "economy", ECONOMY_WRITE_SCOPE, AGENT_ERROR, SYNC_SOURCES;
|
|
8957
9559
|
var init_serve = __esm(() => {
|
|
8958
9560
|
init_database();
|
|
8959
9561
|
init_pricing();
|
|
@@ -8974,8 +9576,6 @@ var init_serve = __esm(() => {
|
|
|
8974
9576
|
ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
8975
9577
|
AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
8976
9578
|
SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
8977
|
-
DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
8978
|
-
FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
8979
9579
|
});
|
|
8980
9580
|
|
|
8981
9581
|
// src/cli/commands/menubar.ts
|
|
@@ -8986,16 +9586,16 @@ __export(exports_menubar, {
|
|
|
8986
9586
|
menubarStart: () => menubarStart,
|
|
8987
9587
|
menubarInstall: () => menubarInstall
|
|
8988
9588
|
});
|
|
8989
|
-
import
|
|
9589
|
+
import chalk5 from "chalk";
|
|
8990
9590
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
8991
|
-
import { cpSync, existsSync as
|
|
9591
|
+
import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
8992
9592
|
import { tmpdir, arch } from "os";
|
|
8993
|
-
import { join as
|
|
9593
|
+
import { join as join15 } from "path";
|
|
8994
9594
|
function getArch() {
|
|
8995
9595
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
8996
9596
|
}
|
|
8997
9597
|
function isInstalled() {
|
|
8998
|
-
return
|
|
9598
|
+
return existsSync16(APP_PATH);
|
|
8999
9599
|
}
|
|
9000
9600
|
function isRunning() {
|
|
9001
9601
|
try {
|
|
@@ -9007,13 +9607,13 @@ function isRunning() {
|
|
|
9007
9607
|
}
|
|
9008
9608
|
async function menubarInstall(opts) {
|
|
9009
9609
|
if (isInstalled() && !opts.force) {
|
|
9010
|
-
console.log(
|
|
9011
|
-
console.log(
|
|
9610
|
+
console.log(chalk5.yellow("Economy Bar is already installed. Use --force to reinstall."));
|
|
9611
|
+
console.log(chalk5.dim(` Location: ${APP_PATH}`));
|
|
9012
9612
|
return;
|
|
9013
9613
|
}
|
|
9014
9614
|
const cpuArch = getArch();
|
|
9015
|
-
console.log(
|
|
9016
|
-
console.log(
|
|
9615
|
+
console.log(chalk5.cyan(`\u2192 Detecting architecture: ${cpuArch}`));
|
|
9616
|
+
console.log(chalk5.cyan("\u2192 Fetching latest release info..."));
|
|
9017
9617
|
let assetUrl;
|
|
9018
9618
|
try {
|
|
9019
9619
|
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
|
|
@@ -9029,54 +9629,54 @@ async function menubarInstall(opts) {
|
|
|
9029
9629
|
throw new Error(`No asset found for ${assetName}. Check releases at https://github.com/${REPO}/releases`);
|
|
9030
9630
|
assetUrl = asset.browser_download_url;
|
|
9031
9631
|
} catch (e) {
|
|
9032
|
-
console.error(
|
|
9632
|
+
console.error(chalk5.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
9033
9633
|
process.exit(1);
|
|
9034
9634
|
}
|
|
9035
|
-
const zipPath =
|
|
9036
|
-
const extractDir =
|
|
9037
|
-
console.log(
|
|
9635
|
+
const zipPath = join15(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
9636
|
+
const extractDir = join15(tmpdir(), "economy-bar-extracted");
|
|
9637
|
+
console.log(chalk5.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
9038
9638
|
try {
|
|
9039
9639
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
9040
9640
|
if (!res.ok)
|
|
9041
9641
|
throw new Error(`Download failed: ${res.status}`);
|
|
9042
9642
|
const buffer = await res.arrayBuffer();
|
|
9043
9643
|
writeFileSync3(zipPath, Buffer.from(buffer));
|
|
9044
|
-
console.log(
|
|
9644
|
+
console.log(chalk5.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
9045
9645
|
} catch (e) {
|
|
9046
|
-
console.error(
|
|
9646
|
+
console.error(chalk5.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
9047
9647
|
process.exit(1);
|
|
9048
9648
|
}
|
|
9049
|
-
console.log(
|
|
9649
|
+
console.log(chalk5.cyan("\u2192 Installing to /Applications..."));
|
|
9050
9650
|
try {
|
|
9051
9651
|
rmSync(extractDir, { recursive: true, force: true });
|
|
9052
9652
|
mkdirSync4(extractDir, { recursive: true });
|
|
9053
9653
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
9054
9654
|
if (isInstalled())
|
|
9055
9655
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
9056
|
-
cpSync(
|
|
9656
|
+
cpSync(join15(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
9057
9657
|
try {
|
|
9058
9658
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
9059
9659
|
} catch {}
|
|
9060
9660
|
rmSync(zipPath, { force: true });
|
|
9061
9661
|
rmSync(extractDir, { recursive: true, force: true });
|
|
9062
|
-
console.log(
|
|
9662
|
+
console.log(chalk5.green(`\u2713 Installed to ${APP_PATH}`));
|
|
9063
9663
|
} catch (e) {
|
|
9064
|
-
console.error(
|
|
9664
|
+
console.error(chalk5.red(`\u2717 Install failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
9065
9665
|
process.exit(1);
|
|
9066
9666
|
}
|
|
9067
|
-
console.log(
|
|
9667
|
+
console.log(chalk5.cyan("\u2192 Launching Economy Bar..."));
|
|
9068
9668
|
try {
|
|
9069
9669
|
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
9070
|
-
console.log(
|
|
9670
|
+
console.log(chalk5.bold.green(`
|
|
9071
9671
|
\u2713 Economy Bar is running in your menu bar!`));
|
|
9072
|
-
console.log(
|
|
9672
|
+
console.log(chalk5.dim(" Make sure economy serve is running: economy serve"));
|
|
9073
9673
|
} catch (e) {
|
|
9074
|
-
console.log(
|
|
9674
|
+
console.log(chalk5.yellow("\u26A0 Installed but could not auto-launch. Open from /Applications manually."));
|
|
9075
9675
|
}
|
|
9076
9676
|
}
|
|
9077
9677
|
function menubarUninstall() {
|
|
9078
9678
|
if (!isInstalled()) {
|
|
9079
|
-
console.log(
|
|
9679
|
+
console.log(chalk5.yellow("Economy Bar is not installed."));
|
|
9080
9680
|
return;
|
|
9081
9681
|
}
|
|
9082
9682
|
if (isRunning()) {
|
|
@@ -9086,26 +9686,26 @@ function menubarUninstall() {
|
|
|
9086
9686
|
} catch {}
|
|
9087
9687
|
}
|
|
9088
9688
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
9089
|
-
console.log(
|
|
9689
|
+
console.log(chalk5.green("\u2713 Economy Bar uninstalled"));
|
|
9090
9690
|
}
|
|
9091
9691
|
function menubarStart() {
|
|
9092
9692
|
if (!isInstalled()) {
|
|
9093
|
-
console.error(
|
|
9693
|
+
console.error(chalk5.red("Economy Bar is not installed. Run: economy menubar install"));
|
|
9094
9694
|
process.exit(1);
|
|
9095
9695
|
}
|
|
9096
9696
|
execFileSync3("open", [APP_PATH], { stdio: "ignore" });
|
|
9097
|
-
console.log(
|
|
9697
|
+
console.log(chalk5.green("\u2713 Economy Bar launched"));
|
|
9098
9698
|
}
|
|
9099
9699
|
function menubarStop() {
|
|
9100
9700
|
if (!isRunning()) {
|
|
9101
|
-
console.log(
|
|
9701
|
+
console.log(chalk5.yellow("Economy Bar is not running."));
|
|
9102
9702
|
return;
|
|
9103
9703
|
}
|
|
9104
9704
|
try {
|
|
9105
9705
|
execFileSync3("osascript", ["-e", 'quit app "Economy Bar"'], { stdio: "ignore" });
|
|
9106
|
-
console.log(
|
|
9706
|
+
console.log(chalk5.green("\u2713 Economy Bar stopped"));
|
|
9107
9707
|
} catch {
|
|
9108
|
-
console.log(
|
|
9708
|
+
console.log(chalk5.yellow("Could not quit Economy Bar gracefully"));
|
|
9109
9709
|
}
|
|
9110
9710
|
}
|
|
9111
9711
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/economy";
|
|
@@ -9114,369 +9714,10 @@ var init_menubar = () => {};
|
|
|
9114
9714
|
// src/cli/index.ts
|
|
9115
9715
|
import { Command } from "commander";
|
|
9116
9716
|
import { registerEventsCommands } from "@hasna/events/commander";
|
|
9117
|
-
import
|
|
9118
|
-
|
|
9119
|
-
// src/cli/brains.ts
|
|
9120
|
-
import { writeFile, mkdir } from "fs/promises";
|
|
9121
|
-
import { join as join3 } from "path";
|
|
9122
|
-
import chalk from "chalk";
|
|
9123
|
-
|
|
9124
|
-
// src/lib/gatherer.ts
|
|
9125
|
-
init_store();
|
|
9126
|
-
var SYSTEM_PROMPT = "You are a cost-aware AI assistant that tracks API usage, identifies expensive patterns, and helps optimize spending.";
|
|
9127
|
-
function hasCostData(summary) {
|
|
9128
|
-
return summary.total_usd > 0 || summary.sessions > 0 || summary.requests > 0 || summary.tokens > 0;
|
|
9129
|
-
}
|
|
9130
|
-
var gatherTrainingData = async (options = {}) => {
|
|
9131
|
-
const limit = options.limit ?? 500;
|
|
9132
|
-
const examples = [];
|
|
9133
|
-
try {
|
|
9134
|
-
const store = getStore();
|
|
9135
|
-
const periods = ["today", "week", "month", "all"];
|
|
9136
|
-
for (const period of periods) {
|
|
9137
|
-
try {
|
|
9138
|
-
const s = await store.summary(period);
|
|
9139
|
-
if (!hasCostData(s))
|
|
9140
|
-
continue;
|
|
9141
|
-
examples.push({
|
|
9142
|
-
messages: [
|
|
9143
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9144
|
-
{ role: "user", content: `What did I spend on AI ${period === "all" ? "in total" : period}?` },
|
|
9145
|
-
{
|
|
9146
|
-
role: "assistant",
|
|
9147
|
-
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.`
|
|
9148
|
-
}
|
|
9149
|
-
]
|
|
9150
|
-
});
|
|
9151
|
-
} catch {}
|
|
9152
|
-
}
|
|
9153
|
-
const sessions = await store.sessions({
|
|
9154
|
-
limit: Math.min(Math.floor(limit / 4), 50),
|
|
9155
|
-
since: options.since?.toISOString().substring(0, 10)
|
|
9156
|
-
});
|
|
9157
|
-
for (const s of sessions) {
|
|
9158
|
-
examples.push({
|
|
9159
|
-
messages: [
|
|
9160
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9161
|
-
{
|
|
9162
|
-
role: "user",
|
|
9163
|
-
content: `How much did the session "${s.id.substring(0, 12)}" cost?`
|
|
9164
|
-
},
|
|
9165
|
-
{
|
|
9166
|
-
role: "assistant",
|
|
9167
|
-
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)}.`
|
|
9168
|
-
}
|
|
9169
|
-
]
|
|
9170
|
-
});
|
|
9171
|
-
examples.push({
|
|
9172
|
-
messages: [
|
|
9173
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9174
|
-
{
|
|
9175
|
-
role: "user",
|
|
9176
|
-
content: `What was the token usage for session ${s.id.substring(0, 12)}?`
|
|
9177
|
-
},
|
|
9178
|
-
{
|
|
9179
|
-
role: "assistant",
|
|
9180
|
-
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}).`
|
|
9181
|
-
}
|
|
9182
|
-
]
|
|
9183
|
-
});
|
|
9184
|
-
if (examples.length >= limit)
|
|
9185
|
-
break;
|
|
9186
|
-
}
|
|
9187
|
-
const modelBreakdown = await store.modelBreakdown();
|
|
9188
|
-
if (modelBreakdown.length > 0) {
|
|
9189
|
-
const topModels = modelBreakdown.slice(0, 5);
|
|
9190
|
-
examples.push({
|
|
9191
|
-
messages: [
|
|
9192
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9193
|
-
{ role: "user", content: "Which AI models have I spent the most on?" },
|
|
9194
|
-
{
|
|
9195
|
-
role: "assistant",
|
|
9196
|
-
content: `Model cost breakdown (top ${topModels.length}):
|
|
9197
|
-
${topModels.map((m) => `- ${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)}, ${m.requests} requests, ${m.total_tokens.toLocaleString()} tokens`).join(`
|
|
9198
|
-
`)}`
|
|
9199
|
-
}
|
|
9200
|
-
]
|
|
9201
|
-
});
|
|
9202
|
-
for (const m of topModels) {
|
|
9203
|
-
examples.push({
|
|
9204
|
-
messages: [
|
|
9205
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9206
|
-
{ role: "user", content: `How much have I spent on ${m.model}?` },
|
|
9207
|
-
{
|
|
9208
|
-
role: "assistant",
|
|
9209
|
-
content: `${m.model} (${m.agent}): $${m.cost_usd.toFixed(4)} total across ${m.requests.toLocaleString()} requests and ${m.total_tokens.toLocaleString()} tokens.`
|
|
9210
|
-
}
|
|
9211
|
-
]
|
|
9212
|
-
});
|
|
9213
|
-
}
|
|
9214
|
-
}
|
|
9215
|
-
const projectBreakdown = await store.projectBreakdown();
|
|
9216
|
-
if (projectBreakdown.length > 0) {
|
|
9217
|
-
const topProjects = projectBreakdown.slice(0, 5);
|
|
9218
|
-
examples.push({
|
|
9219
|
-
messages: [
|
|
9220
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9221
|
-
{ role: "user", content: "Which projects are costing the most?" },
|
|
9222
|
-
{
|
|
9223
|
-
role: "assistant",
|
|
9224
|
-
content: `Project cost breakdown (top ${topProjects.length}):
|
|
9225
|
-
${topProjects.map((p) => `- ${p.project_name || "unknown"}: $${p.cost_usd.toFixed(4)}, ${p.sessions} sessions`).join(`
|
|
9226
|
-
`)}`
|
|
9227
|
-
}
|
|
9228
|
-
]
|
|
9229
|
-
});
|
|
9230
|
-
for (const p of topProjects.slice(0, 3)) {
|
|
9231
|
-
examples.push({
|
|
9232
|
-
messages: [
|
|
9233
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9234
|
-
{ role: "user", content: `What is the AI spend for project "${p.project_name}"?` },
|
|
9235
|
-
{
|
|
9236
|
-
role: "assistant",
|
|
9237
|
-
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"}.`
|
|
9238
|
-
}
|
|
9239
|
-
]
|
|
9240
|
-
});
|
|
9241
|
-
}
|
|
9242
|
-
}
|
|
9243
|
-
try {
|
|
9244
|
-
const budgets = await store.listBudgets();
|
|
9245
|
-
if (budgets.length > 0) {
|
|
9246
|
-
examples.push({
|
|
9247
|
-
messages: [
|
|
9248
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9249
|
-
{ role: "user", content: "How am I tracking against my AI spending budgets?" },
|
|
9250
|
-
{
|
|
9251
|
-
role: "assistant",
|
|
9252
|
-
content: `Budget status:
|
|
9253
|
-
${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(`
|
|
9254
|
-
`)}`
|
|
9255
|
-
}
|
|
9256
|
-
]
|
|
9257
|
-
});
|
|
9258
|
-
}
|
|
9259
|
-
} catch {}
|
|
9260
|
-
try {
|
|
9261
|
-
const goals = await store.listGoals();
|
|
9262
|
-
if (goals.length > 0) {
|
|
9263
|
-
examples.push({
|
|
9264
|
-
messages: [
|
|
9265
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9266
|
-
{ role: "user", content: "Am I on track with my AI cost reduction goals?" },
|
|
9267
|
-
{
|
|
9268
|
-
role: "assistant",
|
|
9269
|
-
content: `Goal progress:
|
|
9270
|
-
${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(`
|
|
9271
|
-
`)}`
|
|
9272
|
-
}
|
|
9273
|
-
]
|
|
9274
|
-
});
|
|
9275
|
-
}
|
|
9276
|
-
} catch {}
|
|
9277
|
-
const weekSummary = await store.summary("week");
|
|
9278
|
-
const monthSummary = await store.summary("month");
|
|
9279
|
-
if (weekSummary.total_usd > 0) {
|
|
9280
|
-
const projectedMonthly = weekSummary.total_usd / 7 * 30;
|
|
9281
|
-
examples.push({
|
|
9282
|
-
messages: [
|
|
9283
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9284
|
-
{ role: "user", content: "Based on my usage, what will I spend this month?" },
|
|
9285
|
-
{
|
|
9286
|
-
role: "assistant",
|
|
9287
|
-
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)}.`
|
|
9288
|
-
}
|
|
9289
|
-
]
|
|
9290
|
-
});
|
|
9291
|
-
}
|
|
9292
|
-
if (modelBreakdown.length > 1) {
|
|
9293
|
-
const expensive = modelBreakdown[0];
|
|
9294
|
-
const cheaper = modelBreakdown[modelBreakdown.length - 1];
|
|
9295
|
-
examples.push({
|
|
9296
|
-
messages: [
|
|
9297
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
9298
|
-
{ role: "user", content: "How can I reduce my AI API costs?" },
|
|
9299
|
-
{
|
|
9300
|
-
role: "assistant",
|
|
9301
|
-
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.`
|
|
9302
|
-
}
|
|
9303
|
-
]
|
|
9304
|
-
});
|
|
9305
|
-
}
|
|
9306
|
-
} catch {}
|
|
9307
|
-
const finalExamples = examples.slice(0, limit);
|
|
9308
|
-
return { source: "economy", examples: finalExamples, count: finalExamples.length };
|
|
9309
|
-
};
|
|
9310
|
-
|
|
9311
|
-
// src/lib/model-config.ts
|
|
9312
|
-
init_database();
|
|
9313
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
9314
|
-
import { dirname, join as join2 } from "path";
|
|
9315
|
-
var DEFAULT_MODEL = "gpt-4o-mini";
|
|
9316
|
-
function getModelConfigPath() {
|
|
9317
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join2(getDataDir(), "config.json");
|
|
9318
|
-
}
|
|
9319
|
-
function loadConfig() {
|
|
9320
|
-
try {
|
|
9321
|
-
const configPath = getModelConfigPath();
|
|
9322
|
-
if (existsSync2(configPath)) {
|
|
9323
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
9324
|
-
}
|
|
9325
|
-
} catch {}
|
|
9326
|
-
return {};
|
|
9327
|
-
}
|
|
9328
|
-
function saveConfig(config) {
|
|
9329
|
-
const configPath = getModelConfigPath();
|
|
9330
|
-
const dir = dirname(configPath);
|
|
9331
|
-
if (!existsSync2(dir))
|
|
9332
|
-
mkdirSync2(dir, { recursive: true });
|
|
9333
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + `
|
|
9334
|
-
`);
|
|
9335
|
-
}
|
|
9336
|
-
function getActiveModel() {
|
|
9337
|
-
return loadConfig().activeModel ?? DEFAULT_MODEL;
|
|
9338
|
-
}
|
|
9339
|
-
function setActiveModel(id) {
|
|
9340
|
-
const config = loadConfig();
|
|
9341
|
-
config.activeModel = id;
|
|
9342
|
-
saveConfig(config);
|
|
9343
|
-
}
|
|
9344
|
-
function clearActiveModel() {
|
|
9345
|
-
const config = loadConfig();
|
|
9346
|
-
delete config.activeModel;
|
|
9347
|
-
saveConfig(config);
|
|
9348
|
-
}
|
|
9349
|
-
|
|
9350
|
-
// src/cli/brains.ts
|
|
9351
|
-
init_database();
|
|
9352
|
-
function registerBrainsCommand(program) {
|
|
9353
|
-
const brainsCmd = program.command("brains").description("Fine-tune an AI model on your economy cost data");
|
|
9354
|
-
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) => {
|
|
9355
|
-
const limit = opts.limit ? parseInt(opts.limit, 10) : 500;
|
|
9356
|
-
console.log(chalk.cyan(`Gathering up to ${limit} training examples from economy data...`));
|
|
9357
|
-
try {
|
|
9358
|
-
const result = await gatherTrainingData({ limit });
|
|
9359
|
-
if (result.count === 0) {
|
|
9360
|
-
console.log(chalk.yellow("No training examples found. Make sure you have cost data synced."));
|
|
9361
|
-
console.log(chalk.dim("Run: economy sync"));
|
|
9362
|
-
return;
|
|
9363
|
-
}
|
|
9364
|
-
const defaultDir = join3(getDataDir(), "training");
|
|
9365
|
-
await mkdir(defaultDir, { recursive: true });
|
|
9366
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
9367
|
-
const outputPath = opts.output ?? join3(defaultDir, `training-${timestamp}.jsonl`);
|
|
9368
|
-
const jsonl = result.examples.map((ex) => JSON.stringify(ex)).join(`
|
|
9369
|
-
`);
|
|
9370
|
-
await writeFile(outputPath, jsonl, "utf-8");
|
|
9371
|
-
console.log(chalk.green(`\u2713 Gathered ${result.count} training examples`));
|
|
9372
|
-
console.log(chalk.dim(` Written to: ${outputPath}`));
|
|
9373
|
-
console.log(`
|
|
9374
|
-
${chalk.dim("Next step:")} economy brains train --base-model gpt-4o-mini`);
|
|
9375
|
-
} catch (e) {
|
|
9376
|
-
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
9377
|
-
process.exit(1);
|
|
9378
|
-
}
|
|
9379
|
-
});
|
|
9380
|
-
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) => {
|
|
9381
|
-
const baseModel = opts.baseModel ?? "gpt-4o-mini";
|
|
9382
|
-
const name = opts.name ?? "economy-assistant";
|
|
9383
|
-
console.log(chalk.cyan("Starting fine-tuning job..."));
|
|
9384
|
-
console.log(chalk.dim(` Base model: ${baseModel}`));
|
|
9385
|
-
console.log(chalk.dim(` Name: ${name}`));
|
|
9386
|
-
let datasetPath = opts.dataset;
|
|
9387
|
-
if (!datasetPath) {
|
|
9388
|
-
const { readdirSync: readdirSync2 } = await import("fs");
|
|
9389
|
-
const trainingDir = join3(getDataDir(), "training");
|
|
9390
|
-
try {
|
|
9391
|
-
const files = readdirSync2(trainingDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
9392
|
-
if (files.length === 0) {
|
|
9393
|
-
console.error(chalk.red("No training data found. Run: economy brains gather"));
|
|
9394
|
-
process.exit(1);
|
|
9395
|
-
}
|
|
9396
|
-
datasetPath = join3(trainingDir, files[0]);
|
|
9397
|
-
console.log(chalk.dim(` Dataset: ${datasetPath}`));
|
|
9398
|
-
} catch {
|
|
9399
|
-
console.error(chalk.red("Training directory not found. Run: economy brains gather"));
|
|
9400
|
-
process.exit(1);
|
|
9401
|
-
}
|
|
9402
|
-
}
|
|
9403
|
-
try {
|
|
9404
|
-
const brains = await import("@hasna/brains");
|
|
9405
|
-
const startFinetune = brains["startFinetune"] ?? brains["start_finetune"];
|
|
9406
|
-
if (typeof startFinetune !== "function") {
|
|
9407
|
-
console.error(chalk.red("@hasna/brains not found or startFinetune not exported."));
|
|
9408
|
-
console.error(chalk.dim("Install with: bun add @hasna/brains"));
|
|
9409
|
-
process.exit(1);
|
|
9410
|
-
}
|
|
9411
|
-
const job = await startFinetune({
|
|
9412
|
-
provider: "openai",
|
|
9413
|
-
baseModel,
|
|
9414
|
-
name,
|
|
9415
|
-
dataset: datasetPath
|
|
9416
|
-
});
|
|
9417
|
-
const jobId = job["id"] ?? job["fine_tune_job_id"] ?? job["jobId"];
|
|
9418
|
-
console.log(chalk.green(`\u2713 Fine-tuning job started: ${String(jobId ?? "unknown")}`));
|
|
9419
|
-
console.log(`
|
|
9420
|
-
${chalk.dim("Check status:")} economy brains status ${String(jobId ?? "")}`);
|
|
9421
|
-
console.log(`${chalk.dim("When complete, set model:")} economy brains model set <model-id>`);
|
|
9422
|
-
} catch (e) {
|
|
9423
|
-
console.error(chalk.red(`Error starting fine-tune: ${e instanceof Error ? e.message : String(e)}`));
|
|
9424
|
-
process.exit(1);
|
|
9425
|
-
}
|
|
9426
|
-
});
|
|
9427
|
-
const modelCmd = brainsCmd.command("model").description("View or set the active fine-tuned model").action(() => {
|
|
9428
|
-
const active = getActiveModel();
|
|
9429
|
-
const isDefault = active === DEFAULT_MODEL;
|
|
9430
|
-
console.log(`Active model: ${chalk.cyan(active)}${isDefault ? chalk.dim(" (default)") : chalk.green(" (fine-tuned)")}`);
|
|
9431
|
-
if (isDefault) {
|
|
9432
|
-
console.log(chalk.dim(`
|
|
9433
|
-
To set a fine-tuned model: economy brains model set <model-id>`));
|
|
9434
|
-
}
|
|
9435
|
-
});
|
|
9436
|
-
modelCmd.command("set <id>").description("Set the active fine-tuned model ID").action((id) => {
|
|
9437
|
-
setActiveModel(id);
|
|
9438
|
-
console.log(chalk.green(`\u2713 Active model set to: ${id}`));
|
|
9439
|
-
console.log(chalk.dim(" Economy AI analysis will now use this model."));
|
|
9440
|
-
});
|
|
9441
|
-
modelCmd.command("clear").description(`Reset to default model (${DEFAULT_MODEL})`).action(() => {
|
|
9442
|
-
clearActiveModel();
|
|
9443
|
-
console.log(chalk.green(`\u2713 Active model cleared, using default: ${DEFAULT_MODEL}`));
|
|
9444
|
-
});
|
|
9445
|
-
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) => {
|
|
9446
|
-
if (!jobId) {
|
|
9447
|
-
console.error(chalk.red("Usage: economy brains status <job-id>"));
|
|
9448
|
-
process.exit(1);
|
|
9449
|
-
}
|
|
9450
|
-
try {
|
|
9451
|
-
const brains = await import("@hasna/brains");
|
|
9452
|
-
const getFinetuneStatus = brains["getFinetuneStatus"] ?? brains["get_finetune_status"];
|
|
9453
|
-
if (typeof getFinetuneStatus !== "function") {
|
|
9454
|
-
console.error(chalk.red("@hasna/brains not installed. Run: bun add @hasna/brains"));
|
|
9455
|
-
process.exit(1);
|
|
9456
|
-
}
|
|
9457
|
-
const status = await getFinetuneStatus({
|
|
9458
|
-
jobId,
|
|
9459
|
-
provider: opts.provider ?? "openai"
|
|
9460
|
-
});
|
|
9461
|
-
console.log(`Job ${chalk.cyan(jobId)}:`);
|
|
9462
|
-
console.log(` Status: ${String(status["status"] ?? "unknown")}`);
|
|
9463
|
-
if (status["fine_tuned_model"]) {
|
|
9464
|
-
console.log(` Fine-tuned model: ${chalk.green(String(status["fine_tuned_model"]))}`);
|
|
9465
|
-
console.log(`
|
|
9466
|
-
${chalk.dim("Set it active:")} economy brains model set ${String(status["fine_tuned_model"])}`);
|
|
9467
|
-
}
|
|
9468
|
-
if (status["error"]) {
|
|
9469
|
-
console.log(chalk.red(` Error: ${String(status["error"])}`));
|
|
9470
|
-
}
|
|
9471
|
-
} catch (e) {
|
|
9472
|
-
console.error(chalk.red(`Error: ${e instanceof Error ? e.message : String(e)}`));
|
|
9473
|
-
process.exit(1);
|
|
9474
|
-
}
|
|
9475
|
-
});
|
|
9476
|
-
}
|
|
9717
|
+
import chalk6 from "chalk";
|
|
9477
9718
|
|
|
9478
9719
|
// src/cli/commands/todos.ts
|
|
9479
|
-
import
|
|
9720
|
+
import chalk from "chalk";
|
|
9480
9721
|
var ROADMAP_VERSION = "2026-05-23-b";
|
|
9481
9722
|
var ROADMAP_GOAL = "Unified AI spend across 4+ machines: auto-sync local telemetry, track subscription usage & savings, ingest all major coding agents, and expose proactive CLI/MCP intelligence \u2014 not just per-machine API cost estimates.";
|
|
9482
9723
|
var ROADMAP_PHASES = [
|
|
@@ -10072,7 +10313,7 @@ var ROADMAP_PHASES = [
|
|
|
10072
10313
|
},
|
|
10073
10314
|
{
|
|
10074
10315
|
id: "12.4",
|
|
10075
|
-
title: "economy init \u2014 detect agents, set
|
|
10316
|
+
title: "economy init \u2014 detect agents, set HASNA_ECONOMY_MACHINE_ID, cloud URL, MCP install, cloud schedule, cron hint",
|
|
10076
10317
|
status: "done",
|
|
10077
10318
|
deps: ["9.7", "12.1"]
|
|
10078
10319
|
},
|
|
@@ -10151,7 +10392,7 @@ var ROADMAP_PHASES = [
|
|
|
10151
10392
|
},
|
|
10152
10393
|
{
|
|
10153
10394
|
id: "14.5",
|
|
10154
|
-
title: "economy serve auth token (
|
|
10395
|
+
title: "economy serve auth token (HASNA_ECONOMY_API_TOKEN) \u2014 localhost-only default, optional LAN bind",
|
|
10155
10396
|
status: "done"
|
|
10156
10397
|
}
|
|
10157
10398
|
]
|
|
@@ -10161,10 +10402,10 @@ var DEFAULT_TODO_LIST_LIMIT = 20;
|
|
|
10161
10402
|
var TODO_TITLE_WIDTH = 140;
|
|
10162
10403
|
function statusLabel(status) {
|
|
10163
10404
|
if (status === "done")
|
|
10164
|
-
return
|
|
10405
|
+
return chalk.green("done");
|
|
10165
10406
|
if (status === "in_progress")
|
|
10166
|
-
return
|
|
10167
|
-
return
|
|
10407
|
+
return chalk.yellow("in progress");
|
|
10408
|
+
return chalk.dim("pending");
|
|
10168
10409
|
}
|
|
10169
10410
|
function countByStatus(tasks) {
|
|
10170
10411
|
return tasks.reduce((acc, task) => {
|
|
@@ -10182,7 +10423,7 @@ function parseListLimit(value) {
|
|
|
10182
10423
|
return;
|
|
10183
10424
|
const parsed = Number(value);
|
|
10184
10425
|
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10185
|
-
console.error(
|
|
10426
|
+
console.error(chalk.red("--limit must be a positive integer"));
|
|
10186
10427
|
process.exit(1);
|
|
10187
10428
|
}
|
|
10188
10429
|
return parsed;
|
|
@@ -10198,12 +10439,12 @@ function matchingTasks(phases, status) {
|
|
|
10198
10439
|
}
|
|
10199
10440
|
function printTodosHelp() {
|
|
10200
10441
|
console.log();
|
|
10201
|
-
console.log(
|
|
10442
|
+
console.log(chalk.bold.cyan(" Economy Roadmap \u2014 fleet, usage & savings"));
|
|
10202
10443
|
console.log();
|
|
10203
|
-
console.log(
|
|
10444
|
+
console.log(chalk.dim(` Version: ${ROADMAP_VERSION}`));
|
|
10204
10445
|
console.log(` ${ROADMAP_GOAL}`);
|
|
10205
10446
|
console.log();
|
|
10206
|
-
console.log(
|
|
10447
|
+
console.log(chalk.bold(" Commands"));
|
|
10207
10448
|
console.log(" economy todos Overview of phases and progress");
|
|
10208
10449
|
console.log(" economy todos list Compact task list (first 20 by default)");
|
|
10209
10450
|
console.log(" economy todos list --verbose Full task list with dependencies");
|
|
@@ -10211,12 +10452,12 @@ function printTodosHelp() {
|
|
|
10211
10452
|
console.log(" economy todos list --phase <id> Tasks for one phase (e.g. phase-9)");
|
|
10212
10453
|
console.log(" economy todos show <task-id> Task detail (e.g. 9.7)");
|
|
10213
10454
|
console.log();
|
|
10214
|
-
console.log(
|
|
10455
|
+
console.log(chalk.bold(" Multi-machine storage"));
|
|
10215
10456
|
console.log(" local on-box SQLite; economy sync ingests this machine's agent data");
|
|
10216
10457
|
console.log(" cloud CLI/MCP reads and writes the shared /v1 API directly with URL + key");
|
|
10217
10458
|
console.log(" fleet machine_id keeps shared summaries and per-machine filters distinct");
|
|
10218
10459
|
console.log();
|
|
10219
|
-
console.log(
|
|
10460
|
+
console.log(chalk.bold(" Agents covered"));
|
|
10220
10461
|
console.log(" claude Claude Code telemetry + Pro/Max quota (partial)");
|
|
10221
10462
|
console.log(" codex Codex SQLite + ChatGPT subscription mode");
|
|
10222
10463
|
console.log(" opencode ~/.local/share/opencode/storage JSON + OTLP");
|
|
@@ -10225,7 +10466,7 @@ function printTodosHelp() {
|
|
|
10225
10466
|
console.log(" hermes ~/.hermes/state.db sessions + cost routing");
|
|
10226
10467
|
console.log(" gemini/takumi Existing; extend with usage quota where available");
|
|
10227
10468
|
console.log();
|
|
10228
|
-
console.log(
|
|
10469
|
+
console.log(chalk.bold(" Key metrics (target)"));
|
|
10229
10470
|
console.log(" api_equivalent_usd What the same tokens would cost at list API prices");
|
|
10230
10471
|
console.log(" subscription_fee_usd Monthly plan cost (prorated per period)");
|
|
10231
10472
|
console.log(" included_consumed_usd Usage counted against plan inclusion");
|
|
@@ -10235,30 +10476,30 @@ function printTodosHelp() {
|
|
|
10235
10476
|
}
|
|
10236
10477
|
function printTodosOverview() {
|
|
10237
10478
|
console.log();
|
|
10238
|
-
console.log(
|
|
10239
|
-
console.log(
|
|
10479
|
+
console.log(chalk.bold.cyan(" Economy Roadmap"));
|
|
10480
|
+
console.log(chalk.dim(` ${ROADMAP_VERSION} \u2014 ${ROADMAP_GOAL}`));
|
|
10240
10481
|
console.log();
|
|
10241
10482
|
const allTasks = ROADMAP_PHASES.flatMap((phase) => phase.tasks);
|
|
10242
10483
|
const totals = countByStatus(allTasks);
|
|
10243
|
-
console.log(` ${
|
|
10484
|
+
console.log(` ${chalk.green(String(totals.done))} done \xB7 ${chalk.yellow(String(totals.in_progress))} in progress \xB7 ${chalk.dim(String(totals.pending))} pending \xB7 ${allTasks.length} total`);
|
|
10244
10485
|
console.log();
|
|
10245
10486
|
for (const phase of ROADMAP_PHASES) {
|
|
10246
10487
|
const counts = countByStatus(phase.tasks);
|
|
10247
10488
|
const progress = phase.tasks.length > 0 ? Math.round(counts.done / phase.tasks.length * 100) : 0;
|
|
10248
|
-
console.log(` ${
|
|
10249
|
-
console.log(
|
|
10489
|
+
console.log(` ${chalk.bold(phase.id)} ${phase.title} ${chalk.dim(`(${progress}% \xB7 ${phase.tasks.length} tasks)`)}`);
|
|
10490
|
+
console.log(chalk.dim(` ${phase.summary}`));
|
|
10250
10491
|
}
|
|
10251
10492
|
console.log();
|
|
10252
|
-
console.log(
|
|
10253
|
-
console.log(
|
|
10254
|
-
console.log(
|
|
10493
|
+
console.log(chalk.dim(" Run: economy todos list --limit 20"));
|
|
10494
|
+
console.log(chalk.dim(" Run: economy todos list --verbose"));
|
|
10495
|
+
console.log(chalk.dim(" Run: economy todos --help"));
|
|
10255
10496
|
console.log();
|
|
10256
10497
|
}
|
|
10257
10498
|
function printTodosList(opts) {
|
|
10258
10499
|
const phases = opts.phase ? ROADMAP_PHASES.filter((phase) => phase.id === opts.phase) : ROADMAP_PHASES;
|
|
10259
10500
|
if (phases.length === 0) {
|
|
10260
|
-
console.log(
|
|
10261
|
-
console.log(
|
|
10501
|
+
console.log(chalk.red(`Unknown phase: ${opts.phase}`));
|
|
10502
|
+
console.log(chalk.dim(`Valid phases: ${ROADMAP_PHASES.map((phase) => phase.id).join(", ")}`));
|
|
10262
10503
|
return;
|
|
10263
10504
|
}
|
|
10264
10505
|
const allMatches = matchingTasks(phases, opts.status);
|
|
@@ -10278,10 +10519,10 @@ function printTodosList(opts) {
|
|
|
10278
10519
|
return;
|
|
10279
10520
|
}
|
|
10280
10521
|
console.log();
|
|
10281
|
-
console.log(
|
|
10282
|
-
console.log(
|
|
10522
|
+
console.log(chalk.bold.cyan(" Economy Roadmap Tasks"));
|
|
10523
|
+
console.log(chalk.dim(` ${allMatches.length} matching tasks \xB7 showing ${visible.length}${visible.length < allMatches.length ? ` of ${allMatches.length}` : ""}`));
|
|
10283
10524
|
if (!opts.verbose && visible.length < allMatches.length) {
|
|
10284
|
-
console.log(
|
|
10525
|
+
console.log(chalk.dim(" Use --verbose for all tasks or economy todos show <task-id> for details."));
|
|
10285
10526
|
}
|
|
10286
10527
|
console.log();
|
|
10287
10528
|
const visibleByPhase = new Map;
|
|
@@ -10294,20 +10535,20 @@ function printTodosList(opts) {
|
|
|
10294
10535
|
const tasks = visibleByPhase.get(phase.id) ?? [];
|
|
10295
10536
|
if (tasks.length === 0)
|
|
10296
10537
|
continue;
|
|
10297
|
-
console.log(
|
|
10538
|
+
console.log(chalk.bold.cyan(` ${phase.id} \u2014 ${phase.title}`));
|
|
10298
10539
|
if (opts.verbose)
|
|
10299
|
-
console.log(
|
|
10540
|
+
console.log(chalk.dim(` ${phase.summary}`));
|
|
10300
10541
|
console.log();
|
|
10301
10542
|
for (const task of tasks) {
|
|
10302
|
-
const deps = task.deps?.length ?
|
|
10543
|
+
const deps = task.deps?.length ? chalk.dim(` deps: ${task.deps.join(", ")}`) : "";
|
|
10303
10544
|
const title = opts.verbose ? task.title : truncateText(task.title, TODO_TITLE_WIDTH);
|
|
10304
|
-
console.log(` ${
|
|
10545
|
+
console.log(` ${chalk.dim(task.id.padEnd(5))} ${statusLabel(task.status).padEnd(14)} ${title}${opts.verbose && deps ? `
|
|
10305
10546
|
${" ".repeat(22)}${deps}` : ""}`);
|
|
10306
10547
|
}
|
|
10307
10548
|
console.log();
|
|
10308
10549
|
}
|
|
10309
10550
|
if (visible.length < allMatches.length) {
|
|
10310
|
-
console.log(
|
|
10551
|
+
console.log(chalk.dim(` ... ${allMatches.length - visible.length} more tasks hidden. Use --limit <n>, --verbose, --phase <id>, or economy todos show <task-id>.`));
|
|
10311
10552
|
console.log();
|
|
10312
10553
|
}
|
|
10313
10554
|
}
|
|
@@ -10317,19 +10558,19 @@ function printTodoShow(taskId) {
|
|
|
10317
10558
|
if (!task)
|
|
10318
10559
|
continue;
|
|
10319
10560
|
console.log();
|
|
10320
|
-
console.log(
|
|
10561
|
+
console.log(chalk.bold.cyan(` ${task.id} \u2014 ${task.title}`));
|
|
10321
10562
|
console.log(` Phase: ${phase.id} (${phase.title})`);
|
|
10322
10563
|
console.log(` Status: ${statusLabel(task.status)}`);
|
|
10323
10564
|
if (task.deps?.length) {
|
|
10324
10565
|
console.log(` Depends on: ${task.deps.join(", ")}`);
|
|
10325
10566
|
}
|
|
10326
|
-
console.log(
|
|
10567
|
+
console.log(chalk.dim(`
|
|
10327
10568
|
${phase.summary}`));
|
|
10328
10569
|
console.log();
|
|
10329
10570
|
return;
|
|
10330
10571
|
}
|
|
10331
|
-
console.log(
|
|
10332
|
-
console.log(
|
|
10572
|
+
console.log(chalk.red(`Task not found: ${taskId}`));
|
|
10573
|
+
console.log(chalk.dim("Run: economy todos list"));
|
|
10333
10574
|
}
|
|
10334
10575
|
function registerTodosCommand(program) {
|
|
10335
10576
|
const todosCmd = program.command("todos").description("Roadmap and granular tasks \u2014 fleet sync, usage tracking, subscription savings").option("--help-roadmap", "Show roadmap help (usage vs API spend, agents, metrics)").action((opts) => {
|
|
@@ -10342,7 +10583,7 @@ function registerTodosCommand(program) {
|
|
|
10342
10583
|
todosCmd.command("list").description("List granular roadmap tasks").option("--phase <id>", "Filter by phase id (e.g. phase-4)").option("--status <status>", "Filter by status: pending|in_progress|done").option("--limit <n>", "Maximum tasks to print (default: 20)").option("--verbose", "Print all matching tasks with phase summaries and dependencies").option("--json", "Output the full filtered roadmap as JSON").action((opts) => {
|
|
10343
10584
|
const status = opts.status;
|
|
10344
10585
|
if (status && !["pending", "in_progress", "done"].includes(status)) {
|
|
10345
|
-
console.error(
|
|
10586
|
+
console.error(chalk.red("--status must be one of: pending, in_progress, done"));
|
|
10346
10587
|
process.exit(1);
|
|
10347
10588
|
}
|
|
10348
10589
|
printTodosList({
|
|
@@ -10368,9 +10609,9 @@ init_tui();
|
|
|
10368
10609
|
init_store();
|
|
10369
10610
|
init_pricing();
|
|
10370
10611
|
init_agents();
|
|
10371
|
-
import
|
|
10372
|
-
import { existsSync as
|
|
10373
|
-
import { join as
|
|
10612
|
+
import chalk3 from "chalk";
|
|
10613
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10614
|
+
import { join as join3 } from "path";
|
|
10374
10615
|
|
|
10375
10616
|
// src/cli/commands/completion.ts
|
|
10376
10617
|
var TOP_LEVEL = [
|
|
@@ -10516,6 +10757,36 @@ function printCompletion(shell) {
|
|
|
10516
10757
|
// src/cli/commands/extras.ts
|
|
10517
10758
|
init_paths();
|
|
10518
10759
|
init_billing_diff();
|
|
10760
|
+
init_cloud_storage();
|
|
10761
|
+
|
|
10762
|
+
// src/lib/api-display-url.ts
|
|
10763
|
+
function gatewayApiV1Root(raw) {
|
|
10764
|
+
if (typeof raw !== "string")
|
|
10765
|
+
return null;
|
|
10766
|
+
const trimmed = raw.trim();
|
|
10767
|
+
if (!trimmed)
|
|
10768
|
+
return null;
|
|
10769
|
+
let parsed;
|
|
10770
|
+
try {
|
|
10771
|
+
parsed = new URL(trimmed);
|
|
10772
|
+
} catch {
|
|
10773
|
+
return null;
|
|
10774
|
+
}
|
|
10775
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "api.hasna.com")
|
|
10776
|
+
return null;
|
|
10777
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
10778
|
+
return null;
|
|
10779
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
10780
|
+
if (segments.length === 1 && segments[0] !== "v1") {
|
|
10781
|
+
return `https://api.hasna.com/${segments[0]}/v1`;
|
|
10782
|
+
}
|
|
10783
|
+
if (segments.length === 2 && segments[1] === "v1") {
|
|
10784
|
+
return `https://api.hasna.com/${segments[0]}/v1`;
|
|
10785
|
+
}
|
|
10786
|
+
return null;
|
|
10787
|
+
}
|
|
10788
|
+
|
|
10789
|
+
// src/cli/commands/extras.ts
|
|
10519
10790
|
function fmt2(usd) {
|
|
10520
10791
|
return "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
10521
10792
|
}
|
|
@@ -10531,7 +10802,7 @@ function fmtTokens(n) {
|
|
|
10531
10802
|
function parsePeriod(value, fallback = "month") {
|
|
10532
10803
|
const p = value ?? fallback;
|
|
10533
10804
|
if (!["today", "yesterday", "week", "month", "year", "all"].includes(p)) {
|
|
10534
|
-
console.error(
|
|
10805
|
+
console.error(chalk3.red("--period must be today|yesterday|week|month|year|all"));
|
|
10535
10806
|
process.exit(1);
|
|
10536
10807
|
}
|
|
10537
10808
|
return p;
|
|
@@ -10541,7 +10812,7 @@ function parseLimit(value, fallback, verbose) {
|
|
|
10541
10812
|
return Number.POSITIVE_INFINITY;
|
|
10542
10813
|
const parsed = Number(value ?? String(fallback));
|
|
10543
10814
|
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
10544
|
-
console.error(
|
|
10815
|
+
console.error(chalk3.red("--limit must be a positive integer"));
|
|
10545
10816
|
process.exit(1);
|
|
10546
10817
|
}
|
|
10547
10818
|
return parsed;
|
|
@@ -10556,28 +10827,28 @@ function registerExtendedCommands(program) {
|
|
|
10556
10827
|
return;
|
|
10557
10828
|
}
|
|
10558
10829
|
console.log();
|
|
10559
|
-
console.log(
|
|
10830
|
+
console.log(chalk3.bold.cyan(` Usage \u2014 ${period}`));
|
|
10560
10831
|
console.log();
|
|
10561
10832
|
if (snaps.length === 0) {
|
|
10562
|
-
console.log(
|
|
10833
|
+
console.log(chalk3.yellow(" No usage snapshots yet. Run: economy sync --cursor"));
|
|
10563
10834
|
} else {
|
|
10564
10835
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10565
10836
|
const visibleSnaps = snaps.slice(0, limit);
|
|
10566
10837
|
if (visibleSnaps.length < snaps.length) {
|
|
10567
|
-
console.log(
|
|
10838
|
+
console.log(chalk3.dim(` ${snaps.length} snapshots \xB7 showing ${visibleSnaps.length}`));
|
|
10568
10839
|
console.log();
|
|
10569
10840
|
}
|
|
10570
10841
|
for (const s of visibleSnaps) {
|
|
10571
|
-
console.log(` ${
|
|
10842
|
+
console.log(` ${chalk3.white(s.agent.padEnd(10))} ${s.metric.padEnd(28)} ${s.value}${s.unit ? ` ${s.unit}` : ""} ${chalk3.dim(s.date)}`);
|
|
10572
10843
|
}
|
|
10573
10844
|
if (visibleSnaps.length < snaps.length) {
|
|
10574
|
-
console.log(
|
|
10845
|
+
console.log(chalk3.dim(`
|
|
10575
10846
|
... ${snaps.length - visibleSnaps.length} more snapshots hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10576
10847
|
}
|
|
10577
10848
|
}
|
|
10578
10849
|
const spendLabel = agent ? `${agent} API-equivalent spend:` : "Fleet API-equivalent spend:";
|
|
10579
10850
|
console.log(`
|
|
10580
|
-
${
|
|
10851
|
+
${chalk3.dim(spendLabel)} ${fmt2(summary.total_usd)}`);
|
|
10581
10852
|
console.log();
|
|
10582
10853
|
});
|
|
10583
10854
|
program.command("savings [period]").description("Subscription vs API-equivalent savings breakdown").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--json", "Output JSON").action(async (periodArg, opts) => {
|
|
@@ -10589,13 +10860,13 @@ function registerExtendedCommands(program) {
|
|
|
10589
10860
|
return;
|
|
10590
10861
|
}
|
|
10591
10862
|
console.log();
|
|
10592
|
-
console.log(
|
|
10863
|
+
console.log(chalk3.bold.cyan(` Savings \u2014 ${period}${agent ? ` (${agent})` : ""}`));
|
|
10593
10864
|
console.log();
|
|
10594
10865
|
console.log(` API equivalent: ${fmt2(savings.api_equivalent_usd)}`);
|
|
10595
10866
|
console.log(` Subscription fee: ${fmt2(savings.subscription_fee_usd)}`);
|
|
10596
10867
|
console.log(` Included consumed: ${fmt2(savings.included_consumed_usd)}`);
|
|
10597
10868
|
console.log(` On-demand: ${fmt2(savings.on_demand_usd)}`);
|
|
10598
|
-
console.log(` ${
|
|
10869
|
+
console.log(` ${chalk3.bold("Saved:")} ${chalk3.green(fmt2(savings.saved_usd))}`);
|
|
10599
10870
|
console.log();
|
|
10600
10871
|
});
|
|
10601
10872
|
const subsCmd = program.command("subscriptions").description("Manage subscription plans");
|
|
@@ -10611,7 +10882,7 @@ function registerExtendedCommands(program) {
|
|
|
10611
10882
|
reset_policy: "monthly",
|
|
10612
10883
|
active: 1
|
|
10613
10884
|
});
|
|
10614
|
-
console.log(
|
|
10885
|
+
console.log(chalk3.green(`\u2713 Subscription set: ${opts.provider} / ${opts.plan}`));
|
|
10615
10886
|
});
|
|
10616
10887
|
subsCmd.command("list").description("List subscription plans").option("--limit <n>", "Maximum subscription rows to print (default: 20)").option("--verbose", "Show all subscription rows").option("--json", "Output JSON").action(async (opts) => {
|
|
10617
10888
|
const rows = await getStore().listSubscriptions();
|
|
@@ -10620,28 +10891,28 @@ function registerExtendedCommands(program) {
|
|
|
10620
10891
|
return;
|
|
10621
10892
|
}
|
|
10622
10893
|
if (rows.length === 0) {
|
|
10623
|
-
console.log(
|
|
10894
|
+
console.log(chalk3.yellow("No subscriptions configured."));
|
|
10624
10895
|
return;
|
|
10625
10896
|
}
|
|
10626
10897
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10627
10898
|
const visibleRows = rows.slice(0, limit);
|
|
10628
10899
|
console.log();
|
|
10629
10900
|
if (visibleRows.length < rows.length) {
|
|
10630
|
-
console.log(
|
|
10901
|
+
console.log(chalk3.dim(` ${rows.length} subscriptions \xB7 showing ${visibleRows.length}`));
|
|
10631
10902
|
console.log();
|
|
10632
10903
|
}
|
|
10633
10904
|
for (const r of visibleRows) {
|
|
10634
|
-
console.log(` ${
|
|
10905
|
+
console.log(` ${chalk3.white(r.provider)} / ${r.plan} ${fmt2(r.monthly_fee_usd)}/mo included ${fmt2(r.included_usage_usd)} ${r.agent ?? "all agents"}`);
|
|
10635
10906
|
}
|
|
10636
10907
|
if (visibleRows.length < rows.length) {
|
|
10637
|
-
console.log(
|
|
10908
|
+
console.log(chalk3.dim(`
|
|
10638
10909
|
... ${rows.length - visibleRows.length} more subscriptions hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10639
10910
|
}
|
|
10640
10911
|
console.log();
|
|
10641
10912
|
});
|
|
10642
10913
|
subsCmd.command("remove <id>").description("Remove subscription by id").action(async (id) => {
|
|
10643
10914
|
await getStore().removeSubscription(id);
|
|
10644
|
-
console.log(
|
|
10915
|
+
console.log(chalk3.green("\u2713 Subscription removed"));
|
|
10645
10916
|
});
|
|
10646
10917
|
program.command("status").description("One-line fleet health and spend summary").action(async () => {
|
|
10647
10918
|
console.log(buildStatusLine(await gatherStatusData()));
|
|
@@ -10654,17 +10925,26 @@ function registerExtendedCommands(program) {
|
|
|
10654
10925
|
const paths = [
|
|
10655
10926
|
["claude", agentPaths().claudeProjects],
|
|
10656
10927
|
["codex", agentPaths().codexDb],
|
|
10657
|
-
["gemini",
|
|
10658
|
-
["opencode",
|
|
10928
|
+
["gemini", join3(agentPaths().geminiTmp, "..")],
|
|
10929
|
+
["opencode", join3(agentPaths().opencodeMessages, "..", "..")],
|
|
10659
10930
|
["pi", agentPaths().piSessions],
|
|
10660
10931
|
["hermes", agentPaths().hermesDb]
|
|
10661
10932
|
];
|
|
10662
10933
|
for (const [agent, path] of paths) {
|
|
10663
|
-
checks.push({ ok:
|
|
10934
|
+
checks.push({ ok: existsSync2(path), msg: `${agent}: ${existsSync2(path) ? path : "not found"}` });
|
|
10664
10935
|
}
|
|
10665
10936
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
10666
10937
|
}
|
|
10667
|
-
checks.push({ ok: true, msg: `storage: ${cloud ? "
|
|
10938
|
+
checks.push({ ok: true, msg: `storage: ${cloud ? "shared API (credential resolved via @hasna/contracts)" : "local (explicit opt-in)"}` });
|
|
10939
|
+
if (cloud) {
|
|
10940
|
+
const gatewayRoot = gatewayApiV1Root(process.env.HASNA_ECONOMY_API_URL ?? process.env.ECONOMY_API_URL);
|
|
10941
|
+
if (gatewayRoot)
|
|
10942
|
+
checks.push({ ok: true, msg: `api: ${gatewayRoot}` });
|
|
10943
|
+
const report = economyTransportReport();
|
|
10944
|
+
if (report.ok && report.authority) {
|
|
10945
|
+
checks.push({ ok: true, msg: `credential source: ${report.authority.apiKeySource ?? "(unknown)"} (tier ${report.authority.apiKeyTier ?? "?"})` });
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10668
10948
|
if (!cloud) {
|
|
10669
10949
|
const db = openDatabase();
|
|
10670
10950
|
ensurePricingSeeded(db);
|
|
@@ -10687,19 +10967,49 @@ function registerExtendedCommands(program) {
|
|
|
10687
10967
|
}
|
|
10688
10968
|
checks.push(billingDriftCheck(await store.billingDiff("month")));
|
|
10689
10969
|
console.log();
|
|
10690
|
-
console.log(
|
|
10970
|
+
console.log(chalk3.bold.cyan(" Economy Doctor"));
|
|
10691
10971
|
console.log();
|
|
10692
10972
|
for (const c of checks) {
|
|
10693
|
-
console.log(` ${c.ok ?
|
|
10973
|
+
console.log(` ${c.ok ? chalk3.green("\u2713") : chalk3.yellow("!")} ${c.msg}`);
|
|
10694
10974
|
}
|
|
10695
10975
|
console.log();
|
|
10696
10976
|
});
|
|
10977
|
+
program.command("transport").description("Report the resolved client transport and credential source (never the key)").option("--json", "Output JSON").action((opts) => {
|
|
10978
|
+
const report = economyTransportReport();
|
|
10979
|
+
if (!report.ok)
|
|
10980
|
+
process.exitCode = 1;
|
|
10981
|
+
if (opts.json) {
|
|
10982
|
+
console.log(JSON.stringify(report, null, 2));
|
|
10983
|
+
return;
|
|
10984
|
+
}
|
|
10985
|
+
if (!report.ok) {
|
|
10986
|
+
console.log(chalk3.red(" economy transport: no hosted credential resolves."));
|
|
10987
|
+
for (const issue of report.issues)
|
|
10988
|
+
console.log(chalk3.dim(` ${issue}`));
|
|
10989
|
+
console.log(chalk3.dim(" Set HASNA_ECONOMY_API_KEY (Keychain, ~/.hasna/economy/config/credentials, or env), or set HASNA_ECONOMY_LOCAL=1 for the local store."));
|
|
10990
|
+
return;
|
|
10991
|
+
}
|
|
10992
|
+
if (report.transport === "sqlite") {
|
|
10993
|
+
console.log(` transport: ${report.transport} (${report.source}) \u2014 on-box SQLite store`);
|
|
10994
|
+
return;
|
|
10995
|
+
}
|
|
10996
|
+
console.log(` transport: ${report.transport} (${report.source ?? "http"})`);
|
|
10997
|
+
const a = report.authority;
|
|
10998
|
+
if (a) {
|
|
10999
|
+
console.log(` api url: ${a.baseUrl}`);
|
|
11000
|
+
console.log(` url source: ${a.apiUrlSource ?? "(none)"}`);
|
|
11001
|
+
console.log(` key source: ${a.apiKeySource ?? "(none)"}`);
|
|
11002
|
+
console.log(` key tier: ${a.apiKeyTier ?? "(none)"}`);
|
|
11003
|
+
if (a.warning)
|
|
11004
|
+
console.log(chalk3.dim(` warning: ${a.warning}`));
|
|
11005
|
+
}
|
|
11006
|
+
});
|
|
10697
11007
|
program.command("init").description("First-run setup wizard hints").action(async () => {
|
|
10698
|
-
console.log(
|
|
11008
|
+
console.log(chalk3.bold.cyan(`
|
|
10699
11009
|
Economy Init
|
|
10700
11010
|
`));
|
|
10701
11011
|
console.log(" Local mode (on-box SQLite):");
|
|
10702
|
-
console.log(" 1. Set machine id: export
|
|
11012
|
+
console.log(" 1. Set machine id: export HASNA_ECONOMY_MACHINE_ID=spark01");
|
|
10703
11013
|
console.log(" 2. Cursor token: export CURSOR_SESSION_TOKEN=...");
|
|
10704
11014
|
console.log(" 3. Run ingest: economy sync --verbose");
|
|
10705
11015
|
console.log(" 4. MCP install: economy mcp --all");
|
|
@@ -10746,16 +11056,16 @@ function registerExtendedCommands(program) {
|
|
|
10746
11056
|
const period = parsePeriod(opts.period, "month");
|
|
10747
11057
|
const diff = await getStore().billingDiff(period);
|
|
10748
11058
|
console.log();
|
|
10749
|
-
console.log(
|
|
11059
|
+
console.log(chalk3.bold.cyan(` Billing diff \u2014 ${period}`));
|
|
10750
11060
|
console.log(` Estimated: ${fmt2(diff.estimated_usd)}`);
|
|
10751
11061
|
console.log(` Actual: ${fmt2(diff.actual_usd)}`);
|
|
10752
11062
|
const deltaPct = diff.comparable ? `${diff.delta_usd >= 0 ? "+" : ""}${diff.delta_pct.toFixed(1)}%` : "n/a";
|
|
10753
11063
|
console.log(` Delta: ${fmt2(Math.abs(diff.delta_usd))} (${deltaPct})`);
|
|
10754
11064
|
if (!diff.comparable)
|
|
10755
|
-
console.log(
|
|
11065
|
+
console.log(chalk3.yellow(`
|
|
10756
11066
|
${billingDriftCheck(diff).msg}`));
|
|
10757
11067
|
if (diff.is_alert)
|
|
10758
|
-
console.log(
|
|
11068
|
+
console.log(chalk3.yellow(`
|
|
10759
11069
|
Suggestion: economy sync --recalculate && economy billing sync`));
|
|
10760
11070
|
console.log();
|
|
10761
11071
|
});
|
|
@@ -10772,17 +11082,17 @@ function registerFleetCommands(program) {
|
|
|
10772
11082
|
const limit = parseLimit(opts.limit, 20, opts.verbose);
|
|
10773
11083
|
const visibleMachines = machines.slice(0, limit);
|
|
10774
11084
|
console.log();
|
|
10775
|
-
console.log(
|
|
11085
|
+
console.log(chalk3.bold.cyan(` Fleet \u2014 ${period}`));
|
|
10776
11086
|
console.log(` Total: ${fmt2(summary.total_usd)} \xB7 ${summary.sessions} sessions \xB7 ${summary.requests} requests`);
|
|
10777
11087
|
if (visibleMachines.length < machines.length) {
|
|
10778
|
-
console.log(
|
|
11088
|
+
console.log(chalk3.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
10779
11089
|
}
|
|
10780
11090
|
console.log();
|
|
10781
11091
|
for (const m of visibleMachines) {
|
|
10782
|
-
console.log(` ${
|
|
11092
|
+
console.log(` ${chalk3.white(m.machine_id.padEnd(12))} ${fmt2(m.total_cost_usd).padEnd(10)} ${m.sessions} sessions`);
|
|
10783
11093
|
}
|
|
10784
11094
|
if (visibleMachines.length < machines.length) {
|
|
10785
|
-
console.log(
|
|
11095
|
+
console.log(chalk3.dim(`
|
|
10786
11096
|
... ${machines.length - visibleMachines.length} more machines hidden. Use --limit <n>, --verbose, or --json.`));
|
|
10787
11097
|
}
|
|
10788
11098
|
console.log();
|
|
@@ -10929,14 +11239,14 @@ init_billing();
|
|
|
10929
11239
|
init_package_metadata();
|
|
10930
11240
|
init_pricing();
|
|
10931
11241
|
init_store();
|
|
10932
|
-
import { execSync } from "child_process";
|
|
10933
11242
|
|
|
10934
11243
|
// src/lib/cloud-ingest.ts
|
|
10935
11244
|
init_database();
|
|
10936
11245
|
init_pricing();
|
|
10937
11246
|
init_sync_all();
|
|
10938
11247
|
init_billing();
|
|
10939
|
-
import {
|
|
11248
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync, writeFileSync } from "fs";
|
|
11249
|
+
import { dirname, join as join12 } from "path";
|
|
10940
11250
|
|
|
10941
11251
|
// src/lib/sync-maintenance.ts
|
|
10942
11252
|
init_database();
|
|
@@ -10977,15 +11287,39 @@ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snaps
|
|
|
10977
11287
|
function getIngestCachePath() {
|
|
10978
11288
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
10979
11289
|
return process.env["HASNA_ECONOMY_INGEST_CACHE"];
|
|
10980
|
-
|
|
11290
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || undefined;
|
|
11291
|
+
return join12(cacheDir({ app: "economy", home }), "ingest-cache.json");
|
|
10981
11292
|
}
|
|
10982
|
-
function
|
|
10983
|
-
|
|
10984
|
-
|
|
10985
|
-
|
|
11293
|
+
function readIngestCache(path) {
|
|
11294
|
+
let text;
|
|
11295
|
+
try {
|
|
11296
|
+
text = readFileSync10(path, "utf8");
|
|
11297
|
+
} catch {
|
|
11298
|
+
return [];
|
|
11299
|
+
}
|
|
11300
|
+
try {
|
|
11301
|
+
const parsed = JSON.parse(text);
|
|
11302
|
+
const rows = Array.isArray(parsed?.ingest_state) ? parsed.ingest_state : [];
|
|
11303
|
+
return rows.filter((row) => typeof row === "object" && row !== null && typeof row.source === "string" && typeof row.key === "string" && typeof row.value === "string");
|
|
11304
|
+
} catch {
|
|
11305
|
+
return [];
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
function writeIngestCache(path, rows) {
|
|
11309
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
11310
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
11311
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, ingest_state: rows }), { mode: 384 });
|
|
11312
|
+
renameSync(tmp, path);
|
|
11313
|
+
}
|
|
11314
|
+
function loadIngestState2(scratch, rows) {
|
|
11315
|
+
scratch.prepare(`DELETE FROM ingest_state`).run();
|
|
11316
|
+
const insert = scratch.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
|
|
10986
11317
|
for (const row of rows)
|
|
10987
11318
|
insert.run(row.source, row.key, row.value);
|
|
10988
11319
|
}
|
|
11320
|
+
function dumpIngestState(scratch) {
|
|
11321
|
+
return scratch.prepare(`SELECT source, key, value FROM ingest_state`).all();
|
|
11322
|
+
}
|
|
10989
11323
|
function exportIngestRows(db) {
|
|
10990
11324
|
const body = {};
|
|
10991
11325
|
for (const table2 of CLOUD_INGEST_TABLES) {
|
|
@@ -11013,11 +11347,9 @@ async function postIngest(cloud, body) {
|
|
|
11013
11347
|
async function syncAllToCloud(cloud, opts = {}) {
|
|
11014
11348
|
const scratch = openDatabase(":memory:", true);
|
|
11015
11349
|
ensurePricingSeeded(scratch);
|
|
11016
|
-
const
|
|
11350
|
+
const cachePath = opts.cachePath ?? getIngestCachePath();
|
|
11017
11351
|
try {
|
|
11018
|
-
|
|
11019
|
-
cache2.prepare(`DELETE FROM ingest_state`).run();
|
|
11020
|
-
copyIngestState(cache2, scratch);
|
|
11352
|
+
loadIngestState2(scratch, opts.force ? [] : readIngestCache(cachePath));
|
|
11021
11353
|
const result = await syncAll(scratch, opts);
|
|
11022
11354
|
if (opts.backfillMachine)
|
|
11023
11355
|
backfillMachineId(scratch);
|
|
@@ -11025,19 +11357,16 @@ async function syncAllToCloud(cloud, opts = {}) {
|
|
|
11025
11357
|
await recalculateZeroCostRequests(scratch);
|
|
11026
11358
|
const body = exportIngestRows(scratch);
|
|
11027
11359
|
if (Object.keys(body).length === 0) {
|
|
11028
|
-
|
|
11360
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
11029
11361
|
return { ...result, posted: false };
|
|
11030
11362
|
}
|
|
11031
11363
|
const response = await postIngest(cloud, body);
|
|
11032
|
-
|
|
11364
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
11033
11365
|
return { ...result, posted: true, ingested: response.ingested, total: response.total };
|
|
11034
11366
|
} finally {
|
|
11035
11367
|
try {
|
|
11036
11368
|
scratch.close();
|
|
11037
11369
|
} catch {}
|
|
11038
|
-
try {
|
|
11039
|
-
cache2.close();
|
|
11040
|
-
} catch {}
|
|
11041
11370
|
}
|
|
11042
11371
|
}
|
|
11043
11372
|
async function billingSyncToCloud(cloud, opts = {}) {
|
|
@@ -11081,6 +11410,43 @@ async function billingSyncToCloud(cloud, opts = {}) {
|
|
|
11081
11410
|
init_cloud_storage();
|
|
11082
11411
|
init_billing_diff();
|
|
11083
11412
|
|
|
11413
|
+
// src/lib/autosync-gate.ts
|
|
11414
|
+
init_database();
|
|
11415
|
+
init_store();
|
|
11416
|
+
var AUTOSYNC_STATE_SOURCE = "autosync";
|
|
11417
|
+
var AUTOSYNC_STATE_KEY = "last_run";
|
|
11418
|
+
var DEFAULT_AUTOSYNC_INTERVAL_MS = 10 * 60000;
|
|
11419
|
+
function autosyncIntervalMs(env = process.env) {
|
|
11420
|
+
const raw = env["HASNA_ECONOMY_AUTOSYNC_INTERVAL"];
|
|
11421
|
+
if (raw == null || raw.trim() === "")
|
|
11422
|
+
return DEFAULT_AUTOSYNC_INTERVAL_MS;
|
|
11423
|
+
const seconds = Number(raw);
|
|
11424
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
11425
|
+
return DEFAULT_AUTOSYNC_INTERVAL_MS;
|
|
11426
|
+
return Math.round(seconds * 1000);
|
|
11427
|
+
}
|
|
11428
|
+
function autosyncMarkerStore(env = process.env) {
|
|
11429
|
+
if (isCloudStore(env))
|
|
11430
|
+
return openDatabase(getIngestCachePath());
|
|
11431
|
+
return openDatabase();
|
|
11432
|
+
}
|
|
11433
|
+
function autosyncLastRun(db, env = process.env) {
|
|
11434
|
+
const store = db ?? autosyncMarkerStore(env);
|
|
11435
|
+
const value = getIngestState(store, AUTOSYNC_STATE_SOURCE, AUTOSYNC_STATE_KEY);
|
|
11436
|
+
const ts = value == null ? NaN : Number(value);
|
|
11437
|
+
return Number.isFinite(ts) && ts > 0 ? ts : 0;
|
|
11438
|
+
}
|
|
11439
|
+
function markAutoSync(db, env = process.env) {
|
|
11440
|
+
const store = db ?? autosyncMarkerStore(env);
|
|
11441
|
+
setIngestState(store, AUTOSYNC_STATE_SOURCE, AUTOSYNC_STATE_KEY, String(Date.now()));
|
|
11442
|
+
}
|
|
11443
|
+
function autoSyncDue(db, env = process.env) {
|
|
11444
|
+
const interval = autosyncIntervalMs(env);
|
|
11445
|
+
if (interval === 0)
|
|
11446
|
+
return true;
|
|
11447
|
+
return Date.now() - autosyncLastRun(db, env) >= interval;
|
|
11448
|
+
}
|
|
11449
|
+
|
|
11084
11450
|
// src/lib/contracts-client/transport.ts
|
|
11085
11451
|
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
11086
11452
|
|
|
@@ -11088,15 +11454,14 @@ import { HasnaHttpError } from "@hasna/contracts/client";
|
|
|
11088
11454
|
var program = new Command;
|
|
11089
11455
|
program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
|
|
11090
11456
|
async function autoSync(opts = {}) {
|
|
11091
|
-
if (isCloudStore())
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
await syncAllToCloud(cloud, opts);
|
|
11457
|
+
if (isCloudStore())
|
|
11458
|
+
return;
|
|
11459
|
+
if (!autoSyncDue(undefined, process.env))
|
|
11095
11460
|
return;
|
|
11096
|
-
}
|
|
11097
11461
|
const db = openDatabase();
|
|
11098
11462
|
ensurePricingSeeded(db);
|
|
11099
11463
|
await syncAll(db, opts);
|
|
11464
|
+
markAutoSync(db, process.env);
|
|
11100
11465
|
}
|
|
11101
11466
|
function sparkline(values) {
|
|
11102
11467
|
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
@@ -11119,7 +11484,7 @@ function fmt4(usd) {
|
|
|
11119
11484
|
} else {
|
|
11120
11485
|
formatted = "$0.00";
|
|
11121
11486
|
}
|
|
11122
|
-
return
|
|
11487
|
+
return chalk6.green(formatted);
|
|
11123
11488
|
}
|
|
11124
11489
|
function fmtTokens2(n) {
|
|
11125
11490
|
if (n >= 1e9)
|
|
@@ -11135,25 +11500,25 @@ function fmtCount(n) {
|
|
|
11135
11500
|
}
|
|
11136
11501
|
function fmtAgent(agent) {
|
|
11137
11502
|
if (agent === "claude")
|
|
11138
|
-
return
|
|
11503
|
+
return chalk6.blue("claude");
|
|
11139
11504
|
if (agent === "codex")
|
|
11140
|
-
return
|
|
11505
|
+
return chalk6.yellow("codex");
|
|
11141
11506
|
if (agent === "gemini")
|
|
11142
|
-
return
|
|
11507
|
+
return chalk6.green("gemini");
|
|
11143
11508
|
if (agent === "takumi")
|
|
11144
|
-
return
|
|
11509
|
+
return chalk6.magenta("takumi");
|
|
11145
11510
|
if (agent === "opencode")
|
|
11146
|
-
return
|
|
11511
|
+
return chalk6.cyan("opencode");
|
|
11147
11512
|
if (agent === "cursor")
|
|
11148
|
-
return
|
|
11513
|
+
return chalk6.white("cursor");
|
|
11149
11514
|
if (agent === "pi")
|
|
11150
|
-
return
|
|
11515
|
+
return chalk6.hex("#FF6B6B")("pi");
|
|
11151
11516
|
if (agent === "hermes")
|
|
11152
|
-
return
|
|
11153
|
-
return
|
|
11517
|
+
return chalk6.hex("#9B59B6")("hermes");
|
|
11518
|
+
return chalk6.gray(agent);
|
|
11154
11519
|
}
|
|
11155
11520
|
function fail(message) {
|
|
11156
|
-
console.error(
|
|
11521
|
+
console.error(chalk6.red(message));
|
|
11157
11522
|
process.exit(1);
|
|
11158
11523
|
}
|
|
11159
11524
|
function parseFiniteCliNumber(value, option) {
|
|
@@ -11205,7 +11570,7 @@ function resolveRowLimit(value, verbose, fallback) {
|
|
|
11205
11570
|
function printHiddenRowsHint(total, shown, detail) {
|
|
11206
11571
|
if (total <= shown)
|
|
11207
11572
|
return;
|
|
11208
|
-
console.log(
|
|
11573
|
+
console.log(chalk6.dim(` ... ${total - shown} more rows hidden. ${detail}`));
|
|
11209
11574
|
}
|
|
11210
11575
|
function requireCliChoice(value, option, allowed) {
|
|
11211
11576
|
const selected = value ?? allowed[0];
|
|
@@ -11215,11 +11580,11 @@ function requireCliChoice(value, option, allowed) {
|
|
|
11215
11580
|
}
|
|
11216
11581
|
function printTable(headers, rows) {
|
|
11217
11582
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
|
|
11218
|
-
const
|
|
11583
|
+
const sep = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
11219
11584
|
const header = headers.map((h, i) => ` ${h.padEnd(widths[i] ?? 0)} `).join("\u2502");
|
|
11220
|
-
console.log(`\u250C${
|
|
11585
|
+
console.log(`\u250C${sep.replace(/\u253C/g, "\u252C")}\u2510`);
|
|
11221
11586
|
console.log(`\u2502${header}\u2502`);
|
|
11222
|
-
console.log(`\u251C${
|
|
11587
|
+
console.log(`\u251C${sep}\u2524`);
|
|
11223
11588
|
for (const row of rows) {
|
|
11224
11589
|
const line = row.map((cell, i) => {
|
|
11225
11590
|
const plain = cell.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -11227,19 +11592,19 @@ function printTable(headers, rows) {
|
|
|
11227
11592
|
}).join("\u2502");
|
|
11228
11593
|
console.log(`\u2502${line}\u2502`);
|
|
11229
11594
|
}
|
|
11230
|
-
console.log(`\u2514${
|
|
11595
|
+
console.log(`\u2514${sep.replace(/\u253C/g, "\u2534")}\u2518`);
|
|
11231
11596
|
}
|
|
11232
11597
|
function accountDisplayName(row) {
|
|
11233
11598
|
return row.account_email || row.account_name || row.account_key || "unknown";
|
|
11234
11599
|
}
|
|
11235
11600
|
function printAccountBreakdown(rows) {
|
|
11236
11601
|
printTable(["Account", "Agent", "Source", "Sessions", "Requests", "Tokens", "API Eq", "Billable", "Included"], rows.map((r) => [
|
|
11237
|
-
|
|
11602
|
+
chalk6.white(accountDisplayName(r)),
|
|
11238
11603
|
fmtAgent(r.account_tool),
|
|
11239
|
-
|
|
11604
|
+
chalk6.dim(r.account_source || "unknown"),
|
|
11240
11605
|
String(r.sessions),
|
|
11241
11606
|
String(r.requests),
|
|
11242
|
-
|
|
11607
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11243
11608
|
fmt4(r.api_equivalent_usd),
|
|
11244
11609
|
fmt4(r.billable_usd),
|
|
11245
11610
|
fmt4(r.subscription_included_usd)
|
|
@@ -11258,13 +11623,13 @@ function parseSinceDate2(since) {
|
|
|
11258
11623
|
async function printSummary(label, period) {
|
|
11259
11624
|
const s = await getStore().summary(period);
|
|
11260
11625
|
console.log();
|
|
11261
|
-
console.log(
|
|
11626
|
+
console.log(chalk6.bold.cyan(` ${label}`));
|
|
11262
11627
|
console.log();
|
|
11263
11628
|
printTable(["Metric", "Value"], [
|
|
11264
11629
|
["Total cost", fmt4(s.total_usd)],
|
|
11265
|
-
["Sessions",
|
|
11266
|
-
["Requests",
|
|
11267
|
-
["Tokens",
|
|
11630
|
+
["Sessions", chalk6.yellow(fmtCount(s.sessions))],
|
|
11631
|
+
["Requests", chalk6.yellow(fmtCount(s.requests))],
|
|
11632
|
+
["Tokens", chalk6.yellow(fmtTokens2(s.tokens))]
|
|
11268
11633
|
]);
|
|
11269
11634
|
console.log();
|
|
11270
11635
|
}
|
|
@@ -11286,7 +11651,7 @@ program.action(async () => {
|
|
|
11286
11651
|
}, {});
|
|
11287
11652
|
dailyValues = Object.values(daily);
|
|
11288
11653
|
console.log();
|
|
11289
|
-
console.log(
|
|
11654
|
+
console.log(chalk6.bold.cyan(" Economy"));
|
|
11290
11655
|
console.log();
|
|
11291
11656
|
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
11292
11657
|
["Today", fmt4(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens2(t.tokens)],
|
|
@@ -11295,13 +11660,13 @@ program.action(async () => {
|
|
|
11295
11660
|
]);
|
|
11296
11661
|
if (dailyValues.length > 0) {
|
|
11297
11662
|
console.log(`
|
|
11298
|
-
${
|
|
11663
|
+
${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
11299
11664
|
}
|
|
11300
11665
|
if (projects.length > 0) {
|
|
11301
11666
|
console.log(`
|
|
11302
|
-
${
|
|
11667
|
+
${chalk6.dim("Top projects:")}`);
|
|
11303
11668
|
for (const p of projects) {
|
|
11304
|
-
console.log(` ${
|
|
11669
|
+
console.log(` ${chalk6.white(p.project_name.padEnd(25))} ${fmt4(p.cost_usd)}`);
|
|
11305
11670
|
}
|
|
11306
11671
|
}
|
|
11307
11672
|
console.log();
|
|
@@ -11310,7 +11675,7 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11310
11675
|
if (isCloudStore()) {
|
|
11311
11676
|
const cloud = economyCloudStorage();
|
|
11312
11677
|
if (!cloud.active) {
|
|
11313
|
-
console.log(
|
|
11678
|
+
console.log(chalk6.yellow("cloud mode: sync transport unavailable (no credential resolved; set HASNA_ECONOMY_API_KEY or write ~/.hasna/economy/config/credentials)"));
|
|
11314
11679
|
return;
|
|
11315
11680
|
}
|
|
11316
11681
|
const result2 = await syncAllToCloud(cloud, {
|
|
@@ -11330,11 +11695,11 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11330
11695
|
});
|
|
11331
11696
|
if (result2.posted) {
|
|
11332
11697
|
const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table2, n]) => `${n} ${table2}`);
|
|
11333
|
-
console.log(
|
|
11698
|
+
console.log(chalk6.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
|
|
11334
11699
|
} else {
|
|
11335
|
-
console.log(
|
|
11700
|
+
console.log(chalk6.dim("\u2713 no new provider data to push (mtime cache current)"));
|
|
11336
11701
|
}
|
|
11337
|
-
console.log(
|
|
11702
|
+
console.log(chalk6.bold.green(`
|
|
11338
11703
|
\u2713 Sync complete`));
|
|
11339
11704
|
return;
|
|
11340
11705
|
}
|
|
@@ -11346,19 +11711,19 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11346
11711
|
const { syncOpenProjectsRegistry: syncOpenProjectsRegistry2 } = await Promise.resolve().then(() => (init_open_projects(), exports_open_projects));
|
|
11347
11712
|
const p = await syncOpenProjectsRegistry2(db);
|
|
11348
11713
|
if (opts.verbose)
|
|
11349
|
-
console.log(
|
|
11714
|
+
console.log(chalk6.dim(`Synced open-projects registry: ${p.imported} imported, ${p.skipped} skipped`));
|
|
11350
11715
|
} catch (e) {
|
|
11351
11716
|
if (opts.verbose)
|
|
11352
|
-
console.log(
|
|
11717
|
+
console.log(chalk6.dim(`open-projects registry sync skipped: ${e instanceof Error ? e.message : String(e)}`));
|
|
11353
11718
|
}
|
|
11354
11719
|
}
|
|
11355
11720
|
if (opts.force) {
|
|
11356
11721
|
const ingestSources = [...AGENTS, "codex-codewith", "loops"];
|
|
11357
11722
|
db.exec(`DELETE FROM ingest_state WHERE source IN (${ingestSources.map((a) => `'${a}'`).join(", ")})`);
|
|
11358
11723
|
if (opts.verbose)
|
|
11359
|
-
console.log(
|
|
11724
|
+
console.log(chalk6.dim("Cleared ingest cache"));
|
|
11360
11725
|
}
|
|
11361
|
-
process.stdout.write(
|
|
11726
|
+
process.stdout.write(chalk6.cyan("\u2192 Ingesting agent data... "));
|
|
11362
11727
|
const result = await syncAll(db, {
|
|
11363
11728
|
claude: opts.claude,
|
|
11364
11729
|
takumi: opts.takumi,
|
|
@@ -11371,32 +11736,32 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11371
11736
|
loops: opts.loops,
|
|
11372
11737
|
verbose: opts.verbose
|
|
11373
11738
|
});
|
|
11374
|
-
console.log(
|
|
11739
|
+
console.log(chalk6.green(`\u2713 deduped ${result.deduped}`));
|
|
11375
11740
|
if (opts.backfillMachine) {
|
|
11376
11741
|
const machine = getMachineId();
|
|
11377
11742
|
const r = backfillMachineId(db);
|
|
11378
|
-
console.log(
|
|
11743
|
+
console.log(chalk6.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
|
|
11379
11744
|
}
|
|
11380
11745
|
if (opts.recalculate) {
|
|
11381
11746
|
const { getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
11382
11747
|
const r = await recalculateZeroCostRequests(db);
|
|
11383
|
-
console.log(
|
|
11748
|
+
console.log(chalk6.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
|
|
11384
11749
|
const zeroCostBuckets = r.buckets;
|
|
11385
11750
|
if (zeroCostBuckets.length > 0) {
|
|
11386
|
-
console.log(
|
|
11751
|
+
console.log(chalk6.yellow("\u2192 Zero-cost token buckets remain:"));
|
|
11387
11752
|
for (const row of zeroCostBuckets) {
|
|
11388
11753
|
const pricing = getPricingFromDb2(db, row.model);
|
|
11389
11754
|
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";
|
|
11390
|
-
console.log(
|
|
11755
|
+
console.log(chalk6.yellow(` ${row.agent}/${row.model}: ${row.requests} requests, ${fmtTokens2(row.total_tokens)} tokens (${status})`));
|
|
11391
11756
|
}
|
|
11392
|
-
console.log(
|
|
11757
|
+
console.log(chalk6.dim(" Add pricing with `economy pricing set <model> --input ... --output ...`, then rerun `economy sync --recalculate`."));
|
|
11393
11758
|
}
|
|
11394
11759
|
}
|
|
11395
11760
|
try {
|
|
11396
11761
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
11397
11762
|
await checkAndFireWebhooks2(db);
|
|
11398
11763
|
} catch {}
|
|
11399
|
-
console.log(
|
|
11764
|
+
console.log(chalk6.bold.green(`
|
|
11400
11765
|
\u2713 Sync complete`));
|
|
11401
11766
|
});
|
|
11402
11767
|
program.command("today").description("Cost summary for today").action(async () => {
|
|
@@ -11426,7 +11791,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
11426
11791
|
search: opts.search
|
|
11427
11792
|
});
|
|
11428
11793
|
if (sessions.length === 0) {
|
|
11429
|
-
console.log(
|
|
11794
|
+
console.log(chalk6.yellow("No sessions found."));
|
|
11430
11795
|
return;
|
|
11431
11796
|
}
|
|
11432
11797
|
const f = opts.format ?? "table";
|
|
@@ -11448,13 +11813,13 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
11448
11813
|
}
|
|
11449
11814
|
console.log();
|
|
11450
11815
|
printTable(["Session ID", "Agent", "Project", "Cost", "Tokens", "Requests", "Started"], sessions.map((s) => [
|
|
11451
|
-
|
|
11816
|
+
chalk6.dim(s.id.substring(0, 12)),
|
|
11452
11817
|
fmtAgent(s.agent),
|
|
11453
|
-
|
|
11818
|
+
chalk6.white(s.project_name || chalk6.dim("unknown")),
|
|
11454
11819
|
fmt4(s.total_cost_usd),
|
|
11455
|
-
|
|
11820
|
+
chalk6.cyan(fmtTokens2(s.total_tokens)),
|
|
11456
11821
|
fmtCount(s.request_count),
|
|
11457
|
-
|
|
11822
|
+
chalk6.dim(s.started_at.substring(0, 16))
|
|
11458
11823
|
]));
|
|
11459
11824
|
console.log();
|
|
11460
11825
|
});
|
|
@@ -11464,17 +11829,17 @@ program.command("top").description("Most expensive sessions").option("-n <n>", "
|
|
|
11464
11829
|
const sinceDate = opts.since ? parseSinceDate2(opts.since) : undefined;
|
|
11465
11830
|
const sessions = await getStore().topSessions(count, agent, sinceDate);
|
|
11466
11831
|
if (sessions.length === 0) {
|
|
11467
|
-
console.log(
|
|
11832
|
+
console.log(chalk6.yellow("No sessions found. Run `economy sync` first."));
|
|
11468
11833
|
return;
|
|
11469
11834
|
}
|
|
11470
11835
|
console.log();
|
|
11471
11836
|
printTable(["#", "Project", "Agent", "Cost", "Tokens", "Started"], sessions.map((s, i) => [
|
|
11472
|
-
|
|
11473
|
-
|
|
11837
|
+
chalk6.dim(String(i + 1)),
|
|
11838
|
+
chalk6.white(s.project_name || chalk6.dim("unknown")),
|
|
11474
11839
|
fmtAgent(s.agent),
|
|
11475
11840
|
fmt4(s.total_cost_usd),
|
|
11476
|
-
|
|
11477
|
-
|
|
11841
|
+
chalk6.cyan(fmtTokens2(s.total_tokens)),
|
|
11842
|
+
chalk6.dim(s.started_at.substring(0, 16))
|
|
11478
11843
|
]));
|
|
11479
11844
|
console.log();
|
|
11480
11845
|
});
|
|
@@ -11493,10 +11858,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11493
11858
|
}
|
|
11494
11859
|
const visibleRows = rows.slice(0, limit);
|
|
11495
11860
|
printTable(["Project", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11496
|
-
|
|
11861
|
+
chalk6.white(r.project_name || chalk6.dim("unknown")),
|
|
11497
11862
|
String(r.sessions),
|
|
11498
11863
|
String(r.requests),
|
|
11499
|
-
|
|
11864
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11500
11865
|
fmt4(r.cost_usd)
|
|
11501
11866
|
]));
|
|
11502
11867
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11511,7 +11876,7 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11511
11876
|
fmtAgent(r.agent),
|
|
11512
11877
|
String(r.sessions),
|
|
11513
11878
|
String(r.requests),
|
|
11514
|
-
|
|
11879
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11515
11880
|
fmt4(r.api_equivalent_usd),
|
|
11516
11881
|
fmt4(r.billable_usd),
|
|
11517
11882
|
fmt4(r.subscription_included_usd)
|
|
@@ -11534,15 +11899,15 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11534
11899
|
return;
|
|
11535
11900
|
}
|
|
11536
11901
|
if (rows.length === 0) {
|
|
11537
|
-
console.log(
|
|
11902
|
+
console.log(chalk6.yellow("No cost-center usage yet. Run `economy sync --loops` or ingest app/service usage with /ingest."));
|
|
11538
11903
|
} else {
|
|
11539
11904
|
const visibleRows = rows.slice(0, limit);
|
|
11540
11905
|
printTable(["Kind", "Cost Center", "Sessions", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11541
|
-
|
|
11542
|
-
|
|
11906
|
+
chalk6.white(r.kind),
|
|
11907
|
+
chalk6.white(r.name),
|
|
11543
11908
|
String(r.sessions),
|
|
11544
11909
|
String(r.requests),
|
|
11545
|
-
|
|
11910
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11546
11911
|
fmt4(r.cost_usd)
|
|
11547
11912
|
]));
|
|
11548
11913
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11555,10 +11920,10 @@ program.command("breakdown").description("Cost breakdown by model, agent, projec
|
|
|
11555
11920
|
}
|
|
11556
11921
|
const visibleRows = rows.slice(0, limit);
|
|
11557
11922
|
printTable(["Model", "Agent", "Requests", "Tokens", "Cost"], visibleRows.map((r) => [
|
|
11558
|
-
|
|
11923
|
+
chalk6.white(r.model),
|
|
11559
11924
|
fmtAgent(r.agent),
|
|
11560
11925
|
String(r.requests),
|
|
11561
|
-
|
|
11926
|
+
chalk6.cyan(fmtTokens2(r.total_tokens)),
|
|
11562
11927
|
fmt4(r.cost_usd)
|
|
11563
11928
|
]));
|
|
11564
11929
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
|
|
@@ -11574,15 +11939,15 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
11574
11939
|
return;
|
|
11575
11940
|
}
|
|
11576
11941
|
if (rows.length === 0) {
|
|
11577
|
-
console.log(
|
|
11942
|
+
console.log(chalk6.yellow("No account-attributed sessions yet. Run `economy sync` first."));
|
|
11578
11943
|
return;
|
|
11579
11944
|
}
|
|
11580
11945
|
console.log();
|
|
11581
|
-
console.log(
|
|
11946
|
+
console.log(chalk6.bold.cyan(` Accounts \u2014 ${period}`));
|
|
11582
11947
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11583
11948
|
const visibleRows = rows.slice(0, limit);
|
|
11584
11949
|
if (visibleRows.length < rows.length) {
|
|
11585
|
-
console.log(
|
|
11950
|
+
console.log(chalk6.dim(` ${rows.length} rows \xB7 showing ${visibleRows.length}`));
|
|
11586
11951
|
}
|
|
11587
11952
|
console.log();
|
|
11588
11953
|
printAccountBreakdown(visibleRows);
|
|
@@ -11614,27 +11979,27 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
11614
11979
|
limit_usd: limitUsd,
|
|
11615
11980
|
alert_at_percent: alertAtPercent
|
|
11616
11981
|
});
|
|
11617
|
-
console.log(
|
|
11982
|
+
console.log(chalk6.green(`\u2713 Budget set: ${opts.costCenter ?? opts.project ?? "global"} \u2014 ${period} $${limitUsd}`));
|
|
11618
11983
|
});
|
|
11619
11984
|
budgetCmd.command("list").description("List all budgets").option("--limit <n>", "Maximum budget rows to print (default: 20)").option("--verbose", "Show all budget rows").action(async (opts) => {
|
|
11620
11985
|
const statuses = await getStore().listBudgets();
|
|
11621
11986
|
if (statuses.length === 0) {
|
|
11622
|
-
console.log(
|
|
11987
|
+
console.log(chalk6.yellow("No budgets set."));
|
|
11623
11988
|
return;
|
|
11624
11989
|
}
|
|
11625
11990
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11626
11991
|
const visibleStatuses = statuses.slice(0, limit);
|
|
11627
11992
|
console.log();
|
|
11628
11993
|
if (visibleStatuses.length < statuses.length) {
|
|
11629
|
-
console.log(
|
|
11994
|
+
console.log(chalk6.dim(` ${statuses.length} budgets \xB7 showing ${visibleStatuses.length}`));
|
|
11630
11995
|
console.log();
|
|
11631
11996
|
}
|
|
11632
11997
|
printTable(["Scope", "Period", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((b) => {
|
|
11633
11998
|
const pct = b.percent_used.toFixed(1);
|
|
11634
|
-
const status = b.is_over_limit ?
|
|
11635
|
-
const pctColor = b.is_over_limit ?
|
|
11999
|
+
const status = b.is_over_limit ? chalk6.red("OVER") : b.is_over_alert ? chalk6.yellow("ALERT") : chalk6.green("OK");
|
|
12000
|
+
const pctColor = b.is_over_limit ? chalk6.red(pct + "%") : b.is_over_alert ? chalk6.yellow(pct + "%") : chalk6.green(pct + "%");
|
|
11636
12001
|
return [
|
|
11637
|
-
|
|
12002
|
+
chalk6.white(b.cost_center_id ?? b.project_path ?? "global"),
|
|
11638
12003
|
b.period,
|
|
11639
12004
|
fmt4(b.limit_usd),
|
|
11640
12005
|
fmt4(b.current_spend_usd),
|
|
@@ -11647,62 +12012,62 @@ budgetCmd.command("list").description("List all budgets").option("--limit <n>",
|
|
|
11647
12012
|
});
|
|
11648
12013
|
budgetCmd.command("remove <id>").description("Remove a budget by ID").action(async (id) => {
|
|
11649
12014
|
await getStore().removeBudget(id);
|
|
11650
|
-
console.log(
|
|
12015
|
+
console.log(chalk6.green(`\u2713 Budget removed`));
|
|
11651
12016
|
});
|
|
11652
12017
|
var projectCmd = program.command("project").description("Manage tracked projects");
|
|
11653
12018
|
projectCmd.command("add <path>").description("Add a project").option("--name <name>", "Human-readable name").action(async (path, opts) => {
|
|
11654
12019
|
const { basename: basename4 } = __require("path");
|
|
11655
12020
|
const name = opts.name ?? basename4(path);
|
|
11656
12021
|
await getStore().addProject(path, name);
|
|
11657
|
-
console.log(
|
|
12022
|
+
console.log(chalk6.green(`\u2713 Project added: ${path}`));
|
|
11658
12023
|
});
|
|
11659
12024
|
projectCmd.command("list").description("List all projects with costs").option("--limit <n>", "Maximum project rows to print (default: 20)").option("--verbose", "Show all project rows").action(async (opts) => {
|
|
11660
12025
|
const projects = await getStore().listProjects();
|
|
11661
12026
|
if (projects.length === 0) {
|
|
11662
|
-
console.log(
|
|
12027
|
+
console.log(chalk6.yellow("No projects tracked yet."));
|
|
11663
12028
|
return;
|
|
11664
12029
|
}
|
|
11665
12030
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11666
12031
|
const visibleProjects = projects.slice(0, limit);
|
|
11667
12032
|
console.log();
|
|
11668
12033
|
if (visibleProjects.length < projects.length) {
|
|
11669
|
-
console.log(
|
|
12034
|
+
console.log(chalk6.dim(` ${projects.length} projects \xB7 showing ${visibleProjects.length}`));
|
|
11670
12035
|
console.log();
|
|
11671
12036
|
}
|
|
11672
12037
|
printTable(["Project", "Path", "Sessions", "Cost", "Last Active"], visibleProjects.map((p) => [
|
|
11673
|
-
|
|
11674
|
-
|
|
12038
|
+
chalk6.white(p.project_name || chalk6.dim("unknown")),
|
|
12039
|
+
chalk6.dim(p.project_path.substring(0, 40)),
|
|
11675
12040
|
String(p.sessions),
|
|
11676
12041
|
fmt4(p.cost_usd),
|
|
11677
|
-
|
|
12042
|
+
chalk6.dim(p.last_active?.substring(0, 16) ?? "\u2014")
|
|
11678
12043
|
]));
|
|
11679
12044
|
printHiddenRowsHint(projects.length, visibleProjects.length, "Use --limit <n>, --verbose, or economy project show <path>.");
|
|
11680
12045
|
console.log();
|
|
11681
12046
|
});
|
|
11682
12047
|
projectCmd.command("remove <path>").description("Remove a project (keeps historical data)").action(async (path) => {
|
|
11683
12048
|
await getStore().removeProject(path);
|
|
11684
|
-
console.log(
|
|
12049
|
+
console.log(chalk6.green(`\u2713 Project removed`));
|
|
11685
12050
|
});
|
|
11686
12051
|
projectCmd.command("rename <path> <name>").description("Rename a project").action(async (path, name) => {
|
|
11687
12052
|
try {
|
|
11688
12053
|
await getStore().renameProject(path, name);
|
|
11689
12054
|
} catch (e) {
|
|
11690
|
-
console.error(
|
|
12055
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
11691
12056
|
process.exit(1);
|
|
11692
12057
|
}
|
|
11693
|
-
console.log(
|
|
12058
|
+
console.log(chalk6.green(`\u2713 Renamed to: ${name}`));
|
|
11694
12059
|
});
|
|
11695
12060
|
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
11696
12061
|
await autoSync();
|
|
11697
12062
|
const detail = await getStore().projectDetail(nameOrPath);
|
|
11698
12063
|
if (!detail) {
|
|
11699
|
-
console.log(
|
|
12064
|
+
console.log(chalk6.yellow(`No sessions found for: ${nameOrPath}`));
|
|
11700
12065
|
return;
|
|
11701
12066
|
}
|
|
11702
12067
|
const dailyValues = detail.daily.map((d) => d.cost_usd);
|
|
11703
12068
|
console.log();
|
|
11704
|
-
console.log(
|
|
11705
|
-
console.log(
|
|
12069
|
+
console.log(chalk6.bold.cyan(` ${detail.project_name}`));
|
|
12070
|
+
console.log(chalk6.dim(` ${detail.project_path}`));
|
|
11706
12071
|
console.log();
|
|
11707
12072
|
printTable(["Metric", "Value"], [
|
|
11708
12073
|
["Total cost", fmt4(detail.total_cost_usd)],
|
|
@@ -11711,20 +12076,20 @@ projectCmd.command("show <nameOrPath>").description("Detailed project breakdown
|
|
|
11711
12076
|
]);
|
|
11712
12077
|
if (dailyValues.length > 0) {
|
|
11713
12078
|
console.log(`
|
|
11714
|
-
${
|
|
12079
|
+
${chalk6.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
11715
12080
|
}
|
|
11716
12081
|
if (detail.models.length > 0) {
|
|
11717
12082
|
console.log(`
|
|
11718
|
-
${
|
|
12083
|
+
${chalk6.dim("Model breakdown:")}`);
|
|
11719
12084
|
for (const m of detail.models) {
|
|
11720
|
-
console.log(` ${
|
|
12085
|
+
console.log(` ${chalk6.white(m.model.padEnd(30))} ${fmt4(m.cost_usd)} (${fmtCount(m.requests)} reqs)`);
|
|
11721
12086
|
}
|
|
11722
12087
|
}
|
|
11723
12088
|
if (detail.top_sessions.length > 0) {
|
|
11724
12089
|
console.log(`
|
|
11725
|
-
${
|
|
12090
|
+
${chalk6.dim("Top sessions:")}`);
|
|
11726
12091
|
for (const s of detail.top_sessions) {
|
|
11727
|
-
console.log(` ${
|
|
12092
|
+
console.log(` ${chalk6.dim(s.id.substring(0, 12))} ${fmt4(s.total_cost_usd)} ${chalk6.dim(String(s.started_at).substring(0, 16))}`);
|
|
11728
12093
|
}
|
|
11729
12094
|
}
|
|
11730
12095
|
console.log();
|
|
@@ -11733,18 +12098,18 @@ var configCmd = program.command("config").description("Manage economy configurat
|
|
|
11733
12098
|
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
11734
12099
|
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11735
12100
|
setConfigValue2(_key, _value);
|
|
11736
|
-
console.log(
|
|
12101
|
+
console.log(chalk6.green(`\u2713 ${_key} = ${_value}`));
|
|
11737
12102
|
});
|
|
11738
12103
|
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
11739
12104
|
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
11740
|
-
console.log(getConfigValue2(key) ??
|
|
12105
|
+
console.log(getConfigValue2(key) ?? chalk6.dim("(not set)"));
|
|
11741
12106
|
});
|
|
11742
12107
|
configCmd.command("webhook-test").description("Send a test payload to the configured webhook URL").action(async () => {
|
|
11743
|
-
const { loadConfig:
|
|
11744
|
-
const config =
|
|
12108
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
12109
|
+
const config = loadConfig2();
|
|
11745
12110
|
const url = config["webhook-url"];
|
|
11746
12111
|
if (!url) {
|
|
11747
|
-
console.log(
|
|
12112
|
+
console.log(chalk6.yellow("No webhook-url configured. Run: economy config set webhook-url <url>"));
|
|
11748
12113
|
return;
|
|
11749
12114
|
}
|
|
11750
12115
|
const payload = {
|
|
@@ -11761,21 +12126,21 @@ configCmd.command("webhook-test").description("Send a test payload to the config
|
|
|
11761
12126
|
});
|
|
11762
12127
|
const text = await res.text().catch(() => "");
|
|
11763
12128
|
if (res.ok) {
|
|
11764
|
-
console.log(
|
|
12129
|
+
console.log(chalk6.green(`\u2713 Webhook responded: HTTP ${res.status}`));
|
|
11765
12130
|
if (text)
|
|
11766
|
-
console.log(
|
|
12131
|
+
console.log(chalk6.dim(text.slice(0, 200)));
|
|
11767
12132
|
} else {
|
|
11768
|
-
console.log(
|
|
12133
|
+
console.log(chalk6.red(`\u2717 Webhook failed: HTTP ${res.status}`));
|
|
11769
12134
|
if (text)
|
|
11770
|
-
console.log(
|
|
12135
|
+
console.log(chalk6.dim(text.slice(0, 200)));
|
|
11771
12136
|
}
|
|
11772
12137
|
} catch (e) {
|
|
11773
|
-
console.log(
|
|
12138
|
+
console.log(chalk6.red(`\u2717 Request failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
11774
12139
|
}
|
|
11775
12140
|
});
|
|
11776
12141
|
configCmd.action(async () => {
|
|
11777
|
-
const { loadConfig:
|
|
11778
|
-
const config =
|
|
12142
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
12143
|
+
const config = loadConfig2();
|
|
11779
12144
|
console.log();
|
|
11780
12145
|
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
11781
12146
|
console.log();
|
|
@@ -11787,17 +12152,17 @@ pricingCmd.command("list").description("List all model prices").option("--limit
|
|
|
11787
12152
|
const visibleRows = rows.slice(0, limit);
|
|
11788
12153
|
console.log();
|
|
11789
12154
|
if (visibleRows.length < rows.length) {
|
|
11790
|
-
console.log(
|
|
12155
|
+
console.log(chalk6.dim(` ${rows.length} pricing rows \xB7 showing ${visibleRows.length}`));
|
|
11791
12156
|
console.log();
|
|
11792
12157
|
}
|
|
11793
12158
|
printTable(["Model", "Input/1M", "Output/1M", "CacheR/1M", "CacheW/1M", "CacheStorage/1M-h", "Out/1k"], visibleRows.map((r) => [
|
|
11794
|
-
|
|
12159
|
+
chalk6.white(r.model),
|
|
11795
12160
|
fmt4(r.input_per_1m),
|
|
11796
12161
|
fmt4(r.output_per_1m),
|
|
11797
12162
|
fmt4(r.cache_read_per_1m),
|
|
11798
12163
|
r.cache_write_1h_per_1m ? `${fmt4(r.cache_write_per_1m)} / ${fmt4(r.cache_write_1h_per_1m)}` : fmt4(r.cache_write_per_1m),
|
|
11799
12164
|
fmt4(r.cache_storage_per_1m_hour ?? 0),
|
|
11800
|
-
|
|
12165
|
+
chalk6.dim(fmt4(r.output_per_1m / 1000))
|
|
11801
12166
|
]));
|
|
11802
12167
|
printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n> or --verbose.");
|
|
11803
12168
|
console.log();
|
|
@@ -11818,79 +12183,35 @@ pricingCmd.command("set <model>").description("Set pricing for a model").option(
|
|
|
11818
12183
|
cache_write_1h_per_1m: cacheWrite1h,
|
|
11819
12184
|
cache_storage_per_1m_hour: cacheStorage
|
|
11820
12185
|
});
|
|
11821
|
-
console.log(
|
|
12186
|
+
console.log(chalk6.green(`\u2713 Pricing updated for ${model}`));
|
|
11822
12187
|
});
|
|
11823
12188
|
pricingCmd.command("remove <model>").description("Remove pricing for a model").action(async (model) => {
|
|
11824
12189
|
await getStore().removePricing(model);
|
|
11825
|
-
console.log(
|
|
12190
|
+
console.log(chalk6.green(`\u2713 Pricing removed for ${model}`));
|
|
11826
12191
|
});
|
|
11827
12192
|
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
11828
12193
|
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
11829
12194
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
11830
12195
|
startServer2(port);
|
|
11831
12196
|
});
|
|
11832
|
-
program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
|
|
11833
|
-
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
11834
|
-
const url = `http://localhost:${port}`;
|
|
11835
|
-
let serverRunning = false;
|
|
11836
|
-
try {
|
|
11837
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
|
|
11838
|
-
serverRunning = res.ok;
|
|
11839
|
-
} catch {}
|
|
11840
|
-
if (!serverRunning) {
|
|
11841
|
-
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
11842
|
-
const { spawn } = await import("child_process");
|
|
11843
|
-
const { resolve: resolve2, dirname: dirname4 } = await import("path");
|
|
11844
|
-
const serveScript = resolve2(dirname4(process.argv[1]), "..", "server", "index.js");
|
|
11845
|
-
const child = spawn(process.execPath, [serveScript], {
|
|
11846
|
-
detached: true,
|
|
11847
|
-
stdio: "ignore",
|
|
11848
|
-
env: { ...process.env, ECONOMY_PORT: String(port) }
|
|
11849
|
-
});
|
|
11850
|
-
child.unref();
|
|
11851
|
-
let attempts = 0;
|
|
11852
|
-
while (attempts < 20) {
|
|
11853
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
11854
|
-
try {
|
|
11855
|
-
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
|
|
11856
|
-
if (res.ok) {
|
|
11857
|
-
serverRunning = true;
|
|
11858
|
-
break;
|
|
11859
|
-
}
|
|
11860
|
-
} catch {}
|
|
11861
|
-
attempts++;
|
|
11862
|
-
}
|
|
11863
|
-
if (serverRunning) {
|
|
11864
|
-
console.log(chalk7.green(`\u2713 Server started`));
|
|
11865
|
-
} else {
|
|
11866
|
-
console.log(chalk7.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
11867
|
-
}
|
|
11868
|
-
}
|
|
11869
|
-
console.log(chalk7.cyan(`Opening ${url}`));
|
|
11870
|
-
try {
|
|
11871
|
-
execSync(`open ${url}`);
|
|
11872
|
-
} catch {
|
|
11873
|
-
console.log(chalk7.yellow(`Open your browser at ${url}`));
|
|
11874
|
-
}
|
|
11875
|
-
});
|
|
11876
12197
|
program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
|
|
11877
12198
|
const doAll = opts.all || !opts.claude && !opts.codex && !opts.gemini;
|
|
11878
12199
|
if (opts.claude || doAll) {
|
|
11879
|
-
console.log(
|
|
12200
|
+
console.log(chalk6.bold.cyan(`
|
|
11880
12201
|
Claude Code:`));
|
|
11881
|
-
console.log(
|
|
12202
|
+
console.log(chalk6.white(" claude mcp add --transport stdio --scope user economy -- economy-mcp"));
|
|
11882
12203
|
}
|
|
11883
12204
|
if (opts.codex || doAll) {
|
|
11884
|
-
console.log(
|
|
12205
|
+
console.log(chalk6.bold.yellow(`
|
|
11885
12206
|
Codex (~/.codex/config.toml):`));
|
|
11886
|
-
console.log(
|
|
12207
|
+
console.log(chalk6.white(` [mcp_servers.economy]
|
|
11887
12208
|
command = "economy-mcp"
|
|
11888
12209
|
args = []`));
|
|
11889
12210
|
}
|
|
11890
12211
|
if (opts.gemini || doAll) {
|
|
11891
|
-
console.log(
|
|
12212
|
+
console.log(chalk6.bold.green(`
|
|
11892
12213
|
Gemini (~/.gemini/settings.json):`));
|
|
11893
|
-
console.log(
|
|
12214
|
+
console.log(chalk6.white(' "mcpServers": { "economy": { "command": "economy-mcp", "args": [] } }'));
|
|
11894
12215
|
}
|
|
11895
12216
|
console.log();
|
|
11896
12217
|
});
|
|
@@ -11899,12 +12220,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11899
12220
|
await autoSync();
|
|
11900
12221
|
const detail = await getStore().sessionDetail(id);
|
|
11901
12222
|
if (!detail) {
|
|
11902
|
-
console.log(
|
|
12223
|
+
console.log(chalk6.red(`Session not found: ${id}`));
|
|
11903
12224
|
process.exit(1);
|
|
11904
12225
|
}
|
|
11905
12226
|
const { session, requests } = detail;
|
|
11906
12227
|
console.log();
|
|
11907
|
-
console.log(
|
|
12228
|
+
console.log(chalk6.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
11908
12229
|
console.log();
|
|
11909
12230
|
printTable(["Field", "Value"], [
|
|
11910
12231
|
["Agent", String(session["agent"])],
|
|
@@ -11917,12 +12238,12 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11917
12238
|
]);
|
|
11918
12239
|
if (requests.length > 0) {
|
|
11919
12240
|
const visibleRequests = requests.slice(0, requestLimit);
|
|
11920
|
-
console.log(
|
|
12241
|
+
console.log(chalk6.dim(`
|
|
11921
12242
|
Requests (${visibleRequests.length}${visibleRequests.length < requests.length ? ` of ${requests.length}` : ""}):
|
|
11922
12243
|
`));
|
|
11923
12244
|
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], visibleRequests.map((r) => [
|
|
11924
|
-
|
|
11925
|
-
|
|
12245
|
+
chalk6.dim(String(r["timestamp"]).substring(11, 19)),
|
|
12246
|
+
chalk6.white(String(r["model"]).substring(0, 22)),
|
|
11926
12247
|
fmtTokens2(r["input_tokens"]),
|
|
11927
12248
|
fmtTokens2(r["output_tokens"]),
|
|
11928
12249
|
fmtTokens2(r["cache_read_tokens"]),
|
|
@@ -11930,7 +12251,7 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
11930
12251
|
fmt4(r["cost_usd"])
|
|
11931
12252
|
]));
|
|
11932
12253
|
if (requests.length > visibleRequests.length) {
|
|
11933
|
-
console.log(
|
|
12254
|
+
console.log(chalk6.dim(` ... ${requests.length - visibleRequests.length} more requests hidden. Use --limit <n> or --verbose.`));
|
|
11934
12255
|
}
|
|
11935
12256
|
}
|
|
11936
12257
|
console.log();
|
|
@@ -11940,27 +12261,27 @@ program.command("machines").description("List all machines that have synced data
|
|
|
11940
12261
|
const machines = await getStore().machines();
|
|
11941
12262
|
const current = getMachineId();
|
|
11942
12263
|
if (machines.length === 0) {
|
|
11943
|
-
console.log(
|
|
12264
|
+
console.log(chalk6.yellow(`No machine data yet. Current machine: ${current}`));
|
|
11944
12265
|
return;
|
|
11945
12266
|
}
|
|
11946
12267
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
11947
12268
|
const visibleMachines = machines.slice(0, limit);
|
|
11948
12269
|
console.log();
|
|
11949
|
-
console.log(
|
|
12270
|
+
console.log(chalk6.bold.cyan(" Machines"));
|
|
11950
12271
|
if (visibleMachines.length < machines.length) {
|
|
11951
|
-
console.log(
|
|
12272
|
+
console.log(chalk6.dim(` ${machines.length} machines \xB7 showing ${visibleMachines.length}`));
|
|
11952
12273
|
}
|
|
11953
12274
|
console.log();
|
|
11954
12275
|
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], visibleMachines.map((m) => [
|
|
11955
|
-
m.machine_id === current ?
|
|
12276
|
+
m.machine_id === current ? chalk6.green(`${m.machine_id} (this)`) : chalk6.white(m.machine_id),
|
|
11956
12277
|
fmtCount(m.sessions),
|
|
11957
12278
|
fmtCount(m.requests),
|
|
11958
12279
|
fmt4(m.total_cost_usd),
|
|
11959
|
-
|
|
12280
|
+
chalk6.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
11960
12281
|
]));
|
|
11961
12282
|
printHiddenRowsHint(machines.length, visibleMachines.length, "Use --limit <n> or --verbose.");
|
|
11962
12283
|
console.log(`
|
|
11963
|
-
${
|
|
12284
|
+
${chalk6.dim("Current machine:")} ${chalk6.bold(current)}`);
|
|
11964
12285
|
console.log();
|
|
11965
12286
|
});
|
|
11966
12287
|
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
@@ -11987,7 +12308,7 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
11987
12308
|
if (opts.output) {
|
|
11988
12309
|
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
11989
12310
|
writeFileSync4(opts.output, csv);
|
|
11990
|
-
console.log(
|
|
12311
|
+
console.log(chalk6.green(`\u2713 Exported to ${opts.output}`));
|
|
11991
12312
|
} else {
|
|
11992
12313
|
process.stdout.write(csv);
|
|
11993
12314
|
}
|
|
@@ -12043,11 +12364,11 @@ program.command("compare <period1> <period2>").description("Compare two periods
|
|
|
12043
12364
|
const d = v1 - v2;
|
|
12044
12365
|
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
12045
12366
|
const sign = d >= 0 ? "+" : "";
|
|
12046
|
-
const color = d > 0 ?
|
|
12367
|
+
const color = d > 0 ? chalk6.red : d < 0 ? chalk6.green : chalk6.dim;
|
|
12047
12368
|
return color(`${sign}${pct}%`);
|
|
12048
12369
|
}
|
|
12049
12370
|
console.log();
|
|
12050
|
-
console.log(
|
|
12371
|
+
console.log(chalk6.bold.cyan(` ${p1} vs ${p2}`));
|
|
12051
12372
|
console.log();
|
|
12052
12373
|
printTable(["Metric", p1, p2, "Change"], [
|
|
12053
12374
|
["Cost", fmt4(a.cost), fmt4(b.cost), delta(a.cost, b.cost)],
|
|
@@ -12063,12 +12384,12 @@ program.command("forecast").description("Project end-of-month cost based on curr
|
|
|
12063
12384
|
const cheapest = f.cheapest_day;
|
|
12064
12385
|
const mostExpensive = f.most_expensive_day;
|
|
12065
12386
|
console.log();
|
|
12066
|
-
console.log(
|
|
12387
|
+
console.log(chalk6.bold.cyan(` Forecast (${f.day_of_month} of ${f.days_in_month} days)`));
|
|
12067
12388
|
console.log();
|
|
12068
12389
|
printTable(["Metric", "Value"], [
|
|
12069
12390
|
["Spent so far", fmt4(f.spent_so_far_usd)],
|
|
12070
12391
|
["Daily average", fmt4(f.daily_avg_usd)],
|
|
12071
|
-
[
|
|
12392
|
+
[chalk6.bold("Projected total"), chalk6.bold(fmt4(f.projected_usd).replace(chalk6.green(""), ""))],
|
|
12072
12393
|
["Last 7-day rate", `${fmt4(f.last7_daily_avg_usd)}/day \u2192 ${fmt4(f.last7_projected_usd)}`],
|
|
12073
12394
|
["Cheapest day", cheapest ? `${fmt4(cheapest.cost_usd)} (${cheapest.date})` : "\u2014"],
|
|
12074
12395
|
["Most expensive", mostExpensive ? `${fmt4(mostExpensive.cost_usd)} (${mostExpensive.date})` : "\u2014"]
|
|
@@ -12079,14 +12400,14 @@ program.command("efficiency").description("Show output/input token ratio per mod
|
|
|
12079
12400
|
await autoSync();
|
|
12080
12401
|
const models = await getStore().efficiency();
|
|
12081
12402
|
console.log();
|
|
12082
|
-
console.log(
|
|
12403
|
+
console.log(chalk6.bold.cyan(" Token Efficiency"));
|
|
12083
12404
|
console.log();
|
|
12084
12405
|
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
12085
12406
|
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
12086
12407
|
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
12087
12408
|
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
12088
12409
|
const costPer1kOutput = m.output > 0 ? fmt4(m.cost / m.output * 1000) : "\u2014";
|
|
12089
|
-
return [
|
|
12410
|
+
return [chalk6.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
12090
12411
|
}));
|
|
12091
12412
|
console.log();
|
|
12092
12413
|
});
|
|
@@ -12118,29 +12439,29 @@ goalCmd.command("set").description("Set a spending goal").option("--period <peri
|
|
|
12118
12439
|
agent,
|
|
12119
12440
|
limit_usd: limitUsd
|
|
12120
12441
|
});
|
|
12121
|
-
console.log(
|
|
12442
|
+
console.log(chalk6.green(`\u2713 Goal set: ${period} $${limitUsd}${opts.project ? ` (${opts.project})` : ""}`));
|
|
12122
12443
|
});
|
|
12123
12444
|
goalCmd.command("list").description("List all goals with progress").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
|
|
12124
12445
|
const statuses = await getStore().listGoals();
|
|
12125
12446
|
if (statuses.length === 0) {
|
|
12126
|
-
console.log(
|
|
12447
|
+
console.log(chalk6.yellow("No goals set."));
|
|
12127
12448
|
return;
|
|
12128
12449
|
}
|
|
12129
12450
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
12130
12451
|
const visibleStatuses = statuses.slice(0, limit);
|
|
12131
12452
|
console.log();
|
|
12132
12453
|
if (visibleStatuses.length < statuses.length) {
|
|
12133
|
-
console.log(
|
|
12454
|
+
console.log(chalk6.dim(` ${statuses.length} goals \xB7 showing ${visibleStatuses.length}`));
|
|
12134
12455
|
console.log();
|
|
12135
12456
|
}
|
|
12136
12457
|
printTable(["Period", "Scope", "Limit", "Spent", "Used%", "Status"], visibleStatuses.map((g) => {
|
|
12137
12458
|
const pct = g.percent_used.toFixed(1);
|
|
12138
12459
|
const scope = g.project_path ?? g.agent ?? "global";
|
|
12139
|
-
const status = g.is_over ?
|
|
12140
|
-
const pctColor = g.is_over ?
|
|
12460
|
+
const status = g.is_over ? chalk6.red("OVER") : g.is_at_risk ? chalk6.yellow("AT RISK") : chalk6.green("ON TRACK");
|
|
12461
|
+
const pctColor = g.is_over ? chalk6.red(pct + "%") : g.is_at_risk ? chalk6.yellow(pct + "%") : chalk6.green(pct + "%");
|
|
12141
12462
|
return [
|
|
12142
12463
|
g.period,
|
|
12143
|
-
|
|
12464
|
+
chalk6.white(scope),
|
|
12144
12465
|
fmt4(g.limit_usd),
|
|
12145
12466
|
fmt4(g.current_spend_usd),
|
|
12146
12467
|
pctColor,
|
|
@@ -12152,12 +12473,12 @@ goalCmd.command("list").description("List all goals with progress").option("--li
|
|
|
12152
12473
|
});
|
|
12153
12474
|
goalCmd.command("remove <id>").description("Remove a goal").action(async (id) => {
|
|
12154
12475
|
await getStore().removeGoal(id);
|
|
12155
|
-
console.log(
|
|
12476
|
+
console.log(chalk6.green(`\u2713 Goal removed`));
|
|
12156
12477
|
});
|
|
12157
12478
|
goalCmd.command("status").description("Quick goal progress summary").option("--limit <n>", "Maximum goal rows to print (default: 20)").option("--verbose", "Show all goal rows").action(async (opts) => {
|
|
12158
12479
|
const statuses = await getStore().listGoals();
|
|
12159
12480
|
if (statuses.length === 0) {
|
|
12160
|
-
console.log(
|
|
12481
|
+
console.log(chalk6.yellow("No goals set."));
|
|
12161
12482
|
return;
|
|
12162
12483
|
}
|
|
12163
12484
|
const limit = resolveRowLimit(opts.limit, opts.verbose, 20);
|
|
@@ -12169,7 +12490,7 @@ goalCmd.command("status").description("Quick goal progress summary").option("--l
|
|
|
12169
12490
|
const barFilled = Math.round(pct / 10);
|
|
12170
12491
|
const barEmpty = 10 - barFilled;
|
|
12171
12492
|
const bar = "\u2588".repeat(barFilled) + "\u2591".repeat(barEmpty);
|
|
12172
|
-
const statusStr = g.is_over ?
|
|
12493
|
+
const statusStr = g.is_over ? chalk6.red("\u2717 OVER") : g.is_at_risk ? chalk6.yellow("\u26A0 AT RISK") : chalk6.green("\u2713 ON TRACK");
|
|
12173
12494
|
const label = `${g.period} (${scope})`.padEnd(20);
|
|
12174
12495
|
console.log(` ${label} ${bar} ${fmt4(g.current_spend_usd)} / ${fmt4(g.limit_usd)} (${g.percent_used.toFixed(0)}%) ${statusStr}`);
|
|
12175
12496
|
}
|
|
@@ -12181,7 +12502,7 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
12181
12502
|
try {
|
|
12182
12503
|
const t = type.toLowerCase();
|
|
12183
12504
|
if (!(t in label)) {
|
|
12184
|
-
console.error(
|
|
12505
|
+
console.error(chalk6.red(`Unknown type: ${type}. Use: budget | project | goal | pricing`));
|
|
12185
12506
|
process.exit(1);
|
|
12186
12507
|
}
|
|
12187
12508
|
const store = getStore();
|
|
@@ -12199,9 +12520,9 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
12199
12520
|
await store.removePricing(id);
|
|
12200
12521
|
break;
|
|
12201
12522
|
}
|
|
12202
|
-
console.log(
|
|
12523
|
+
console.log(chalk6.green(`\u2713 ${label[t]} ${id} removed`));
|
|
12203
12524
|
} catch (e) {
|
|
12204
|
-
console.error(
|
|
12525
|
+
console.error(chalk6.red(`Failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
12205
12526
|
process.exit(1);
|
|
12206
12527
|
}
|
|
12207
12528
|
});
|
|
@@ -12213,51 +12534,51 @@ billingCmd.command("sync").description("Sync actual billing from Anthropic, Open
|
|
|
12213
12534
|
if (isCloudStore()) {
|
|
12214
12535
|
const cloud = economyCloudStorage();
|
|
12215
12536
|
if (!cloud.active) {
|
|
12216
|
-
console.log(
|
|
12537
|
+
console.log(chalk6.yellow("cloud mode: billing sync transport unavailable (no credential resolved; set HASNA_ECONOMY_API_KEY or write ~/.hasna/economy/config/credentials)"));
|
|
12217
12538
|
return;
|
|
12218
12539
|
}
|
|
12219
12540
|
const result = await billingSyncToCloud(cloud, { days, anthropic: opts.anthropic, openai: opts.openai, gemini: opts.gemini });
|
|
12220
12541
|
for (const [provider, p] of Object.entries(result.providers)) {
|
|
12221
12542
|
if (p.ok)
|
|
12222
|
-
console.log(
|
|
12543
|
+
console.log(chalk6.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
|
|
12223
12544
|
else
|
|
12224
|
-
console.log(
|
|
12545
|
+
console.log(chalk6.red(`\u2717 ${provider}: ${p.error}`));
|
|
12225
12546
|
}
|
|
12226
12547
|
if (result.posted)
|
|
12227
|
-
console.log(
|
|
12548
|
+
console.log(chalk6.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
|
|
12228
12549
|
return;
|
|
12229
12550
|
}
|
|
12230
12551
|
const db = openDatabase();
|
|
12231
12552
|
const doAll = !opts.anthropic && !opts.openai && !opts.gemini;
|
|
12232
12553
|
if (opts.anthropic || doAll) {
|
|
12233
|
-
process.stdout.write(
|
|
12554
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing Anthropic billing... "));
|
|
12234
12555
|
try {
|
|
12235
12556
|
const r = await syncAnthropicBilling(db, { days });
|
|
12236
|
-
console.log(
|
|
12557
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12237
12558
|
} catch (e) {
|
|
12238
|
-
console.log(
|
|
12559
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12239
12560
|
}
|
|
12240
12561
|
}
|
|
12241
12562
|
if (opts.openai || doAll) {
|
|
12242
|
-
process.stdout.write(
|
|
12563
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing OpenAI billing... "));
|
|
12243
12564
|
try {
|
|
12244
12565
|
const r = await syncOpenAIBilling(db, { days });
|
|
12245
|
-
console.log(
|
|
12566
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12246
12567
|
} catch (e) {
|
|
12247
|
-
console.log(
|
|
12568
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12248
12569
|
}
|
|
12249
12570
|
}
|
|
12250
12571
|
if (opts.gemini || doAll) {
|
|
12251
|
-
process.stdout.write(
|
|
12572
|
+
process.stdout.write(chalk6.cyan("\u2192 Syncing Gemini billing... "));
|
|
12252
12573
|
try {
|
|
12253
12574
|
const r = await syncGeminiBilling(db, { days });
|
|
12254
12575
|
if (r.skipped) {
|
|
12255
|
-
console.log(
|
|
12576
|
+
console.log(chalk6.yellow(`skipped \u2014 ${r.skipped}`));
|
|
12256
12577
|
} else {
|
|
12257
|
-
console.log(
|
|
12578
|
+
console.log(chalk6.green(`\u2713 ${r.days} days, $${r.totalUsd.toFixed(2)}`));
|
|
12258
12579
|
}
|
|
12259
12580
|
} catch (e) {
|
|
12260
|
-
console.log(
|
|
12581
|
+
console.log(chalk6.red(`\u2717 ${e instanceof Error ? e.message : String(e)}`));
|
|
12261
12582
|
}
|
|
12262
12583
|
}
|
|
12263
12584
|
});
|
|
@@ -12267,22 +12588,21 @@ billingCmd.command("show").description("Show actual billing totals vs our estima
|
|
|
12267
12588
|
const actual = await store.billingSummary(period);
|
|
12268
12589
|
const estimated = await store.summary(period);
|
|
12269
12590
|
console.log();
|
|
12270
|
-
console.log(
|
|
12591
|
+
console.log(chalk6.bold.cyan(` Billing ${period} (actual from admin APIs)
|
|
12271
12592
|
`));
|
|
12272
|
-
printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [
|
|
12593
|
+
printTable(["Provider", "Actual (billed)"], Object.entries(actual.by_provider).map(([p, c]) => [chalk6.white(p), fmt4(c)]));
|
|
12273
12594
|
console.log();
|
|
12274
|
-
console.log(` ${
|
|
12275
|
-
console.log(` ${
|
|
12595
|
+
console.log(` ${chalk6.bold("Actual total:")} ${fmt4(actual.total_usd)}`);
|
|
12596
|
+
console.log(` ${chalk6.dim("Our estimate:")} ${fmt4(estimated.total_usd)}`);
|
|
12276
12597
|
const diff = estimated.total_usd - actual.total_usd;
|
|
12277
12598
|
const pct = billingDeltaPct(estimated.total_usd, actual.total_usd);
|
|
12278
12599
|
const rendered = pct === null ? "n/a" : `${diff >= 0 ? "+" : ""}${pct.toFixed(1)}%`;
|
|
12279
|
-
console.log(` ${
|
|
12600
|
+
console.log(` ${chalk6.dim("Difference:")} ${fmt4(Math.abs(diff))} (${rendered})`);
|
|
12280
12601
|
if (pct === null) {
|
|
12281
|
-
console.log(
|
|
12602
|
+
console.log(chalk6.yellow(` ${chalk6.dim("Note:")} no provider billing recorded for ${period} \u2014 drift is UNKNOWN, not zero.`));
|
|
12282
12603
|
}
|
|
12283
12604
|
console.log();
|
|
12284
12605
|
});
|
|
12285
|
-
registerBrainsCommand(program);
|
|
12286
12606
|
registerTodosCommand(program);
|
|
12287
12607
|
registerBriefCommand(program, { beforeRead: () => autoSync({ dedupe: false }) });
|
|
12288
12608
|
registerExtendedCommands(program);
|